Deseche un StreamWriter sin declarar una variable en una línea

El siguiente comando de Powershell no puede copiar todo el archivo; siempre faltan algunos personajes al final.

[System.IO.StreamWriter]::new('C:\TEMP\b.csv', [System.Text.Encoding]::UTF8).Write([System.IO.StreamReader]::new('C:\Temp\a.csv', [System.Text.Encoding]::GetEncoding('iso-8859-1')).ReadToEnd())

Sospecho que es porque el escritor no vacía los últimos bits porque esto copia todo el archivo:

$X = [System.IO.StreamReader]::new('C:\Temp\a.csv', [System.Text.Encoding]::GetEncoding('iso-8859-1'))
$Y = [System.IO.StreamWriter]::new('C:\TEMP\b.csv', [System.Text.Encoding]::UTF8)
$Y.Write($X.ReadAll())
$X.Dispose()
$Y.Dispose()

¿Es posible deshacerse de (y vaciar) el lector y el escritor sin haber creado variables para hacer referencia a ellos?

EDITAR: probé este one-liner usando streamreader/writer con la esperanza de que el búfer de lectura del lector se transfiriera directamente al búfer de escritura del escritor en lugar de esperar a que el lector leyera el archivo completo en la memoria y luego escribiera. ¿Qué técnica podría lograr eso?

Personalmente, encuentro que el código que no declara un objeto de un solo uso a menudo es más limpio/más sucinto, pero mi atención se centra en comprender si los objetos se desechan a sí mismos y cómo, no el estilo.

No hay necesidad de evitar variables o escribir en una línea, pero este comportamiento no es lo que esperaba. En VBA, uno puede copiar un archivo como ese y confiar en que se deshará de sí mismo correctamente sin tener que declarar una variable y vaciar explícitamente (creo).

Sub Cpy()
With New Scripting.FileSystemObject
    .CreateTextFile("c:\Temp\Out.txt").Write .OpenTextFile("C:\Temp\In.txt", ForReading).ReadAll
End With
End Sub

Se puede lograr un comportamiento similar en una clase de VBA personalizada escribiendo el código de "limpieza" apropiado en un Class_Terminate()procedimiento. Supuse que Streamwriter eliminaría los datos de manera similar al finalizar a través de la recolección de elementos no utilizados una vez que se ejecuta la línea y ya no hay una variable asociada con ella.

También noté que el archivo permanece bloqueado y no puedo eliminarlo hasta que cierre la sesión de PowerShell. ¿Hay alguna forma de vaciar el contenido y liberar el archivo sin haber declarado una variable con la que trabajar?

Answer

Solo para mostrarle que esto es posible y más fácil de hacer, usando los métodos estáticos de System.IO.File, WriteAllText()y ReadAllText().

Lo siguiente consulta la API https://loripsum.net/ para obtener párrafos aleatorios y escribe en un archivo usando la iso-8859-1codificación. Luego lee esos archivos y escribe una copia usando la misma codificación y, por último, compara los hashes de ambos archivos. Como puede ver, leer y escribir se hace todo como una sola línea.

Las usingdeclaraciones se pueden eliminar, pero deberá usar los nombres de tipo completo.

Establezca la ubicación en una carpeta temporal para realizar pruebas.

using namespace System.IO
using namespace System.Text

$fileRead = [Path]::Combine($pwd.Path, 'test.txt')
$fileWrite = [Path]::Combine($pwd.Path, 'test-copy.txt')

$loremIpsum = Invoke-RestMethod 'https://loripsum.net/api/5/short/headers/plaintext'
[File]::WriteAllText($fileWrite, $loremIpsum, [Encoding]::GetEncoding('iso-8859-1'))

[File]::WriteAllText(
    $fileWrite,
    [File]::ReadAllText($fileRead, [Encoding]::GetEncoding('iso-8859-1')),
    [Encoding]::GetEncoding('iso-8859-1')
)

