Implicações de segurança do esquecimento de citar uma variável nos shells bash / POSIX

206

Se você acompanha o unix.stackexchange.com há algum tempo, esperamos que você já saiba que deixar uma variável sem aspas no contexto da lista (como em echo $var) nos shells Bourne / POSIX (zsh é a exceção) tem um significado muito especial e não deve ser feito, a menos que você tenha uma boa razão para fazê-lo.

É discutido longamente em uma série de perguntas e respostas aqui (Exemplos: ? Por que o meu shell choke script em espaços ou outros caracteres especiais , quando é colocar aspas necessário? , Expansão de uma variável shell e efeito de glob e dividido sobre ele , Citado vs expansão de cadeia não citada)

Esse tem sido o caso desde o lançamento inicial do shell Bourne no final dos anos 70 e não foi alterado pelo shell Korn (um dos maiores arrependimentos de David Korn (pergunta nº 7) ) ou bashque copiou principalmente o shell Korn, e é isso como isso foi especificado pelo POSIX / Unix.

Agora, ainda estamos vendo várias respostas aqui e até ocasionalmente código de shell publicamente divulgado, onde as variáveis ​​não são citadas. Você pensaria que as pessoas já teriam aprendido até agora.

Na minha experiência, existem principalmente três tipos de pessoas que omitem citar suas variáveis:

  • iniciantes. Essas podem ser desculpadas, pois é uma sintaxe completamente não intuitiva. E é nosso papel neste site educá-los.

  • pessoas esquecidas.

  • pessoas que não estão convencidas, mesmo depois de repetidas marteladas, que pensam que certamente o autor do shell Bourne não pretendia citar todas as nossas variáveis .

Talvez possamos convencê-los se expormos o risco associado a esse tipo de comportamento.

Qual é a pior coisa que pode acontecer se você esquecer de citar suas variáveis. É realmente tão ruim assim?

De que tipo de vulnerabilidade estamos falando aqui?

Em que contextos isso pode ser um problema?

Stéphane Chazelas
fonte
8
BashPitfalls é algo que você vai gostar, eu acho.
precisa saber é o seguinte
backlink deste artigo que escrevi , obrigado pela
redação
5
Gostaria de sugerir a adição de um quarto grupo: as pessoas que foram atingidos na cabeça por excessivas citando muitas vezes, talvez por membros do terceiro grupo tirando sua frustração sobre os outros (a vítima se tornar o valentão). O triste, é claro, é que aqueles do quarto grupo podem acabar falhando em citar as coisas quando mais importa.
Ack

Respostas:

201

Preâmbulo

Primeiro, eu diria que não é o caminho certo para resolver o problema. É um pouco como dizer " você não deve matar pessoas porque senão irá para a cadeia ".

Da mesma forma, você não cita sua variável porque, de outra forma, está apresentando vulnerabilidades de segurança. Você cita suas variáveis ​​porque é errado não fazê-lo (mas se o medo da prisão pode ajudar, por que não)?

Um pequeno resumo para aqueles que acabaram de pular no trem.

Na maioria dos shells, deixar uma expansão variável sem aspas (embora isso (e o restante desta resposta) também se aplique à substituição de comando ( `...`ou $(...)) e expansão aritmética ( $((...))ou $[...])) tem um significado muito especial. A melhor maneira de descrevê-lo é como invocar algum tipo de operador split + glob implícito¹ .

cmd $var

em outro idioma seria escrito algo como:

cmd(glob(split($var)))

$varé primeiro dividido em uma lista de palavras de acordo com regras complexas que envolvem o $IFSparâmetro especial (a parte dividida ) e, em seguida, cada palavra resultante dessa divisão é considerada um padrão que é expandido para uma lista de arquivos correspondentes (a parte glob ) .

