Thumbnail Configuring the CLI for Microsoft 365 in Azure Functions

Configuring the CLI for Microsoft 365 in Azure Functions

It’s been some time ago that I wrote a series of blog posts on running the CLI for Microsoft 365 in Azure Functions running on the PowerShell stack. The CLI has evolved in the meantime. This and my own experiences prompted me to write a fourth post in this series.

The CLI for Microsoft 365 has a set of config keys that can be configured to optimize your experience running commands. This is useful because the CLI can is a tool that can be used in different ways: interactively or in (automated) scripts. It stands to reason that you would configure it differently when running an automated script in an Azure Function than you would when running from the terminal.

The following is a set of best practices regarding configurations to use:

By default, the CLI boasts a nice spinner that shows you that the command is running. This is great when running interactively, but when executing commands in an Azure Function, it’s unnecessary and even problematic. Due to the way PowerShell is set-up in Azure Functions, CLI scripts tend to show a lot of red lines when the spinner is enabled.

The spinner in action in your terminal:

spinner

The same spinner botching up Azure Functions logs:

Result: ERROR: - Running command...

Exception             :
    Type        : System.Management.Automation.RemoteException
     ErrorRecord :
         Exception             :
             Type    : System.Management.Automation.ParentContainsErrorRecordException
             Message : - Running command...
             HResult : -2146233087
         CategoryInfo          : NotSpecified: (:) [], ParentContainsErrorRecordException
         FullyQualifiedErrorId : RuntimeException
     Message     : - Running command...
     HResult     : -2146233087

This is because the spinner is writing to the error stream of the console, for which support is limited in Azure Functions. Even if it does not really interfere with the execution of the script, it’s not a nice sight. So it’s best to disable the spinner altogether when running in an Azure Function.

Disabling just the spinner can be done as follows:

m365 cli config set --key showSpinner --value false 2>$null

The addition of 2>$null is necessary to ensure that the command to disable the spinner does not itself show a spinner. 😅

By default, errors that are thrown by the CLI are written to the error stream. This results in nice red lines. However, in an Azure Function, you’d typically like more control on what you want to do when errors occur. For example: you might want to stop executing the rest of the script, or you might want to write a nicer error message to your logging provider / Application Insights. The support for doing these kinds of things is better for common PowerShell modules then it is for the CLI for Microsoft 365. It’s not a PowerShell module after all. (Which has its benefits as well as its challenges.)

To make the most of this, there are two config keys that I usually set:

m365 cli config set --key errorOutput --value stdout
m365 cli config set --key printErrorsAsPlainText --value false

This will result in errors being returned like the regular command output. This would mean that we could do something like this:

# If we're getting an error because the list does not exist
$response = m365 spo list get --webUrl $siteUrl --title $faultyListName | ConvertFrom-Json

# ...we can now work with the error
if ($response.error -ne $null) {
    throw $response.error
}

# ...and work with the returned object if it returns successfully
Write-Host "The list ID is $($response.Id)"

Aside from this it’s also helpful to disable printed help sections when running into errors:

m365 cli config set --key showHelpOnFailure --value false

The CLI has some other nice interactive features, like prompting the user to fill in required options, or prompting the user to select items when more than one is returned where one was expected. These are great when running interactively, but not so much when running in an Azure Function.

An example of a prompt:

prompt

You can disable these kinds of prompts and interactivity as follows:

m365 cli config set --key prompt --value false
m365 cli config set --key autoOpenLinksInBrowser --value false
m365 cli config set --key copyDeviceCodeToClipboard	 --value false

To be sure, these values are already false by default. But it’s good to be sure, no?

Now all this is a lot to remember. Instead of all these choices, the CLI for Microsoft 365 also has a command to configure them all correctly in one go. It’s called m365 setup and you can run this in an Azure function to configure all the relevant config keys in a single oneliner:

m365 setup --scripting 2>$null

Note the 2>$null again!

