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

Pet*_*r.O 5 shell bash

我还没有完全理解 shell 扩展(希望有一天我会)......
我看到了对超级用户问题的评论,但我想我仍然停在路边......

在没有外壳的情况下使用 Linux 就像在城市交通中以 50 公里/小时的速度驾驶法拉利。所有的乐趣都会消失......

我不明白下面的例子.. 什么层次结构或其他什么导致第二个例子“数组项目计数:”与第一个例子不同?

外壳引入了“空间”发生了什么?或者它echo是引入空间的那个,而外壳是(也许)使用\ 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?
#
Run Code Online (Sandbox Code Playgroud)

Gil*_*il' 5

一个 shell 命令(更准确地说,是一个“简单命令”)由一个单词列表组成。每个单词可以是任意字符串(shell 单词可以包含空格和标点符号)。

当您运行 时echo -n *,shell 对 执行路径名扩展(也称为文件名生成或通配)*,并用匹配的文件名列表替换它。所以展开之后,这个命令由四个字组成:echo-nf 1f 2。该命令echo使用两个参数运行,并打印其参数,中间有一个空格(并且由于该-n选项而没有终止换行符)。所以输出是f 1 f 2练习:创建另一个名称由两个连续空格组成的文件 run 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[@]}"
Run Code Online (Sandbox Code Playgroud)

进一步练习:创建一个名称为单个星号 ( touch '*') 的文件并再次运行这些命令。你明白输出吗?

旁白: zsh 不会对变量扩展执行分词或路径名扩展。这使得编程变得更加明智。


Sie*_*geX 2

来自男人的狂欢

EXPANSION 扩展是在命令行被分割成单词后执行的。执行了七种扩展:大括号扩展、波形符扩展、参数和变量扩展、命令替换、算术扩展、分词和路径名扩展。扩展的顺序是:大括号扩展、波形符扩展、参数、变量和算术扩展以及命令替换(以从左到右的方式完成)、分词和路径名扩展。

array=( $names )
Run Code Online (Sandbox Code Playgroud)

这给你 4 个条目的原因是因为未加引号的$names参数会进一步受到基于内部字段分隔符(默认情况下)的单词分割的影响。如果您要引用来禁止分词,那么您只会得到一个具有 value 的数组元素,这又不是您想要的。IFS<space><tab><newline>"$names"f 1 f 2

array=( * )
Run Code Online (Sandbox Code Playgroud)

另一方面,上面的内容仅受路径名扩展的影响,而路径名扩展恰好是最后执行的扩展。结果不受分词的影响,因此您可以获得所需的 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
Run Code Online (Sandbox Code Playgroud)

一种更优雅的方法是使用 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 )
Run Code Online (Sandbox Code Playgroud)

更新

扩展是在命令行上被分割成单词后执行的。

我可以想象,如果上面的引用进一步说明分词是倒数第二个发生的扩展,人们会感到多么困惑。

在我看来,更好的表达引用的方式是:

将命令行拆分为 参数后,将在命令行上执行扩展。

shell 上参数的分割总是由空格完成,而这些参数将进一步受到扩展。如果您想在参数中包含空格,则必须使用引用转义IFS不会增强参数拆分,只会增强单词拆分。

考虑这个例子:

$ touch f{1,2}; IFS="#"; rm f1#f2
rm: cannot remove `f1#f2': No such file or directory
Run Code Online (Sandbox Code Playgroud)

请注意,设置为IFS#没有改变 shell 仍然只看到一个参数的事实f1#f2;顺便说一句,它还进一步受到各种扩展的影响。

如果你还没有的话,我强烈推荐你自己使用BashFAQ进行 aquatint 操作。我特别强烈建议您阅读以下两个补充条目:

  1. 论据
  2. 分词