For the specific use case given, Santiago Squarzon's helpful answer is indeed the best solution: using the static methods of the static System.IO.File
class obviates the need for instances representing files that require calling a .Close()
method or explicit disposing of.
To read lazily and therefore support overlapping reading and writing, line by line, you can use the static [System.IO.File]::ReadLines()
and [System.IO.File]::WriteAllLines()
methods, but note that this approach (a) invariably uses platform-native [Environment]::NewLine
-format newlines in the output file, irrespective of what newline format the input file uses, and (b) invariably adds a trailing newline in this format, even if the input file had no trailing newline.
Overcoming these limitations would require use of a lower-level, raw-byte API, System.IO.FileStream
- which again requires explicit disposal (see bottom section).
Given that your approach reads the entire input file into memory first and then writes, you could even make do with PowerShell cmdlets, assuming you're running PowerShell (Core) 7+, which writes BOM-less UTF-8 files by default, and whose -Encoding
parameter accepts any supported encoding, such as ISO-8859-1 in your case:
# PowerShell (Core) 7+ only
Get-Content -Raw -Encoding iso-8859-1 C:\TEMP\a.csv |
Set-Content -NoNewLine C:\TEMP\b.csv
As for your general question:
As of PowerShell (Core) 7.2.1:
PowerShell has no construct equivalent to C#'s using
statement that allows automatic disposing of objects whose type implements the System.IDisposable
interface (which, in the case of file I/O APIs, implicitly closes the files).
GitHub issue #9886 discusses adding such a statement, but the discussion suggests that it likely won't be implemented.
Note: While PowerShell does have a family of statements starting with keyword using
, they serve different purposes - see the conceptual about_Using help topic.
A future PowerShell version will support a clean { ... }
(or cleanup { ... }
) block that is automatically called when an advanced function or script terminates, which allows performing any necessary function-script-level cleanup (disposing of objects) - see RFC #294.
It is up to each type implementing the IDisposable
interface whether it calls the .Dispose()
methods from the finalizer. Only if so is an object automatically disposed of eventually, by the garbage collector.
For System.IO.StreamWriter
and also the lower-level System.IO.FileStream
class, this appears not to be the case, so in PowerShell you must call .Close()
or .Dispose()
explicitly, which is best done from the finally
block of a try
/ catch
/ finally
statement.
You can cut down on the ceremony somewhat by combining the aspects of object construction and variable assignment, but a robust idiom still requires a lot of ceremony:
$x = $y = $null
try {
($y = [System.IO.StreamWriter]::new('C:\TEMP\b.csv', [System.Text.Encoding]::UTF8)).
Write(
($x = [System.IO.StreamReader]::new('C:\Temp\a.csv', [System.Text.Encoding]::GetEncoding('iso-8859-1'))).
ReadToEnd()
)
} finally {
if ($x) { $x.Dispose() }
if ($y) { $y.Dispose() }
}
A helper function, Use-Object
(source code below) can alleviate this a bit:
Use-Object
([System.IO.StreamReader]::new('C:\Temp\a.csv',[System.Text.Encoding]::GetEncoding('iso-8859-1'))),
([System.IO.StreamWriter]::new('C:\TEMP\b.csv', [System.Text.Encoding]::UTF8)) `
{ $_[1].Write($_[0].ReadToEnd()) }
Note how the disable objects passed as the first argument are referenced via $_
as an array in the script-block argument (as usual you may use $PSItem
in lieu of $_
).
A more readable alternative:
Use-Object
([System.IO.StreamReader]::new('C:\Temp\a.csv',[System.Text.Encoding]::GetEncoding('iso-8859-1'))),
([System.IO.StreamWriter]::new('C:\TEMP\b.csv', [System.Text.Encoding]::UTF8)) `
{
$reader, $writer = $_
$writer.Write($reader.ReadToEnd())
}
Or, perhaps even better, albeit with slightly different semantics (which will rarely matter),[1] as Darin suggests:
Use-Object
($reader = [System.IO.StreamReader]::new('C:\Temp\a.csv',[System.Text.Encoding]::GetEncoding('iso-8859-1'))),
($writer = [System.IO.StreamWriter]::new('C:\TEMP\b.csv', [System.Text.Encoding]::UTF8)) `
{
$writer.Write($reader.ReadToEnd())
}
Use-Object
source code:
function Use-Object {
param(
[Parameter(Mandatory)] $ObjectsToDispose, # a single object or array
[Parameter(Mandatory)] [scriptblock] $ScriptBlock
)
try {
ForEach-Object $ScriptBlock -InputObject $ObjectsToDispose
}
finally {
foreach ($o in $ObjectsToDispose) {
if ($o -is [System.IDisposable]) {
$o.Dispose()
}
}
}
}
[1] With this syntax, you're creating the variables in the caller's scope, not in the function's, but this won't matter, as long as you don't try to assign different objects to these variables with the intent of also making the caller see such changes. (If you tried that, you would create a function-local copy of the variable that the caller won't see) - see this answer for details.