¿Por qué fallan las opciones en una variable entre comillas, pero funcionan cuando no están entre comillas?

18

Leí sobre eso debería citar variables en bash, por ejemplo, "$ foo" en lugar de $ foo. Sin embargo, mientras escribía un script, encontré un caso en el que funciona sin comillas pero no con ellas:

wget_options='--mirror --no-host-directories'
local_root="$1" # ./testdir recieved from command line
remote_root="$2" # ftp://XXX recieved from command line 
relative_path="$3" # /XXX received from command line

Este funciona:

wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

Este no (tenga en cuenta las comillas dobles alrededor de $ wget_options):

wget "$wget_options" --directory_prefix="$local_root" "$remote_root$relative_path"
  • ¿Cuál es la razón para esto?

  • Es la primera línea la buena versión; ¿O debería sospechar que hay un error oculto en algún lugar que causa este comportamiento?

  • En general, ¿dónde encuentro buena documentación para entender cómo funciona bash y sus citas? Al escribir este script, siento que comencé a trabajar en una base de prueba y error en lugar de entender las reglas.

z32a7ul
fuente
3
Su pregunta se responde aquí: mywiki.wooledge.org/BashFAQ/050
glenn jackman
3
Vaya a la fuente de las reglas: el manual bash . Preste mucha atención a la sección 3.5 "Expansiones de Shell", especialmente la división de palabras y la expansión de nombre de archivo: estos dos factores son lo que utiliza comillas para controlar.
Glenn Jackman
44
Creo que ayuda a entender cómo funcionan los argumentos de línea de comandos en un nivel bajo. Cuando se ejecuta un programa, recibe argumentos como una lista de listas de caracteres (lo suficientemente cerca). Cada lista interna es lo que llamamos un "argumento". La mayoría de los programas dependen de la separación lógica entre argumentos. Aquí, verá que wgetno sabe qué --mirror --no-host-directoriessignifica (como un argumento), pero lo maneja cuando se divide en dos argumentos. Muy pocos programas tratan espacios y comillas especialmente una vez que están dentro del vector de argumento. El problema es que bash, y otros shells, deben ser>
HTNW
2
> utilizado por humanos. Sería molesto definir manualmente los límites entre los argumentos, por lo que los shells se dividen en espacios en blanco para convertir una línea (una lista de caracteres) en un vector de argumento (una lista de listas de caracteres). La expansión variable es una de las primeras expansiones bash, por lo que puede imaginar que $aes exactamente equivalente a escribir directamente su contenido. Ahora el problema es evidente: se a="-a -b"; cmd "$a"expande cmd "-a -b", pero cmdprobablemente no sabe lo que eso significa. cmd $aexpande a cmd -a -b, lo que probablemente hace el trabajo.
HTNW

Respuestas:

28

Básicamente, debe duplicar las expansiones de variables entre comillas para protegerlas de la división de palabras (y la generación de nombre de archivo). Sin embargo, en tu ejemplo,

wget_options='--mirror --no-host-directories'
wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

la división de palabras es exactamente lo que quieres .

Con "$wget_options"(citado), wgetno sabe qué hacer con el argumento único --mirror --no-host-directoriesy se queja

wget: unknown option -- mirror --no-host-directories

Para wgetver las dos opciones --mirrory, --no-host-directoriespor separado, tiene que ocurrir la división de palabras.

Hay formas más robustas de hacer esto. Si está utilizando basho cualquier otro shell que utiliza matrices como bashdo, consulte la respuesta de Glenn Jackman . La respuesta de Gilles describe adicionalmente una solución alternativa para conchas más simples como el estándar /bin/sh. Ambos esencialmente almacenan cada opción como un elemento separado en una matriz.

Pregunta relacionada con buenas respuestas: ¿Por qué mi script de shell se ahoga en espacios en blanco u otros caracteres especiales?


La doble cita de expansiones variables es una buena regla general. Hacer eso . Entonces tenga en cuenta los muy pocos casos en los que no debe hacer eso. Estos se le presentarán a través de mensajes de diagnóstico, como el mensaje de error anterior.

También hay algunos casos en los que no necesita citar expansiones variables. Pero de todos modos es más fácil continuar usando comillas dobles, ya que no hace mucha diferencia. Uno de esos casos es

