如何实现Invoke-SilentlyAndReturnExitCode作为Powershell模块功能?

时间:2019-04-22 22:39:05

标签: powershell error-handling module scope

请注意:

方法

PS C:\> (Get-Command Invoke-SilentlyAndReturnExitCode).ScriptBlock
param([scriptblock]$Command, $Folder)

    $ErrorActionPreference = 'Continue'
    Push-Location $Folder
    try
    {
        & $Command > $null 2>&1
        $LASTEXITCODE
    }
    catch
    {
        -1
    }
    finally
    {
        Pop-Location
    }

PS C:\>

静音命令

PS C:\> $ErrorActionPreference = "Stop"
PS C:\> $Command = { cmd /c dir xo-xo-xo }
PS C:\> & $Command > $null 2>&1
cmd : File Not Found
At line:1 char:14
+ $Command = { cmd /c dir xo-xo-xo }
+              ~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (File Not Found:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

PS C:\>    

如您所见,它失败并带有异常。但是我们可以轻松地将其静音,对吧?

PS C:\> $ErrorActionPreference = 'SilentlyContinue'
PS C:\> & $Command > $null 2>&1
PS C:\> $LASTEXITCODE
1
PS C:\>

一切都很好。现在,我的功能执行相同操作,因此让我们尝试一下:

PS C:\> $ErrorActionPreference = "Stop"
PS C:\> Invoke-SilentlyAndReturnExitCode $Command
-1
PS C:\>

赞!它返回-1,而不是1。

问题似乎是该函数内的$ErrorActionPreference设置实际上并未传播到命令范围。确实,让我添加一些输出:

PS C:\> (Get-Command Invoke-SilentlyAndReturnExitCode).ScriptBlock
param([scriptblock]$Command, $Folder)

    $ErrorActionPreference = 'Continue'
    Push-Location $Folder
    try
    {
        Write-Host $ErrorActionPreference
        & $Command > $null 2>&1
        $LASTEXITCODE
    }
    catch
    {
        -1
    }
    finally
    {
        Pop-Location
    }

PS C:\> $Command = { Write-Host $ErrorActionPreference ; cmd /c dir xo-xo-xo }
PS C:\> Invoke-SilentlyAndReturnExitCode $Command
Continue
Stop
-1
PS C:\>

因此,问题实际上出在$ErrorActionPreference上-为什么它不传播? Powershell使用动态作用域,因此命令定义不应捕获其值,而应使用函数中的值。那么发生了什么?如何解决?

1 个答案:

答案 0 :(得分:1)

tl; dr

由于您的Invoke-SilentlyAndReturnExitCode函数是在模块中定义的,因此必须在该模块范围内重新创建脚本块,才能看到该模块- $ErrorActionPreference的本地Continue值:

# Use an in-memory module to demonstrate the behavior.
$null = New-Module {
    Function Invoke-SilentlyAndReturnExitCode {
    param([scriptblock] $Command, $Folder)

      $ErrorActionPreference = 'Continue'
      Push-Location $Folder
      try
      {
          Write-Host $ErrorActionPreference # local value

          # *Recreate the script block in the scope of this module*,
          # which makes it see the module's variables.
          $Command = [scriptblock]::Create($Command.ToString())

          # Invoke the recreated script block, suppressing all output.
          & $Command  *>$null

          # Output the exit code.
          $LASTEXITCODE
      }
      catch
      {
          -1
      }
      finally
      {
          Pop-Location
      }
    }
}

$ErrorActionPreference = 'Stop'
$Command = { Out-Host -InputObject $ErrorActionPreference; cmd /c dir xo-xo-xo }
Invoke-SilentlyAndReturnExitCode $Command

在Windows上,以上内容现在可以按预期打印以下内容:

Continue
Continue
1

也就是说,重新创建的$Command脚本块看到了本地函数$ErrorActionPreference的值,并且catch块未 触发。

注意事项

  • 仅当$Command脚本块不包含对原始作用域中的变量的引用(除了 global 作用域中的变量之外),此才起作用。

  • 替代 避免此限制的目的是定义模块外部的功能 (假设您还从模块外部的代码中调用它。)


背景信息

此行为表示您的Invoke-SilentlyAndReturnExitCode函数是在模块 中定义的,并且每个模块都有自己的范围域(范围层次)。 / p>

您的$Command脚本块,因为它是在模块的外部中定义的,因此绑定到 default 范围域,甚至是从模块内部执行时也是如此,它将继续查看来自定义它的作用域 的变量。

因此,$Command仍会看到Stop $ErrorActionPreference的值,即使函数中模块源代码的 Continue ,原因是在模块函数内设置了$ErrorActionPreference的本地副本。

令人惊讶的是,控制行为的仍然是$ErrorActionPreference内的$Command,而不是函数局部值。

对于2>$null的重定向,例如*>$null有效,而Stop是有效的$ErrorActionPreference值,则仅存在来自外部程序的stderr输出-是否表示真正的错误不是-触发终止错误,因此触发catch分支。

这种特殊行为-明确抑制 stderr输出的意图触发了错误-应该被视为 bug ,并已在this GitHub issue中报告。

一般行为-在定义范围内执行的脚本块-不明显,但在设计上


注意:此答案的其余部分是其原始形式,其中包含一般背景信息,但是并不涵盖上述模块方面。


  • *> $null可用于使命令的所有输出静默-无需抑制成功输出流(>,隐含1>)和错误输出流( 2>)。

  • 通常,$ErrorActionPreference外部程序(例如git)的错误输出没有影响,因为 stderr 的输出是外部程序默认情况下绕过 PowerShell的错误流。

    • 但是有一个例外:将$ErrorActionPreference设置为'Stop'实际上会使诸如2>&1*>$null之类的重定向引发终止错误,如果外部程序因为git会产生任何stderr输出。
      this GitHub issue中讨论了这种意外行为。

    • 否则,对外部程序的调用永远不会触发try / catch语句将处理的终止错误。只能通过自动$LASTEXITCODE变量来推断成功或失败。

因此,如果在模块外部定义(并调用)函数,则编写如下的函数

function Invoke-SilentlyAndReturnExitCode {
  param([scriptblock]$Command, $Folder)

  # Set a local copy of $ErrorActionPreference,
  # which will go out of scope on exiting this function.
  # For *> $null to effectively suppress stderr output from 
  # external programs *without triggering a terminating error*
  # any value other than 'Stop' will do.
  $ErrorActionPreference = 'Continue'

  Push-Location $Folder

  try {
    # Invoke the script block and suppress all of its output.
    # Note that if the script block calls an *external program*, the
    # catch handler will never get triggered - unless the external program
    # cannot be found.
    & $Command *> $null
    $LASTEXITCODE
  }
  catch {
    # Output the exit code used by POSIX-like shells such
    # as Bash to signal that an executable could not be found.
    127
  } finally {
    Pop-Location
  }
}