Cómo llamar correctamente a un exe externo desde PowerShell — la trampa de las comillas en argumentos, los códigos de salida y la codificación de caracteres

· Actualizado el: · · PowerShell, Windows, Automatización, Scripts, Procesos, Codificación de caracteres, Mejora operativa, Procesamiento por lotes

«Funciona en el símbolo del sistema, pero en cuanto lo traslado a un script de PowerShell la herramienta externa empieza a devolver errores» — es un fenómeno que se encuentra casi siempre al convertir archivos por lotes a PowerShell o al automatizar EXE internos y CLI de código abierto. La causa casi nunca está en la lógica, sino en el camino que recorren los argumentos hasta llegar al programa. Una ruta con espacios, un argumento que incluye comillas dobles, una cadena que contiene % o ( ): cualquiera de estos detalles puede hacer que el argumento que creía haber pasado llegue transformado en otra cosa.

Lo que complica aún más las cosas es que este comportamiento cambió en PowerShell 7.3. Un workaround escrito para que funcione en 5.1 se convierte en un doble escape en 7, y un script escrito en 7 se rompe en 5.1. En entornos en japonés, a esto se suma la corrupción de caracteres en la salida.

En este artículo repasamos, desde la especificación, cómo se transmiten los argumentos al llamar a un programa externo (comando nativo) desde PowerShell: el uso correcto de --%, ProcessStartInfo cuando se necesita garantizar la entrega, el manejo del código de salida y de stderr, y las soluciones a la corrupción de caracteres, todo desde una perspectiva práctica. El diseño de la gestión de errores en sí se trata en «Gestión de errores y diseño de reintentos en PowerShell», así que este artículo se centra en la «frontera con el proceso externo».

Primero, compruebe su propio entorno

Como este artículo gira en torno a las diferencias entre versiones, antes de empezar a leer compruebe en cuál de los dos mundos se encuentra.

$PSVersionTable.PSVersion

Si obtiene 5.1.x, está en Windows PowerShell 5.1; si obtiene 7.x, está en PowerShell 7. La versión 7.3 es el punto de corte en la forma de pasar argumentos, así que tenga en cuenta que 7.0 a 7.2 todavía no incluyen el nuevo modo descrito en el capítulo 3. Si su entorno tiene ambas versiones instaladas, el comportamiento cambiará según cuál sea el host en ejecución.

1. Conclusión principal

  • PowerShell también analiza por su cuenta los argumentos de los programas externos. Todo lo que sigue a una llamada a comando se analiza en «modo de argumentos» (argument mode), donde los valores con espacios deben encerrarse entre comillas. Símbolos como , ( ) { } | & < > @ # son metacaracteres, así que si desea pasarlos como texto literal debe escaparlos con una comilla invertida (backtick). 1
  • PowerShell 7.3 cambió la forma de pasar argumentos (cambio disruptivo). Ahora se conservan las comillas incrustadas y los argumentos de cadena vacía, y el comportamiento se puede elegir con $PSNativeCommandArgumentPassing. El valor predeterminado en Windows es Windows. 12
  • En el modo Windows, solo cmd.exe, cscript.exe, wscript.exe y los archivos .bat .cmd .js .vbs .wsf usan la forma de pasar argumentos tradicional (Legacy). Es una excepción pensada para mantener la compatibilidad con los activos de scripts por lotes antiguos. 1
  • El token de detención de análisis --% es el último recurso para tratar todo lo que sigue como texto literal. Sin embargo, solo las variables de entorno con el formato %VAR% se expanden, las variables de PowerShell no se pueden usar en absoluto, y el efecto dura únicamente hasta el siguiente salto de línea o la tubería. Tampoco se puede escribir redirección. 1
  • Si necesita pasar con seguridad el valor de una variable, la primera opción es pasarlo en un array. Con el splatting de & $exe @argArray, cada elemento se convierte en un argumento independiente. Con ProcessStartInfo.ArgumentList puede delegar en .NET el ensamblado de las comillas, pero esta es una API de .NET Core 2.1 en adelante y no está disponible en 5.1. 13
  • No pase entradas no confiables a un archivo por lotes. En Windows, los argumentos de un archivo por lotes se entregan a cmd.exe como una cadena de línea de comandos sin procesar, por lo que la documentación oficial también advierte que «la entrada no confiable debe pasarse por otro medio». 1
  • El éxito o fracaso se determina con $LASTEXITCODE. Un código de salida distinto de cero no se convierte, de forma predeterminada, en un error de PowerShell, ni entra en un bloque try/catch. 4
  • La corrupción de caracteres se corrige por separado en el lado que envía y en el que recibe. La decodificación de la salida de un comando externo corresponde a [Console]::OutputEncoding, mientras que la cadena que PowerShell envía por tubería a un comando externo depende de $OutputEncoding. 2
  • Si tiene dudas, use Trace-Command -Name ParameterBinding para ver qué argumentos se pasaron realmente. A partir de PowerShell 7.3 también se puede rastrear el enlace de argumentos de los comandos nativos. 15