variable=$other_variable

Otro es

case $variable in
    ...) ... ;;
esac
Kusalananda
fuente
2
Antes de usar ese operador split + glob, es posible que deba asegurarse de que $IFScontiene el valor correcto. Aquí debe dividirse en el espacio y el texto no contiene ninguna pestaña o nueva línea, por lo que el valor predeterminado de $IFSsería suficiente, pero si ese código se va a usar en una función que se puede invocar en un contexto donde $IFSpodría haberse modificado , querrás configurarlo de $IFSantemano (y posiblemente restaurarlo después o usar un ámbito local si el resto del código asume que no se ha modificado $IFS)
Stéphane Chazelas
32

La forma más sólida de codificar es usar una matriz:

wget_options=(
    --mirror 
    --no-host-directories
    --directory_prefix="$1"
)
wget "${wget_options[@]}" "$2/$3"
Glenn Jackman
fuente
Esta es la respuesta correcta. Referencia
l0b0
2
Es una buena respuesta, así que lo voté pero Kusalanda me ayudó más a entender por qué mi código estaba equivocado y solo puedo aceptar uno.
z32a7ul
Me encontraba en un mundo de problemas hasta que alguien en la lista rsync me mostró esta construcción. Es particularmente útil si algunos de los elementos pueden ser cadenas vacías. Esto hace que desaparezcan las cadenas vacías. Algunos comandos tienen gusto cpy rsyncharán cosas inesperadas si su comando se expande a algo así rsync '' rest of parameters. Esto es ideal para construir un comando pieza por pieza condicionalmente y luego ejecutarlo una vez en un solo lugar.
Joe
17

Está intentando almacenar una lista de cadenas en una variable de cadena. No encaja No importa cómo acceda a la variable, algo está roto.

wget_options='--mirror --no-host-directories'establece la variable wget_optionsen una cadena que contiene un espacio. En este punto, no hay forma de saber si se supone que el espacio es parte de una opción o un separador entre las opciones.

Cuando accede a la variable con una sustitución entre comillas wget "$wget_options", el valor de la variable se usa como una cadena. Esto significa que se pasa como un parámetro único a wget, por lo que es una sola opción. Esto se rompe en su caso porque pretendía que significara múltiples opciones.

Cuando utiliza una sustitución sin comillas wget $wget_options, el valor de la variable de cadena se somete a un proceso de expansión denominado "split + glob":

  1. Tome el valor de la variable y divídalo en partes delimitadas por espacios en blanco (suponiendo que no haya modificado la $IFSvariable). Esto da como resultado una lista intermedia de cadenas.
  2. Para cada elemento de la lista intermedia, si se trata de un patrón comodín que coincide con uno o más archivos, reemplace ese elemento por la lista de archivos coincidentes.

Esto sucede en su ejemplo, porque el proceso de división convierte el espacio en un separador, pero no funciona en general, ya que una opción podría contener espacios y caracteres comodín.

En ksh, bash, yash y zsh, puede usar una variable de matriz. Una matriz en terminología de shell es una lista de cadenas, por lo que no hay pérdida de información. Para hacer una variable de matriz, ponga paréntesis alrededor de los elementos de la matriz al asignar el valor a la variable. Para acceder a todos los elementos de la matriz, use : esta es una generalización de , que forma una lista de los elementos de la matriz. Tenga en cuenta que aquí también necesita las comillas dobles, de lo contrario, cada elemento se somete a split + glob."${VARIABLE[@]}""$@"

wget_options=(--mirror --no-host-directories --user-agent="I can haz spaces")
wget "${wget_options[@]}" 

En simple sh, no hay variables de matriz. Si no le importa perder los argumentos posicionales, puede usarlos para almacenar una lista de cadenas.

set -- --mirror --no-host-directories --user-agent="I can haz spaces"
wget "$@" 

Para obtener más información, consulte ¿Por qué mi script de shell se ahoga en espacios en blanco u otros caracteres especiales?

Gilles 'SO- deja de ser malvado'
fuente
Para sh llanura una subcapa preservaría los argumentos posicionales: (set -- ...; exec wget "$@" ...).
John Kugelman apoya a Mónica el