shell 文件名扩展如何分隔 ( * ) 列表中的项目?

shell 文件名扩展如何分隔 ( * ) 列表中的项目?

我还没有完全理解 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-nf 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 个单词:f1f2。因此该数组包含四个元素(每个元素都是一个字符的单词)。练习:对于名称中包含两个连续空格的额外文件,现在数组的确切内容是什么?

接下来,你尝试了array=( * )。这里,数组中有一个单词,受通常的扩展影响,最后一个是路径名扩展。由于有两个匹配文件,因此该数组包含两个单词,即每个文件的名称:f 1f 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常见问题解答如果你还没有。我特别强烈建议您阅读以下两个补充条目:

  1. 论点
  2. 分词

相关内容