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


104

这是一系列情况,它们echo $var可能显示出与刚刚分配的值不同的值。无论分配的值是“双引号”,“单引号”还是未引号,都会发生这种情况。

如何使外壳正确设置变量?

星号

预期的输出是/* 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

多个空间

我期望表标题可以仔细对齐,但是多个空格要么消失,要么合而为一!

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

标签

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

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

2
感谢您这样做。我经常遇到换行。所以var=$(cat file)是好的,但echo "$var"需要。
2015年

3
顺便说一句,这也是BashPitfalls#14:mywiki.wooledge.org/BashPitfalls#echo_.24foo
Charles Duffy


Answers:


139

在上述所有情况下,变量均已正确设置,但未正确读取!正确的方法是在引用使用双引号

echo "$var"

这在所有给出的示例中给出了期望值。总是引用变量引用!


为什么?

当变量不加引号时,它将:

  1. 进行字段拆分,将值拆分为多个空格(默认情况下):

    之前: /* Foobar is free software */

    后:/*Foobarisfreesoftware*/

  2. 这些单词中的每一个都将进行路径名扩展,其中模式被扩展为匹配文件:

    之前: /*

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

  3. 最后,所有参数都传递给echo,这会将它们写成一个空格

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

    而不是变量的值。

引用该变量时,它将:

  1. 代替它的价值。
  2. 没有步骤2。

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


它并不总是有效。我可以举个例子:paste.ubuntu.com/p/8RjR6CS668
recolic

1
对, $(..)尾随换行符。您可以var=$(cat file; printf x); var="${var%x}"用来解决它。
另一个人

17

您可能想知道为什么会这样。连同那个家伙的出色解释,找到了为什么我的shell脚本为什么在空白或其他特殊字符上感到窒息的参考撰写者 GillesUnix&Linux中

为什么我需要写"$foo"?没有引号会怎样?

$foo并不意味着“取变量的值foo”。这意味着要复杂得多:

  • 首先,取变量的值。
  • 字段拆分:将该值视为用空格分隔的字段列表,然后构建结果列表。例如,如果该变量包含foo * bar ​然后此步骤的结果为第3元素的列表foo*bar
  • 生成文件名:将每个字段都视为全局字段(即通配符模式),然后将其替换为与此模式匹配的文件名列表。如果该模式与任何文件都不匹配,则将其保留不变。在我们的示例中,这导致包含以下内容的列表foo的列表,其次是当前目录中的文件列表,最后是 bar。如果当前目录是空的,结果是foo*bar

请注意,结果是一个字符串列表。Shell语法中有两个上下文:列表上下文和字符串上下文。字段拆分和文件名生成仅在列表上下文中发生,但这是大多数时间。双引号分隔字符串上下文:整个双引号字符串是单个字符串,不能分割。(例外: "$@"扩展到位置参数列表,例如"$@",等同于"$1" "$2" "$3"是否存在三个位置参数。请参阅$ *和$ @有什么区别?

使用以下命令进行替换也一样 $(foo)或使用 `foo`。附带说明一下,不要使用`foo`:它的引用规则很奇怪且不可移植,并且所有现代shell都支持$(foo),除了具有直观的引用规则外,它是完全等效的。

算术替换的输出也经历相同的扩展,但是通常不必担心,因为它仅包含不可扩展的字符(假设IFS不包含数字或 -)。

看到 何时需要双引号?有关可以省略引号的情况的更多详细信息。

除非您想让所有这些麻烦事发生,否则请记住始终在变量和命令替换周围使用双引号。请当心:省略引号不仅会导致错误,还会导致 安全漏洞


7

除了其他问题造成无法引用,-n并且-e可以通过消耗echo作为参数。(对于POSIX规范echo,仅前者是合法的,但是几种常见的实现方式违反了规范并消耗了-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

是的,为此,我们需要一个很好的重复!我同意这符合问题标题,但我认为它不会在这里得到应有的重视。关于“为什么我的-e/ -n/反斜杠不显示?” 的新问题怎么样?我们可以在此处适当地添加链接。
另一个人

您的意思是消耗-n以及
PesaThe'Jan

1
@PesaThe,不,我是说-e。当for的标准echo的第一个参数为时-n,它不指定输出,因此在这种情况下,任何/所有可能的输出都是合法的;没有这样的规定-e
查尔斯·达菲,

哦...我看不懂。让我们为此怪我英语。感谢您的解释。
PesaThe'Jan


2

echo $var输出高度取决于IFS变量的值。默认情况下,它包含空格,制表符和换行符:

[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

如果IFS的值为空,则不执行任何字段拆分。

设置为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 ~]$ 

2
您还必须set -f防止出现水珠现象
另一个人

@thatotherguy,您的第一个路径扩展示例真的必要吗?随着IFS设置为空,echo $var将扩大到echo '/* Foobar is free software */'和没有单引号的字符串内进行路径扩展。
ks1322

1
是。如果您mkdir "/this thing called Foobar is free software etc/"会看到它还在扩展。对于该[a-z]示例来说,显然更加实用。
另一个人

我知道,这是有道理的[a-z]
ks1322

2

ks1322回答帮助我在使用时识别了问题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

-2

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

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

尽管这有点麻烦,但是它确实为输出增加了更多的多样性,因为您可以替换任何字符作为数组变量之间的分隔符。


2
但这将所有空格替换为换行符。报价会保留现有的换行符和空格。
user000001

是的,是的。我想这取决于变量中的内容。我实际上使用tr其他方法从文本文件创建数组。
Alek

3
通过不正确地引用变量,然后使用多余的多余处理方法来解决问题,这不是很好的编程方法。
点钟

@Alek,...呃,什么?无需tr根据文本文件正确/正确地创建数组-您可以通过设置IFS指定所需的分隔符。例如:IFS=$'\n' read -r -d '' -a arrayname < <(cat file.txt && printf '\0')从bash 3.2(广泛发行的最旧版本)一直到最后,如果cat失败,则将退出状态正确设置为false 。如果你想,说,标签,而不是换行,你只需更换$'\n'$'\t'
查尔斯·达菲

1
@Alek,...如果您执行的是arrayname=( $( cat file | tr '\n' ' ' ) ),则表示在多个层次上都存在问题:遍历您的结果(因此*变成了当前目录中的文件列表),并且无需使用tr(或cat,就此而言;只要使用arrayname=$( $(<file) )它,它就会以相同的方式被破坏,但效率低下)。
查尔斯·达菲
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.