If you want to know what the command does you can try running it on your system without the scripting flag: m365 setup. You’ll be guided through an awesome wizard that will help you configure the CLI for Microsoft 365 for your specific needs. Plus it will show the specific keys that will be set.

Ok, so once you’ve set your configurations correctly, you’re ready to do some scripting work. But how do you work optimally with the responses and errors that are returned by the CLI?

Generally speaking, what you’ll note is that a lot of people pipe their CLI-commands into the PowerShell JSON-Converter commandlet:

$sites = m365 spo site list | ConvertFrom-Json

This is very useful. Unlike PowerShell modules, the CLI returns JSON-formatted text by default. Using ConvertFrom-Json enables you to work with the script responses as if they’re PowerShell objects and arrays:

foreach($site in $sites) {
    Write-Host "Site title: $($site.Title)"
}

But if you’ve been considering point 2. above, this will force you to check if an error occurred after each command execution, which is a bit much. The best way to deal with that is to add your own function instead of ConvertFrom-Json that will handle this for you.

Consider the following PowerShell script:

function Invoke-CLICommand {
    [cmdletbinding()]
    param(
      [parameter(Mandatory = $true, ValueFromPipeline = $true)] $input
    )

    $output = $input

    if ($null -eq $output) {
        return $null
    }

    $parsedOutput = $output | ConvertFrom-Json

    if ($parsedOutput -isnot [Array] -and $null -ne $parsedOutput.error) {
        throw $parsedOutput.error
    }    

    return $parsedOutput
}

try {

    $siteUrl = "https://contoso.sharepoint.com/sites/project-x"

    $site = m365 spo site get --url $siteUrl | Invoke-CLICommand

    $list = m365 spo list get --webUrl $site.Url --title "Some title" | Invoke-CLICommand

    # ...continue and do something with the list

}
catch {
    # Gracefully handle your error here
    Write-Error $_.Exception
}

This script abstracts away all details of working with responses and errors to a separate Invoke-CLICommand function. Now you can just pipe the CLI-commands into this function and write a clean script.

Using the try-catch block you can also rest assured that an error will end up being gracefully handled in the way you see fit.

Working with try-catch blocks is nice. It allows you to keep your script clean and have all the error handling in a central place. However, logged exceptions may now reference the wrong line number: the line in the try-catch block where Write-Error was executed. This can be annoying, because it makes it harder to find the actual line of the script that caused the error. You can fix this by writing the error and its invocation info. This will ensure that the correct line number is logged as well, I do this as follows:

Write-Error ($_.Exception | Format-List -Force | Out-String)
Write-Error ($_.InvocationInfo | Format-List -Force | Out-String)

This will result in everything that I need being logged to Application Insights.

Working with the CLI in the context of an Azure Function is a bit different than working with it interactively. However, with the right configurations and some additional best practices, you can make it be a smooth ride!

Happy scripting!


cli-microsoft365 azurefunction powershell
Support me by sharing this

More

More blogs

Don't trust $PWD in Azure PowerShell Functions
Don't trust $PWD in Azure PowerShell Functions

Note to self: do not trust $PWD in Azure PowerShell Functions.

Read more
Using the on-behalf-of flow in Azure PowerShell Functions
Using the on-behalf-of flow in Azure PowerShell Functions

A step by step guide on how to use the on-behalf-of flow in Azure PowerShell Functions.

Read more
Getting notified of changes in guests accessing SharePoint
Getting notified of changes in guests accessing SharePoint

Part 3 on how to use the CLI for Microsoft 365 and Azure Functions: How to get notified of updates in guests accessing SharePoint.

Read more

Thanks

Thanks for reading

Thanks for reading my blog, I hope you got what you came for. Blogs of others have been super important during my work. This site is me returning the favor. If you read anything you do not understand because I failed to clarify it enough, please drop me a post using my socials or the contact form.


Warm regards,
Martin

Microsoft MVP | Microsoft 365 Architect

Microsoft MVP horizontal