Por que as opções em uma variável citada falham, mas funcionam quando não são citadas?

18

Eu li sobre isso que devo citar variáveis ​​no bash, por exemplo, "$ foo" em vez de $ foo. No entanto, ao escrever um script, encontrei um caso em que funciona sem aspas, mas não com elas:

wget_options='--mirror --no-host-directories'
local_root="$1" # ./testdir recieved from command line
remote_root="$2" # ftp://XXX recieved from command line 
relative_path="$3" # /XXX received from command line

Este funciona:

wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

Este não (observe as aspas duplas entre $ wget_options):

wget "$wget_options" --directory_prefix="$local_root" "$remote_root$relative_path"
  • Qual é a razão para isto?

  • A primeira linha é a boa versão; ou devo suspeitar que há um erro oculto em algum lugar que causa esse comportamento?

  • Em geral, onde encontro boa documentação para entender como o bash e suas citações funcionam? Ao escrever este script, sinto que comecei a trabalhar com base em tentativa e erro, em vez de entender as regras.

z32a7ul
fonte
3
Sua pergunta é respondida aqui: mywiki.wooledge.org/BashFAQ/050
Glenn Jackman
3
Vá para a fonte para obter as regras: o manual do bash . Preste muita atenção à seção 3.5 "Expansões do shell", especialmente divisão de palavras e expansão de nome de arquivo - esses dois fatores são o que você usa aspas para controlar.
Glenn Jackman
4
Acho que ajuda a entender como os argumentos da linha de comando funcionam em um nível baixo. Quando um programa é executado, ele recebe argumentos como uma lista de listas de caracteres (suficientemente perto). Cada lista interna é o que chamamos de "argumento". A maioria dos programas depende da separação lógica entre args. Aqui, você vê que wgetnão sabe o que --mirror --no-host-directoriessignifica (como um argumento), mas lida com isso quando é dividido em dois argumentos. Muito poucos programas tratam espaços e aspas especialmente quando estão dentro do vetor de argumento. O problema é que bash, e outras conchas, são destinadas a ser>
HTNW
2
> usado por seres humanos. Seria irritante definir manualmente os limites entre argumentos, para que as conchas se dividam em espaço em branco para transformar uma linha (uma lista de caracteres) em um vetor de argumento (uma lista de listas de caracteres). A expansão variável é uma das primeiras expansões bash, então você pode imaginar que $aé exatamente equivalente a escrever diretamente seu conteúdo. Agora a questão é evidente: a="-a -b"; cmd "$a"expande para cmd "-a -b", mas cmdprovavelmente não sabe o que isso significa. cmd $aexpande-se para cmd -a -b, o que provavelmente faz o trabalho.
HTNW 27/08/17

Respostas:

28

Basicamente, você deve colocar aspas variáveis ​​nas expansões para protegê-las da divisão de palavras (e geração de nome de arquivo). No entanto, no seu exemplo,

wget_options='--mirror --no-host-directories'
wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

A divisão de palavras é exatamente o que você deseja .

Com "$wget_options"(citado), wgetnão sabe o que fazer com o argumento único --mirror --no-host-directoriese reclama

wget: unknown option -- mirror --no-host-directories

Para wgetver as duas opções --mirrore --no-host-directoriescomo separadas, a divisão de palavras deve ocorrer.

Existem maneiras mais robustas de fazer isso. Se você estiver usando bashou qualquer outro shell que use matrizes como bashdo, consulte a resposta de glenn jackman . A resposta de Gilles descreve adicionalmente uma solução alternativa para cascas mais simples, como o padrão /bin/sh. Ambos armazenam essencialmente cada opção como um elemento separado em uma matriz.

Pergunta relacionada com boas respostas: Por que meu script de shell engasga com espaços em branco ou outros caracteres especiais?


A citação dupla de expansões variáveis ​​é uma boa regra geral. Faça isso . Então esteja ciente dos poucos casos em que você não deve fazer isso. Eles se apresentarão a você por meio de mensagens de diagnóstico, como a mensagem de erro acima.

Existem também alguns casos em que você não precisa citar expansões variáveis. Mas é mais fácil continuar usando aspas duplas de qualquer maneira, pois isso não faz muita diferença. Um desses casos é

