我正在练习外壳脚本我正在尝试制作一个简单的脚本,该脚本将目录作为参数,循环遍历其中的每个文件并打印出其名称和大小。
#!/bin/bash
# A practice shell script to try and display a list of file names
# and their sizes using the output of ls -l and cut.
declare -i index
export index=0
export name=""
export size=0
for file in $1 ; do
index+=1
name=`basename $file`
size=`ls -l $file | cut -d " " -f 5`
echo "$index: $name, size: $size bytes"
done
当我./*
作为参数给出时,它会针对一个文件执行此操作,仅此而已。但是,如果我编辑上面的代码并将其放在./*
的位置$1
,它就会工作并循环当前目录中的所有文件。
$1
当应该等于时,为什么它不做同样的事情./*
?
答案1
原因是您调用脚本的 shell 扩展了通配模式./*
前它被传递给脚本。这意味着,如果您的通配模式与例如匹配file1.txt
,file4.txt
则将脚本调用为
./my_script.sh ./*
实际上会被解释为
./my_script.sh file1.txt file2.txt file3.txt file4.txt
这些将是 shell 脚本看到的参数。
如需进一步阅读,请查看关于 shell 扩展顺序的部分在 Bash 参考手册中。
有两种可能性可以解决该问题:
- 如果您确定始终想要迭代给定目录中的所有文件,请将目录作为参数传递,然后迭代
for f in "$1"/* do # operations on "$f" done
- 或者,如果您确定只传递要操作的文件名,请迭代整个参数列表,如下所示
for f in "$@" do # operations on "$f" done
如果您想通过将 glob 模式传递到脚本中来实现这一点 - 这当然是一个有趣的练习 - 这也是可能的(请参阅 @ilkkachu 的评论)。正如 @fra-san 在评论中提到的,该方法具有优点 - 它可以为脚本使用增加更多灵活性,并且它规避了 shell 命令行参数的限制(参见“参数列表太长”;尽管 RAM 会仍然限制生成的文件名列表的长度) - 但需要您格外小心。
- 您可以通过将参数括在引号(单引号或双引号)中或使用反斜杠转义 glob 字符来防止 shell 扩展 glob:
./my_script.sh "./*" ./my_script.sh './*' ./my_script.sh ./\*
- 在脚本内部,您将引用位置参数
$1
未引用的这样它实际上是由 shell 解释的(这是我们经常想要避免的)。 - 由于“解释”不仅涉及扩展(见上文),还涉及分词,因此您需要设置输入字段分隔符
IFS
到空字符串以确保不会发生分词。 - 循环
for
看起来像IFS= for f in $1 do # Operations on "$f" done
关于脚本的一些一般注意事项:
- 始终引用 shell 变量,特别是当它们包含文件名时,否则您的脚本将偶然发现其中包含空格或其他甚至更奇特的字符的文件名 - 请记住,即使换行符也是文件名允许的字符(恶心)!
ls
解析is的输出非常沮丧出于类似的原因。如果您想识别特定文件的属性,该stat
工具是更好的选择。为了确定文件的大小,例如,您可以使用size=$(stat --printf="%s" "$f")
- 这是受到推崇的使用“新”
$( ... )
样式进行命令替换,而不是旧的“反引号样式”` ... `
。 - 检查 shell 脚本是一个好习惯
shellcheck
,也可以在许多 Linux 发行版中作为独立工具使用,以防止这种(和其他)可能的错误源。
答案2
您需要了解 bash 如何解释通配符和参数。当存在通配符时,bash会及时解释它并用所有匹配的文件替换它。当您用 ./* 替换 $1 时,就会发生这种情况 - 它会从当前目录中获取所有文件并循环遍历它们。
当你有
for file in $1 ; do
它只需要第一个参数。那不是你想要的。如果你想循环遍历所有文件,你需要使用:
for file in $* ; do
(这将接受所有参数)
或者,您可以使用 - 循环遍历它们,shift
只要还有其他参数,它就会删除第一个参数:
while [ $# -gt 0 ]
do
file=$1
shift
...
done