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.
Table of Contents
- Part 1 - how to run a CLI for Microsoft 365 script on an Azure Function
- Part 2 - Getting notified of service incidents in Microsoft Teams
- Part 3 - Getting notified of changes in guests accessing SharePoint
- Part 4 - Configuring the CLI for Microsoft 365 in Azure Functions (This blog)
How to optimally configure the CLI on an Azure Function
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:
1. Hiding the Spinner
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:
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. 😅
2. Configuring error handling
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
3. Disabling interactivity
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:
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?
One ring to rule them all: using CLI-defined presets
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.
Working with command responses and errors
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.
Try-catch blocks and line numbers
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.
Conclusion
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!
Sources
- CLI for Microsoft 365 - Manage your Microsoft 365 tenant and SharePoint Framework projects on any platform
- CLI for Microsoft 365 - m365 setup - Using CLI-defined presets
- CLI for Microsoft 365 - m365 cli config set - Configuring the CLI
cli-microsoft365 azurefunction powershell
Support me by sharing this
More
More blogs
Don't trust $PWD in Azure PowerShell Functions
Note to self: do not trust $PWD in Azure PowerShell Functions.
Read moreUsing 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 moreGetting 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 moreThanks
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