我还没有完全理解 shell 扩展(希望有一天我会)...
我看到了这个评论超级用户问题,但我想我仍然停在路边......
使用没有 shell 的 Linux 就像在城市交通中以 50 公里/小时的速度驾驶法拉利一样。所有的乐趣都会消失......
我不明白以下示例。什么层次结构或其他什么导致第二个示例“数组项计数:”与第一个示例不同?
shell引入“空间”发生了什么?或者是它echo
引入了空格,并且 shell(可能)使用 \0?
#!/bin/bash
# Make a couple of files whose names contain a space.
junkd=$HOME/junkd
mkdir $junkd # || exit 1
cd $junkd
touch f\ {1..2}
#
echo -n * |xxd # This shows a space between the two names.
names=$(echo -n * )
echo -n "$names" |xxd # This shows a space between the two names.
#
# So far, it seems that the shell is inserting a space between each filename.
#
array=( $names )
echo "array item count: ${#array[@]}"
# 4 items... This shows that a space is the delimiter char ....
#
array=( * )
echo "array item count: ${#array[@]}"
# 2 items... What happened to the shell introduced space?
#
答案1
shell 命令(更准确地说,“简单命令”)由单词列表组成。每个单词可以是任意字符串(shell 单词可以包含空格和标点符号)。
当您运行 时echo -n *
,shell 对 执行路径名扩展(也称为文件名生成或通配符)*
,并将其替换为匹配文件名列表。所以展开后,这个命令由echo
、-n
、f 1
、四个字组成f 2
。该命令echo
使用两个参数运行,并打印其参数,参数之间有一个空格(并且由于该-n
选项而不会终止换行符)。所以输出是f 1 f 2
.练习:创建另一个文件,其名称由两个连续空格组成,运行echo -n *
,并确保您理解输出。
当您运行时names=$(echo -n * )
,命令的输出存储在names
变量中。在这里,该行相当于names='f 1 f 2'
.
现在我们开始了array=( $names )
。这是一个数组赋值,但在这种情况下它不会影响扩展。由于$names
是不带引号的变量扩展,因此它会进行分词,然后进行路径名扩展。分词意味着变量的值(是一个字符串)在每个空白序列处被分成几部分(有关精确的规则,请IFS
在 shell 的文档中搜索)。你最终可能会得到零个、一个或多个单词;这里字符串被分成 4 个单词:f
、1
、f
和2
。因此该数组包含四个元素(每个元素都是一个字符的单词)。练习:对于名称中包含两个连续空格的额外文件,现在数组的确切内容是什么?
接下来,你尝试了array=( * )
。这里,数组中有一个单词,受通常的扩展影响,最后一个是路径名扩展。由于有两个匹配文件,因此该数组包含两个单词,即每个文件的名称:f 1
和f 2
。
对于shell编程实践来说,我们可以从这个分析中得到什么建议呢?首先,有通常的 shell 编程原则:始终在变量扩展周围加上双引号,除非您有充分的理由不这样做。然后,不要将列表存储在字符串变量中。如果要存储文件名列表,则直接将其放入数组中:
files=(*)
ls -l "${files[@]}"
进一步练习:创建一个名称为单个星号 ( touch '*'
) 的文件并再次运行这些命令。你明白输出吗?
另外:zsh 不会对变量扩展执行分词或路径名扩展。这使得编程变得更加明智。
答案2
从男人狂欢
扩张 扩展是在命令行上被分割成单词后执行的。执行的扩展有七种:大括号扩展、波形符扩展、参数和变量扩展、命令替换、算术扩展、分词和路径名扩展。扩展的顺序是:大括号扩展、波形符扩展、参数、变量和算术扩展以及命令替换(以从左到右的方式完成)、分词和路径名扩展。
array=( $names )
这给你 4 个条目的原因是因为未加引号的$names
参数进一步受到分词基于IFS
默认的内部字段分隔符<space><tab><newline>
。如果您要引用"$names"
来禁止分词,那么您只会得到一个具有 value 的数组元素f 1 f 2
,这又不是您想要的。
array=( * )
另一方面,上述内容仅受路径名扩展这恰好是最后执行的扩展。结果是不是进行分词,因此您可以获得所需的 2 个元素。
如果您想array=( $names )
工作,那么您需要以某种方式用非空格字符分隔文件名,该字符也不包含在文件名中。然后您需要将 IFS 设置为该字符。
$ names=$(echo f* | sed "s/ /#/2")
$ echo $names
f 1#f 2
$ IFS='#' array=( $names )
$ echo ${#array[@]}
2
$ echo ${array[0]}
f 1
一种更优雅的方法是使用 NUL 字节\0
作为文件名分隔符,因为它保证永远不会成为文件名的一部分。为了实现这一点,我们需要使用find
带有-print0
标志的命令以及read
在 NUL 上分隔的内置命令。我们还需要清除 IFS,这样就不会执行空格上的分词。
#!/bin/bash
unset array
while IFS= read -r -d $'\0' name; do
array+=( "$name" )
done < <(find . -type f -name "f*" -print0 )
更新
扩展是在命令行上被分割成单词后执行的。
我可以想象人们会如何被上面的引言所困惑,只是为了进一步说明这一点分词是倒数第二个发生的扩展。
在我看来,更好的表达引用的方式是:
拆分后在命令行上进行扩展 论点。
shell 上参数的分割是总是由空白完成,这些参数将进一步扩展。如果你想在你的参数中有空格,你必须使用引用或者逃跑。IFS
不会增强参数拆分,只会增强单词拆分。
考虑这个例子:
$ touch f{1,2}; IFS="#"; rm f1#f2
rm: cannot remove `f1#f2': No such file or directory
请注意,设置为IFS
并#
没有改变 shell 仍然只看到一个参数的事实f1#f2
;顺便说一句,它还进一步受到各种扩展的影响。
我强烈推荐你自己用Bash常见问题解答如果你还没有。我特别强烈建议您阅读以下两个补充条目: