Unfortunately, there is no good solution for this. Deterministic cleanup seems to be a glaring omission in PowerShell. It could be as simple as introducing a new cleanup
block that is always called regardless of how the pipeline ends, but alas, even version 5 seems to offer nothing new here (it introduces classes, but without cleanup mechanics).
That said, there are some not-so-good solutions. Simplest, if you enumerate over the $input
variable rather than use begin
/process
/end
you can use 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() }
}
}
This has the big drawback that your function will hold up the entire pipeline until everything is available (basically the whole function is treated as a big end
block), which is obviously a deal breaker if your function is intended to process lots of input.
The second approach is to stick with begin
/process
/end
and manually process Control-C as input, since this is really the problematic bit. But by no means the only problematic bit, because you also want to handle exceptions in this case -- end
is basically useless for purposes of cleanup, since it is only invoked if the entire pipeline is successfully processed. This requires an unholy mix of trap
, try
/finally
and flags:
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
}
}
Verbose as it is, this is the shortest "correct" solution I can come up with. It does go through contortions to ensure the Control-C status is restored properly and we never attempt to catch an exception (because PowerShell is bad at rethrowing them); the solution could be slightly simpler if we didn't care about such niceties. I'm not even going to try to make a statement about performance. :-)
If someone has ideas on how to improve this, I'm all ears. Obviously checking for Control-C could be factored out to a function, but beyond that it seems hard to make it simpler (or at least more readable) because we're forced to use the begin
/process
/end
mold.
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…