如何向调用shell/进程最好地指示PowerShell模块的故障



如果我有一个PowerShell模块作为可执行程序的包装器,并且我想将失败通知父进程,以便可以通过编程方式检查它,我该如何最好地做到这一点?

调用模块的可能方式(非详尽):

  1. 从PowerShell shell(PowerShell或pwsh)中
  2. 从命令提示符(例如powershell -Command <module fn>)
  3. 来自创建PowerShell进程的外部程序(例如,通过调用powershell -Command <module fn>))

如果我在可执行文件失败时从模块抛出异常,比如

if ($LastExitCode -gt 0) { throw $LastExitCode; }

它似乎涵盖了所有的要求。如果抛出异常并且模块被称为

  1. 在PowerShell shell中,$?变量设置为False
  2. 在命令提示符下,%errorlevel%变量设置为1
  3. 从外部进程将exit代码设置为1

因此,父进程可以根据模块的调用方式检查故障。

这种方法的一个小缺点是,所有退出代码都无法与父进程通信(它在$?中返回True/False,或者在exit代码中返回0/1),但更令人恼火的是,输出上显示的异常消息对某些人来说太冗长了(它能被抑制吗?):

+     if ($LastExitCode -gt 0) { throw $LastExitCode; }
+                                ~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : OperationStopped: (9:Int32) [], RuntimeException
+ FullyQualifiedErrorId : 9

有什么更好的方法可以将从PowerShell模块调用的可执行文件的失败传达给父进程吗?

感谢

您最好为模块的函数提供包装.ps1脚本文件,然后您可以通过PowerShell CLI的-File参数调用该文件:

# yourWrapperScript.ps1
# Default to 'yourFunction' as the function to invoke,
# but allow passing a different command name, optionally with 
# arguments, too.
# From inside PowerShell, you could even pass a script block.
param(
$funcName = 'yourFunction'
)
# Make all commands executed in this script *abort* on emitting *any
# error*.
# Note that this aborts on the *first* PowerShell error reported.
# (A function could be emitting multiple, non-terminating errors.)
# The assumption is that your function itself checks $LASTEXITCODE 
# after calling the wrapped external program and issues a PowerShell error in response.
$ErrorActionPreference = 'Stop'
$global:LASTEXITCODE = 0 # Reset the $LASTEXITCODE value.
try {
# Call the function, passing any additional arguments through.
& $funcName @args
# If no PowerShell error was reported, assume that the
# function succeeded.
exit 0
}
catch {
# Emit the message associated with the PowerShell error that occurred.
# Note: 
#  * In *PowerShell (Core) 7+*, emitting the error message
#    via Write-Error *is* a one-liner, but (a) 
#    invariably prefixed with the function name and (b)
#    printed in *red. If that's acceptable, you can use
#      $_ | Write-Error
#  * In *Windows PowerShell*, the error is "noisy", and the only
#    way to avoid that is to write directly to stderr, as shown
#    below. 
# Print the error message directly to stderr.
[Console]::Error.WriteLine($_)
if ($LASTEXITCODE) {
# The error is assumed to have been reported in response
# to the external-program call reporting a nonzero exit code.
# Use that exit code.
# Note: if that assumption is too broad, you'll ned to examine
#       the details of the [System.Management.Automation.ErrorRecord] instance reflected in $_.
exit $LASTEXITCODE
} else {
# An error unrelated to the external-program call.
# Report a nonzero exit code of your choice. 
exit 1
}
}

然后将包装器脚本(比如yourWrapperScript.ps1)传递给CLI的-File参数:

powershell -File yourWrapperScript.ps1

-CommandCLI参数不同,如果通过exit设置,-File确实传递.ps1脚本文件的特定退出代码

这种方法的缺点是:

  • yourWrapperScript.ps1必须在当前目录中,在$env:PATH环境变量中列出的目录中,或者您必须通过其完整路径引用它

    • 如果您将脚本与模块捆绑在一起(只需将其放置在模块目录中),则只有当您知道它在标准模块安装目录之一中时(如$env:PSModulePath中所列,尽管在Unix上,环境变量仅存在于PowerShell会话内)

    • 另一种选择是将脚本作为一个单独的、可安装的脚本分发,该脚本可以与Install-Script一起安装。

  • 除非准备将函数名作为参数(例如powershell -File yourWrapperScript.ps1 yourFunction)传递,否则每个函数都需要一个单独的.ps1包装器。


繁琐的替代方案是使用-Command并将上面的代码作为一行代码传递,并封装在"..."中(来自PowerShell外部):

# Code truncated for brevity.
powershell -Command "param(..."

有关PowerShell CLI的全面概述,请参阅这篇文章。


如果为了通过-Command只调用一个函数,您愿意使用:

  • 外部程序报告的特定退出代码的丢失,并将任何非零代码映射到1

  • a";"嘈杂";,多行Windows PowerShell错误消息(在PowerShell(Core)7+中问题较少,其中消息打印为单行,尽管总是用红色表示,前缀为函数名)

您有两个选项:

  • 坚持原来的方法,在函数中使用throw,以响应外部程序调用后$LASTEXITCODE非零。这会导致脚本终止(致命)错误。

    • 这意味着PowerShell CLI进程立即中止,退出代码为1。类似地,如果您的函数也是从PowerShell脚本调用的,整个脚本(及其调用方)将立即中止-这可能是所需的,也可能不是所需的。如果您希望避免此类错误,请参阅下一点。

    • 另外请注意,通过二进制模块实现的cmdlet(与PowerShell代码中实现的类似cmdlet的高级函数相反)不会,而且事实上,不能发出这样的脚本-终止错误,只有语句–终止错误。

  • 假设函数调用是onlylast语句,则将函数的$?设置为$false,而不中断整体执行,-CommandCLI参数也会将其转换为退出代码1

    • 这只能通过从函数中发出一个或多个非终止错误或终止错误的语句来隐式完成。

    • 为了让它们从PowerShell中编写的函数中正确设置$?,您的函数(a)必须是高级函数,(b)必须使用$PSCmdlet.WriteError()(非终止错误)或$PSCmdlet.ThrowTerminatingError()(语句终止错误);值得注意的是,使用Write-Error不起作用。

    • 不幸的是,调用这些方法并不重要;zett42在一个(不幸的)被删除的答案中展示了这项技术;你也可以在GitHub issue#的评论中找到一个例子(该问题还包含有关PowerShell错误类型的其他背景信息)。


有关PowerShell令人困惑的复杂错误处理的概述,请参阅GitHub文档第1583期。

PowerShellexit关键字有一个可选参数。如果该参数是一个整数,则将其用作进程退出代码。通过这种方式,您可以传播包装的可执行文件的错误代码。

Python捕获PowerShell退出代码的示例:

:~> py
Python 3.7.9 [...]
>>> from subprocess import run
>>> res = run('powershell -command "exit 123"')
>>> res.returncode
123

最新更新