正如标题中所写,我想在 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
实用程序(又名prename
或file-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,或两者兼而有之。另外值得注意的是,它支持-0
NUL 分隔输入的选项(例如 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
其中oe
glob 限定符根据所提供的表达式(这里返回 0 到 32767 之间的随机数)的计算来定义 glob 扩展的顺序,这实际上给你一个像文件一样的打乱顺序。
然后,我们按顺序将这些文件重命名为递增的数量,因此无论目录中有多少文件,它都会起作用。