Em um script Bash, estou tentando armazenar as opções que estou usando rsync
em uma variável separada. Isso funciona bem para opções simples (como --recursive
), mas estou tendo problemas com --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 você pode ver, passar --exclude='.*'
para rsync
"manualmente" funciona bem ( .bar
não é copiado), não funciona quando as opções são armazenadas em uma variável primeiro.
Suponho que isso esteja relacionado às aspas ou ao curinga (ou ambos), mas não consegui descobrir exatamente o que está errado.
Em geral, é uma má ideia rebaixar uma lista de itens separados em uma única string, seja uma lista de opções de linha de comando ou uma lista de nomes de caminho.
Usando uma matriz em vez disso:
ou
e depois...
Desta forma, a cotação das opções individuais é mantida (desde que você faça aspas duplas na expansão de
${rsync_options[@]}
). Ele também permite que você manipule facilmente as entradas individuais da matriz, se você precisar fazer isso, antes de chamarrsync
.Em qualquer shell POSIX, pode-se usar a lista de parâmetros posicionais para isso:
Novamente, aspas duplas na expansão de
$@
são críticas aqui.Relacionado tangencialmente:
O problema é que quando você coloca os dois conjuntos de opções em uma string, as aspas simples
--exclude
do valor da opção se tornam parte desse valor. Por isso,teria funcionado¹... mas é melhor (como mais seguro) usar um array ou os parâmetros posicionais com entradas entre aspas individualmente. Fazer isso também permitiria que você usasse coisas com espaços, se necessário, e evita que o shell execute a geração de nome de arquivo (globbing) nas opções.
¹ desde que
$IFS
não seja modificado e que não haja nenhum arquivo cujo nome comece--exclude=.
no diretório atual e que as opçõesnullglob
oufailglob
shell não estejam definidas.@Kusalananda já explicou o problema básico e como resolvê-lo, e a entrada Bash FAQ vinculada por @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
):Opções de passagem "manualmente":
Como esperado, as partes
-rnv
e--exclude='.*'
são divididas em dois argumentos, pois são separados por espaços em branco sem aspas (isso é chamado de divisão de palavras ).Observe também que as aspas
.*
foram removidas: as aspas simples dizem ao shell para passar 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 :
Isso ocorre por dois motivos: as aspas duplas usadas na definição
$OPTS
impedem um tratamento especial das aspas simples, de modo que as últimas fazem parte do valor:Quando agora usamos
$OPTS
como argumento para um comando, as aspas são processadas antes da expansão do parâmetro , de modo que 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 por um ponto e termina com aspas simples. Obviamente não era isso que se pretendia.Uma solução alternativa seria omitir as aspas duplas ao definir
$OPTS
: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 evitar a expansão do padrão , mas isso não era estritamente necessário neste caso especial :Acontece que o Bash executa a expansão do padrão, mas o padrão
--exclude=.*
não corresponde a nenhum arquivo, então o padrão é passado para o comando. Comparar:No entanto, não citar o padrão é perigoso, porque se (por qualquer motivo) houver um arquivo correspondente
--exclude=.*
, o padrão será expandido:Finalmente, vamos ver por que usar um array evita meu problema de citação (além das outras vantagens de usar arrays para armazenar argumentos de comando).
Ao definir o array, a divisão de palavras e o tratamento de aspas acontecem conforme o esperado:
Ao passar as opções para o comando, usamos a sintaxe
"${ARRAY[@]}"
, que expande cada elemento do array em uma palavra separada:Quando escrevemos funções e scripts de shell, nos quais os argumentos são passados para serem processados, os argumentos serão passados em variáveis nomeadas numericamente, por exemplo, $1, $2, $3
Por exemplo :
bash my_script.sh Hello 42 World
Dentro
my_script.sh
de , os comandos usarão$1
para se referir a Hello,$2
to42
e$3
forWorld
A referência da variável,
$0
, será expandida para o nome do script atual, por exemplomy_script.sh
Não jogue todo o código com comandos como variáveis.
Tenha em mente :
1 Evite usar nomes de variáveis em letras maiúsculas em scripts.
2 Não use aspas, use $(...) em vez disso, aninha melhor.