2. Por qué se corrompen los argumentos — la premisa del modo de argumentos

PowerShell descompone la línea de comandos en tokens y la interpreta en uno de dos modos: modo de expresión y modo de argumentos. En cuanto aparece una llamada a comando, todo lo que sigue se analiza en modo de argumentos. En este modo, la entrada se trata básicamente como una «cadena expandible»: lo que empieza por $ es una referencia a variable, una comilla marca el inicio de una cadena, ( ) marca el inicio de una expresión, y así sucesivamente — los símbolos tienen significado sintáctico. 1

Es decir, una cadena que funciona si se pega tal cual en el símbolo del sistema puede interpretarse de otra manera en PowerShell. Un ejemplo clásico es icacls. 1

# Funciona en cmd.exe, pero en la época de PowerShell 2.0 los paréntesis se interpretaban como una expresión y daban error
icacls X:\VMS /grant Dom\HVAdmin:(CI)(OI)F

# Escapar los metacaracteres con comilla invertida (difícil de leer)
icacls X:\VMS /grant Dom\HVAdmin:`(CI`)`(OI`)F

# Declarar "a partir de aquí, todo literal" con el token de detención de análisis (PowerShell 3.0 en adelante)
icacls X:\VMS --% /grant Dom\HVAdmin:(CI)(OI)F

Otra premisa importante es el camino por el que los argumentos, ya analizados, llegan al programa. En Windows PowerShell 5.1, los argumentos analizados se reensamblan en una sola cadena separada por espacios antes de pasarse al proceso. Durante este «reensamblado», las comillas incluidas dentro de un argumento pueden perderse, o los argumentos de cadena vacía pueden desaparecer — este es el accidente clásico de la era 5.1. 1

3. El cambio disruptivo de PowerShell 7.3 — $PSNativeCommandArgumentPassing

PowerShell 7.3 cambió esta forma de ensamblado. Es un punto en el que la documentación oficial afirma claramente que se trata de un «cambio disruptivo respecto al comportamiento de Windows PowerShell 5.1». 1

El nuevo comportamiento se controla con la variable de configuración $PSNativeCommandArgumentPassing, que admite tres valores: Legacy (tradicional), Standard y Windows. El valor predeterminado en la plataforma Windows es Windows, y en plataformas no Windows es Standard. 12

La diferencia entre Windows y Standard se reduce a un solo punto: en el modo Windows, las siguientes llamadas pasan automáticamente al modo Legacy. 1

Llamadas a las que se aplica automáticamente el modo Legacy
cmd.exe / cscript.exe / wscript.exe
Archivos con extensión .bat .cmd .js .vbs .wsf

Es una excepción para evitar que los scripts por lotes o WSH antiguos «se rompan porque, en cuanto se actualiza PowerShell a 7, cambia la forma en que reciben los argumentos». Dicho de otro modo, si establece explícitamente $PSNativeCommandArgumentPassing en Standard o Legacy, esta detección deja de aplicarse. 1

El nuevo modo mejora los dos puntos siguientes. 1

TestExe -echoargs, que aparece en los siguientes ejemplos, es una herramienta de verificación que se limita a mostrar cada argumento recibido en el formato Arg 0 is <...>. Forma parte de los activos de prueba del propio PowerShell y no es un comando incluido de forma estándar en Windows. En el capítulo 8 se explica cómo comprobar lo mismo en su propio entorno, así que por ahora piense en ella simplemente como «una ventana que muestra los argumentos tal como llegan».

# (1) Las comillas incrustadas en la cadena se conservan
$a = 'a" "b'
TestExe -echoargs $a 'c" "d' e" "f
# Arg 0 is <a" "b>
# Arg 1 is <c" "d>
# Arg 2 is <e f>

# (2) El argumento de cadena vacía se conserva en lugar de desaparecer
TestExe -echoargs '' a b ''
# Arg 0 is <>
# Arg 1 is <a>
# Arg 2 is <b>
# Arg 3 is <>

Si desea pasar tal cual una cadena de ruta entre comillas como "C:\Program Files (x86)\Microsoft\", en los modos Windows / Standard puede escribirla de forma directa. 1

# 7.3 en adelante (modo Windows / Standard)
TestExe -echoargs '"C:\Program Files (x86)\Microsoft\"'

