This answer uses the perl rename utility (aka prename or file-rename). NOT to be confused with the rename from util-linux, which has completely different command-line options and capabilities, or any other rename command.
One of the very nice things about the perl rename utility is that you can not only do relatively trivial sed-like transformations on filenames (e.g. rename 's/foo/bar/' *), you can use ANY algorithm implemented in perl to rename files. Each filename is held in perl's $_ special variable and will be renamed iff $_ is changed.
Which is what the following rename one-liner does:
$ 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' *
The -n option to rename makes this a dry-run, which shows what it would do if you let it.
Remove the -n (or replace it with -v for verbose output) to make it actually rename the files.
BTW, rename can take the list of filenames to be renamed as command-line arguments, or from STDIN, or both. Also worth noting is that it supports a -0 option for NUL-separated input (e.g. from find ... -print0).
NOTE: the results.txt file can't be reliably used to reverse the renaming if any of the original filenames contained \n newline characters. If you can't be certain of that, then use NULs to separate each record in results.txt instead of newlines. i.e. replace the second-last line of the rename script with:
print RESULTS "$rnd\t$_\0";
FYI, to reverse:
$ 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]*
The ,2 in the split /\t/,$_,2 line limits the split to a maximum of two fields - even if there is more than one tab character in the input record...so tab characters in the filename won't break the script.
A naive reversal like sed -e 's/^/mv /' results.txt | sh would break on filenames containing spaces, tabs, etc or shell meta-characters.
FYI: I just read Kusalananda's answer, and feel that it's worth pointing out that this answer does just repeatedly roll d100 and check if we've already used that random number.
That's partly because there's little or no performance impact for only 100 random numbers, and partly because it's far less of a performance issue in perl....bash is slow, and loops in bash are especially slow. But mostly because it didn't occur to me until I read his answer :)
In theory, this loop could be endless (with a very bad sequence of random numbers) or take a very long time. In practice, both outcomes are extremely unlikely, although generating an unused number is likely to take longer for each iteration of the loop (but this is unlikely to even be noticeable by a human).
It is possible to pre-generate a randomised array as in K's answer, e.g. using the List::Util module's shuffle() function.
$ 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++]' *