目前我正在使用以下 zsh-snippet 来选择小批量文件以进行进一步处理
for f in $(ls /some/path/*.txt | head -2) ; do
echo unpacking $f
./prepare.sh $f && rm -v $f
done
有没有比$(ls ... | head -2)
zsh 更好的替代方案?
我的任务的总体概述。我正在创建一个数据集来训练神经网络。机器学习任务的细节在这里并不重要。数据集创建的任务需要我手动处理大量文件。为此,我将它们复制到一个单独的目录中。然后,我随机选择几个文件(本示例中的输出中的前两个ls
),调用一些预处理例程,检查其结果,将其中一些文件移动到正在创建的数据集中,并删除其余文件。清理完毕后,我再次执行上面的命令。
此外,我想提高我的 shell 编程技能并学习新的东西:)
选择这些“第一个”文件的顺序并不重要,因为所有这些文件最终都会被处理。
换句话说,我正在for
循环内与 PC 一起工作,并希望它在几次迭代后暂停并等待我。
伪代码。
for f in /some/path/*.txt ; do
echo unpacking $f
./prepare $f
if human wants to review ; then
human is reviewing then cleans, and PC waits
fi
done
这种奇怪的过程的原因是,一个“源”.txt
文件的预处理会创建几十个其他文件,我需要查看所有这些文件并选择一些适合训练网络的样本(通常是 1-2 个)。
我可以运行for f in /some/path/*.txt ; do ./prepare $f ; done
,但这个命令会创建数百个文件,这个数量令人难以承受。
答案1
全局限定符
Glob 限定符可以替代大多数文件的使用ls
或find
枚举文件。它们是 zsh 的独特功能。
例如,$(ls /some/path/*.txt | head -2)
(按字典顺序枚举文件,只保留前两个文件)相当于zsh中的1/some/path/*.txt(N[1,2])
。限定符N
确保在没有匹配项时列表为空,并且限定符将匹配项限制在指定范围内。[from,to]
如果没有N
限定符,在默认选项下,如果没有匹配的文件,您的脚本将退出并显示错误消息。
您可以使用o
orO
限定符来控制文件的顺序。例如,/some/path/*.txt(Nom[1,2])
获取两个最新的文件。
1 有一些细微的差别,通常对 zsh 有利。使用ls
包含特殊字符(例如空格或换行符或无效字节序列)的文件名往往会出现问题,而 zsh 的内置功能可以在所有文件名上可靠地工作。错误管理在极端情况下是不同的。在这里,由于您忘记了-d
选项,如果其中一些文件属于类型,ls
您也会遇到问题*.txt
目录asls
会列出它们的内容。
不过,我不明白获取两个文件如何有助于实现您的总体目标。如果您希望有一种方法来处理所有文件,但允许人员查看前几个文件,您可以显示步骤/继续/中止提示。像这样的东西:
pause=1
for f in /some/path/*.txt ; do
print -ru2 unpacking $f
./prepare $f
if ((pause)); then
print -ru2 -- "$f output is ready for review."
c=
while [[ $c != [anq] ]]; do
read -k1 "c?Process (N)ext, (A)ll, (Q)uit? " && c=${c:l}
done
echo
case $c in
a) pause=0;;
q) break;;
esac
fi
done
答案2
您可以在循环中使用计数器for
。这应该适用于任何 POSIX 兼容的 shell。
这相当于问题中的第一个代码片段。
i=0
for f in /some/path/*.txt ; do
if [ "$((i += 1))" -gt 2 ] ; then
break
fi
echo "unpacking $f"
./prepare.sh "$f" && rm -v "$f"
done
正如所写吉尔斯的回答,zsh
具有以更简单的方式实现这一点的功能。有关 glob 限定符的解释,请参阅此答案。
for f in /some/path/*.txt(N[1,2]) ; do
echo "unpacking $f"
./prepare.sh "$f" && rm -v "$f"
done
您也可以等待一些输入,而不是打破循环。
i=0
for f in /some/path/*.txt ; do
if [ "$((i += 1))" -gt 2 ] ; then
i=1
printf "press Enter to continue"
read dummy
fi
printf "unpacking %s\n" "$f"
./prepare.sh "$f" && rm -v "$f"
done
注1:在条件分支中,我使用i=1
代替,i=0
因为脚本已经在本次迭代中处理下一组的第一个文件。
笔记2:我将计数和条件放在循环的开头而不是结尾,因为此时很明显还有另一个文件要处理。这可以避免在最后一个文件之后出现暂停。
注3:我编辑了脚本并添加了引号,$((i += 1))
因为斯蒂芬·查泽拉斯在一个中提到评论在大多数 POSIX 兼容 shell 中,将对算术扩展的结果执行通配和分割,这可能会导致不必要的影响,尤其是在IFS
包含十进制数字时(我认为这是一种不寻常的情况)。此外,如果该模式与任何文件都不匹配,则将执行循环迭代并将其f
设置为文字文件名模式。这可能会导致错误消息或意外结果。为了避免这种情况,请使用 例如 使循环体成为条件if [ -f "$f" ] ...
。如果这些特殊情况与预期(交互式)用途无关,则脚本可以保持更简单。
答案3
在 Bash 中,您可以将文件名放入数组中并在数组切片上循环。并循环重复:
#!/bin/bash
shopt -s nullglob
files=(/some/path/*.txt)
for (( i = 0; i < "${#files[@]}"; i += 2)); do
for f in "${files[@]:i:2}"; do
printf "processing %s\n" "$f";
done
read -p "press enter to continue with the next set (or end)..."
done
或者进行重组以避免最后可能出现的提示,但代价是失去简单性:
#!/bin/bash
shopt -s nullglob
files=(/some/path/*.txt)
i=0
while true; do
for f in "${files[@]:i:2}"; do
printf "processing %s\n" "$f";
done
(( i += 2 ))
(( i >= "${#files[@]}" )) && break
read -p "press enter to continue with the next set..."
done
答案4
我建议使用gnu-parallel
- 将您的任务放入脚本中
#!/bin/bash
echo unpacking ${1}
/full/path/to/prepare.sh ${1} && rm -v ${1}
- 通过以下方式运行它
gnu-parallel
(可执行文件的实际名称可能会有所不同,具体取决于您的发行版以及程序的安装方式)
parallel --halt soon,success=5 /path/to/script {} ::: *.txt
parallel
默认情况下,每个 CPU 运行一个作业,--halt soon,success=5
意味着“成功运行 5 个作业后停止处理,但让正在运行的作业完成”。{}
替换文件名,并且:::
是参数列表的分隔符。
它不会等待您这边的“继续”,但由于您删除了原始文件,因此您可以重新启动该过程并且没有双打。