【问题标题】:Shell script to delete whose files names are not in a text file用于删除文件名不在文本文件中的 Shell 脚本
【发布时间】: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

怎么做?

【问题讨论】:

    标签: shell file scripting


    【解决方案1】:

    使用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 选项。

    【讨论】:

    • 很酷的功能;我不知道这件事。似乎是最干净,最有效的解决方案。作为奖励,当文本文件列出我在回答中讨论的模式本身时,它也将起作用。 (希望这是期望的行为。)
    【解决方案2】:

    这是一种适用于 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 模式而不是单个文件名,例如 *.jpgmy*.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 进程替换,如果文件名包含新行,则会中断。

    【讨论】:

      【解决方案3】:

      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> 是命令行参数的数量,Mkeepme.txt 文件中的条目数)。如果你只有几十个文件,这应该没问题。不幸的是,我不知道在 Bash 中检查集合成员资格的更好方法。 (我们不能将文件名用作关联数组中的键,因为它们可能不是正确的标识符。)如果您关心许多文件的性能,那么使用更强大的语言(如 Python)可能值得考虑。

      我还想提一下,上面的例子只是比较字符串。它不会意识到important.txt./important.txt 是同一个文件,因此删除该文件。在比较之前使用readlink -f 将文件名转换为规范路径会更可靠。

      此外,您的用户可能希望能够将全局模式(如 important.* 放入要保留的文件列表中。如果您想处理这些,则需要额外的逻辑。

      总体而言,指定要删除的文件似乎有点危险,因为错误是不利的。

      【讨论】:

        【解决方案4】:

        如果文件名中没有空格或特殊转义字符,则这些(或它们的变体)中的任何一个都可以工作:

        1. rm -v $(stat -c %n * | sort excluded_file_list | uniq -u)

        2. stat -c %n * | grep -vf excluded_file_list | xargs rm -v

        【讨论】:

          猜你喜欢
          • 2012-04-26
          • 1970-01-01
          • 1970-01-01
          • 2022-07-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-01-25
          相关资源
          最近更新 更多