Saltar al contenido

PowerShell elimina las comillas dobles de los argumentos de la línea de comandos

Solución:

Es una cosa conocida:

Es MUY DIFÍCIL pasar parámetros a aplicaciones que requieren cadenas entre comillas. Hice esta pregunta en IRC con una “sala llena” de expertos en PowerShell, y alguien tardó una hora en encontrar una forma (originalmente comencé a publicar aquí que simplemente no es posible). Esto rompe completamente la capacidad de PowerShell de servir como un shell de propósito general, porque no podemos hacer cosas simples como ejecutar sqlcmd. El trabajo número uno de un shell de comandos debería ser ejecutar aplicaciones de línea de comandos … Como ejemplo, al intentar usar SqlCmd de SQL Server 2008, hay un parámetro -v que toma una serie de parámetros de nombre: valor. Si el valor tiene espacios, debe citarlo …

… no hay una forma única de escribir una línea de comando para invocar esta aplicación correctamente, por lo que incluso después de dominar las 4 o 5 formas diferentes de citar y escapar de las cosas, todavía está adivinando cuál funcionará cuando … o, simplemente puede pagar a cmd y terminar con eso.

TL; DR

Si solo desea una solución para Powershell 5, consulte:

ConvertTo-ArgvQuoteForPoSh.ps: Powershell V5 (y código C #) para permitir el escape de argumentos de comandos nativos

La pregunta que intentaré responder

…, parece que PowerShell está eliminando las comillas dobles de los argumentos de la línea de comandos, incluso cuando se escapan correctamente.

PS C:Documents and SettingsNick> echo.exe '"hello"'
hello 
PS C:Documents and SettingsNick> echo.exe '"hello"' 
"hello"

Observe que las comillas dobles están ahí cuando se pasan al cmdlet echo de PowerShell, pero cuando se pasan como argumento a echo.exe, el las comillas dobles se eliminan a menos que se salgan con una barra invertida (aunque el carácter de escape de PowerShell es una comilla invertida, no una barra invertida).

Esto me parece un error. Si paso las cadenas de escape correctas a PowerShell, entonces PowerShell debe encargarse de cualquier escape que sea necesario porque sin embargo invoca el comando.

¿Que esta pasando aqui?

El trasfondo sin Powershell

El hecho de que necesita escapar de las comillas con barras invertidas tiene nada a con powershell, pero con el CommandLineToArgvW función que utilizan todos los programas msvcrt y C # para construir el argv matriz de la línea de comando de una sola cadena que pasa el proceso de Windows.

Los detalles se explican en Todo el mundo cita los argumentos de la línea de comandos de forma incorrecta y básicamente se reduce al hecho de que esta función históricamente tiene reglas de escape muy poco intuitivas:

  • 2n barras invertidas seguidas de comillas producen n barras invertidas seguidas de comillas iniciales / finales. Esto no se convierte en parte del argumento analizado, pero alterna el modo “entre comillas”.
  • (2n) + 1 barras invertidas seguidas de una comilla nuevamente producen n barras invertidas seguidas de una comilla literal (“). Esto no cambia el modo” entre comillas “.
  • n barras invertidas no seguidas de comillas simplemente producen n barras invertidas.

que conduce a la función de escape genérica descrita (cita corta de la lógica aquí):

CommandLine.push_back (L'"');

for (auto It = Argument.begin () ; ; ++It) {
      unsigned NumberBackslashes = 0;

      while (It != Argument.end () && *It == L'\') {
              ++It;
              ++NumberBackslashes;
      }

      if (It == Argument.end ()) {
              // Escape all backslashes, but let the terminating
              // double quotation mark we add below be interpreted
              // as a metacharacter.
              CommandLine.append (NumberBackslashes * 2, L'\');
              break;
      } else if (*It == L'"') {
              // Escape all backslashes and the following
              // double quotation mark.
              CommandLine.append (NumberBackslashes * 2 + 1, L'\');
              CommandLine.push_back (*It);
      } else {
              // Backslashes aren't special here.
              CommandLine.append (NumberBackslashes, L'\');
              CommandLine.push_back (*It);
      }
}

CommandLine.push_back (L'"');

Los detalles de Powershell

Ahora, hasta Powershell 5 (incluido PoSh 5.1.18362.145 en Win10 / 1909) PoSh conoce básicamente estas reglas, ni debería decirse, porque estas reglas no son realmente generales, porque cualquier ejecutable que llame podría, en teoría, usar algún otro medio para interpretar la línea de comando pasada.

Lo que nos lleva a …

Las reglas de cotización de Powershell

Qué PoSh lo hace Sin embargo, trate de averiguar si la cadenas lo pasa como argumentos a los comandos nativos que deben citarse porque contienen espacios en blanco.

PoSh – en contraste con cmd.exe – analiza mucho más el comando que le entregas, ya que tiene que resolver variables y conoce múltiples argumentos.

Entonces, dado un comando como

$firs="whaddyaknow"
$secnd = 'it may have spaces'
$third = 'it may also have "quotes" and other " weird \ stuff'
EchoArgs.exe $firs $secnd $third

Powershell tiene que tomar una postura sobre cómo crear el soltero cadena CommandLine para Win32 CreateProcess (o más bien el C # Process.Start) llamarlo eventualmente tendrá que hacer.

El enfoque que toma Powershell es extraño y se volvió más complicado en PoSh V7, y por lo que puedo seguir, tiene que ver con la forma en que powershell trata las comillas desequilibradas en una cadena sin comillas. La larga historia corta es esta:

Powershell cotizará automáticamente (incluirá">) una sola cadena de argumento, si contiene espacios y los espacios no se mezclan con un número impar de comillas dobles (sin formato).

Las reglas de cotización específicas de PoSh V5 lo hacen imposible para pasar una determinada categoría de cadena como argumento único a un proceso hijo.

PoSh V7 solucionó esto, de modo que siempre que todas las cotizaciones sean " escapó, lo cual deben ser de todos modos para pasar CommandLineToArgvW – podemos pasar cualquier cadena aribtrary de PoSh a un ejecutable secundario que use CommandLineToArgvW.

Aquí están las reglas como código C # extraídas del repositorio de github de PoSh para una clase de herramienta nuestra:

Reglas de cotización de PoSh V5

    public static bool NeedQuotesPoshV5(string arg)
    {
        // bool needQuotes = false;
        int quoteCount = 0;
        for (int i = 0; i < arg.Length; i++)
        {
            if (arg[i] == '"')
            {
                quoteCount += 1;
            }
            else if (char.IsWhiteSpace(arg[i]) && (quoteCount % 2 == 0))
            {
                // needQuotes = true;
                return true;
            }
        }
        return false;
    }

Reglas de cotización de PoSh V7

    internal static bool NeedQuotesPoshV7(string arg)
    {
        bool followingBackslash = false;
        // bool needQuotes = false;
        int quoteCount = 0;
        for (int i = 0; i < arg.Length; i++)
        {
            if (arg[i] == '"' && !followingBackslash)
            {
                quoteCount += 1;
            }
            else if (char.IsWhiteSpace(arg[i]) && (quoteCount % 2 == 0))
            {
                // needQuotes = true;
                return true;
            }

            followingBackslash = arg[i] == '\';
        }
        // return needQuotes;
        return false;
    }

Oh, sí, y también agregaron en un intento a medias de escapar correctamente de la cadena citada en V7:

if (NeedQuotes(arg))
{
      _arguments.Append('"');
      // need to escape all trailing backslashes so the native command receives it correctly
      // according to http://www.daviddeley.com/autohotkey/parameters/parameters.htm#WINCRULESDOC
      _arguments.Append(arg);
      for (int i = arg.Length - 1; i >= 0 && arg[i] == '\'; i--)
      {
              _arguments.Append('\');
      }

      _arguments.Append('"');

La situación de Powershell

Input to EchoArgs             | Output V5 (powershell.exe)  | Output V7 (pwsh.exe)
===================================================================================
EchoArgs.exe 'abc def'        | Arg 0 is <abc def>          | Arg 0 is <abc def>
------------------------------|-----------------------------|---------------------------
EchoArgs.exe '"nospace"'    | Arg 0 is <"nospace">        | Arg 0 is <"nospace">
------------------------------|-----------------------------|---------------------------
EchoArgs.exe '""nospace""'  | Arg 0 is <"nospace">        | Arg 0 is <"nospace">
------------------------------|-----------------------------|---------------------------
EchoArgs.exe 'a"bc def'      | Arg 0 is <a"bc>             | Arg 0 is <a"bc def>
                              | Arg 1 is <def>              |
------------------------------|-----------------------------|---------------------------
   ...

Estoy recortando más ejemplos aquí por razones de tiempo. De todos modos, no deberían agregar demasiado a la respuesta.

La solución Powershell

Para pasar cadenas arbitrarias de Powershell a un comando nativo usando CommandLineToArgvW, tenemos que:

  • escapar correctamente de todas las comillas y barras invertidas en el argumento fuente
    • Esto significa reconocer el manejo especial del extremo de la cadena para las barras diagonales inversas que tiene V7. (Esta parte no está implementada en el código siguiente).
  • y determinar si powershell cotizará automáticamente nuestra cadena de escape y si no la cotizará automáticamente, citarla nosotros mismos.

    • y asegúrese de que la cadena que citamos nosotros mismos no se cotice automáticamente por powershell: esto es lo que rompe V5.

Código fuente de Powershell V5 para escapar correctamente todos los argumentos a cualquier comando nativo

He puesto el código completo en Gist, ya que es demasiado largo para incluirlo aquí: ConvertTo-ArgvQuoteForPoSh.ps: Powershell V5 (y código C #) para permitir el escape de argumentos de comandos nativos

  • Tenga en cuenta que este código hace todo lo posible, pero para algunas cadenas con comillas en la carga útil y V5, simplemente debe agregar un espacio inicial a los argumentos que pasa. (Consulte el código para obtener detalles lógicos).

Personalmente, evito usar ” para escapar de cosas en PowerShell, porque técnicamente no es un carácter de escape de shell. Obtuve resultados impredecibles con él. En cadenas entre comillas dobles, puede utilizar "" para obtener una comilla doble incrustada, o escapar de ella con una marca inversa:

PS C:UsersDroj> "string ""with`" quotes"
string "with" quotes

Lo mismo ocurre con las comillas simples:

PS C:UsersDroj> 'string ''with'' quotes'
string 'with' quotes

Lo extraño de enviar parámetros a programas externos es que hay un nivel adicional de evaluación de cotizaciones. No sé si esto es un error, pero supongo que no se cambiará, porque el comportamiento es el mismo cuando usa Start-Process y pasa argumentos. Start-Process toma una matriz para los argumentos, lo que aclara un poco las cosas, en términos de cuántos argumentos se envían realmente, pero esos argumentos parecen evaluarse un tiempo adicional.

Entonces, si tengo una matriz, puedo configurar los valores de los argumentos para que tengan comillas incrustadas:

PS C:cygwinhomeDroj> $aa="arg="foo"", 'arg=""""bar""""'
PS C:cygwinhomeDroj> echo $aa
arg="foo"
arg=""""bar""""

El argumento de la “barra” tiene suficiente para cubrir la evaluación oculta adicional. Es como si enviara ese valor a un cmdlet entre comillas dobles y luego enviara ese resultado nuevamente entre comillas dobles:

PS C:cygwinhomeDroj> echo "arg=""""bar""""" # level one
arg=""bar""
PS C:cygwinhomeDroj> echo "arg=""bar""" # hidden level
arg="bar"

Uno esperaría que estos argumentos se pasaran a comandos externos tal cual, como lo son a cmdlets como ‘echo “https://foroayuda.es/” write-output’, pero no lo son, debido a ese nivel oculto:

PS C:cygwinhomeDroj> $aa="arg="foo"", 'arg=""""bar""""'
PS C:cygwinhomeDroj> start c:cygwinbinecho $aa -nonew -wait
arg=foo arg="bar"

No sé la razón exacta de esto, pero el comportamiento es como si se estuviera dando otro paso indocumentado bajo las sábanas que vuelve a analizar las cadenas. Por ejemplo, obtengo el mismo resultado si envío la matriz a un cmdlet, pero agrego un nivel de análisis al hacerlo invoke-expression:

PS C:cygwinhomeDroj> $aa="arg="foo"", 'arg=""""bar""""'
PS C:cygwinhomeDroj> iex "echo $aa"
arg=foo
arg="bar"

… que es exactamente lo que obtengo cuando envío estos argumentos al ‘echo.exe’ de mi instancia externa de Cygwin:

PS C:cygwinhomeDroj> c:cygwinbinecho 'arg="foo"' 'arg=""""bar""""'
arg=foo arg="bar"
¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *