Powershell函数处置或中止处理程序



我有一个管道函数,它在begin块中分配一些需要在最后处理的资源。我曾尝试在end块中执行此操作,但当函数执行中止时(例如ctrl+c),它不会被调用。

我该如何修改以下代码以确保$sw始终被处理:

function Out-UnixFile([string] $Path, [switch] $Append) {
    <#
    .SYNOPSIS
    Sends output to a file encoded with UTF-8 without BOM with Unix line endings.
    #>
    begin {
        $encoding = new-object System.Text.UTF8Encoding($false)
        $sw = new-object System.IO.StreamWriter($Path, $Append, $encoding)
        $sw.NewLine = "`n"
    }
    process { $sw.WriteLine($_) }
    # FIXME not called on Ctrl+C
    end { $sw.Close() }
}

EDIT:简化函数

不幸的是,没有很好的解决方案。确定性清理似乎是PowerShell中的一个明显遗漏。它可以简单到引入一个新的cleanup块,无论管道如何结束,它都会被调用,但遗憾的是,即使是版本5似乎也没有提供任何新功能(它引入了类,但没有清理机制)。

也就是说,还有一些不太好的解决方案。最简单的是,如果在$input变量上枚举而不是使用begin/process/end,则可以使用try/finally:

function Out-UnixFile([string] $Path, [switch] $Append) {
    <#
    .SYNOPSIS
    Sends output to a file encoded with UTF-8 without BOM with Unix line endings.
    #>
    $encoding = new-object System.Text.UTF8Encoding($false)
    $sw = $null
    try {
        $sw = new-object System.IO.StreamWriter($Path, $Append, $encoding)
        $sw.NewLine = "`n"
        foreach ($line in $input) {
            $sw.WriteLine($line)
        }
    } finally {
        if ($sw) { $sw.Close() }
    }
}

这有一个很大的缺点,即您的函数将阻塞整个管道,直到所有东西都可用(基本上整个函数被视为一个大的end块),如果您的函数要处理大量输入,这显然是一个破坏交易的因素。

第二种方法是坚持使用begin/process/end,并手动处理Control-C作为输入,因为这确实是个问题。但决不是只有的问题位,因为在这种情况下您还想处理异常——end对于清理来说基本上是无用的,因为只有在整个管道成功处理时才会调用它。这需要traptry/finally和标志的邪恶混合:

function Out-UnixFile([string] $Path, [switch] $Append) {
  <#
  .SYNOPSIS
  Sends output to a file encoded with UTF-8 without BOM with Unix line endings.
  #>
  begin {
    $old_treatcontrolcasinput = [console]::TreatControlCAsInput
    [console]::TreatControlCAsInput = $true
    $encoding = new-object System.Text.UTF8Encoding($false)
    $sw = new-object System.IO.StreamWriter($Path, $Append, $encoding)
    $sw.NewLine = "`n"
    $end = {
      [console]::TreatControlCAsInput = $old_treatcontrolcasinput
      $sw.Close()
    }
  }
  process {
    trap {
      &$end
      break
    }
    try {
      if ($break) { break }
      $sw.WriteLine($_)
    } finally {
      if ([console]::KeyAvailable) {
        $key = [console]::ReadKey($true)
        if (
          $key.Modifiers -band [consolemodifiers]"control" -and 
          $key.key -eq "c"
        ) { 
          $break = $true
        }
      }
    }
  }
  end {
    &$end
  }
}

虽然很详细,但这是我能想出的最短的"正确"解决方案。它确实经过了扭曲,以确保Control-C状态正确恢复,并且我们从不试图捕获异常(因为PowerShell不善于重新引发异常);如果我们不关心这些细节,解决方案可能会稍微简单一些。我甚至不会试图就表现发表声明。:-)

如果有人对如何改进这一点有想法,我会洗耳恭听。显然,检查Control-C可以被分解为一个函数,但除此之外,似乎很难使其更简单(或至少更可读),因为我们被迫使用begin/process/end模型。

可以在C#中编写它,在C#中可以实现IDisposable-确认在ctrl-c的情况下由powershell调用。

我将保留这个问题,以防有人在powershell中想出一些方法。

using System;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Text;
namespace MarcWi.PowerShell
{
    [Cmdlet(VerbsData.Out, "UnixFile")]
    public class OutUnixFileCommand : PSCmdlet, IDisposable
    {
        [Parameter(Mandatory = true, Position = 0)]
        public string FileName { get; set; }
        [Parameter(ValueFromPipeline = true)]
        public PSObject InputObject { get; set; }
        [Parameter]
        public SwitchParameter Append { get; set; }
        public OutUnixFileCommand()
        {
            InputObject = AutomationNull.Value;
        }
        public void Dispose()
        {
            if (sw != null)
            {
                sw.Close();
                sw = null;
            }
        }
        private StreamWriter sw;
        protected override void BeginProcessing()
        {
            base.BeginProcessing();
            var encoding = new UTF8Encoding(false);
            sw = new StreamWriter(FileName, Append, encoding);
            sw.NewLine = "n";
        }
        protected override void ProcessRecord()
        {
            sw.WriteLine(InputObject);
        }
        protected override void EndProcessing()
        {
            base.EndProcessing();
            Dispose();
        }
    }
}

以下是PowerShell的"using"实现(来自Solutionizing.Net)。using是PowerShell中的保留字,因此别名为PSUsing:

function Using-Object {
    param (
        [Parameter(Mandatory = $true)]
        [Object]
        $inputObject = $(throw "The parameter -inputObject is required."),
        [Parameter(Mandatory = $true)]
        [ScriptBlock]
        $scriptBlock
    )
    if ($inputObject -is [string]) {
        if (Test-Path $inputObject) {
            [system.reflection.assembly]::LoadFrom($inputObject)
        } elseif($null -ne (
              new-object System.Reflection.AssemblyName($inputObject)
              ).GetPublicKeyToken()) {
            [system.reflection.assembly]::Load($inputObject)
        } else {
            [system.reflection.assembly]::LoadWithPartialName($inputObject)
        }
    } elseif ($inputObject -is [System.IDisposable] -and $scriptBlock -ne $null) {
        Try {
            &$scriptBlock
        } Finally {
            if ($inputObject -ne $null) {
                $inputObject.Dispose()
            }
            Get-Variable -scope script |
                Where-Object {
                    [object]::ReferenceEquals($_.Value.PSBase, $inputObject.PSBase)
                } |
                Foreach-Object {
                    Remove-Variable $_.Name -scope script
                }
        }
    } else {
        $inputObject
    }
}
New-Alias -Name PSUsing -Value Using-Object

示例用法:

psusing ($stream = new-object System.IO.StreamReader $PSHOMEtypes.ps1xml) {             
    foreach ($_ in 1..5) { $stream.ReadLine() }             
}

显然,这只是Jeroen第一个答案的一些包装,但可能对其他找到答案的人有用。

最新更新