O @Kusalananda já explicou o problema básico e como resolvê-lo, e a entrada Perguntas frequentes do Bash vinculada a @glenn jackmann também fornece muitas informações úteis. Aqui está uma explicação detalhada do que está acontecendo no meu problema com base nesses recursos.
Usaremos um pequeno script que imprime cada um de seus argumentos em uma linha separada para ilustrar as coisas ( argtest.bash
):
#!/bin/bash
for var in "$@"
do
echo "$var"
done
Passando opções "manualmente":
$ ./argtest.bash -rnv --exclude='.*'
-rnv
--exclude=.*
Como esperado, as partes -rnv
e --exclude='.*'
são divididas em dois argumentos, pois são separadas por espaço em branco não citado (isso é chamado de divisão de palavras ).
Observe também que as aspas .*
foram removidas: as aspas simples dizem ao shell para transmitir seu conteúdo sem interpretação especial , mas as aspas em si não são passadas para o comando .
Se agora armazenarmos as opções em uma variável como uma string (em vez de usar uma matriz), as aspas não serão removidas :
$ OPTS="--exclude='.*'"
$ ./argtest.bash $OPTS
--exclude='.*'
Isso ocorre por dois motivos: as aspas duplas usadas na definição $OPTS
impedem o tratamento especial das aspas simples, portanto, as últimas fazem parte do valor:
$ echo $OPTS
--exclude='.*'
Quando agora usamos $OPTS
como argumento para um comando, as aspas são processadas antes da expansão dos parâmetros , portanto as aspas $OPTS
ocorrem "tarde demais".
Isso significa que (no meu problema original) rsync
usa o padrão de exclusão '.*'
(com aspas!) Em vez do padrão .*
- ele exclui arquivos cujo nome começa com aspas simples seguido de um ponto e termina com uma aspas simples. Obviamente, não era isso que se pretendia.
Uma solução alternativa seria omitir as aspas duplas ao definir $OPTS
:
$ OPTS2=--exclude='.*'
$ ./argtest.bash $OPTS2
--exclude=.*
No entanto, é uma boa prática sempre citar atribuições de variáveis devido a diferenças sutis em casos mais complexos.
Como @Kusalananda observou, não citar .*
também teria funcionado. Eu adicionei as aspas para impedir a expansão do padrão , mas isso não era estritamente necessário neste caso especial :
$ ./argtest.bash --exclude=.*
--exclude=.*
Acontece que Bash faz executar expansão padrão, mas o padrão --exclude=.*
não corresponde a qualquer arquivo, para que o padrão é passado para o comando. Comparar:
$ touch some_file
$ ./argtest.bash some_*
some_file
$ ./argtest.bash does_not_exit_*
does_not_exit_*
No entanto, não citar o padrão é perigoso, porque se (por qualquer motivo) houver um arquivo correspondente --exclude=.*
, o padrão será expandido:
$ touch -- --exclude=.special-filenames-happen
$ ./argtest.bash --exclude=.*
--exclude=.special-filenames-happen
Finalmente, vamos ver por que usar uma matriz evita meu problema de citação (além das outras vantagens de usar matrizes para armazenar argumentos de comando).
Ao definir a matriz, a divisão de palavras e o tratamento de cotações acontecem conforme o esperado:
$ 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=.*
Ao passar as opções para o comando, usamos a sintaxe "${ARRAY[@]}"
, que expande cada elemento da matriz em uma palavra separada:
$ ./argtest.bash "${ARRAY_OPTS[@]}"
-rnv
--exclude=.*