Usando variáveis ​​de shell para opções de comando

19

Em um script Bash, estou tentando armazenar as opções para as quais estou usando rsyncem 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 ( .barnão é copiado), mas 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 o que exatamente está errado.

Florian Brucker
fonte

Respostas:

38

Em geral, é uma má ideia rebaixar uma lista de itens separados em uma única sequência, independentemente de ser uma lista de opções de linha de comando ou uma lista de nomes de caminho.

Usando uma matriz:

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

ou

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

e depois...

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

Dessa forma, a cotação das opções individuais é mantida (contanto que você duplique a expansão de ${rsync_options[@]}). Ele também permite que você manipule facilmente as entradas individuais da matriz, caso precise, antes de chamar rsync.

Em qualquer shell POSIX, pode-se usar a lista de parâmetros posicionais para isso:

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

rsync "$@" source/ target

Novamente, citar duas vezes a expansão de $@é crítico aqui.

Relacionado tangencialmente:


O problema é que, quando você coloca os dois conjuntos de opções em uma string, as aspas simples --excludedo valor da opção se tornam parte desse valor. Conseqüentemente,

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

teria funcionado¹ ... mas é melhor (como mais seguro) usar uma matriz ou os parâmetros posicionais com entradas entre aspas individuais. Fazer isso também permitirá que você use itens com espaços, se necessário, e evite que o shell execute a geração de nome de arquivo (globbing) nas opções.


¹ desde que $IFSnão seja modificado e que não exista nenhum arquivo cujo nome comece --exclude=.no diretório atual e que as opções nullglobou failglobshell não estejam definidas.

Kusalananda
fonte
O uso de uma matriz funciona bem, obrigado por sua resposta detalhada!
Florian Brucker
3

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 -rnve --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 $OPTSimpedem o tratamento especial das aspas simples, portanto, as últimas fazem parte do valor:

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

Quando agora usamos $OPTScomo argumento para um comando, as aspas são processadas antes da expansão dos parâmetros , portanto as aspas $OPTSocorrem "tarde demais".

Isso significa que (no meu problema original) rsyncusa 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=.*
Florian Brucker
fonte
Isso me deixou confuso por um longo tempo; portanto, uma explicação detalhada como essa é útil.
Joe Joe
0

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, comandos irá usar $1para se referir a Olá, $2para 42, e $3paraWorld

A referência da variável $0, será expandida para o nome do script atual, por exemplomy_script.sh

Não reproduza o código inteiro com comandos como variáveis.

Tenha em mente :

1 Evite usar nomes de variáveis ​​com letras maiúsculas em scripts.

2 Não use aspas, use $ (...), pois nidifica melhor.

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ão-corredor
fonte