【发布时间】:2017-11-21 01:10:37
【问题描述】:
我有一个包含文件名列表的 txt 文件
例子:
10.jpg
11.jpg
12.jpeg
...
在文件夹中,此文件应防止删除过程,其他文件应删除。
所以我想要这个问题的相反逻辑:Shell command/script to delete files whose names are in a text file
怎么做?
【问题讨论】:
我有一个包含文件名列表的 txt 文件
例子:
10.jpg
11.jpg
12.jpeg
...
在文件夹中,此文件应防止删除过程,其他文件应删除。
所以我想要这个问题的相反逻辑:Shell command/script to delete files whose names are in a text file
怎么做?
【问题讨论】:
使用extglob和Bash扩展模式匹配!(pattern-list):
!(模式列表)
匹配除给定模式之一之外的任何内容
其中,模式列表是由 | 分隔的一个或多个模式的列表。extglob
如果设置,则启用上述扩展模式匹配功能。
例如:
$ ls
10.jpg 11.jpg 12.jpeg 13.jpg 14.jpg 15.jpg 16.jpg a.txt
$ shopt -s extglob
$ shopt | grep extglob
extglob on
$ cat a.txt
10.jpg
11.jpg
12.jpeg
$ tr '\n' '|' < a.txt
10.jpg|11.jpg|12.jpeg|
$ ls !(`tr '\n' '|' < a.txt`)
13.jpg 14.jpg 15.jpg 16.jpg a.txt
根据示例,删除的文件为13.jpg 14.jpg 15.jpg 16.jpg a.txt。
所以使用extglob和!(pattern-list),我们可以根据文件内容获取排除的文件。
此外,如果您想排除以. 开头的条目,则可以使用shopt -s dotglob 打开dotglob 选项。
【讨论】:
这是一种适用于 bash GLOBIGNORE 的方式:
$ cat file2
10.jpg
11.jpg
12.jpg
$ ls *.jpg
10.jpg 11.jpg 12.jpg 13.jpg
$ echo $GLOBIGNORE
$ GLOBIGNORE=$(tr '\n' ':' <file2 )
$ echo $GLOBIGNORE
10.jpg:11.jpg:12.jpg:
$ ls *.jpg
13.jpg
很明显,通配符会忽略 GLOBIGNORE bash 变量中包含的任何内容(文件、模式等)。
这就是为什么最后一个 ls 只报告文件 13.jpg 的原因,因为文件 10,11 和 12.jpg 被忽略了。
因此,使用rm *.jpg 将只删除我系统中的13.jpg:
$ rm -iv *.jpg
rm: remove regular empty file '13.jpg'? y
removed '13.jpg'
完成后,您只需将 GLOBIGNORE 设置为 null:
$ GLOBIGNORE=
值得一提的是,在 GLOBIGNORE 中,您还可以应用 glob 模式而不是单个文件名,例如 *.jpg 或 my*.mp3 等
替代方案:
我们可以使用编程技术(grep、awk 等)来比较 ignorefile 中存在的文件名和当前目录下的文件:
$ awk 'NR==FNR{f[$0];next}(!($0 in f))' file2 <(find . -type f -name '*.jpg' -printf '%f\n')
13.jpg
$ rm -iv "$(awk 'NR==FNR{f[$0];next}(!($0 in f))' file2 <(find . -type f -name '*.jpg' -printf '%f\n'))"
rm: remove regular empty file '13.jpg'? y
removed '13.jpg'
注意:这也使用了 bash 进程替换,如果文件名包含新行,则会中断。
【讨论】:
George Vasiliou 的答案的另一种选择是读取包含文件名的文件,以继续使用 Bash 内置 mapfile,然后检查每个要删除的文件是否在该列表中。
#! /bin/bash -eu
mapfile -t keepthose <keepme.txt
declare -a deletethose
for f in "$@"
do
keep=0
for not in "${keepthose[@]}"
do
[ "${not}" = "${f}" ] && keep=1 || :
done
[ ${keep} -gt 0 ] || deletethose+=("${f}")
done
# Remove the 'echo' if you really want to delete files.
echo rm -f "${deletethose[@]}"
-t 选项使mapfile 从它从文件中读取的行中删除尾随换行符。但是,不会修剪其他空白。如果您的文件名实际上包含空格,这可能是您想要的,但如果有人不小心在他们想要保留的重要文件的名称之前或之后放置了一个空格,它也可能会导致微妙的意外。
请注意,我首先构建了应删除的文件列表,然后将它们全部删除,而不是单独删除每个文件。这样可以节省一些子流程调用。
列表中的查找,如上面编码,具有线性复杂度,这给出了整个脚本的二次复杂度(准确地说,N × M 其中 N i> 是命令行参数的数量,M 是keepme.txt 文件中的条目数)。如果你只有几十个文件,这应该没问题。不幸的是,我不知道在 Bash 中检查集合成员资格的更好方法。 (我们不能将文件名用作关联数组中的键,因为它们可能不是正确的标识符。)如果您关心许多文件的性能,那么使用更强大的语言(如 Python)可能值得考虑。
我还想提一下,上面的例子只是比较字符串。它不会意识到important.txt 和./important.txt 是同一个文件,因此删除该文件。在比较之前使用readlink -f 将文件名转换为规范路径会更可靠。
此外,您的用户可能希望能够将全局模式(如 important.* 放入要保留的文件列表中。如果您想处理这些,则需要额外的逻辑。
总体而言,指定要不删除的文件似乎有点危险,因为错误是不利的。
【讨论】:
如果文件名中没有空格或特殊转义字符,则这些(或它们的变体)中的任何一个都可以工作:
rm -v $(stat -c %n * | sort excluded_file_list | uniq -u)
stat -c %n * | grep -vf excluded_file_list | xargs rm -v
【讨论】: