Usar variables de shell para opciones de comando

19

En un script Bash, estoy tratando de almacenar las opciones que estoy usando rsyncen una variable separada. Esto funciona bien para opciones simples (como --recursive), pero me encuentro con problemas con --exclude='.*':

$ find source
source
source/.bar
source/foo

$ rsync -rnv --exclude='.*' source/ dest
sending incremental file list
foo

sent 57 bytes  received 19 bytes  152.00 bytes/sec
total size is 0  speedup is 0.00 (DRY RUN)

$ RSYNC_OPTIONS="-rnv --exclude='.*'"

$ rsync $RSYNC_OPTIONS source/ dest
sending incremental file list
.bar
foo

sent 78 bytes  received 22 bytes  200.00 bytes/sec
total size is 0  speedup is 0.00 (DRY RUN)

Como puede ver, pasar --exclude='.*'a rsync"manualmente" funciona bien ( .barno se copia), no funciona cuando las opciones se almacenan primero en una variable.

Supongo que esto está relacionado con las comillas o el comodín (o ambos), pero no he podido averiguar qué es exactamente lo que está mal.

Florian Brucker
fuente

Respuestas:

38

En general, es una mala idea degradar una lista de elementos separados en una sola cadena, sin importar si es una lista de opciones de línea de comando o una lista de nombres de ruta.

Usando una matriz en su lugar:

rsync_options=( -rnv --exclude='.*' )

o

rsync_options=( -r -n -v --exclude='.*' )

y después...

rsync "${rsync_options[@]}" source/ target

De esta forma, se mantiene la cotización de las opciones individuales (siempre y cuando doble la expansión de ${rsync_options[@]}). También le permite manipular fácilmente las entradas individuales de la matriz, en caso de que necesite hacerlo, antes de llamar rsync.

En cualquier shell POSIX, uno puede usar la lista de parámetros posicionales para esto:

set -- -rnv --exclude='.*'

rsync "$@" source/ target

Nuevamente, la doble cita de la expansión de $@es crítica aquí.

Relacionado tangencialmente:


El problema es que cuando coloca los dos conjuntos de opciones en una cadena, las comillas simples --excludedel valor de la opción se convierten en parte de ese valor. Por lo tanto,

RSYNC_OPTIONS='-rnv --exclude=.*'

habría funcionado¹ ... pero es mejor (como más seguro) usar una matriz o los parámetros posicionales con entradas citadas individualmente. Hacerlo también le permitiría usar cosas con espacios en ellas, si fuera necesario, y evitaría que el shell realice la generación de nombre de archivo (globalización) en las opciones.


¹ siempre que $IFSno se modifique y que no hay ningún archivo cuyo nombre empieza con --exclude=.en el directorio actual, y que los nullglobo failglobno está ajustada concha opciones.

Kusalananda
fuente
Usar una matriz funciona bien, ¡gracias por tu respuesta detallada!
Florian Brucker
3

@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 -rnvy --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 $OPTSevitan el tratamiento especial de las comillas simples, por lo que estas últimas son parte del valor:

$ echo $OPTS
--exclude='.*'

Cuando ahora lo usamos $OPTScomo argumento para un comando, las comillas se procesan antes de la expansión del parámetro , por lo que las comillas $OPTSocurren "demasiado tarde".

Esto significa que (en mi problema original) rsyncusa 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=.*
Florian Brucker
fuente
Esto me confundió durante mucho tiempo, por lo que una explicación detallada como esta es útil.
Joe
0

Cuando escribimos funciones y scripts de shell, en los cuales los argumentos se pasan para ser procesados, los argumentos se pasarán en variables con nombres numéricos, por ejemplo, $ 1, $ 2, $ 3

Por ejemplo :

bash my_script.sh Hello 42 World

En el interior my_script.sh, los comandos se usarán $1para referirse a Hello, $2to 42y $3forWorld

La referencia de variable $0, se expandirá al nombre del script actual, p. Ej.my_script.sh

No juegues todo el código con comandos como variables.

Tener en cuenta :

1 Evite usar nombres de variables con mayúsculas en los scripts.

2 No use comillas inversas, use $ (...) en su lugar, anida mejor.

if [ $# -ne 2 ]
then
    echo "Usage: $(basename $0) DIRECTORY BACKUP_DIRECTORY"
    exit 1
fi

directory=$1
backup_directory=$2
current_date=$(date +%Y-%m-%dT%H-%M-%S)
backup_file="${backup_directory}/${current_date}.backup"

tar cv "$directory" | openssl des3 -salt | split -b 1024m - "$backup_file"
campeón-corredor
fuente