Por exemplo, se $varcontiver *.txt,/var/*.xmle $IFS contiver ,, cmdseria chamado com vários argumentos, sendo o primeiro cmde os próximos os txt arquivos no diretório atual e os xmlarquivos em /var.

Se você quisesse chamar cmdapenas com os dois argumentos literais cmd e *.txt,/var/*.xml, escreveria:

cmd "$var"

que seria em seu outro idioma mais familiar:

cmd($var)

O que queremos dizer com vulnerabilidade em um shell ?

Afinal, sabe-se desde o início que os scripts de shell não devem ser usados ​​em contextos sensíveis à segurança. Certamente, OK, deixar uma variável sem citação é um bug, mas isso não pode causar tanto dano, pode?

Bem, apesar de alguém dizer a você que scripts de shell nunca devem ser usados ​​para CGIs da Web, ou que, felizmente, a maioria dos sistemas não permite scripts de shell setuid / setgid hoje em dia, uma coisa que faz o shellshock (o bug bash remotamente explorável que fez o manchetes em setembro de 2014) revelado é que os shells ainda são amplamente utilizados onde provavelmente não deveriam: nos CGIs, nos scripts de gancho do cliente DHCP, nos comandos sudoers, invocados por (se não como ) comandos setuid ...

Às vezes sem saber. Por exemplo, system('cmd $PATH_INFO') em um script php/ perl/ pythonCGI chama um shell para interpretar essa linha de comando (para não mencionar o fato de que cmdele próprio pode ser um script shell e seu autor nunca esperou que fosse chamado de um CGI).

Você tem uma vulnerabilidade quando há um caminho para a escalada de privilégios, ou seja, quando alguém (vamos chamá-lo de atacante ) é capaz de fazer algo a que não se destina.

Invariavelmente, isso significa que o invasor fornece dados, que são processados ​​por um usuário / processo privilegiado que, inadvertidamente, faz algo que não deveria estar fazendo, na maioria dos casos por causa de um bug.

Basicamente, você tem um problema quando seu código de buggy processa dados sob o controle do invasor .

Agora, nem sempre é óbvio de onde vêm esses dados , e muitas vezes é difícil saber se seu código processará dados não confiáveis.

No que diz respeito às variáveis, no caso de um script CGI, é bastante óbvio, os dados são os parâmetros CGI GET / POST e coisas como parâmetros de cookies, caminho, host ...

Para um script setuid (executando como um usuário quando chamado por outro), são os argumentos ou variáveis ​​de ambiente.

Outro vetor muito comum são os nomes de arquivos. Se você estiver obtendo uma lista de arquivos de um diretório, é possível que os arquivos tenham sido plantados lá pelo invasor .

Nesse sentido, mesmo no prompt de um shell interativo, você pode estar vulnerável (ao processar arquivos em /tmpou ~/tmp por exemplo).

Até mesmo um ~/.bashrcpode ser vulnerável (por exemplo, basho interpretará quando chamado sshpara executar o ForcedCommand mesmo em gitimplantações de servidor com algumas variáveis ​​sob o controle do cliente).

Agora, um script não pode ser chamado diretamente para processar dados não confiáveis, mas pode ser chamado por outro comando que o faça. Ou seu código incorreto pode ser copiado e colado em scripts (por você, três anos depois ou por um de seus colegas). Um local em que é particularmente crítico é a resposta nos sites de perguntas e respostas, pois você nunca saberá onde as cópias do seu código podem acabar.

Direto aos negócios; quão ruim é isso?

Deixar uma variável (ou substituição de comando) sem aspas é de longe a fonte número um de vulnerabilidades de segurança associadas ao código do shell. Em parte porque esses erros geralmente se traduzem em vulnerabilidades, mas também porque é muito comum ver variáveis ​​não citadas.

Na verdade, ao procurar vulnerabilidades no código do shell, a primeira coisa a fazer é procurar variáveis ​​não citadas. É fácil identificar, geralmente um bom candidato, geralmente fácil rastrear os dados controlados pelo invasor.

Há um número infinito de maneiras pelas quais uma variável não citada pode se transformar em uma vulnerabilidade. Vou apenas dar algumas tendências comuns aqui.

Divulgação de informação

A maioria das pessoas encontra bugs associados a variáveis ​​não citadas por causa da parte dividida (por exemplo, é comum os arquivos terem espaços em seus nomes hoje em dia e o espaço ter o valor padrão do IFS). Muitas pessoas vão ignorar a parte glob . A parte glob é pelo menos tão perigosa quanto a parte dividida .

Observar as entradas externas não autorizadas significa que o invasor pode fazer você ler o conteúdo de qualquer diretório.

No:

echo You entered: $unsanitised_external_input

se $unsanitised_external_inputcontiver /*, significa que o invasor pode ver o conteúdo de /. Nada demais. Torna-se mais interessante, porém, com o /home/*qual fornece uma lista de nomes de usuário na máquina /tmp/*, /home/*/.forwardpara dicas de outras práticas perigosas, /etc/rc*/*para serviços ativados ... Não há necessidade de nomeá-los individualmente. Um valor de /* /*/* /*/*/*...apenas listará todo o sistema de arquivos.

