Acabei de atribuir uma variável, mas echo $ variable mostra outra coisa


104

Aqui está uma série de casos onde echo $varpode mostrar um valor diferente do que foi atribuído. Isso acontece independentemente de o valor atribuído ter "aspas duplas", 'aspas simples' ou não.

Como faço para que o shell defina minha variável corretamente?

Asteriscos

A saída esperada é /* Foobar is free software */, mas em vez disso, recebo uma lista de nomes de arquivos:

$ var="/* Foobar is free software */"
$ echo $var 
/bin /boot /dev /etc /home /initrd.img /lib /lib64 /media /mnt /opt /proc ...

Colchetes

O valor esperado é [a-z], mas às vezes recebo uma única letra!

$ var=[a-z]
$ echo $var
c

Feeds de linha (novas linhas)

O valor esperado é uma lista de linhas separadas, mas em vez disso, todos os valores estão em uma linha!

$ cat file
foo
bar
baz

$ var=$(cat file)
$ echo $var
foo bar baz

Múltiplos espaços

Eu esperava um cabeçalho de tabela cuidadosamente alinhado, mas em vez disso, vários espaços ou desaparecem ou são reduzidos em um!

$ var="       title     |    count"
$ echo $var
title | count

Abas

Eu esperava dois valores separados por tabulação, mas em vez disso, recebo dois valores separados por espaço!

$ var=$'key\tvalue'
$ echo $var
key value

2
Obrigado por fazer isso. Eu encontro a linha alimenta um com frequência. Então var=$(cat file)está bom, mas echo "$var"é necessário.
snd

3
A propósito, este também é o BashPitfalls # 14: mywiki.wooledge.org/BashPitfalls#echo_.24foo
Charles Duffy


Além disso, consulte também stackoverflow.com/questions/2414150/…
tripleee

Respostas:


139

Em todos os casos acima, a variável está configurada corretamente, mas não foi lida corretamente! A maneira certa é usar aspas duplas ao fazer referência :

echo "$var"

Isso dá o valor esperado em todos os exemplos dados. Sempre cite referências de variáveis!


Por quê?

Quando uma variável não está entre aspas , ela:

  1. Submeta a divisão do campo onde o valor é dividido em várias palavras em espaços em branco (por padrão):

    Antes: /* Foobar is free software */

    Depois: /*, Foobar, is, free, software,*/

  2. Cada uma dessas palavras sofrerá expansão do nome do caminho , onde os padrões são expandidos em arquivos correspondentes:

    Antes: /*

    Depois: /bin, /boot, /dev, /etc, /home, ...

  3. Por fim, todos os argumentos são passados ​​para echo, que os escreve separados por espaços simples , dando

    /bin /boot /dev /etc /home Foobar is free software Desktop/ Downloads/

    em vez do valor da variável.

Quando a variável é citada, ela:

  1. Seja substituído por seu valor.
  2. Não há etapa 2.

É por isso que você deve sempre citar todas as referências de variáveis , a menos que precise especificamente de divisão de palavras e expansão do nome do caminho. Ferramentas como shellcheck existem para ajudar e alertam sobre aspas ausentes em todos os casos acima.


nem sempre está funcionando. Posso dar um exemplo: paste.ubuntu.com/p/8RjR6CS668
recólico

1
Sim, $(..)tiras seguindo os feeds de linha. Você pode usar var=$(cat file; printf x); var="${var%x}"para contornar isso.
aquele outro cara de

17

Você pode querer saber por que isso está acontecendo. Junto com a ótima explicação daquele outro cara , encontre uma referência de Por que meu script de shell se engasga com espaços em branco ou outros caracteres especiais? escrito por Gilles em Unix e Linux :

Por que eu preciso escrever "$foo" ? O que acontece sem as aspas?

$foonão significa “tomar o valor da variável foo”. Significa algo muito mais complexo:

  • Primeiro, pegue o valor da variável.
  • Divisão de campo: trate esse valor como uma lista de campos separados por espaços em branco e crie a lista resultante. Por exemplo, se a variável contém foo * bar ​, em seguida, o resultado deste passo é a lista de 3-elemento foo, *,bar .
  • Geração de nome de arquivo: trate cada campo como um glob, ou seja, como um padrão curinga, e substitua-o pela lista de nomes de arquivo que correspondem a esse padrão. Se o padrão não corresponder a nenhum arquivo, ele não será modificado. Em nosso exemplo, isso resulta na lista contendo foo, seguido pela lista de arquivos no diretório atual e, finalmente bar. Se o diretório atual está vazio, o resultado é foo, *, bar.

Observe que o resultado é uma lista de strings. Existem dois contextos na sintaxe do shell: contexto de lista e contexto de string. A divisão de campo e a geração de nome de arquivo acontecem apenas no contexto de lista, mas isso ocorre na maioria das vezes. Aspas duplas delimitam um contexto de string: toda a string entre aspas duplas é uma única string, que não deve ser dividida. (Exceção: "$@" para expandir para a lista de parâmetros posicionais, por exemplo, "$@"é equivalente "$1" "$2" "$3"se houver três parâmetros posicionais. Consulte Qual é a diferença entre $ * e $ @? )

O mesmo acontece com a substituição de comandos com $(foo) ou com `foo`. Em uma nota lateral, não use `foo`: suas regras de citação são estranhas e não portáteis, e todos os shells modernos suportam o $(foo)que é absolutamente equivalente, exceto por ter regras de citação intuitivas.

A saída da substituição aritmética também sofre as mesmas expansões, mas isso normalmente não é uma preocupação, pois contém apenas caracteres não expansíveis (supondo IFSque não contenha dígitos ou -).

Consulte Quando é necessário fazer aspas duplas? para obter mais detalhes sobre os casos em que você pode omitir as aspas.

A menos que você queira que toda essa bobagem aconteça, apenas lembre-se de sempre usar aspas duplas nas substituições de variáveis ​​e comandos. Tome cuidado: omitir as aspas pode levar não apenas a erros, mas a brechas de segurança .


7

Além de outros problemas causados ​​pela falta de citação, -ne -epodem ser consumidos echocomo argumentos. (Apenas o primeiro é legal de acordo com a especificação POSIX echo, mas várias implementações comuns violam a especificação e consomem -etambém).

Para evitar isso, use em printfvez deecho quando os detalhes importam.

Portanto:

$ vars="-e -n -a"
$ echo $vars      # breaks because -e and -n can be treated as arguments to echo
-a
$ echo "$vars"
-e -n -a

No entanto, a citação correta nem sempre salvará você ao usar echo:

$ vars="-n"
$ echo $vars
$ ## not even an empty line was printed

... Considerando que você vai economizar com printf:

$ vars="-n"
$ printf '%s\n' "$vars"
-n

Oba, precisamos de uma boa dedup para isso! Concordo que isso se encaixa no título da pergunta, mas não acho que terá a visibilidade que merece aqui. Que tal uma nova pergunta à la "Por que minha barra invertida -e/ -n/ não está aparecendo?" Podemos adicionar links a partir daqui conforme apropriado.
aquele outro cara

Você quis dizer consumir -ntambém ?
PesaThe

1
@PesaThe, não, eu quis dizer -e. O padrão para echonão especifica a saída quando seu primeiro argumento é -n, tornando qualquer / todas as saídas possíveis válidas nesse caso; não existe tal disposição para -e.
Charles Duffy

Oh ... eu não consigo ler. Vamos culpar meu inglês por isso. Obrigada pelo esclarecimento.
PesaThe


2

echo $vara saída depende altamente do valor da IFSvariável. Por padrão, ele contém espaço, tabulação e caracteres de nova linha:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$

Isso significa que quando o shell está fazendo a divisão de campo (ou divisão de palavra), ele usa todos esses caracteres como separadores de palavras. Isso é o que acontece ao fazer referência a uma variável sem aspas duplas para ecoá-la ( $var) e, portanto, a saída esperada é alterada.

Uma maneira de evitar a divisão de palavras (além de usar aspas duplas) é definir IFScomo nulo. Consulte http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_05 :

Se o valor de IFS for nulo, nenhuma divisão de campo deve ser realizada.

Definir como nulo significa definir como valor vazio:

IFS=

Teste:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$
[ks@localhost ~]$ var=$'key\nvalue'
[ks@localhost ~]$ echo $var
key value
[ks@localhost ~]$ IFS=
[ks@localhost ~]$ echo $var
key
value
[ks@localhost ~]$ 

2
Você também teria que set -fevitar globbing
aquele outro cara

@thatotherguy, é realmente necessário para o seu primeiro exemplo com expansão de caminho? Com IFSdefinido como nulo, echo $varserá expandido para echo '/* Foobar is free software */'e a expansão do caminho não será realizada dentro de strings entre aspas simples.
ks1322

1
Sim. Se você mkdir "/this thing called Foobar is free software etc/"verá que ele ainda se expande. É obviamente mais prático para o [a-z]exemplo.
aquele outro cara de

Eu vejo, isso faz sentido, por [a-z]exemplo.
ks1322

2

A resposta de ks1322 me ajudou a identificar o problema ao usar docker-compose exec:

Se você omitir o -Tsinalizador, docker-compose execadicione um caractere especial que interrompa a saída, vemos em bvez de 1b:

$ test=$(/usr/local/bin/docker-compose exec db bash -c "echo 1")
$ echo "${test}b"
b
echo "${test}" | cat -vte
1^M$

Com -Tbandeira, docker-compose execfunciona como esperado:

$ test=$(/usr/local/bin/docker-compose exec -T db bash -c "echo 1")
$ echo "${test}b"
1b

-2

Além de colocar a variável entre aspas, também se pode traduzir a saída da variável usando tre convertendo espaços em novas linhas.

$ echo $var | tr " " "\n"
foo
bar
baz

Embora isso seja um pouco mais complicado, ele adiciona mais diversidade à saída, pois você pode substituir qualquer caractere como separador entre as variáveis ​​da matriz.


2
Mas isso substitui todos os espaços por novas linhas. As citações preservam as novas linhas e espaços existentes.
user000001

Sim, é verdade. Suponho que depende do que está dentro da variável. Na verdade, eu uso tro contrário para criar matrizes de arquivos de texto.
Alek

3
Criar um problema não citando a variável corretamente e depois contorná-la com um processo extra maluco não é uma boa programação.
tripleee 01 de

@Alek, ... err, o quê? Não é trnecessário criar adequadamente / corretamente um array a partir de um arquivo de texto - você pode especificar qualquer separador que desejar definindo IFS. Por exemplo: IFS=$'\n' read -r -d '' -a arrayname < <(cat file.txt && printf '\0')funciona todo o caminho até o bash 3.2 (a versão mais antiga em grande circulação) e define corretamente o status de saída como falso se você catfalhar. E se você quiser, digamos, tabulações em vez de novas linhas, basta substituir o $'\n'por $'\t'.
Charles Duffy

1
@Alek, ... se você está fazendo algo como arrayname=( $( cat file | tr '\n' ' ' ) ), então isso está quebrado em várias camadas: está agregando seus resultados (então um se *transforma em uma lista de arquivos no diretório atual) e funcionaria tão bem sem otr ( ou o cat, por falar nisso; um poderia apenas usar arrayname=$( $(<file) )e seria quebrado da mesma forma, mas de forma menos ineficiente).
Charles Duffy
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.