variable=$other_variable

Outro é

case $variable in
    ...) ... ;;
esac
Kusalananda
fonte
2
Antes de usar esse operador split + glob, pode ser necessário certificar-se de que $IFScontém o valor correto. Aqui você precisa dividir o espaço e o texto passa a não conter nenhuma guia ou nova linha, portanto o valor padrão de $IFSfaria, mas se esse código for usado em uma função que pode ser chamada em um contexto em que $IFSpoderia ter sido modificado , que você gostaria de definir $IFSde antemão (e, possivelmente, restaurá-lo mais tarde ou usar um escopo local para ele se o resto do código assume uma não modificada $IFS)
Stéphane Chazelas
32

A maneira mais robusta de codificar é usar uma matriz:

wget_options=(
    --mirror 
    --no-host-directories
    --directory_prefix="$1"
)
wget "${wget_options[@]}" "$2/$3"
Glenn Jackman
fonte
Esta é a resposta certa. Referência
l0b0 27/08/17
2
É uma boa resposta, então eu a votei, mas a Kusalanda me ajudou mais a entender por que meu código estava errado e posso aceitar apenas uma.
Z32a7ul
Eu estava enfrentando um mundo de problemas até que alguém na lista rsync me mostrou essa construção. É particularmente útil se alguns dos elementos puderem ser cadeias vazias. Isso faz com que as cadeias vazias desapareçam. Alguns comandos gostam cpe rsyncfarão coisas inesperadas se o seu comando se expandir para algo como rsync '' rest of parameters. Isso é ótimo para criar uma peça de comando por peça condicionalmente e depois executá-la uma vez em um só lugar.
Joe
17

Você está tentando armazenar uma lista de strings em uma variável de string. Não serve. Não importa como você acessa a variável, algo está quebrado.

wget_options='--mirror --no-host-directories'define a variável wget_optionscomo uma sequência que contém um espaço. Neste ponto, não há como saber se o espaço deve fazer parte de uma opção ou um separador entre as opções.

Quando você acessa a variável com uma substituição entre aspas wget "$wget_options", o valor da variável é usado como uma sequência. Isso significa que ele é passado como um único parâmetro para wget, portanto, é uma única opção. Isso ocorre no seu caso, porque você pretendia que isso significasse várias opções.

Quando você usa uma substituição não cotada wget $wget_options, o valor da variável de sequência passa por um processo de expansão apelidado de "split + glob":

  1. Pegue o valor da variável e divida-o em partes delimitadas por espaços em branco (assumindo que você não modificou a $IFSvariável). Isso resulta em uma lista intermediária de seqüências de caracteres.
  2. Para cada elemento da lista intermediária, se for um padrão curinga que corresponda a um ou mais arquivos, substitua esse elemento pela lista de arquivos correspondentes.

Isso funciona no seu exemplo, porque o processo de divisão transforma o espaço em um separador, mas geralmente não funciona, pois uma opção pode conter espaços e caracteres curinga.

No ksh, bash, yash e zsh, você pode usar uma variável de matriz. Uma matriz na terminologia do shell é uma lista de cadeias, portanto, não há perda de informações. Para criar uma variável de matriz, coloque parênteses em torno dos elementos da matriz ao atribuir o valor à variável. Para acessar todos os elementos da matriz, use - isto é uma generalização de , que forma uma lista dos elementos da matriz. Observe que você também precisa de aspas duplas aqui, caso contrário, cada elemento passa por divisão + glob."${VARIABLE[@]}""$@"

wget_options=(--mirror --no-host-directories --user-agent="I can haz spaces")
wget "${wget_options[@]}" 

No sh simples, não há variáveis ​​de matriz. Se você não se importa de perder os argumentos posicionais, pode usá-los para armazenar uma lista de strings.

set -- --mirror --no-host-directories --user-agent="I can haz spaces"
wget "$@" 

Para obter mais informações, consulte Por que meu script de shell é bloqueado em espaço em branco ou em outros caracteres especiais?

Gilles 'SO- parar de ser mau'
fonte
Para sh planície um subshell preservaria os argumentos posicionais: (set -- ...; exec wget "$@" ...).
John Kugelman apoia Monica