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

9

Em um script de Bash, estou tentando armazenar as opções que estou usando para 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.

    
por Florian Brucker 30.07.2018 / 15:52

2 respostas

23

Em geral, é uma má ideia rebaixar uma lista de itens separados em uma única sequência, não importando se é 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 mais tarde ...

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

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

Em qualquer shell parecido com o Bourne, pode-se usar a lista de parâmetros posicionais para isso:

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

rsync "$@" source/ target

Novamente, duplicar a expansão de $@ é fundamental aqui.

Tangencialmente relacionado:

A questão é que quando você coloca os dois conjuntos de opções em uma string, as aspas simples do valor da opção --exclude tornam-se parte desse valor. Portanto,

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

teria funcionado ... mas é melhor (quanto mais seguro) usar uma matriz com entradas citadas individualmente. Usar um array também permite que você use coisas com espaços, se for necessário, e evita que o shell execute geração de nome de arquivo (globbing) nas opções.

¹ contanto que $IFS não seja modificado e que não haja nenhum arquivo cujo nome comece com --exclude=. no diretório atual

    
por 30.07.2018 / 15:55
1

@ Kusalananda já explicou o problema básico e como resolvê-lo, e o entrada de FAQ da Bash , linkada 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 script pequeno que imprime cada um dos seus argumentos em uma linha separada para ilustrar as coisas ( argtest.bash ):

#!/bin/bash

for var in "$@"
do
    echo "$var"
done

Opções de passagem "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 sem aspas (isso é chamado divisão de palavras ).

Observe também que as aspas em torno de .* foram removidas: as aspas simples informam ao shell para passar seu conteúdo sem interpretação especial , mas as citações em si não são passadas para o comando .

Se agora armazenarmos as opções em uma variável como uma string (em oposição a usar uma matriz), as citações não serão removidas :

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

$ ./argtest.bash $OPTS
--exclude='.*'

Isso se deve a dois motivos: as aspas duplas usadas na definição de $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 cotações são processadas antes da expansão do parâmetro . aspas em $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 .* - exclui arquivos cujo nome começa com aspas simples seguidas por um ponto e termina com uma simples cotação. Obviamente, não é 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 as 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 expansão de padrões , mas isso não foi estritamente necessário neste caso especial :

$ ./argtest.bash --exclude=.*
--exclude=.*

Acontece que Bash faz executar expansão de padrão, mas o padrão --exclude=.* não corresponde a nenhum arquivo, então 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 a --exclude=.* , o padrão será expandido:

$ touch -- --exclude=.special-filenames-happen

$ ./argtest.bash --exclude=.*
--exclude=.special-filenames-happen

Por fim, vamos ver porque usar um array evita meu problema de citação (além das outras vantagens de usar arrays para armazenar argumentos de comando).

Ao definir a matriz, a divisão de palavras e o tratamento de cotações ocorrem 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=.*
    
por 31.07.2018 / 09:36

Tags