Vulnerabilidades de negação de serviço.

Levando o caso anterior um pouco longe demais e temos um DoS.

Na verdade, qualquer variável não citada no contexto de lista com entrada não autorizada é pelo menos uma vulnerabilidade de DoS.

Mesmo os scripts de shell especialistas geralmente esquecem de citar coisas como:

#! /bin/sh -
: ${QUERYSTRING=$1}

:é o comando no-op. O que poderia dar errado?

Isso deve ser atribuído $1a $QUERYSTRINGse $QUERYSTRING não estava definido. Essa é uma maneira rápida de tornar também possível um script CGI a partir da linha de comando.

Isso $QUERYSTRINGainda é expandido e, como não é citado, o operador split + glob é chamado.

Agora, existem alguns globs que são particularmente caros para expandir. A /*/*/*/*um é ruim o suficiente, já que significa diretórios listagem até 4 níveis abaixo. Além da atividade de disco e CPU, isso significa armazenar dezenas de milhares de caminhos de arquivo (40k aqui em uma VM de servidor mínima, 10k dos quais diretórios).

Agora /*/*/*/*/../../../../*/*/*/*significa 40k x 10k e /*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*é suficiente para trazer até a máquina mais poderosa de joelhos.

Experimente você mesmo (embora esteja preparado para travar ou travar sua máquina):

a='/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*' sh -c ': ${a=foo}'

Obviamente, se o código for:

echo $QUERYSTRING > /some/file

Então você pode encher o disco.

Basta fazer uma pesquisa no google em shell cgi ou bash cgi ou ksh cgi , e você encontrará algumas páginas que mostram como escrever CGIs em shell. Observe como metade daqueles que processam parâmetros são vulneráveis.

Até o próprio David Korn é vulnerável (veja o manuseio de cookies).

até vulnerabilidades arbitrárias de execução de código

A execução arbitrária de código é o pior tipo de vulnerabilidade, pois se o invasor pode executar qualquer comando, não há limite para o que ele pode fazer.

Essa geralmente é a parte dividida que leva a eles. Essa divisão resulta em vários argumentos a serem passados ​​para comandos quando apenas um é esperado. Enquanto o primeiro deles será usado no contexto esperado, os outros estarão em um contexto diferente, sendo potencialmente interpretados de maneira diferente. Melhor com um exemplo:

awk -v foo=$external_input '$2 == foo'

Aqui, a intenção era atribuir o conteúdo da $external_inputvariável shell à foo awkvariável.

Agora:

$ external_input='x BEGIN{system("uname")}'
$ awk -v foo=$external_input '$2 == foo'
Linux

A segunda palavra resultante da divisão de $external_input não é atribuída, foomas considerada como awkcódigo (aqui que executa um comando arbitrário:) uname.

Isso é um problema especialmente para os comandos que podem executar outros comandos ( awk, env, sed(GNU um), perl, find...) especialmente com as variantes GNU (que aceitam opções depois de argumentos). Às vezes, você não suspeitaria de comandos capazes de executar outras pessoas como ksh, bashou zsh's [ou printf...

for file in *; do
  [ -f $file ] || continue
  something-that-would-be-dangerous-if-$file-were-a-directory
done

Se criarmos um diretório chamado x -o yes, o teste se tornará positivo, porque estamos avaliando uma expressão condicional completamente diferente.

Pior, se criarmos um arquivo chamado x -a a[0$(uname>&2)] -gt 1, com pelo menos todas as implementações do ksh (que inclui a sh maioria dos Unices comerciais e alguns BSDs), que será executado uname porque esses shells executam uma avaliação aritmética nos operadores de comparação numérica do [comando.

$ touch x 'x -a a[0$(uname>&2)] -gt 1'
$ ksh -c 'for f in *; do [ -f $f ]; done'
Linux

O mesmo acontece com bashum nome de arquivo como x -a -v a[0$(uname>&2)].

Obviamente, se eles não conseguirem uma execução arbitrária, o atacante pode se contentar com menos danos (o que pode ajudar a obter uma execução arbitrária). Qualquer comando que possa gravar arquivos ou alterar permissões, propriedade ou ter qualquer efeito principal ou colateral pode ser explorado.

Todo tipo de coisa pode ser feita com nomes de arquivos.

$ touch -- '-R ..'
$ for file in *; do [ -f "$file" ] && chmod +w $file; done

E você acaba ..gravando (recursivamente com o GNU chmod).

Os scripts que executam o processamento automático de arquivos em áreas publicamente graváveis ​​como /tmpdevem ser escritos com muito cuidado.

Sobre [ $# -gt 1 ]

Isso é algo que eu acho exasperante. Algumas pessoas se preocupam em saber se uma expansão específica pode ser problemática para decidir se podem omitir as aspas.

É como dizer. Ei, parece que $#não pode estar sujeito ao operador split + glob, vamos pedir ao shell para dividir + glob . Ou seja, vamos escrever um código incorreto apenas porque é improvável que o bug seja atingido .

Agora, quão improvável é isso? OK, $#(ou $!, $?ou qualquer substituição aritmética) pode conter apenas dígitos (ou -para alguns) de modo que o glob parte está fora. Para a divisão de parte a fazer algo, porém, tudo o que precisamos é para $IFSconter dígitos (ou -).

Com algumas conchas, $IFSpode ser herdado do ambiente, mas se o ambiente não for seguro, o jogo terminará assim mesmo.

Agora, se você escrever uma função como:

my_function() {
  [ $# -eq 2 ] || return
  ...
}

O que isso significa é que o comportamento de sua função depende do contexto em que é chamada. Ou, em outras palavras, $IFS torna-se uma das entradas para ele. Estritamente falando, quando você escreve a documentação da API para sua função, deve ser algo como:

# my_function
#   inputs:
#     $1: source directory
#     $2: destination directory
#   $IFS: used to split $#, expected not to contain digits...

E o código que chama sua função precisa garantir $IFSque não contenha dígitos. Tudo isso porque você não sentiu vontade de digitar esses dois caracteres de aspas duplas.

Agora, para que esse [ $# -eq 2 ]bug se torne uma vulnerabilidade, você precisaria, de alguma forma, do valor de $IFSficar sob controle do invasor . É concebível que isso normalmente não aconteça a menos que o invasor consiga explorar outro bug.

Isso não é inédito. Um caso comum é quando as pessoas esquecem de limpar os dados antes de usá-los na expressão aritmética. Já vimos acima que ele pode permitir a execução arbitrária de código em alguns shells, mas em todos eles, permite ao invasor atribuir a qualquer variável um valor inteiro.

Por exemplo:

n=$(($1 + 1))
if [ $# -gt 2 ]; then
  echo >&2 "Too many arguments"
  exit 1
fi

E com um $1valor with (IFS=-1234567890), essa avaliação aritmética tem o efeito colateral das configurações IFS e o próximo [ comando falha, o que significa que a verificação de muitos argumentos é ignorada.

E quando o operador split + glob não é chamado?

Há outro caso em que são necessárias aspas em torno de variáveis ​​e outras expansões: quando é usado como padrão.

[[ $a = $b ]]   # a `ksh` construct also supported by `bash`
case $a in ($b) ...; esac

não teste se $ae $bé o mesmo (exceto com zsh), mas se $acorresponde ao padrão em $b. E você precisa citar $bse deseja comparar como strings (a mesma coisa em "${a#$b}"ou "${a%$b}"ou "${a##*$b*}"onde $bdeve ser citada, se não for para ser tomada como padrão).

O que isso significa é que [[ $a = $b ]]pode retornar verdadeiro nos casos em que $aé diferente de $b(por exemplo, quando $aé anythinge $bé *) ou pode retornar falso quando são idênticos (por exemplo, quando ambos $ae $bsão [a]).

Isso pode criar uma vulnerabilidade de segurança? Sim, como qualquer bug. Aqui, o invasor pode alterar o fluxo de código lógico do seu script e / ou quebrar as suposições que seu script está fazendo. Por exemplo, com um código como:

if [[ $1 = $2 ]]; then
   echo >&2 '$1 and $2 cannot be the same or damage will incur'
   exit 1
fi

O atacante pode ignorar a verificação passando '[a]' '[a]'.

Agora, se nem a correspondência de padrões nem o operador split + glob se aplicam, qual é o perigo de deixar uma variável sem aspas?

Eu tenho que admitir que escrevo:

a=$b
case $a in...

Lá, citar não prejudica, mas não é estritamente necessário.

No entanto, um efeito colateral de omitir aspas nesses casos (por exemplo, nas respostas das perguntas e respostas) é que ele pode enviar uma mensagem errada aos iniciantes: que pode ser correto não citar variáveis .

Por exemplo, eles podem começar a pensar que, se a=$bestiver tudo bem, export a=$bseria bom (o que não ocorre em muitos shells, como nos argumentos para o exportcomando e no contexto da lista) ou env a=$b.

Que tal zsh?

zshcorrigiu a maioria desses constrangimentos. Em zsh(pelo menos quando não estiver no modo de emulação sh / ksh), se você desejar dividir , globbing ou correspondência de padrões , precisará solicitá-lo explicitamente: $=vardividir e $~varglob ou para que o conteúdo da variável seja tratado como um padrão.

No entanto, a divisão (mas não o globbing) ainda é feita implicitamente na substituição de comandos sem aspas (como em echo $(cmd)).

Além disso, um efeito colateral às vezes indesejável de não citar variáveis ​​é a remoção de vazios . O zshcomportamento é semelhante ao que você pode obter em outros shells, desativando completamente o globbing (com set -f) e dividindo (com IFS=''). Ainda em:

cmd $var

Não haverá divisão + glob , mas se $varestiver vazio, em vez de receber um argumento vazio, cmdnão receberá nenhum argumento.

Isso pode causar erros (como o óbvio [ -n $var ]). Isso pode quebrar as expectativas e suposições de um script e causar vulnerabilidades, mas não posso criar um exemplo não muito abrangente agora).

E quando você faz precisa da divisão + glob operador?

Sim, normalmente é quando você deseja deixar sua variável sem aspas. Mas, então, você precisa ajustar corretamente os operadores de divisão e glob antes de usá-lo. Se você deseja apenas a parte dividida e não a parte glob (que é o caso na maioria das vezes), é necessário desativar o globbing ( set -o noglob/ set -f) e corrigir $IFS. Caso contrário, você também causará vulnerabilidades (como o exemplo de CGI de David Korn mencionado acima).

Conclusão

Em resumo, deixar uma variável (ou substituição de comando ou expansão aritmética) sem citação em shells pode ser muito perigoso, especialmente quando feito em contextos errados, e é muito difícil saber quais são esses contextos errados.

Essa é uma das razões pelas quais é considerada uma má prática .

Obrigado pela leitura até agora. Se isso passar por sua cabeça, não se preocupe. Não se pode esperar que todos compreendam todas as implicações de escrever seu código da maneira que o escrevem. É por isso que temos recomendações de boas práticas , para que possam ser seguidas sem necessariamente entender o porquê.

(e, caso ainda não seja óbvio, evite escrever códigos confidenciais de segurança nos shells).

E cite suas variáveis ​​nas suas respostas neste site!


¹Em ksh93e pdkshe derivados, a expansão de braçadeira também é executada, a menos que o globbing esteja desativado (no caso de ksh93versões até ksh93u +, mesmo quando a braceexpandopção está desativada).

Stéphane Chazelas
fonte
Observe que, com [[, apenas o RHS das comparações precisa ser citado:if [[ $1 = "$2" ]]; then
mirabilos
2
@mirabilos, sim, mas o LHS necessidades não não ser citado, então não há nenhuma razão convincente para não citá-lo lá (se estamos a tomar a decisão consciente de citar, por padrão , uma vez que parece ser a coisa mais sensata a fazer ) Observe também que [[ $* = "$var" ]]não é o mesmo que [[ "$*" = "$var" ]]se o primeiro caractere de $IFSnão seja espaço bash(e também mkshse $IFSestiver vazio, nesse caso, não tenho certeza do que $*é igual, devo gerar um bug?)).
Stéphane Chazelas
11
Sim, você pode citar por padrão lá. Por favor, não há mais bugs sobre divisão de campos no momento, ainda preciso corrigir aqueles que eu conheço (de você e de outras pessoas) primeiro, antes que possamos reavaliar isso.
mirabilos
2
@ Barmar, assumindo que você quis dizer foo='bar; rm *', não, não será, no entanto, listará o conteúdo do diretório atual que pode ser considerado uma divulgação de informações. print $fooin ksh93(onde printestá o substituto para echoesse endereço, algumas de suas deficiências) possui uma vulnerabilidade de injeção de código (por exemplo, com foo='-f%.0d z[0$(uname>&2)]') (você realmente precisa print -r -- "$foo". echo "$foo"ainda está errado e não pode ser corrigido (embora geralmente seja menos prejudicial)).
Stéphane Chazelas
3
Não sou especialista em bash, mas já o codigo há mais de uma década. Usei muito citações, mas principalmente para lidar com espaços em branco incorporados. Agora, eu vou usá-los muito mais! Seria bom se alguém expandisse essa resposta para facilitar um pouco a absorção de todos os pontos positivos. Eu consegui muito, mas também perdi muito. Já é um post longo, mas eu sei que há muito mais aqui para eu aprender. Obrigado!
Joe
34

[Inspirado nesta resposta por cas .]

Mas e se …?

Mas e se meu script definir uma variável com um valor conhecido antes de usá-lo? Em particular, e se definir uma variável para um dos dois ou mais valores possíveis (mas sempre a definir para algo conhecido) e nenhum dos valores contiver caracteres de espaço ou glob? Não é seguro usá-lo sem aspas nesse caso ?

E se um dos valores possíveis for a string vazia e eu estiver dependendo da "remoção de vazios"? Ou seja, se a variável contiver a sequência vazia, não quero obter a sequência vazia no meu comando; Eu não quero nada. Por exemplo,

se alguma_condição
então
    ignorecase = "- i"
outro
    ignorecase = ""
fi
                                        # Observe que as aspas nos comandos acima não são estritamente necessárias. 
grep $ ignorecase   other_ grep _args

Não sei dizer ; isso falhará se for a string vazia.grep "$ignorecase" other_grep_args$ignorecase

Resposta:

Conforme discutido na outra resposta, isso ainda falhará se IFScontiver um -ou um i. Se você garantiu que IFSnão contém nenhum caractere em sua variável (e tem certeza de que sua variável não contém caracteres glob), isso provavelmente é seguro.

Mas há uma maneira mais segura (embora seja um pouco feia e pouco intuitiva): use ${ignorecase:+"$ignorecase"}. Na especificação POSIX Shell Command Language , em  2.6.2 Expansion Parameter ,

${parameter:+[word]}

    Use valor alternativo.   Se parameterfor definido ou nulo, nulo será substituído; caso contrário, a expansão de word (ou uma sequência vazia, se wordfor omitida) deve ser substituída.

O truque aqui, tal como ela é, é que estamos usando ignorecasecomo o parameter e "$ignorecase"como o word. Então ${ignorecase:+"$ignorecase"}significa

Se $ignorecasenão estiver definido ou nulo (ou seja, vazio), nulo (ou seja, nada sem aspas ) será substituído; caso contrário, a expansão de "$ignorecase"será substituída.

Isso nos leva aonde queremos ir: se a variável estiver definida como uma string vazia, ela será "removida" (toda essa expressão complicada não será avaliada em nada - nem mesmo uma string vazia), e se a variável tiver uma variável não valor vazio, obtemos esse valor, citado.


Mas e se …?

Mas e se eu tiver uma variável que eu queira / precise ser dividida em palavras? (Caso contrário, é como o primeiro caso; meu script definiu a variável e tenho certeza de que não contém caracteres glob. Mas pode conter espaço (s) e quero que ela seja dividida em argumentos separados no espaço limites.
PS Eu ainda quero esvazia remoção.)

Por exemplo,

se alguma_condição
então
    critérios = "- tipo f"
outro
    critérios = ""
fi
se alguma_outra_condição
então
    critérios = "$ critérios - tempo +42"
fi
encontre "$ start_directory" $ critérios   other_ encontre _args

Resposta:

Você pode pensar que este é um caso de uso eval.  Não!   Resista à tentação de pensar em usar evalaqui.

Novamente, se você garantiu que IFSnão contém nenhum caractere em sua variável (exceto os espaços que você deseja honrar) e tem certeza de que sua variável não contém caracteres glob, provavelmente o acima é seguro.

Mas, se você estiver usando o bash (ou ksh, zsh ou yash), existe uma maneira mais segura: use uma matriz:

se alguma_condição
então
    critérios = (- tipo f) # Você poderia dizer `criterios = (" - tipo "" f ")`, mas é realmente desnecessário.
outro
    critérios = () # Não use aspas neste comando!
fi
se alguma_outra_condição
então
    critérios + = (- mtime +42) # Nota: não `=`, mas ` + =`, para adicionar (anexar) a uma matriz.
fi
encontre "$ start_directory" "$ {criterios [@]}"   other_ find _args

Do bash (1) ,

Qualquer elemento de uma matriz pode ser referenciado usando . … Se é ou  , a palavra se expande para todos os membros de . Esses subscritos diferem apenas quando a palavra aparece entre aspas duplas. Se a palavra estiver entre aspas duplas,… expande cada elemento de para uma palavra separada.${name[subscript]}subscript@*name${name[@]}name

Portanto, "${criteria[@]}"expande para (no exemplo acima) o zero, dois ou quatro elementos da criteriamatriz, cada um deles citado. Em particular, se nenhuma das condições  s for verdadeira, a criteriamatriz não terá conteúdo (conforme definido pela criteria=()instrução) e "${criteria[@]}"avalia como nada (nem mesmo uma cadeia vazia inconveniente).


Isso se torna especialmente interessante e complicado quando você lida com várias palavras, algumas das quais são dinâmicas (usuário), que você não conhece antecipadamente e podem conter espaço (s) ou outro (s) caractere (s) especial (is). Considerar:

printf "Digite o nome do arquivo para procurar:"
leia fname
se ["$ fname"! = ""]
então
    critérios + = (- nome "$ fname")
fi

Observe que $fnameé citado toda vez que é usado. Isso funciona mesmo se o usuário digitar algo como foo barou foo*"${criteria[@]}"avalia como -name "foo bar"ou -name "foo*". (Lembre-se de que cada elemento da matriz é citado.)

Matrizes não funcionam em todos os shells POSIX; matrizes são um ksh / bash / zsh / yash-ism. Exceto ... há uma matriz que todos os shells suportam: a lista de argumentos, também conhecida como "$@". Se você concluiu a lista de argumentos com a qual foi chamado (por exemplo, copiou todos os "parâmetros posicionais" (argumentos) em variáveis ​​ou os processou), poderá usar a lista arg como uma matriz:

se alguma_condição
então
    set - -type f # Você poderia dizer `set -" -type "" f "`, mas é realmente desnecessário.
outro
    set -
fi
se alguma_outra_condição
então
    set - "$ @" - tempo +42
fi
# Da mesma forma: set - "$ @" -name "$ fname"
encontre "$ start_directory" "$ @"   other_ encontre _args

A "$@"construção (que, historicamente, veio primeiro) tem a mesma semântica que - expande cada argumento (isto é, cada elemento da lista de argumentos) para uma palavra separada, como se você tivesse digitado ."${name[@]}""$1" "$2" "$3" …

Extraindo da especificação POSIX Shell Command Language , em 2.5.2 Parâmetros Especiais ,

@

    Expande para os parâmetros posicionais, começando por um, produzindo inicialmente um campo para cada parâmetro posicional definido. …, Os campos iniciais serão retidos como campos separados,…. Se não houver parâmetros posicionais, a expansão de @deverá gerar zero campos, mesmo quando @estiver entre aspas duplas; ...

O texto completo é um tanto enigmático; o ponto principal é que ele especifica que "$@"deve gerar zero campos quando não houver parâmetros posicionais. Nota histórica: quando "$@"foi introduzido pela primeira vez no shell Bourne (predecessor do bash) em 1979, havia um bug que "$@"era substituído por uma única string vazia quando não havia parâmetros posicionais; consulte O que ${1+"$@"}significa em um script de shell e como ele difere "$@"? A família tradicional Bourne ShellO que ${1+"$@"}significa ... e onde é necessário? e "$@"versus${1+"$@"} .


As matrizes também ajudam na primeira situação:

se alguma_condição
então
    ignorecase = (- i) # Você poderia dizer 'ignorecase = ("- i") `, mas é realmente desnecessário.
outro
    ignorecase = () # Não use aspas neste comando!
fi
grep "$ {ignorecase [@]}"   other_ grep _args

____________________

PS (csh)

Isso deve ser óbvio, mas, para o benefício de pessoas que são novas aqui: csh, tcsh, etc., não são shells Bourne / POSIX. Eles são uma família totalmente diferente. Um cavalo de uma cor diferente. Um outro jogo de bola. Uma raça diferente de gato. Aves de outra pena. E, mais particularmente, uma lata de vermes diferente.

Parte do que foi dito nesta página se aplica ao csh; como: é uma boa ideia citar todas as suas variáveis, a menos que você tenha um bom motivo para não fazê-lo e tenha certeza de que sabe o que está fazendo. Mas, no csh, toda variável é uma matriz - acontece que quase toda variável é uma matriz de apenas um elemento e age de maneira semelhante a uma variável comum do shell nos shells Bourne / POSIX. E a sintaxe é muito diferente (e eu quero dizer muito ). Portanto, não falaremos mais sobre os reservatórios da família csh aqui.

G-Man
fonte
11
Observe que em cshvocê prefere usar $var:qdo "$var"que o último não funciona para variáveis ​​que contêm caracteres de nova linha (ou se você deseja citar os elementos da matriz individualmente, em vez de juntá-los a espaços em um único argumento).
Stéphane Chazelas
No bash, bitrate="--bitrate 320"trabalha com ${bitrate:+$bitrate}e bitrate=--bitrate 128não funciona ${bitrate:+"$bitrate"}porque quebra o comando. É seguro usar ${variable:+$variable}com não "?
Freedo 07/06
@ Freedo: Acho o seu comentário pouco claro. Não sou muito claro o que você quer saber que eu ainda não disse. Veja o segundo cabeçalho “Mas e se” - “Mas e se eu tiver uma variável que eu queira / precise ser dividida em palavras?” Essa é a sua situação e você deve seguir os conselhos. Mas se você não pode (por exemplo, porque você está usando um shell diferente de bash, ksh, zsh ou yash e está usando  $@para outra coisa), ou simplesmente se recusa a usar um array por outros motivos, refiro-me você "Como discutido na outra resposta". ... (continua)
G-Man
(Continua) ... Sua sugestão (usando ${variable:+$variable}sem ") falhará se IFS contém  -,  0, 2, 3, a, b, e, i, rou  t.
G-Man
Bem, minha variável tem os caracteres a, e, b, i 2 e 3 e ainda funciona bem com${bitrate:+$bitrate}
Freedo
11

Eu era cético em relação à resposta de Stéphane, no entanto, é possível abusar $#:

$ set `seq 101`

$ IFS=0

$ echo $#
1 1

ou $ ?:

$ IFS=0

$ awk 'BEGIN {exit 101}'

$ echo $?
1 1

Estes são exemplos inventados, mas o potencial existe.

Steven Penny
fonte