Функция 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() }
}
РЕДАКТИРОВАТЬ: упрощенная функция
3 ответа
Это можно написать в C#, где можно реализовать IDisposable - подтверждено, что он вызывается powershell в случае 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();
}
}
}
К сожалению, нет хорошего решения для этого. Детерминированная очистка, кажется, явное упущение в 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
в основном бесполезен в целях очистки, так как он вызывается только в том случае, если весь конвейер успешно обработан. Это требует нечестивой смеси 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
}
}
Как бы многословно это ни было, это самое короткое "правильное" решение, которое я могу найти. Он проходит через искажения, чтобы гарантировать правильное восстановление состояния Control-C, и мы никогда не пытаемся перехватить исключение (потому что PowerShell плохо отбрасывает их); решение может быть немного проще, если мы не заботимся о таких тонкостях. Я даже не собираюсь пытаться сделать заявление о производительности.:-)
Если у кого-то есть идеи о том, как это улучшить, я весь в ушах. Очевидно, что проверка на Control-C может быть разложена на функцию, но помимо этого, кажется, трудно сделать ее более простой (или, по крайней мере, более читаемой), потому что мы вынуждены использовать begin
/process
/end
плесень.
Ниже приведена реализация "использования" для 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() }
}
Очевидно, что это всего лишь некоторая упаковка вокруг первого ответа Йероена, но может быть полезна для других, кто находит здесь свой путь.