cli-microsoft365: Enhancement: Allow conversion to objects and solid error handling in PowerShell / 'PowerShell integration mode'
Ok, so the issue is twofold:
- Posh objects: everybody who uses m365 in PowerShell often uses
| Convertfrom-Json
to convert the output of the cli to objects for use in scripting. - Error handling: the CLI does not work like PowerShell commandlets. PowerShell has some interesting features here, like being able to configure what a script should do when it runs into an error. (
-ErrorAction
and $ErrorActionPreference There are some differences between how PowerShell interacts with lowerlying commandline tools. All in all this means that when building a solid script, you should write your own code to do some correct error handling.
We’d like to improve the general e2e flow of PowerShell users in these two regards.
💡 Idea
The idea would be to have better integration with PowerShell. By inserting a PowerShell function and piping command output in for processing before returning to the user.
Checkout the screenshot to see how I did that manually and what it causes:
📜 Functional requirements
1. PowerShell object output
- CLI commands should be able to return data as PowerShell objects.
- You should still be able to choose other output forms, like json, text, csv and md, using the —output option.
- Other functionality should continue to be usable, like interactive prompts, verbose and debug mode, help output, etc.
- (Unsure/Optional) You should be able to disable the PowerShell object output.
2. Exception handling
- CLI commands should adhere to the selected
$ErrorActionPreference
: CLI should throw a terminating exception if the preference is configured as ‘Stop’, it should write a non-terminating exception when the preference is set to ‘Continue’, etc. Hence you should be able to catch an error in scripts using ‘try catch’ statements. - The actual error should be shown when a problem occurs. If possible without a stacktrace.
3. Configuration and Usability
- You should be able to configure the integration with a single oneliner. (m365 setup)
- The integration should work in PowerShell 5 and 7.2, 7.3 and above.
- The integration should work in the terminal and in Azure PowerShell Functions.
If you have PowerShell Core running on linux, I noticed that the m365.ps1 file is not getting installed. which would mean the integration would not work there…
⚙️ Implementation details
1. Enabling the integration mode
After installing the CLI, the m365.ps1 file will need to be adapted. This should be done by running the m365 setup command.
m365 setup
An extra question should currently decide if integration mode is wanted. For example: ‘Do you want the CLI to return PowerShell objects?’ This question is only asked if the shell you are using is PowerShell.
…or using presets with a flag
m365 setup --scripting --usePowerShellIntegration
m365 setup --interactive --usePowerShellIntegration
The reason for the extra flag and the extra question is that existing scripts where people already use ConvertFrom-Json
would break. Unless we decide that is not a breaking change.
The default output config key remains
json
, PowerShell mode will piggyback on that output mode. Users can however circumvent the PowerShell object conversion by explicitly using--output json
on commands. To satisfy requirement 1.2.
After updating the CLI, the user needs to rerun the
m365 setup
command, like he also needs to rerunm365 cli completion pwsh update
.
2. What happens when the user enables the integration mode
- When enabling the integration the CLI will look for the location of the m365.ps1 file.
- An extra m365-invoke.ps1 script-file will be saved in the same directory, containing a PowerShell
Invoke-M365
commandlet. - The m365.ps1 file will be updated to include references to the commandlet in that script file.
3. JSON conversion in the Invoke-M365
commandlet
- The commandlet will pipe the output of the command into a variable.
- The variable contents will be converted from json.
- If the conversion fails, the raw output is returned. (which will mean text, csv and md modes can still be used)
4. Exception handling and the integration mode
- The current contents of
$ErrorActionPreference
and$PSNativeCommandUseErrorActionPreference
variables should be cached in the m365.ps1 file and set toContinue
/$false
respectively. (See challenge 2.3 for the reaon) - The
Invoke-M365
commandlet expects an error in the stdout output stream. (Config keyerrorOutput
=stdout
) - The
Invoke-M365
commandlet expects the error in JSON form. (Config keyprintErrorsAsPlainText
=false
) - The
Invoke-M365
commandlet writes the error or throws the error, taking the cached$ErrorActionPreference
into account. - The
$ErrorActionPreference
and$PSNativeCommandUseErrorActionPreference
variables are restored to their original values.
🎬 Scenario’s
1. When using CLI scripting in non-interactive environments
When using the CLI in a non-interactive environment. (For example: Azure PowerShell Functions), users can run the following line of code before doing anything else:
m365 setup --scripting --output none
This command will currently configure all config keys in the correct way.
Using the new PowerShell integration, this can be kept as-is. (Aside from the extra flag --usePowerShellIntegration
) the configuration is exactly the same. The only thing that’s adapted is the m365.ps1 file. Any errors will be written to the stdout stream, and this will be picked up by our PowerShell integration.
2. When using the CLI in interactive environments
When using the CLI in an interactive environment (the terminal), there is an issue with requirement 2.1. For the integration to work we’ll need errors to be written to the stdout stream in JSON form. We’ll need to configure error output to the stdout stream, but this will cause all prompts to be written there as well, freezing any interaction for the user. (See challenge 1).
👺 Challenge 1: The CLI Prompt and device code login cause the Invoke-M365CLICommand
to ‘freeze’.
Read the details
The prompt in the CLI is run bij inquirer during the execution of the command. This poses a problem. If we run the command using `$output = $input`, the `$output` variable will only be assigned a value after the `$input` has run. In practice this means the prompts will not show to the user, and the CLI seems to freeze.There seem to be two solutions:
-
Disable the prompt in this ‘PowerShell integration mode’. We could decide this is just not working well together, and that you would typically use this in scripts, not in interactive mode.
-
Change the prompt to use
stderr
. This should be possible according to the inquirer docs.
The login by device code flow has the same problem as the prompt issue… The login command waits for the user to use the device code. But the device code has not been printed to the screen because the $output-variable is not yet assigned. This poses a problem…
Solution: Pipe the prompt to stderr by configuring inquirer. Also move the device code message to the ora-spinner, which circumvents the stdout stream.
👺 Challenge 2: Native commands (like m365 cli) do not honour PowerShell ErrorAction preferences.
Read the details
In PowerShell there is a variable that controls what you would like failing commandlets to cause. It's called `$ErrorActionPreference`. By default it's set to 'Continue', which means that commandlets that fail will not stop the script execution. The script will continue execution of following lines. This is annoying behavior in scripts, where you sometimes would like Exceptions to be stopping further execution of your script. This is why in PowerShell I often set `$ErrorActionPreference = 'Stop'`.However: Exceptions thrown in in Native commands that are called by PowerShell (cmd files, exe files, bat files, like the CLI for Microsoft 365) do not work well with this currently.
In PowerShell 7.3 there is an experimental feature: $PSNativeCommandUseErrorActionPreference
Setting this to true will force the CLI to follow $ErrorActionPreference
. Errors will be thrown, but the error text is currently not visible in the thrown error.
In PowerShell 7.2 the feature is not present. Scripts will just continue on.
In short: We need to handle exceptions ourselves to work around the (current) drawbacks of PowerShell. To do that, exceptions need to be written to stdout (as json objects). However: When errorOutput
is set to stdout, ALL output goes there, including prompts and the login spinner, meaning we’re again tackled by Challenge 1.
Potential solution: We may to change config keys a bit: need an extra config key to just send the error to stdout
and leave the rest in stderr
. Proposal:
Key | Default value | Description |
---|---|---|
errorOutput |
stderr |
Deprecated until the next major version. |
errorMessageOutput |
stderr |
Set to stdout to write error messages to the output stream. |
verboseMessageOutput |
stderr |
Set to stdout to write verbose messages to the output stream. |
👺 Challenge 3: Colors in the error stream are lost
When the CLI command output is piped into a commandlet, the error stream is redirected in some way as well, losing all colorization in the process.
Solution: I’ve not yet found a solution. Maybe we ought just to accept this side-effect. As it’s not really important.
👺 Challenge 4: Clean errors without stacktrace
Read the details
There are downsides to throwing or writing errors in PowerShell. An ugly error with a useless stacktrace will show, because the command is called from a script file (m365.ps1), which is not what we would want. Clean errors without stacktrace seem hard to get.There are some ideas on how to get clean errors though: how to get clean errors without stacktrace
We’d need some research into what would be an optimal solution.
Potential solution: It seems to be possible to write a clean error when $ErrorActionPreference
is set to Continue
. When throwing an error, we’ll always see the stacktrace.
✅ Tasks
About this issue
- Original URL
- State: open
- Created 10 months ago
- Reactions: 6
- Comments: 24 (24 by maintainers)
I’d say, let’s make it opt-in for now and make it the default behavior in
v8
.By the way: I believe we fixed all occurrences of this. It was due to duplicate properties
Id
andID
on listitem endpoints.Nice job with all this research already @martinlingstuyl. We’re definitely missing some integration with our most commonly used shell. Using
| Convertfrom-Json
is something that is very frustrating to use in scripts. It works most of the time but on occasions, it tends to fail but then you’ve to convert it to a hash table object. And let’s not get started on error handling. That’s just a pain.Regarding your suggestion of enabling integration mode. That would be quite a nice feature out-of-the-block but it would indeed be a breaking change. So I’m also all for adding it as an additional option with
m365 setup
and with the next mayor making it default. My vote for the option name would be--usePowerShellIntegration
. For interactive mode, what about> Do you want to use PowerShell Integration mode?
and then clarify this question more in the docs.Understood. Let’s make it opt-in to avoid breaking people who have figured out using CLI without these proposed improvements.
This is something we definitely need to do. However, if we do this, it will be a breaking change, right?