Powershell 函数处置或中止处理程序
Powershell function dispose or abort handler
我有一个管道函数,它在 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() }
}
编辑:简化函数
不幸的是,对此没有好的解决方案。确定性清理似乎是 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 作为输入,因为这确实是有问题的地方。但绝不是 only 有问题的位,因为在这种情况下您还想处理异常 - end
对于清理目的基本上是无用的,因为它仅在以下情况下被调用整个管道已成功处理。这需要 trap
、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.
#>
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
}
}
虽然冗长,但这是我能想到的最短的 "correct" 解决方案。它确实通过扭曲来确保正确恢复 Control-C 状态,并且我们从不尝试捕获异常(因为 PowerShell 不擅长重新抛出它们);如果我们不关心这些细节,解决方案可能会稍微简单一些。我什至不打算尝试对性能发表声明。 :-)
如果有人对如何改进它有想法,我会洗耳恭听。显然,可以将对 Control-C 的检查分解为一个函数,但除此之外,似乎很难使其更简单(或至少更具可读性),因为我们被迫使用 begin
/process
/end
模具.
可以用 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();
}
}
}
以下是 "using" 的 PowerShell 实现(来自 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 $PSHOME\types.ps1xml) {
foreach ($_ in 1..5) { $stream.ReadLine() }
}
显然,这实际上只是围绕 Jeroen 的第一个答案的一些包装,但可能对其他在这里找到出路的人有用。
我有一个管道函数,它在 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() }
}
编辑:简化函数
不幸的是,对此没有好的解决方案。确定性清理似乎是 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 作为输入,因为这确实是有问题的地方。但绝不是 only 有问题的位,因为在这种情况下您还想处理异常 - end
对于清理目的基本上是无用的,因为它仅在以下情况下被调用整个管道已成功处理。这需要 trap
、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.
#>
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
}
}
虽然冗长,但这是我能想到的最短的 "correct" 解决方案。它确实通过扭曲来确保正确恢复 Control-C 状态,并且我们从不尝试捕获异常(因为 PowerShell 不擅长重新抛出它们);如果我们不关心这些细节,解决方案可能会稍微简单一些。我什至不打算尝试对性能发表声明。 :-)
如果有人对如何改进它有想法,我会洗耳恭听。显然,可以将对 Control-C 的检查分解为一个函数,但除此之外,似乎很难使其更简单(或至少更具可读性),因为我们被迫使用 begin
/process
/end
模具.
可以用 C# 编写它,其中可以实现 IDisposable - 确认在 ctrl
-c
.
如果有人在 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();
}
}
}
以下是 "using" 的 PowerShell 实现(来自 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 $PSHOME\types.ps1xml) {
foreach ($_ in 1..5) { $stream.ReadLine() }
}
显然,这实际上只是围绕 Jeroen 的第一个答案的一些包装,但可能对其他在这里找到出路的人有用。