Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial RFC Native Command Exit Error #88

Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions 1-Draft/RFCNNNN-RFC-Native-Command-Exit-Errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
---
RFC: RFCNNNN-RFC-Native-Command-Exit-Errors
Author: Micheal Padden
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mopadden is this the correct spelling of your name? If you'd prefer to use your GitHub moniker or similar, that might be acceptable too

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's me Micheal Padden. Is it preferrable to use the name or the github moniker?

Status: Draft
Area: Engine, Language
Version: 1.0.0
---

# Native Command Error Handling

Although Powershell has an exception-based error handling framework, it
does not currently apply to native commands.

Exception-based error handling makes it easier to write robust code
as less boilerplate code is needed to check and handle errors.

Powershell scripts using native commands would benefit from being able
to use error handling features like those used by cmdlets.

## Motivation

Native commands return an exit code to the calling application which
will be zero for success or non-zero for failure. A robust script will
not assume success, instead checking the exit code after any statement
calling a native command, using boilerplate like the following:
```Powershell
if( ! $? )
{
exit $lastexitcode;
}
```

The Bourne again shell provides a `set -eo pipefail` exit code handling mode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding of -o pipefail is that it does two things:

  1. stop in the middle of a pipeline if any native exe has non-zero exit
  2. preserve exit code so subsequent commands with zero exit code doesn't clobber it

Current PowerShell behavior has subsequent native commands clobber $LastExitCode today. So I would suggest we don't make any change here. We are just adopting the first behavior above which is if any native exe has non-zero exit and $PSNativeCommandError = Stop, then it terminates right away.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SteveL-MSFT: set -o pipefail doesn't impact the control flow: the pipeline completes as it normally would; the only difference is that the first command participating in the pipeline that reports a non-zero exit code, if any, determines the pipeline's overall exit code; otherwise, it is the last command (which otherwise alone determines the exit code).

The only way for a pipeline in a POSIX-like shell to terminate prematurely is for a participating command to stop reading from the pipeline prematurely, as head does, for instance (by design, and it therefore itself reports exit code 0).

Otherwise, the pipeline runs to completion, and the -e option, if set, then acts on whatever the pipeline's overall exit code is.

that acts as though this boilerplate were included in certain contexts.
Like exception-based error handling, this can exit a stack of functions,
scripts and shells. This allows robust scripts to be written with a minimal
amount of boilerplate.

To support a similar style of error handling with native commands, this
RFC proposes converting native command errors into PowerShell errors.
The Bourne shell compatible `set -e` mode (without `set -o pipefail`) is
_not_ supported.