# Para obtener el mismo resultado en modo Legacy (equivalente a 5.1) hace falta un doble escape de comillas
TestExe -echoargs "\""C:\Program Files (x86)\Microsoft\\"""

Aquí hay algo a tener en cuenta. La barra invertida (\) no es el carácter de escape de PowerShell. Que aparezca \" en el ejemplo anterior se debe a que la API subyacente de .NET (ProcessStartInfo.ArgumentList) trata la barra invertida como carácter de escape; el carácter de escape de la sintaxis de PowerShell es la comilla invertida (`). Que estos dos tipos se mezclen es la principal razón por la que este tema parece complicado. 13

La decisión práctica es sencilla: en entornos donde conviven 5.1 y 7, deje de escribir en forma literal los argumentos que contienen comillas. Si se apoya en «pasar mediante un array» o «usar ProcessStartInfo», que se explican en los capítulos siguientes, prácticamente no se verá afectado por las diferencias de versión. Para la política de convivencia entre 5.1 y 7 en sí, consulte «Diferencias entre Windows PowerShell 5.1 y PowerShell 7».

4. El uso correcto y los límites de --% (token de detención de análisis)

--% es un token que hace que PowerShell pase tal cual, sin interpretarlos, todos los caracteres que le siguen (disponible desde PowerShell 3.0). La documentación oficial indica explícitamente que «está pensado únicamente para usarse con comandos nativos en la plataforma Windows». 1

PS> cmd /c echo "a|b"
'b' is not recognized as an internal or external command,
operable program or batch file.

PS> cmd /c --% echo "a|b"
"a|b"

Es una herramienta potente, pero es necesario conocer con precisión sus restricciones. 1

Restricción Contenido
Solo se expanden las variables de entorno %USERPROFILE%, es decir, cualquier %<nombre>%, siempre se expande. No se puede escapar con %%. Los nombres no definidos pasan tal cual
No se pueden usar variables de PowerShell Valores como $path no se expanden y se pasan como cadena literal
Alcance del efecto Hasta el siguiente salto de línea o la tubería (\|). No se puede extender con continuación de línea mediante comilla invertida, ni terminar con ;
No admite redirección Algo como >archivo.txt se pasa tal cual como argumento al comando de destino

En resumen, --% solo se puede usar cuando lo que se pasa es una cadena completamente fija que no contiene %. En la automatización típica, donde el script construye el valor a partir de variables, esta condición casi nunca se cumple. La práctica de «poner --% por si acaso» se rompe en el instante en que se pasa una contraseña o un comodín que contiene %.

5. Cómo pasar variables con seguridad — splatting de arrays y ProcessStartInfo

Al pasar argumentos que incluyen el valor de una variable, la primera opción es colocar cada argumento por separado en un array y aplicar splatting. Como cada elemento del array se pasa como un argumento independiente, ni siquiera con rutas que contienen espacios hace falta escribir las comillas usted mismo.

$exe  = 'C:\Program Files\MyTool\convert.exe'
$args = @(
    '--input',  'D:\受注データ\2026年07月.csv'   # No hay problema aunque contenga espacios o japonés
    '--output', 'D:\出力\result.json'
    '--mode',   'strict'
)

& $exe @args                      # Splatting de array. Cada elemento se convierte en un argumento
if ($LASTEXITCODE -ne 0) { throw "Error al convertir (ExitCode=$LASTEXITCODE)" }

El operador de llamada & también es necesario para ejecutar un exe cuya ruta contiene espacios ('C:\Program Files\...' por sí solo solo se evalúa como una cadena literal y no se ejecuta).

Si no está seguro de que el argumento haya llegado correctamente, antes de añadir más escapes por conjetura, use el método del capítulo 8 para comprobar los argumentos reales. El camino más rápido es comparar si el argumento recibido es el mismo antes y después de cambiar la forma de escribirlo.

Cuando necesite aún más seguridad — controlar por completo cada argumento uno por uno, o especificar la codificación de la salida por proceso — use directamente ProcessStartInfo de .NET. Los valores añadidos a ArgumentList reciben el comillado adecuado por parte de .NET, así que no se ven afectados ni por el análisis de PowerShell ni por el reanálisis de la línea de comandos. 3

Sin embargo, ArgumentList es una API de .NET Core 2.1 en adelante y no existe en el ProcessStartInfo de Windows PowerShell 5.1, que se ejecuta sobre .NET Framework. 3 En 5.1, construya usted mismo la cadena Arguments del siguiente apartado, o use el splatting de array descrito antes.

# [PowerShell 7 en adelante] Delegar en .NET el ensamblado de los argumentos
$psi = [System.Diagnostics.ProcessStartInfo]::new()
$psi.FileName = 'C:\Program Files\MyTool\convert.exe'
foreach ($a in '--input', $inputPath, '--output', $outputPath) {
    $psi.ArgumentList.Add($a)     # 1 elemento = 1 argumento. No hay que escribir las comillas (exclusivo de 7)
}
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError  = $true
$psi.UseShellExecute        = $false
# Poder indicar explícitamente la codificación de la salida es otra ventaja de ProcessStartInfo (capítulo 6)
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
$psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8

$proc = [System.Diagnostics.Process]::Start($psi)

# La salida estándar y el error estándar deben leerse "al mismo tiempo". Si se lee
# uno de forma síncrona hasta el final y luego el otro, mientras se espera se llena
# el búfer de la otra tubería, el proceso hijo se bloquea al escribir y se produce
# un interbloqueo (deadlock)
$stdoutTask = $proc.StandardOutput.ReadToEndAsync()
$stderrTask = $proc.StandardError.ReadToEndAsync()
$proc.WaitForExit()
$stdout = $stdoutTask.GetAwaiter().GetResult()
$stderr = $stderrTask.GetAwaiter().GetResult()

if ($proc.ExitCode -ne 0) {
    throw "Error al convertir (ExitCode=$($proc.ExitCode)): $stderr"
}

El accidente más frecuente al redirigir la salida es el interbloqueo (deadlock). El búfer de una tubería tiene un límite, y cuando se llena, el proceso hijo se bloquea al intentar escribir. Por eso, no solo es peligroso llamar primero a WaitForExit() y leer después, sino que también es peligroso leer por completo un flujo de forma síncrona con ReadToEnd() antes de leer el otro (si el búfer del lado que no se está leyendo se llena primero, el proceso hijo se detiene y ReadToEnd() nunca regresa).

La secuencia en la que todo se detiene se resume en el siguiente diagrama.

Orden de bloqueo cuando solo un flujo se lee de forma síncronaDiagrama que muestra cómo, si el proceso padre espera a leer stderr por completo mientras el proceso hijo sigue escribiendo en stdout, el búfer de stdout se llena, el hijo se bloquea al escribir y ni ReadToEnd ni WaitForExit regresan, produciendo un interbloqueoProceso padreespera con StandardError.ReadToEnda terminar de leer stderrProceso hijosigue escribiendo en stdoutEl búfer de la tubería de stdout está llenono se vacía porque el padre no está leyendoEl proceso hijo se bloquea al escribirno puede terminar ni cerrar stderrEl ReadToEnd del padre no regresaWaitForExit tampoco regresa= interbloqueo (deadlock)

Figura 1: La secuencia que se detiene al intentar leer de forma síncrona un solo flujo hasta el final

Diseñe el código, como en el ejemplo anterior, para empezar a leer ambos flujos de forma asíncrona antes de esperar, o bien para redirigir solo uno de ellos. Para el tratamiento general de procesos hijos, consulte también «Lista de verificación para gestionar procesos hijos de forma segura en aplicaciones de Windows».

Si usa ProcessStartInfo en Windows PowerShell 5.1, tendrá que pasar a Arguments una sola cadena con las comillas puestas por usted mismo. Escribir a mano este ensamblado es una fuente de errores, así que en 5.1 haga del splatting de array (& $exe @args) su primera opción.

# [5.1] Como no existe ArgumentList, hay que ensamblar uno mismo una sola cadena con comillas
$quote = {
    param([string] $s)
    if ($s -eq '') { return '""' }                   # Si no se convierte la cadena vacía en "", el argumento entero desaparece
    if ($s -notmatch '[\s"]') { return $s }          # Si no hace falta encerrarla, se devuelve tal cual

    # Ajustarse a las reglas de análisis de la línea de comandos de Windows:
    #   (1) Duplicar la secuencia de barras invertidas justo antes de un ", y convertir el " en \"
    #   (2) Duplicar también la secuencia de barras invertidas al final. Como queda justo antes
    #       de la comilla de cierre, si se dejara igual se leería como \" y la comilla no se
    #       cerraría, arrastrando el siguiente argumento
    #       (ejemplo: 'C:\Program Files\input\' → "C:\Program Files\input\\")
    $e = $s -replace '(\\*)"', '$1$1\"'
    $e = $e -replace '(\\+)$', '$1$1'
    '"' + $e + '"'
}
$psi.Arguments = (@('--input', $inputPath, '--output', $outputPath) |
                  ForEach-Object { & $quote $_ }) -join ' '

Solo con mirar la expresión regular es difícil entender qué hace, así que a continuación se muestra la correspondencia entre entrada y salida. Si comprende estas seis filas, no hace falta memorizar la regla en sí.

Valor que desea pasar (contenido de la variable) Cadena que devuelve $quote Regla que se aplica
strict strict No tiene espacios ni comillas, así que se devuelve tal cual sin encerrar
Cadena vacía "" Si no se escribe nada, el argumento desaparece por completo, así que se coloca un par de comillas vacío
D:\日本語\2026年07月.csv D:\日本語\2026年07月.csv Aunque contenga japonés, si no hay espacios no se encierra
C:\Program Files\input "C:\Program Files\input" Como hay un espacio, basta con encerrar todo entre comillas
C:\Program Files\input\ "C:\Program Files\input\\" Regla (2). Se duplica la \ final. Si se dejara una sola, se pegaría a la comilla de cierre y se leería como \", de modo que la comilla no se cerraría y arrastraría el siguiente argumento
say "hi" "say \"hi\"" Regla (1). El " dentro del valor se convierte en \", de modo que se pasa como carácter y no como delimitador
a\"b "a\\\"b" Panorama completo de la regla (1). Primero se duplica la secuencia de \ que precede a ", y después se convierte el " en \"

La última fila es precisamente la razón por la que esta función parece complicada. Esto es así porque se ajusta a la asimetría de las reglas de análisis de la línea de comandos de Windows: la barra invertida solo actúa como carácter de escape «cuando el siguiente carácter es un "».

Precisamente lo minucioso de esta regla es la razón por la que conviene evitar ensamblarla a mano en 5.1. Dicho esto, en Windows PowerShell 5.1 tampoco el splatting de array es una solución universal. Al final, el valor pasado se vuelve a ensamblar en una cadena de línea de comandos con el método tradicional, así que los argumentos de cadena vacía desaparecen y los valores con comillas se deforman. 1 Por lo tanto, en 5.1 elija según el siguiente criterio.

Argumento a pasar en 5.1 Método
Valor normal con espacios o japonés Basta con splatting de array (& $exe @args)
Cadena vacía, valor con comillas ProcessStartInfo + el escape anterior, o --% (solo cadenas fijas)

En PowerShell 7 ambos problemas están resueltos, por lo que esta distinción deja de ser necesaria.

Además, la documentación oficial advierte que no se deben pasar entradas no confiables a un archivo por lotes, ya que los argumentos de un archivo por lotes se entregan a cmd.exe como una cadena de línea de comandos sin procesar. 1 Un diseño que concatena entrada de usuario o nombres de archivo para pasarlos a un archivo por lotes es un caldo de cultivo para la inyección de comandos. Pase los valores mediante un archivo temporal o una variable de entorno, o bien migre el propio archivo por lotes a PowerShell («¿Debería migrar ese archivo por lotes a PowerShell?»).

6. Cómo corregir la codificación de caracteres — [Console]::OutputEncoding y $OutputEncoding

En un entorno en japonés, tarde o temprano se topará con la corrupción de caracteres. El punto clave es que la configuración utilizada cambia según la dirección.

Dirección Configuración utilizada Síntoma
PowerShell recibe la salida de un comando externo [Console]::OutputEncoding El resultado de una herramienta que emite en UTF-8 se corrompe, apareciendo algo como «譁�喧縺�»
PowerShell envía una cadena por tubería a un comando externo $OutputEncoding El japonés enviado se corrompe en el otro extremo

La forma en que se corrompe el texto se entiende mucho mejor comparándola con la cadena original. Si el japonés emitido en UTF-8 se decodifica como CP932 (Shift_JIS), el resultado es el siguiente.

Cadena original Resultado de decodificar la salida UTF-8 como CP932
こんにちは 縺薙s縺ォ縺。縺ッ
エラー 繧ィ繝ゥ繝シ
日本語 譌・譛ャ隱 + bytes que no se pueden decodificar

Una señal reveladora es que el hiragana y el katakana se corrompen en pares de caracteres que empiezan por , o (esto ocurre porque el hiragana y el katakana en UTF-8 empiezan con los bytes E3 81, E3 82 o E3 83, y esos dos primeros bytes corresponden precisamente a esos caracteres en CP932). Cuando se mezclan bytes que no se pueden decodificar, como ocurre con los kanji, aparecen caracteres de sustitución o se pierden caracteres, y la longitud tampoco coincide, como en la tercera fila de la tabla anterior.

$OutputEncoding es la variable de configuración que determina «la codificación que usa PowerShell al enviar una cadena a un comando nativo». 2 Por otro lado, decodificar como cadena la secuencia de bytes que emite un comando externo es responsabilidad de [Console]::OutputEncoding. Cuando se corrige solo uno de los dos y «sigue saliendo mal», casi siempre se debe a que se han confundido ambos.

# Solución habitual al llamar desde Windows PowerShell 5.1 a una herramienta externa que emite en UTF-8
$prevOut = [Console]::OutputEncoding
$prevPs  = $OutputEncoding
try {
    [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)  # UTF-8 sin BOM
    $OutputEncoding           = [System.Text.UTF8Encoding]::new($false)
    $result = & $exe --list
}
finally {
    # Afecta a toda la sesión, así que hay que restaurarlo siempre
    [Console]::OutputEncoding = $prevOut
    $OutputEncoding           = $prevPs
}

Si usa ProcessStartInfo, como se vio en el capítulo anterior, puede especificar StandardOutputEncoding / StandardErrorEncoding por proceso, sin necesidad de tocar la configuración de la sesión. Esta opción tiene menos efectos secundarios. Los aspectos generales de la codificación de caracteres en Windows se resumen en «Codificación de caracteres y saltos de línea en Windows».

7. Código de salida y stderr — evite que un éxito se trate como fallo

El éxito o fracaso de un programa externo se determina con $LASTEXITCODE. Un código de salida distinto de cero no genera un ErrorRecord de forma predeterminada, ni entra en try/catch. 4 Este fundamento se trató en detalle en «Gestión de errores y diseño de reintentos en PowerShell», así que aquí solo se añaden dos puntos específicos de los procesos externos.

(1) Escribir en stderr no es un «fallo». Muchas herramientas CLI escriben en stderr el progreso o los registros. Como PowerShell envía la salida stderr de los comandos nativos al flujo de errores, la pantalla se pone en rojo y da la impresión de que «ha fallado», pero si el código de salida es 0, la ejecución fue un éxito. En Windows PowerShell 5.1, $? podía convertirse en $false con solo escribir en stderr, pero en PowerShell 7 esto se corrigió para que solo se vuelva $false cuando el código de salida es distinto de cero. 6

(2) El tipo de dato tras combinar los flujos con 2>&1 varía según la versión. En Windows PowerShell 5.1, cada línea de stderr se mezcla como un ErrorRecord, pero a partir de PowerShell 7.4 la salida redirigida de los comandos nativos se trata como un flujo de bytes, y tras la combinación se convierte en datos de cadena. 78 Es decir, un código que «clasifica según si es un ErrorRecord» deja de funcionar a partir de 7.4. Si necesita ambas salidas, lo seguro es recibirlas por separado, sin combinarlas.

# [Recomendado] Recibir stdout y stderr por separado (sin verse afectado por las diferencias de versión)
$errFile = [System.IO.Path]::GetTempFileName()
try {
    $stdout = & $exe --import $csvPath 2> $errFile
    $stderr = Get-Content -Path $errFile -Raw

    # Dejar ambos registrados en el log
    $stdout | Add-Content -Path $logPath -Encoding utf8
    if ($stderr) { $stderr | Add-Content -Path $logPath -Encoding utf8 }

    if ($LASTEXITCODE -ne 0) {
        throw "Error al importar (ExitCode=$LASTEXITCODE): $stderr"
    }
    # Si se llega aquí, fue un éxito. No se trata como fallo aunque haya salida en stderr
}
finally {
    Remove-Item $errFile -ErrorAction SilentlyContinue
}

# [Cuando no hace falta distinguir] Si solo quiere volcarlo todo junto al log, combinar está bien
(& $exe --import $csvPath 2>&1) | ForEach-Object { $_.ToString() } |
    Add-Content -Path $logPath -Encoding utf8

8. Cómo comprobar qué se pasó realmente

Añadir más escapes por conjetura solo empeora las cosas. El camino más rápido es ver los argumentos que realmente se pasaron. A partir de PowerShell 7.3, el enlace de argumentos de los comandos nativos se puede rastrear con Trace-Command. 15

Trace-Command -Name ParameterBinding -PSHost -Expression {
    & $exe --input 'D:\受注データ\2026年07月.csv' --mode strict
}
# DEBUG: ... BIND cmd line arg [--input] to position [0]
# DEBUG: ... BIND cmd line arg [D:\受注データ\2026年07月.csv] to position [1]

Si la herramienta de destino es propia, con preparar un modo de verificación que se limite a mostrar los args recibidos, este tipo de investigación se resuelve en un instante (es la misma idea que TestExe -echoargs, presente en el conjunto de herramientas de prueba de PowerShell). 1 Si quiere hacer lo mismo en un entorno 5.1 actual, basta con preparar un pequeño .ps1 que se limite a enumerar $args, o un pequeño EXE que solo muestre los argumentos tal cual.

9. Reglas prácticas (tabla de decisión)

Situación Opciones Criterio de decisión
Argumento con cadena fija (sin %) --% / invocación normal Si el escape resulta engorroso, --% es lo más rápido. Pero no se pueden usar variables1
Pasar el valor de una variable Splatting de array & $exe @args Primera opción. No hace falta escribir las comillas aunque contenga espacios, japonés o símbolos
Quiere la misma forma de escribir en 5.1 y en 7 Splatting de array La sintaxis es común, pero en 5.1 los argumentos con cadena vacía o comillas se rompen (ver la nota siguiente)1
Pasar en 5.1 argumentos con cadena vacía o comillas ProcessStartInfo + escape propio Seguro porque no pasa por el reensamblado de la línea de comandos de 5.1 (capítulo 5)
Controlar por completo cada argumento uno por uno (exclusivo de 7) ProcessStartInfo + ArgumentList Se puede delegar en .NET el ensamblado de las comillas. API de .NET Core 2.1 en adelante3
Necesita otra ventana, otro usuario o elevación Start-Process Obtenga ExitCode con -Wait -PassThru. Redirija la salida a un archivo9
Pasar un valor a un archivo por lotes (.bat) A través de variable de entorno o archivo temporal No pase entradas no confiables como argumento (advertencia oficial)1
La salida sale con caracteres ilegibles [Console]::OutputEncoding (recepción) / $OutputEncoding (envío) La configuración cambia según la dirección. Por proceso, use StandardOutputEncoding2
Determinar éxito o fracaso $LASTEXITCODE Un valor distinto de cero no entra en catch de forma predeterminada. Escribir en stderr no significa fallo46
Quiere distinguir stdout de stderr Recibirlos por separado con redirección El tipo tras combinarlos con 2>&1 difiere entre 5.1 y 7.4 en adelante78
No sabe qué se ha pasado realmente Trace-Command -Name ParameterBinding Mídalo antes de añadir más escapes por conjetura5

10. Resumen

  • PowerShell también analiza en modo de argumentos los argumentos de los programas externos. Como los símbolos tienen significado sintáctico, una cadena que funciona en cmd.exe no siempre pasa tal cual.
  • PowerShell 7.3 cambió la forma de pasar argumentos (cambio disruptivo), y ahora se conservan las comillas incrustadas y las cadenas vacías. En Windows, el modo predeterminado es Windows, y solo cmd.exe, los archivos por lotes, etc. usan el modo Legacy.
  • --% es el último recurso, exclusivo para cadenas fijas. Úselo entendiendo sus restricciones: %VAR% siempre se expande, no se pueden usar variables de PowerShell, y el efecto dura solo hasta el salto de línea o la tubería.
  • Si va a pasar variables, el splatting de array es la primera opción. En PowerShell 7 también puede usar ArgumentList de ProcessStartInfo (no existe en 5.1). El origen de la confusión es que la barra invertida no es el carácter de escape de PowerShell.
  • Cuando redirija tanto la salida estándar como el error estándar, léalos siempre al mismo tiempo. Un código que lea uno de forma síncrona hasta el final provoca un interbloqueo.
  • La solución a la corrupción de caracteres depende de la dirección. Para recibir, [Console]::OutputEncoding; para enviar, $OutputEncoding; y por proceso, StandardOutputEncoding.
  • El éxito o fracaso se determina con $LASTEXITCODE. Escribir en stderr no significa un fallo. Si necesita distinguir stdout de stderr, recíbalos por separado en lugar de combinarlos con 2>&1 (el tipo tras la combinación difiere entre 5.1 y 7.4 en adelante).

Descarga del código de ejemplo

El código tratado en este artículo se distribuye ya listo para ejecutarse. Incluye el módulo de comillado de argumentos y la ejecución mediante ProcessStartInfo.

Descargar el código de ejemplo (zip)

Los ejemplos de este artículo se han ejecutado y verificado realmente con PowerShell 7.6 (22 pruebas de Pester). Si ejecuta Invoke-SampleTests.ps1, incluido en el zip, podrá reproducir la misma verificación en su propio entorno.

# Análisis sintáctico + análisis estático + pruebas de Pester
./Invoke-SampleTests.ps1

Los valores de configuración (rutas, nombres de servidor, ID de inquilino, etc.) son ejemplos. No los ejecute tal cual en un entorno de producción; adáptelos al entorno de su propia organización.

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC se ocupa de la conversión de activos por lotes a PowerShell, el diseño de procesos de automatización que combinan herramientas externas y EXE internos, y la investigación de las causas de los scripts que «a veces funcionan y a veces no, según el entorno».

Referencias

  1. Microsoft Learn, about_Parsing. Sobre la distinción entre el modo de expresión y el modo de argumentos, los metacaracteres del modo de argumentos, el escape con comilla invertida, el hecho de que los argumentos que se pasan a un comando nativo se combinan tras el análisis en una sola cadena separada por espacios, la especificación del token de detención de análisis --% desde PowerShell 3.0 (solo se expanden las variables de entorno, no se puede escapar con %%, el efecto dura hasta el salto de línea o la tubería, no admite redirección), el cambio disruptivo en el análisis de la línea de comandos de los comandos nativos introducido en PowerShell 7.3, los valores de $PSNativeCommandArgumentPassing (Legacy/Standard/Windows) y su valor predeterminado en Windows, el hecho de que en el modo Windows cmd.exe, cscript.exe, wscript.exe y los archivos .bat/.cmd/.js/.vbs/.wsf usan el modo Legacy, que la barra invertida no es el carácter de escape de PowerShell, la advertencia de no pasar entradas no confiables a un archivo por lotes, y el hecho de que desde 7.3 se puede rastrear el enlace de argumentos de los comandos nativos.  2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

  2. Microsoft Learn, about_Preference_Variables. Sobre el hecho de que $PSNativeCommandArgumentPassing es una variable de configuración con un valor predeterminado dependiente de la plataforma, y que $OutputEncoding determina la codificación que se usa cuando PowerShell envía cadenas a otra aplicación.  2 3 4 5

  3. Microsoft Learn, propiedad ProcessStartInfo.ArgumentList. Sobre el hecho de que los argumentos se pueden especificar individualmente como una colección y que el runtime se encarga del comillado y el escape necesarios, que la barra invertida se trata como carácter de escape, y que se aplica a partir de .NET Core 2.1 (no existe en .NET Framework), por lo que no está disponible en Windows PowerShell 5.1, que se ejecuta sobre .NET Framework. Véase también la nota sobre el posible interbloqueo al redirigir y leer de forma síncrona tanto la salida estándar como el error estándar, y cómo evitarlo (leyendo uno de ellos de forma asíncrona), en propiedad Process.StandardOutput 2 3 4 5

  4. Microsoft Learn, about_Error_Handling. Sobre el hecho de que un código de salida distinto de cero de un comando nativo convierte $? en $false y se almacena en $LASTEXITCODE, pero no genera un ErrorRecord ni entra en try/catch.  2 3

  5. Microsoft Learn, Trace-Command. Sobre el rastreo del enlace de parámetros con -Name ParameterBinding, la salida al host con -PSHost, y la especificación del objetivo del rastreo con -Expression 2 3

  6. Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. Sobre el cambio por el cual, en PowerShell 7, escribir en stderr por sí solo ya no convierte $? en $false, y este solo se vuelve $false cuando el código de salida es distinto de cero.  2

  7. Microsoft Learn, about_Redirection. Sobre el sistema de numeración de los flujos de salida de PowerShell, la combinación del flujo de errores con el flujo de éxito mediante 2>&1, y el tratamiento de la salida stderr de los comandos nativos.  2

  8. Microsoft Learn, Novedades de PowerShell 7.4. Sobre el hecho de que los operadores de redirección ahora conservan la salida de los comandos nativos como flujo de bytes, sin que PowerShell interprete ni dé formato al contenido (cambio disruptivo), y de que, como consecuencia, el stderr combinado con 2>&1 se trata como datos de cadena.  2

  9. Microsoft Learn, Start-Process. Sobre el hecho de que, de forma predeterminada, no espera a que termine el nuevo proceso; la espera con -Wait; la obtención del objeto Process y de ExitCode con -PassThru; la redirección a archivo con -RedirectStandardOutput / -RedirectStandardError; la elevación con -Verb RunAs; y la ejecución como otro usuario con -Credential

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

Al llamar a un exe desde PowerShell, las comillas dobles de los argumentos desaparecen. ¿Por qué ocurre esto?
Esto se debe al mecanismo por el cual PowerShell analiza los argumentos antes de pasarlos al programa externo. En Windows PowerShell 5.1, los argumentos ya analizados se vuelven a ensamblar en una sola cadena separada por espacios, por lo que las comillas incrustadas se pierden y los argumentos de cadena vacía desaparecen. PowerShell 7.3 cambió este comportamiento, de modo que ahora se conservan tanto las comillas incrustadas como los argumentos de cadena vacía. Hay que tener presente que en 5.1 este reensamblado también se aplica al splatting de arrays (`& $exe @args`). Si necesita pasar con seguridad valores que contienen comillas o cadenas vacías, el splatting no resuelve el problema. Para cadenas fijas puede usar el token de detención de análisis `--%`, pero no funciona si el valor incluye variables. Lo seguro es aplicar usted mismo las reglas de comillado de la línea de comandos de Windows y pasar el resultado como la cadena Arguments de ProcessStartInfo (ArgumentList es una API de .NET Core 2.1 en adelante y no está disponible en 5.1). En el cuerpo del artículo se muestra un ejemplo de implementación.
¿El uso de `--%` (token de detención de análisis) permite pasar de forma segura cualquier argumento?
No, tiene muchas restricciones y no es una solución universal. A partir de --% todo se trata como literal, pero las referencias a variables de entorno como %USERPROFILE% sí se expanden, por lo que cualquier cadena que contenga % puede sustituirse sin que usted lo desee (tampoco se puede escapar con %%). Además, las variables de PowerShell no se pueden expandir en absoluto, el efecto solo dura hasta el siguiente salto de línea o el símbolo de tubería, y no se puede escribir redirección. En el momento en que necesita pasar el valor de una variable, --% deja de ser una opción; en ese caso, considere ProcessStartInfo o Start-Process.
¿Es seguro pasar a un archivo por lotes (.bat) una cadena recibida de una fuente externa?
Evite pasar entradas no confiables a un archivo por lotes. En Windows, los argumentos de un archivo por lotes se entregan a cmd.exe como una cadena de línea de comandos sin procesar, y la documentación oficial indica explícitamente que "la entrada no confiable debe pasarse por otro medio". Si concatena directamente nombres de archivo o entradas de usuario, queda abierta la posibilidad de inyección de comandos. Lo seguro es pasar los valores mediante un archivo temporal o una variable de entorno, o bien sustituir el archivo por lotes por un script de PowerShell.
La salida de un comando externo aparece con el japonés ilegible. ¿Qué debo corregir?
Ajuste [Console]::OutputEncoding, que es lo que usa PowerShell para decodificar la salida estándar de un comando externo, para que coincida con la codificación que realmente produce ese comando. Si la herramienta emite en UTF-8, configure [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 antes de invocarla. A la inversa, cuando PowerShell envía una cadena por tubería a un comando externo se usa $OutputEncoding, así que conviene pensar por separado en el lado que envía y en el que recibe. La configuración es de ámbito de sesión, de modo que si la cambia temporalmente dentro de un script, no olvide restaurarla.
¿Cómo se decide entre usar Start-Process y la invocación directa (&)?
Para el caso habitual de querer recibir la salida por la canalización o de solo comprobar el código de salida, lo básico es la invocación directa (& o simplemente el nombre del comando). Use Start-Process cuando quiera controlar la "forma de iniciar" el proceso: abrirlo en otra ventana, ejecutarlo como otro usuario, elevarlo a permisos de administrador (-Verb RunAs) o redirigir la salida estándar a un archivo. Sin embargo, Start-Process no espera a que termine el proceso de forma predeterminada, así que si necesita el código de salida debe combinar -Wait y -PassThru y consultar la propiedad ExitCode.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog