我刚刚分配了一个变量,但 echo $variable 显示了其他内容

I just assigned a variable, but echo $variable shows something else

这里有一系列案例,其中 echo $var 可以显示与刚刚分配的值不同的值。无论分配的值是 "double quoted"、'single quoted' 还是未加引号,都会发生这种情况。

如何让 shell 正确设置我的变量?

星号

预期的输出是 /* Foobar is free software */,但我得到的是文件名列表:

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

方括号

期望值是[a-z],但有时我得到的是一个字母!

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

换行(换行)

预期值是一列单独的行,但所有值都在一行上!

$ cat file
foo
bar
baz

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

多个spaces

我希望仔细对齐 table header,但多个 space 要么消失,要么折叠成一个!

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

制表符

我希望有两个制表符分隔的值,但我得到的却是两个 space 分隔的值!

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

在上述所有情况下,变量设置正确,但读取不正确!正确的做法是引用时用双引号:

echo "$var"

这给出了所有给定示例中的预期值。始终引用变量引用!


为什么?

当一个变量未被引用时,它将:

  1. Undergo field splitting 其中值在空格上被拆分为多个单词(默认情况下):

    之前:/* Foobar is free software */

    之后:/*Foobarisfreesoftware*/

  2. 这些词中的每一个都将经历 pathname expansion,其中模式被扩展到匹配文件中:

    之前:/*

    之后:/bin/boot/dev/etc/home、...

  3. 最后,所有的参数都传递给echo,它把它们写出来separated by single spaces,给出

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

    而不是变量的值。

当变量被引用时,它将:

  1. 代入它的值。
  2. 没有第2步

这就是为什么您应该始终引用所有变量引用,除非您特别需要分词和路径名扩展。 shellcheck 之类的工具可以提供帮助,并且会在上述所有情况下警告遗漏引号。

除了将变量放在引号中之外,还可以使用 tr 并将空格转换为换行符来翻译变量的输出。

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

虽然这有点复杂,但它确实增加了输出的多样性,因为您可以将任何字符替换为数组变量之间的分隔符。

您可能想知道为什么会这样。连同 , find a reference of Why does my shell script choke on whitespace or other special characters? written by Gilles in Unix & Linux:

Why do I need to write "$foo"? What happens without the quotes?

$foo does not mean “take the value of the variable foo”. It means something much more complex:

  • First, take the value of the variable.
  • Field splitting: treat that value as a whitespace-separated list of fields, and build the resulting list. For example, if the variable contains foo * bar ​ then the result of this step is the 3-element list foo, *, bar.
  • Filename generation: treat each field as a glob, i.e. as a wildcard pattern, and replace it by the list of file names that match this pattern. If the pattern doesn't match any files, it is left unmodified. In our example, this results in the list containing foo, following by the list of files in the current directory, and finally bar. If the current directory is empty, the result is foo, *, bar.

Note that the result is a list of strings. There are two contexts in shell syntax: list context and string context. Field splitting and filename generation only happen in list context, but that's most of the time. Double quotes delimit a string context: the whole double-quoted string is a single string, not to be split. (Exception: "$@" to expand to the list of positional parameters, e.g. "$@" is equivalent to "" "" "" if there are three positional parameters. See What is the difference between $* and $@?)

The same happens to command substitution with $(foo) or with `foo`. On a side note, don't use `foo`: its quoting rules are weird and non-portable, and all modern shells support $(foo) which is absolutely equivalent except for having intuitive quoting rules.

The output of arithmetic substitution also undergoes the same expansions, but that isn't normally a concern as it only contains non-expandable characters (assuming IFS doesn't contain digits or -).

See When is double-quoting necessary? for more details about the cases when you can leave out the quotes.

Unless you mean for all this rigmarole to happen, just remember to always use double quotes around variable and command substitutions. Do take care: leaving out the quotes can lead not just to errors but to security holes.

echo $var 输出高度依赖于 IFS 变量的值。默认情况下它包含 space、制表符和换行符:

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

这意味着当shell进行字段拆分(或单词拆分)时,它使用所有这些字符作为单词分隔符。当引用一个没有双引号的变量来回显它时会发生这种情况 ($var),因此预期的输出会发生变化。

防止分词的一种方法(除了使用双引号外)是将 IFS 设置为 null。见 http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_05 :

If the value of IFS is null, no field splitting shall be performed.

设置为null表示设置为空 值:

IFS=

测试:

[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 ~]$ 

用户双引号以获得准确的值。像这样:

echo "${var}"

它会正确读取您的值。

除了引用失败导致的其他问题,-n-e可以被echo作为参数消费。 (根据 echo 的 POSIX 规范,只有前者是合法的,但一些常见的实现违反了规范并消耗了 -e

为避免这种情况,在细节重要时使用 printf 而不是 echo

因此:

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

但是,使用 echo:

时,正确的引用并不总能为您省钱
$ vars="-n"
$ echo "$vars"
$ ## not even an empty line was printed

...而它 printf:

拯救你
$ vars="-n"
$ printf '%s\n' "$vars"
-n

帮助我确定了使用 docker-compose exec 时的问题:

如果省略 -T 标志,docker-compose exec 添加一个中断输出的特殊字符,我们看到 b 而不是 1b:

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

使用 -T 标志,docker-compose exec 按预期工作:

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