(Get-FileHash $fileRead).Hash -eq
(Get-FileHash $fileWrite).Hash # => Should be True
  • Para el caso de uso específico dado, la útil respuesta de Santiago Squarzon es, de hecho, la mejor solución: el uso de métodos estáticosSystem.IO.File de la clase estática evita la necesidad de instancias que representan archivos que requieren llamar a un .Close()método o eliminarlos explícitamente.

    • Para leer con pereza y, por lo tanto, admitir lectura y escritura superpuestas , línea por línea , puede usar los métodos static [System.IO.File]::ReadLines()y [System.IO.File]::WriteAllLines(), pero tenga en cuenta que este enfoque (a) usa invariablemente líneas nuevas de formato nativo de la [Environment]::NewLineplataforma en el archivo de salida, independientemente del formato de línea nueva utiliza el archivo de entrada, y (b) invariablemente agrega una nueva línea final en este formato, incluso si el archivo de entrada no tiene una nueva línea final.

    • Superar estas limitaciones requeriría el uso de una API de byte sin procesar de nivel inferior, System.IO.FileStreamque nuevamente requiere una eliminación explícita (consulte la sección inferior).

  • Dado que su enfoque primero lee todo el archivo de entrada en la memoria y luego lo escribe, incluso podría arreglárselas con los cmdlets de PowerShell, suponiendo que está ejecutando PowerShell (Core) 7+ , que escribe archivos UTF-8 sin BOM de forma predeterminada, y cuyo -Encodingparámetro acepta cualquier codificación admitida, como ISO-8859-1 en su caso:

    # PowerShell (Core) 7+ only
    Get-Content -Raw -Encoding iso-8859-1 C:\TEMP\a.csv |
      Set-Content -NoNewLine C:\TEMP\b.csv                            
    

En cuanto a su pregunta general :

A partir de PowerShell (Core) 7.2.1:

  • PowerShell no tiene una construcción equivalente a la usingdeclaración de C# que permite la eliminación automática de objetos cuyo tipo implementa la System.IDisposableinterfaz (que, en el caso de las API de E/S de archivos, cierra implícitamente los archivos).

    • El problema de GitHub n.° 9886 analiza la adición de dicha declaración, pero la discusión sugiere que es probable que no se implemente.

    • Nota: Si bien PowerShell tiene una familia de declaraciones que comienzan con la palabra clave using, sirven para diferentes propósitos; consulte el tema de ayuda conceptual about_Using .

  • Una versión futura de PowerShell admitirá un bloque clean { ... }(ocleanup { ... } ) que se llama automáticamente cuando finaliza una función avanzada o un script, lo que permite realizar cualquier limpieza necesaria a nivel de script de función (eliminación de objetos); consulte RFC #294 .

Depende de cada tipo que implemente la IDisposableinterfaz si llama a los .Dispose()métodos desde el finalizador . Sólo si es así , el recolector de basura desechará automáticamente un objeto .

Para System.IO.StreamWritery también para la clase de nivel inferior System.IO.FileStream, este parece no ser el caso, por lo que en PowerShell debe llamar .Close()o .Dispose() explícitamente , lo que se hace mejor desde el finallybloque de una declaración try// catch.finally

Puede reducir un poco la ceremonia combinando los aspectos de la construcción de objetos y la asignación de variables, pero un idioma robusto aún requiere mucha ceremonia:

$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() }
}

Una función auxiliar Use-Object(código fuente a continuación) puede aliviar esto un poco:

Use-Object 
  ($x = [System.IO.StreamReader]::new('C:\Temp\a.csv',[System.Text.Encoding]::GetEncoding('iso-8859-1'))), 
  ($y = [System.IO.StreamWriter]::new('C:\TEMP\b.csv', [System.Text.Encoding]::UTF8)) `
  { $y.Write($x.ReadToEnd()) }

Use-Objectcódigo fuente :

function Use-Object {
  param( 
    [Parameter(Mandatory)] [array] $ObjectsToDispose,
    [Parameter(Mandatory)] [scriptblock] $ScriptBlock
  )

  try {

    . $ScriptBlock

  } finally {
    foreach ($o in $ObjectsToDispose) {
      if ($o -is [System.IDisposable]) {
        $o.Dispose()
      }
    }
  }
  
}