Citação dentro de $ (substituição de comando) no Bash

114

No meu ambiente Bash eu uso variáveis contendo espaços, e eu uso essas variáveis dentro da substituição de comandos. Infelizmente não consigo encontrar a resposta em SE.

Qual é a maneira correta de citar minhas variáveis? E como devo fazer isso se eles estiverem aninhados?

DIRNAME=$(dirname "$FILE")

ou cito fora da substituição?

DIRNAME="$(dirname $FILE)"

ou ambos?

DIRNAME="$(dirname "$FILE")"

ou eu uso back-ticks?

DIRNAME='dirname "$FILE"'

Qual é o caminho certo para fazer isso? E como posso verificar facilmente se as cotações estão definidas corretamente?

    
por CousinCocaine 06.03.2014 / 15:30

3 respostas

161

Em ordem do pior para o melhor:

  • DIRNAME="$(dirname $FILE)" não fará o que você quer se $FILE contiver espaços em branco ou caracteres globbing \[?* .
  • DIRNAME='dirname "$FILE"' está tecnicamente correto, mas os backticks não são recomendados para a expansão de comandos devido à complexidade extra ao aninhá-los.
  • DIRNAME=$(dirname "$FILE") está correto, mas somente porque essa é uma atribuição . Se você usar a substituição de comando em qualquer outro contexto, como export DIRNAME=$(dirname "$FILE") ou du $(dirname "$FILE") , a falta de cotações causará problemas se o resultado da expansão contiver espaços em branco ou caracteres globbing.
  • DIRNAME="$(dirname "$FILE")" é o caminho recomendado. Você pode substituir DIRNAME= por um comando e um espaço sem alterar nada, e dirname recebe a string correta.

Para melhorar ainda mais:

  • DIRNAME="$(dirname -- "$FILE")" funciona se $FILE começar com um traço.
  • DIRNAME="$(dirname -- "$FILE"; printf x)" && DIRNAME="${DIRNAME%?x}" funciona mesmo se $FILE terminar com uma nova linha, pois $() corta as novas linhas no final da saída e dirname gera uma nova linha após o resultado. Sheesh dirname , por que você tem que ser diferente?

Você pode aninhar expansões de comando o quanto quiser. Com $() você sempre cria um novo contexto de cotação, então você pode fazer coisas assim:

foo "$(bar "$(baz "$(ban "bla")")")"

Você não quer tentar isso com backticks.

    
por 06.03.2014 / 15:39
19

Você sempre pode mostrar os efeitos da cotação variável com printf .

Divisão de palavras feita em var1 :

$ var1="hello     world"
$ printf '[%s]\n' $var1
[hello]
[world]

var1 cotado, por isso, nenhuma palavra é dividida:

$ printf '[%s]\n' "$var1"
[hello     world]

Palavra dividida em var1 dentro de $() , equivalente a echo "hello" "world" :

$ var2=$(echo $var1)
$ printf '[%s]\n' "$var2"
[hello world]

Nenhuma divisão de palavras em var1 , não há problema em não citar o $() :

$ var2=$(echo "$var1")
$ printf '[%s]\n' "$var2"
[hello     world]

Palavra dividida em var1 novamente:

$ var2="$(echo $var1)"
$ printf '[%s]\n' "$var2"
[hello world]

Citando a maneira mais fácil de ter certeza.

$ var2="$(echo "$var1")"
$ printf '[%s]\n' "$var2"
[hello     world]

Problema de globalização

Não citar uma variável também pode levar à expansão glob do seu conteúdo:

$ mkdir test; cd test; touch file1 file2
$ var="*"
$ printf '[%s]\n' $var
[file1]
[file2]
$ printf '[%s]\n' "$var"
[*]

Observe que isso acontece depois que a variável é expandida apenas. Não é necessário citar uma glob durante a atribuição:

$ var=*
$ printf '[%s]\n' $var
[file1]
[file2]
$ printf '[%s]\n' "$var"
[*]

Use set -f para desativar esse comportamento:

$ set -f
$ var=*
$ printf '[%s]\n' $var
[*]

E set +f para reativá-lo:

$ set +f
$ printf '[%s]\n' $var
[file1]
[file2]
    
por 06.03.2014 / 16:12
10

Além da resposta aceita:

Embora eu geralmente concorde com a resposta @ l0b0 aqui, eu suspeito que a colocação de backticks nus nos "piores a melhor "lista é, pelo menos em parte, um resultado da suposição de que $(...) está disponível em todos os lugares. Eu percebo que a pergunta especifica Bash, mas há muitas vezes em que Bash acaba significando /bin/sh , o que pode nem sempre ser realmente o shell Bourne Again completo.

Em particular, o shell Bourne simples não saberá o que fazer com $(...) , então os scripts que alegam ser compatíveis com ele (por exemplo, através de uma linha #!/bin/sh shebang) provavelmente se comportarão mal se forem realmente executados pelo "real" /bin/sh - isso é de especial interesse quando, digamos, produzindo scripts de inicialização, ou empacotando pré e pós-scripts, e pode colocar um em um local surpreendente durante a instalação de um sistema básico.

Se tudo isso soa como algo que você está planejando fazer com essa variável, aninhamento é provavelmente uma preocupação menor do que ter o script na verdade, executar previsivelmente. Quando é um caso bastante simples e portabilidade é uma preocupação, mesmo se eu esperar que o script geralmente seja executado em sistemas em que /bin/sh é Bash, geralmente uso backticks por esse motivo, com várias atribuições em vez de aninhar.

Tendo dito tudo isso, a onipresença de shells que implementam $(...) (Bash, Dash, et al.), nos deixa em um bom lugar para ficar com o POSIX mais bonito, mais fácil de aninhar, e mais recentemente preferido sintaxe na maioria dos casos, por todas as razões @ l0b0 menciona.

Além disso, isso também aparece ocasionalmente no StackOverflow -

por 02.06.2015 / 01:31