我正在尝试使用 foreach 循环处理目录中的一些文件以获取 ls 输出:
IFS=$'\n'
for i in $(ls -1Atu); do
echo "$i"
done
第一次我认为它有效,但是当我创建名称类似于*
or 的文件时filename*
,shell 添加了额外的迭代,因为*
解释为通配符。
但如果我像这样转义 ls 输出for i in "$(ls -1Atu)";
,我将得到一次迭代,并且 $i 将完全包含 ls 的输出。
我们该如何解决这个问题呢?我应该注意到,ls -1Atu
根据我的需要,任何具有更好的变体的结果文件将按时间排序。谢谢
答案1
背景阅读:为什么我的 shell 脚本会因为空格或其他特殊字符而卡住?,为什么你不应该解析 ls 的输出
设置IFS
为换行符意味着在命令替换扩展期间,只有换行符,而不是空格和制表符,才会被视为分隔符。您的方法不支持包含换行符的文件名;这是 1 的一个基本限制ls
。
此外,这就是您遇到的问题,设置IFS
对命令替换扩展时发生的其他事情(即通配符匹配)没有影响。您可以关闭通配符,然后只要文件名不包含换行符,您的脚本就可以工作。
IFS='
'
set -- *"$IFS"*
if [ -e "$1" ]; then
echo >&2 "There are file names with newlines. I cannot cope with this."
exit 2
fi
set -f
for i in $(ls -1Atu); do
printf '%s\n' "$i"
done
set +f
unset IFS
按日期枚举文件的简单、可靠的方法是使用纯 Bourne 风格的 shell 以外的工具,例如 zsh,它具有全局限定符修改通配符匹配的排序方式。
#!/bin/zsh
files_by_access_time=(*(Doa))
1您可以通过一些实现来解决这个问题ls
,但如果您需要可移植性,则不行。
答案2
由于与空白问题等相关的各种原因,不建议解析输出ls
。另一种方法是使用 GNU 版本的find
, sort
, sed
:
find . -mindepth 1 -maxdepth 1 -printf "%A@ %f\0" |
sort -rnz |
sed -z 's/^[0-9.]\+ //'
find
当然,比ls
列出和过滤文件要灵活得多,但它本身不进行排序。- 在这里,我打印以秒为单位的修改时间 (
%A@
),后跟文件名和 ASCII NUL 字符,这是用于分隔文件名的唯一安全字符。 sort -rnz
然后接受 NUL 分隔的输入 (-z
) 并按-n
降序进行数字排序 ( )(因为-t
ofls
首先给出最新的)。- 然后我常常
sed
去掉最初的时间,再次处理 NUL 分隔的行 (-z
)。
生成的输出是 NUL 分隔行的流。在 bash 中(不是普通的 sh),您可以通过以下方式将它们读入变量:
find . -mindepth 1 -maxdepth 1 -printf "%A@ %f\0" |
sort -rnz |
sed -z 's/^[0-9.]\+ //' |
while IFS= read -r -d '' filename
do
# do stuff with filenames
printf "%s\n" "$filename"
done
当然,这有点复杂,但更安全。
答案3
- 您真的是要添加
*
文件名吗? - 或者您的意思是如果具有执行权限,则
ls
给出以结尾的文件名的输出?*
如果只有 的输出问题ls
,您可以简单地通过以下方式解决:
替换ls
为\ls
,这是使用 的非别名版本ls
,它不输出*
答案4
你可以使用这样的东西:
ls -1Atu | while IFS= read -r entry; do
echo "$entry"
done
在这个示例中,输出生成一次,并且该while read entry
部分导致输出被ls
逐行解析,这解决了for
示例中所有内容都放在$i
单轮中的问题。