¿Por qué Shell ignora las comillas en argumentos pasados ​​a través de variables?

Esto funciona como se anuncia:

# example 1 #!/bin/bash grep -ir 'hello world' . 

Esto no:

 # example 2 #!/bin/bash argumentString="-ir 'hello world'" grep $argumentString . 

A pesar de que 'hello world' está encerrado entre comillas en el segundo ejemplo, grep interpreta 'hello como un argumento y world' como otro, lo que significa que, en este caso, 'hello será el patrón de búsqueda y el world' será la ruta de búsqueda .

De nuevo, esto solo ocurre cuando los argumentos se expanden desde la variable argumentString . grep interpreta correctamente 'hello world' como un único argumento en el primer ejemplo.

¿Alguien puede explicar por qué es esto? ¿Existe una forma adecuada de expandir una variable de cadena que preserve la syntax de cada carácter de manera que los comandos de la shell la interpreten correctamente?

Por qué

Cuando la cadena se expande, se divide en palabras, pero no se vuelve a evaluar para encontrar caracteres especiales como comillas o signos de dólar o … Esta es la forma en que el caparazón se comportó “siempre”, ya que el shell Bourne vuelve en 1978 o más o menos.

Fijar

En bash , use una matriz para contener los argumentos:

 argumentArray=(-ir 'hello world') grep "${argumentArray[@]}" . 

O, si es valiente / temerario, use eval :

 argumentString="-ir 'hello world'" eval "grep $argumentString ." 

Por otro lado, la discreción es a menudo la mejor parte del valor, y trabajar con eval es un lugar donde la discreción es mejor que la valentía. Si no tiene el control completo de la cadena eval (si hay alguna entrada de usuario en la cadena de comandos que no se haya validado rigurosamente), entonces se está abriendo a problemas potencialmente graves.

Tenga en cuenta que la secuencia de expansiones para Bash se describe en Expansiones de Shell en el manual GNU Bash. Tenga en cuenta en particular las secciones 3.5.3 Expansión de parámetros de shell, 3.5.7 Word Splitting y 3.5.9 Quote Removal.

Cuando pones caracteres de comillas en variables, simplemente se convierten en literales simples (mira http://mywiki.wooledge.org/BashFAQ/050 ; gracias @tripleee por señalar este enlace)

En cambio, intente usar una matriz para pasar sus argumentos:

 argumentString=(-ir 'hello world') grep "${argumentString[@]}" . 

Dentro de las comillas dobles, las comillas simples pierden su significado especial por lo que son simples personajes ordinarios. Para que eso funcione, use algo como eval :

 eval grep $argumentString . 

que debería volver a ensamblar correctamente la línea de comando de las cadenas concatenadas.

Al ver esta y otras preguntas relacionadas, me sorprende que nadie haya mencionado una subcapa explícita. Para bash y otros shells modernos, puede ejecutar una línea de comando explícitamente. En bash, requiere la opción -c.

 argumentString="-ir 'hello world'" bash -c "grep $argumentString ." 

Funciona exactamente como deseaba el interrogador original. Hay dos restricciones a esta técnica:

  1. Solo puede usar comillas simples dentro del comando o las cadenas de argumentos.
  2. Solo las variables de entorno exportadas estarán disponibles para el comando

Además, esta técnica maneja la redirección y las tuberías, y también funcionan otros shells. También puede usar los comandos internos de bash, así como cualquier otro comando que funcione en la línea de comandos, porque esencialmente le está pidiendo a subshell bash que lo interprete directamente como una línea de comando. Aquí hay un ejemplo más complejo, una variante ls -l algo innecesariamente compleja.

 cmd="prefix=`pwd` && ls | xargs -n 1 echo \'In $prefix:\'" bash -c "$cmd" 

He construido procesadores de comandos tanto de esta forma como con matrices de parámetros. Generalmente, de esta manera es mucho más fácil escribir y depurar, y es trivial repetir el comando que está ejecutando. OTOH, las matrices param funcionan muy bien cuando realmente tienes matrices abstractas de parámetros, en lugar de solo querer una variante de comando simple.