@Kusalananda ya ha explicado el problema básico y cómo resolverlo, y la entrada de preguntas frecuentes de Bash vinculada por @glenn jackmann también proporciona mucha información útil. Aquí hay una explicación detallada de lo que está sucediendo en mi problema basado en estos recursos.
Usaremos un pequeño script que imprima cada uno de sus argumentos en una línea separada para ilustrar cosas ( argtest.bash
):
#!/bin/bash
for var in "$@"
do
echo "$var"
done
Opciones de paso "manualmente":
$ ./argtest.bash -rnv --exclude='.*'
-rnv
--exclude=.*
Como se esperaba, las partes -rnv
y --exclude='.*'
se dividen en dos argumentos, ya que están separados por espacios en blanco sin comillas (esto se llama división de palabras ).
También tenga en cuenta que las comillas .*
se han eliminado: las comillas simples le dicen al shell que pase su contenido sin una interpretación especial , pero las comillas mismas no se pasan al comando .
Si ahora almacenamos las opciones en una variable como una cadena (en lugar de usar una matriz), las comillas no se eliminan :
$ OPTS="--exclude='.*'"
$ ./argtest.bash $OPTS
--exclude='.*'
Esto se debe a dos razones: las comillas dobles utilizadas al definir $OPTS
evitan el tratamiento especial de las comillas simples, por lo que estas últimas son parte del valor:
$ echo $OPTS
--exclude='.*'
Cuando ahora lo usamos $OPTS
como argumento para un comando, las comillas se procesan antes de la expansión del parámetro , por lo que las comillas $OPTS
ocurren "demasiado tarde".
Esto significa que (en mi problema original) rsync
usa el patrón de exclusión '.*'
(¡con comillas!) En lugar del patrón .*
; excluye los archivos cuyo nombre comienza con una comilla simple seguido de un punto y termina con una comilla simple. Obviamente, eso no es lo que se pretendía.
Una solución habría sido omitir las comillas dobles al definir $OPTS
:
$ OPTS2=--exclude='.*'
$ ./argtest.bash $OPTS2
--exclude=.*
Sin embargo, es una buena práctica citar siempre las asignaciones de variables debido a diferencias sutiles en casos más complejos.
Como señaló @Kusalananda, no citar .*
también habría funcionado. Había agregado las citas para evitar la expansión del patrón , pero eso no era estrictamente necesario en este caso especial :
$ ./argtest.bash --exclude=.*
--exclude=.*
Resulta que Bash hace realizar expansión de los patrones, pero el patrón --exclude=.*
no coincide con ningún archivo, por lo que el patrón se pasa al comando. Comparar:
$ touch some_file
$ ./argtest.bash some_*
some_file
$ ./argtest.bash does_not_exit_*
does_not_exit_*
Sin embargo, no citar el patrón es peligroso, porque si (por alguna razón) hubo una coincidencia de archivos --exclude=.*
, el patrón se expande:
$ touch -- --exclude=.special-filenames-happen
$ ./argtest.bash --exclude=.*
--exclude=.special-filenames-happen
Finalmente, veamos por qué usar una matriz evita mi problema de citas (además de las otras ventajas de usar matrices para almacenar argumentos de comando).
Al definir la matriz, la división de palabras y el manejo de comillas ocurren como se esperaba:
$ ARRAY_OPTS=( -rnv --exclude='.*' )
$ echo length of the array: "${#ARRAY_OPTS[@]}"
length of the array: 2
$ echo first element: "${ARRAY_OPTS[0]}"
first element: -rnv
$ echo second element: "${ARRAY_OPTS[1]}"
second element: --exclude=.*
Al pasar las opciones al comando, usamos la sintaxis "${ARRAY[@]}"
, que expande cada elemento de la matriz en una palabra separada:
$ ./argtest.bash "${ARRAY_OPTS[@]}"
-rnv
--exclude=.*