The specification and alternative proposals are based on the
[Equivalent of bash `set -e` #3415](https://github.com/PowerShell/PowerShell/issues/3415)
committee review of the associated
[pull request](https://github.com/PowerShell/PowerShell/pull/3523), and
[implementation plan](https://github.com/PowerShell/PowerShell-RFC/pull/88#issuecomment-613653678)

## Specification

This RFC proposes including native commands in the error handling
framework when the feature is enabled by adding an error to the error
stream if the exit status of a native command is false.

The `$PSNativeCommandErrorAction` preference variable will implement a version
of the `$ErrorActionPreference` variable for native commands. The value will
default to `Ignore` for compatibility with existing behaviour.
- For all values except `Ignore`, an `ErrorRecord` will be added to `$Error` that wraps
the exit code and the command executed that returned the exit code.
- Initially, only the existing values of `$ErrorActionPreference` will be supported.
- The set of values may be extended later to include `MatchErrorActionPreference`,
which should apply the `$ErrorActionPreference` setting to native commands also.
- An enum converter will convert between `$PSNativeCommandError` and
`$ErrorActionPreference` values, where `MatchErrorActionPreference` is converted to the
current value of `$ErrorActionPreference`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea in general, but it seems a "point solution", while what is really need is an "overall solution" to deal with error-handling in PowerShell.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify? Do you refer to other proposals like #187 ?

The reported error record should be created with the following details:
- exception: `ExitCode`, with the exit code of the failed command.
- error id: `"Program {0} failed with unhandled exit code {1}"`, with the command
name and the exit code, from resource string `ProgramFailedToComplete`.
- error category: `ErrorCategory.NotSpecified`.
- object: the (boxed) exit code.

This does not provide the actual semantics of bash `set -eo pipefail` as Bourne shell
style integration with existing existing status handling syntax is not implemented.
As a result, PowerShell script logic for handling native command exit codes would need
to use either `try`..`catch` or existing exit status handling language constructs,
according to the setting of this preference variable.

One way of overriding`$ErrorActionPreference` for a single native command and handling
its exit status explicitly would be to put this logic into a script block and call it
with the invocation operator (`&`).

## Alternative Approaches and Considerations

A number of tweaks to existing behaviours could be combined to give equivalent
functionality to bash `set -eo pipefail`

### Add "strict" native command option.

The `$PSStrictNativeCommand` preference should treat creation of an `ErrorRecord`
for native commands in the same way as this is treated elsewhere.
Possible values are:
- `$false`: (the default) ignore non-zero exit codes.
This is the same as existing Powershell treatment of this case.
- `$true`: Populate the error stream of the native command with an `ErrorRecord`
associated with an `ExitException` exception.

### Modifying existing semantics to consider exit code and exit status

The error will throw an exception, potentially terminating the session, in the same
situation as other non-terminating errors will do this, i.e. where
`$ErrorActionPreference` is set to `"stop"`. This would not be the desired behaviour on
commands where the script already handles a non-zero exit code, which would require
the addition of extra boilerplate to use multiple native commands in combination.
There are a number of ways that this could be made more flexible by integrating exit
code and exit status handling into the language syntax.

#### Convert non-terminating errors to terminating where the command output is used.

This approach implements semantics equivalent to bash `set -eo pipefail`
in the runtime layer.

The `$PSStrictPipeLine` preference variable would govern promotion of a non-terminating
error to a terminating error on getting an object from the pipeline output stream.
Possible values would be:
- `$false`: (the default) an object can be collected from the pipeline output stream
regardless of the command exit value.
This is the same as existing PowerShell treatment of this case.
- `$true`: where the exit status of a native command is `$false`, trying to get an
object from its output stream will create a terminating error from the non-terminating
errors in its error stream.
Conversion to boolean would be structured to ensure that this returns `$false` if the
output pipeline does not contain anything without trying to get an actual value from it.

This would allow syntax like `if`, `while` and pipeline chain operators to be usefully
combined with native commands.

#### Sanitise semantics of treating a native command as a conditional value.

This option in combination with the above enables functionality analogous to
bash `set -eo pipefail`

PowerShell converts the output stream to a boolean value where a native command is used
as a "condition", i.e. the `-not` operator, or an `if`, `elseif` or `while` statement.
This is not particularly useful with native commands, which would tend to produce no
output on success, at least when executed in batch as opposed to interactive mode.

The `$PSUseNativeExitStatus` variable would govern whether exit status is used
in determining the boolean value of a native command for a conditional context.
Possible values would be:
- `$false`: (the default) the boolean value of native command is defined as whether
or not the length of the output stream is non-zero.
This is the same as existing PowerShell treatment of this case.
- `$true`: the boolean value of a native command is it's exit status (`$?`), and

This would allow syntax like `if` and `while` to be usefully combined with
native commands.

#### Add strict pipeline chain failure semantics.

Treat only ignored exit statuses as exceptions.

The `$PSStrictPipeLineChain` preference variable would govern the exit
status in the last command of a pipeline.
Possible values would be:
- `$false`: (the default) would ignore `$false` exit status on the last command.
This is the same as existing PowerShell treatment of this case.
- `$true`: for a pipeline that is being used in a conditional context (see above),
and where the exist status of the last command in the pipeline is `$false`,
would create a terminating error from the non-terminating errors in the command error
stream.

This should improve on the basic specification by allowing the idiom to be usefully
combined with pipeline chaining.

### Use dynamic scope/Set-StrictMode
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While using Version for OS checks does seem wrong, I would love a single command to cover all these:

Set-StrictMode -Version 2
$PSIncludeNativeCommandInErrorActionPreference = $true
$ErrorActionPreference = 'Stop'

Currently I already put 2 of them on top of every script I write.

While having to put 3 of them is still better than the boilerplate checks, I would love something shorter, e.g.

Set-StrictMode -Version 2 -StopOnAllErrors

Copy link

@mpadden mpadden May 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wouldn't be possible to do exactly that, but perhaps something like:
Set-StrictMode -Version 3 -StopOnAllErrors
So Implementing error handling for native errors would be treated as a strictness level.
The -StopOnAllErrors would be equivalent to saying $ErrorActionPreference="stop"

The idea being to keep the connection of native errors to the error handling system separate to the configuration of what action should be taken when an error occurs.

Copy link

@mpadden mpadden May 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a view on wether strictmode versions should be seen strictly (excuse the pun) as governing "language" strictness?
Versions exist for backward compatibility, where a result from "incorrect" code might be better than no result, pending corrections to the code. By that measure, enabling error detection on code that previously gave "successful" but unreliable results seems quite well aligned with strictness versions?
Or would would your case be better described with something like:
Set-StrictMode -Version 2 -NativeErrorVersion 1 -StopOnAllErrors

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure -- it does feel language-related to me, but I think it's clear either way. Might be worth asking people who are currently using Set-StrictMode, but not using $ErrorActionPreference, though I don't know of any.

My goal is to have something very short, e.g. Set-StrictMode -Version 2 -StopOnAllErrors (with All meaning native and non-native) or Set-StrictMode -Version 3. People who need some special case can use $PSIncludeNativeCommandInErrorActionPreference directly, but I think a common case of maximum strictness should be short and easy.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I don't know what the numbers are either way. I would always set strictmode as it's about error detection and 'strict' correctness, but control $ErrorActionPreference separately so I can adjust error handling for testing etc., but I think I see why you something like "throw on all errors" might be useful.


This approach implements dynamic scoping for native command error management using a
cmdlet.

Some of the above would be enabled with `Set-StrictMode -version 6`
instead of with boolean preference variables.

The dynamic scoping approach would improve on earlier listed approaches
by limiting the scope of error handling configuration so that script
functions could not have an effect on the error handling mode in the
calling scope.

If `Set-StrictMode` is used for this, it would need to enable some combination
of enhancements that will not raise errors on scripts that have already implemented
strong native command error handling.


### Use lexical scope/Exception handling extensions

Dynamic scoping approach would have side-effects on called scripts.
This is analogous to the behaviour of Bourne type shells, which maintain
this behaviour for [historic compatibility](http://austingroupbugs.net/view.php?id=52)
reasons.

Lexical scoping of native command error handling would improve on
earlier options by integrating native command error handling fully into
the existing exception handling without out any side-effects in calling
or called scripts.

With this approach, native command error handling mode would be used through
language syntax instead of preference variables or cmdlets. A possible syntax might be
to add a strictness option to the try statement which applies to the lexical scope of
the try statement.

Implementing this functionality only as a lexically scoped extension could
be the preferred option because it becomes a kind of syntactic sugar which is
implemented by the parser and compiler, improving testability, and keeping complexity
out of the runtime layer.