Как реализовать Invoke-SilentlyAndReturnExitCode в качестве функции модуля Powershell? - PullRequest
1 голос
/ 23 апреля 2019

Пожалуйста, соблюдайте:

Метод

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:\>

Yikes!Возвращает -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 Ответ

1 голос
/ 23 апреля 2019

ТЛ; др

Поскольку ваша 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 scope.

  • альтернатива - обойти это ограничение - определить функцию вне модуля (при условии, что вы также вызывая его из кода, который находится вне модулей).


Справочная информация

Поведение подразумевает, что ваша Invoke-SilentlyAndReturnExitCode функция определена в модуле , и каждый модуль имеет свой собственный домен областей (иерархию областей).

Ваш $Command блок сценария, поскольку он был определен вне этого модуля, привязан к области действия default и даже при выполнении изнутри модуля он продолжает видеть переменные из области действия , в которой она была определена .

Следовательно, $Command по-прежнему видит значение Stop $ErrorActionPreference, хотя для кода, создаваемого модулем внутри функции, это будет Continue, из-за установки локальной копии $ErrorActionPreference внутри функции модуля.

Возможно, что удивительно, все еще $ErrorActionPreference в действии внутри $Command управляет поведением, а не значением локальной функции.

С таким перенаправлением, как 2>$null для *>$null, в то время как Stop является эффективным значением $ErrorActionPreference, простое присутствие вывода stderr из внешней программы - указывает ли это на истинную ошибку не - запускает завершающая ошибка и, следовательно, ветвь catch.

Это определенное поведение - когда явное намерение подавить вывод stderr вызывает ошибку - следует рассматривать как ошибку , и в сообщается об этой проблеме GitHub .

Общее поведение , однако - блок сценария, выполняющийся в области действия, в которой он был определен, - хотя и неочевидный, - по проекту .


Примечание : Остальная часть этого ответа является его первоначальной формой, которая содержит общую справочную информацию, которая, однако, не охватывает аспект модуля, рассмотренный выше.


  • *> $null можно использовать для отключения всех выходных данных команды - нет необходимости отдельно подавлять поток вывода успеха (>, подразумевается 1>) и поток вывода ошибок (2>).

  • Как правило, $ErrorActionPreference не влияет на вывод ошибок из внешних программ (например, git), поскольку stderr вывод из внешних программ обход Поток ошибок PowerShell по умолчанию.

    • Однако есть исключение: установка $ErrorActionPreference в 'Stop' фактически делает перенаправления, такие как 2>&1 и *>$null, выдачей завершающей ошибки, если внешняя программа, такая как git, производит какой-либо вывод stderr .
      Это неожиданное поведение обсуждается в этой проблеме GitHub .

    • В противном случае вызов внешней программы никогда не вызовет завершающую ошибку, которую обработает оператор 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
  }
}

...