循环目录中的文件 - 将其名称保存到文件中,然后更改名称

循环目录中的文件 - 将其名称保存到文件中,然后更改名称

正如标题中所写,我想在 bash 中编写一个程序,该程序将执行以下操作:

  • 循环遍历目录中的每个文件(每个目录中有100个文件)

  • 为其分配尚未分配的 1 到 100 之间的随机数

  • 将文件名保存到另一个文件 (results.txt),格式为 X(分配的随机数) 文件名

  • 将文件名更改为随机数

我知道如何循环文件,但休息有点超出了我的能力。我将非常感谢您在这件事上的帮助:)

答案1

此练习的主要困难是为 100 个文件中的每个文件生成唯一的随机数。该问题的解决方案是生成我们知道需要的数字(1 到 100),然后在将数字与文件名配对之前对它们进行打乱(或打乱文件名列表)。您最终不想做的就是掷 100 面骰子,检查您是否已经掷过该数字,如果已经掷过,则重新掷骰子,直到获得之前未掷过的数字。对于大量文件,该过程可能需要很长一段时间。 (有趣的是,几年前我们用这个作为面试问题)。

假设没有文件名包含嵌入的换行符:

paste <( printf '%d\n' {1..100} ) <( printf '%s\n' dir/* | sort -R ) >result.txt

这将使用该实用程序创建两个制表符分隔的列paste。第一列按顺序包含从 1 到 100 的整数。第二列包含目录中100个文件的名称dir(包括目录名称)。名称列表按随机顺序排序(-R选项sort是非标准的,但通常可用)。

您也可以对文件名进行排序,而不是对整数进行打乱:

paste <( printf '%d\n' {1..100} | sort -R ) <( printf '%s\n' dir/* ) >result.txt

要更改文件的名称,请读取该result.txt文件:

while IFS= read -r stuff; do
    number=${stuff%%$'\t'*}   # the thing before the first tab
    pathname=${stuff#*$'\t'}  # the thing after the first tab
    mv -i -- "$pathname" "$(dirname -- "$pathname")/$number"
done <result.txt

对当前目录中的所有子目录运行此命令(运行此命令之前请三思,并始终保留重要数据的备份):

for dirpath in */; do
    paste <( printf '%d\n' {1..100} | sort -R ) <( printf '%s\n' "$dirpath"/* )
done >result.txt

while IFS= read -r stuff; do
    number=${stuff%%$'\t'*}
    pathname=${stuff#*$'\t'}
    mv -i -- "$pathname" "$(dirname -- "$pathname")/$number"
done <result.txt

或者,更精简的,

for dirpath in */; do
    paste <( printf '%d\n' {1..100} | sort -R ) <( printf '%s\n' "$dirpath"/* )
done |
tee result.txt |
while IFS= read -r stuff; do
    number=${stuff%%$'\t'*}
    pathname=${stuff#*$'\t'}
    mv -i -- "$pathname" "$(dirname -- "$pathname")/$number"
done

答案2

这个答案使用了珀尔 rename实用程序(又名prenamefile-rename)。 不是rename与from混淆util-linux,后者具有完全不同的命令行选项和功能,或任何其他rename命令。

perl 重命名实用程序的好处之一是,您不仅可以对文件名进行相对简单的类似 sed 的转换(例如rename 's/foo/bar/' *),您还可以使用任何用 perl 实现的算法来重命名文件。每个文件名都保存在 perl 的$_特殊变量中并且将被重命名当且仅当$_ 已更改。

这就是以下重命名单行代码的作用:

$ rename -n '
  BEGIN{
    open(RESULTS,">","results.txt");
  };

  our $rnd = 0;
  our @used;

  # find a random number from 1..100 that hasnt been used yet.
  until (($rnd > 0) && (!defined($used[$rnd]))) {
    $rnd=int(rand(100)+1);
  };
  $used[$rnd]=1;

  print RESULTS "$rnd\t$_\n";
  $_ = $rnd' *

使其成为一次试运行-n的选项,这表明了它的作用rename如果你允许的话就做吧。

删除-n(或将其替换-v为详细输出)以使其实际重命名文件。

顺便说一句,rename可以将要重命名的文件名列表作为命令行参数,或来自 STDIN,或两者兼而有之。另外值得注意的是,它支持-0NUL 分隔输入的选项(例如 from find ... -print0)。

注意:如果任何原始文件名包含换行符,则该results.txt文件不能可靠地用于反转重命名。\n如果您不能确定这一点,请使用 NUL 来分隔 results.txt 中的每条记录,而不是换行符。即,将重命名脚本的倒数第二行替换为:

  print RESULTS "$rnd\t$_\0";

仅供参考,反转:

$ rename -n '
  our %files;

  BEGIN{
    open(RESULTS,"<","results.txt");
    # local $/ = "\0"; # uncomment for NUL-separated input
    while(<RESULTS>){
      chomp;
      my ($n,$f) = split /\t/,$_,2;
      $files{$n} = $f;
    };
    close(RESULTS);
  };

  if (defined($files{$_})) { $_ = $files{$_} };
  ' [0-9]*

,2行将split /\t/,$_,2分割限制为最多两个字段 - 即使输入记录中存在多个制表符...因此文件名中的制表符不会破坏脚本。

简单的反转sed -e 's/^/mv /' results.txt | sh会破坏包含空格、制表符等或 shell 元字符的文件名。


仅供参考:我刚刚读了 Kusalananda 的答案,觉得值得指出的是,这个答案只需重复滚动 d100 并检查我们是否已经使用了该随机数。

部分原因是仅 100 个随机数对性能影响很小或没有,部分原因是 perl 中的性能问题要小得多……bash 很慢,bash 中的循环尤其慢。但主要是因为直到我读到他的答案时我才想到这一点:)

理论上,这个循环可能是无限的(随机数序列非常糟糕)或者需要很长时间。在实践中,这两种结果都极不可能,尽管生成未使用的数字可能会在循环的每次迭代中花费更长的时间(但这甚至不太可能被人类注意到)。

可以像 K 的答案一样预先生成一个随机数组,例如使用List::Util模块的shuffle()函数。

$ rename -n '
  use List::Util qw(shuffle);
  our $i;
  our @random;

  BEGIN{
    @random = shuffle 1..100;
    $i=0;
    open(RESULTS,">","results.txt");
  };

  print RESULTS "$random[$i]\t$_\n";
  $_ = $random[$i++]' *

答案3

zsh

n=0; for f in dir/*(noe['REPLY=$RANDOM']); do
  mv -i -- $f $f:h/$((++n)) &&
    print -r -- $f was renamed to $n
done > result.txt

其中oeglob 限定符根据所提供的表达式(这里返回 0 到 32767 之间的随机数)的计算来定义 glob 扩展的顺序,这实际上给你一个像文件一样的打乱顺序。

然后,我们按顺序将这些文件重命名为递增的数量,因此无论目录中有多少文件,它都会起作用。

相关内容