3

This is an approach to rename files to random filenames, suggested by Terdon. The advantage of this approach is that it automatically resolves filename collisions:

randomname() { base64 < /dev/urandom | tr -dC a-z0-9 | head -c4; }
for f in *.png; do
 newName=$(randomname).png
 while [ -e "$newName" ]; do
   newName=$(randomname).png
 done
 mv -- "$f" "$newName"
done

And here is another approach, using zmv. The advantage is that you can use it to batch rename files with different extensions:

zmv '*(.*)' '${(Lr:8:)$(uuidgen)}$1'

I want to modify the second approach so that it will automatically resolve filename collisons as the first one, but I don't understand how. Could anybody help with this?

2
  • 2
    My answer to one of your previous questions already shows how to do that (and uses builtins instead of forking a uidgen command, and uses a wider set of characters as you had initially requested). Commented May 28 at 14:27
  • @StéphaneChazelas Many thanks, Stéphane. It seems exactly what I was looking for.
    – jsx97
    Commented May 28 at 15:59

2 Answers 2

3

If you just need to handle multiple extensions, that can be done with a variation of the loop in the first approach. This uses the :e expansion modifier to add the original file extension to the new name (and some zsh-isms to avoid repeating the new-name code):

#!/usr/bin/env zsh
for f in *.*; do
  while [[ -e "${newName::=${(Lr:8:)$(uuidgen)}.${f:e}}" ]] {}
  mv -- "$f" "$newName"
done

With newName::=, the result of the next expansions (the uuidgen call and the extension) will be assigned to the newName variable. The {} is simply an empty loop body, since everything we needed was in the [[ ]] condition.


Using a similar approach with zmv:

#!/usr/bin/env zsh
autoload -Uz zmv
zmv -vn '*.*' '$( 
    while [[ -e "${nn::=${(Lr:8:)$(uuidgen)}.${f:e}}" ]] {}
    printf %s $nn )'

This demonstrates the -n flag, which is one of the reasons to use zmv - it will print what would occur without doing anything, which is very useful for debugging. Remove the -n to actually rename the files. A caveat: this will automatically go to the next name if there's a collision with a file that is already on the file system. But in the very, very unlikely event that uuidgen generates two identical names during one zmv call, zmv will detect the potential collision and exit without renaming any files.


Purely for entertainment, here's a version that avoids almost all external dependencies and uses a base64 encoder written entirely in zsh. It's based on some experiments I did a while back:

bytesToBase64u() {
  local LC_ALL=C i j k; local -a m=({{A..Z},{a..z},{0..9},-,_})
  for i j k ($(printf %d\  \'${(s..P)^1:?})) printf %s ${m[i/4+1]}${m[
    i<<4&48|${j:-0}>>4+1]}${j:+${m[j<<2&60|${k:-0}>>6+1]}}${k:+${m[k&63+1]}}
}
getRandomBytes () {
  b= o= l=${1:?}
  while ((l>0)) {
    LC_ALL=C IFS= read -r -u0 -d '' -k $l b
    LC_ALL=C o+=$b
    ((l-=$#b))
  } </dev/urandom
  printf -v ${2:?} %s $o
}
getName() {
    local rbytes
    getRandomBytes ${1:?} rbytes
    bytesToBase64u rbytes
}
zmv -vn '*.*' '$( 
    while [[ -e "${nn::=file_$(getName 6).${f:e}}" ]] {}
    printf %s $nn )'
0
3

The probability of collisions is low. So just hope that there are no collisions, and try again if there are. You can detect collisions because zmv has a specific error message for that.

attempts=0
while ((++attempts));
      err=$(zmv … 2>&1);
      [[ $# -eq 1 && $err == *" both map to "* && $attempts -le 10 ]];
do
  print -lr -- $err >&2
done
[[ -z $err ]]

The limit to the number of attempts is both in case there's a mistake and the renaming has a high chance of collisions, and in case some other error occurs but the string both map to appears in a file name.


Here are some other approach which are more complex than needed for the given problem, but demonstrate the use of zmv outside its comfort zone. The zmv interface is primarily designed for situations where the method to calculate the replacement of each file name is independent of the other file names. However, there are a few tricks to have dependencies between replacement names.

One thing that's easy is to inject a counter into the replacement. You can increment the counter in an arithmetic expression. This survives from one file name to the next because the replacement expression is not evaluated in a subshell.

function rename_files_to_numbers {
  local i=0
  # Preserve a single extension if any
  zmv $1 '$f:h/$((++i))${${f:e}:+.$f:e}'
}

Running arbitrary code in the replacement expression is easy: it's just command substitution. But that puts the code in a subshell, so you can't keep arbitrary state around between calls. However, for this specific task, all you need is the internal variable from that zmv maintains to keep track of the new file names. I assume that file names don't end with a newline.

zmv '*(.*)' '$(n=$(randomname)$1; while [[ -n $from[$n] || -e $n ]]; do  n=$(randomname)$1; done; print -r -- $n)'

The from variable is undocumented, although it's been there ever since zmv was introduced. What if we didn't want to rely on it? Then it's more complicated, but we can still update state through ${VAR::=VALUE} parameter substitutions. The code below uses n as a temporary variable for the new name and seen[$n] keeps track of new file names that are already allocated. ${…:+} hides the result of a nested parameter expansion.

unset seen; typeset -A seen
zmv '*(.*)' '${n::=$(n=$(randomname)$1; while [[ -n $seen[$n] || -e $n ]]; do  n=$(randomname)$1; done; print -r -- $n)}${${seen[$n]::=1}:+}'

A less intuitive approach that leads to simpler code is to prepare the replacement text as soon as you do the pattern matching. Within the pattern, you can use the e glob qualifier to run arbitrary code.

function assign_new_name {
  n=$(randomname).${REPLY:e};
  while [[ -n $seen[$n] || -e $n ]]; do
    n=$(randomname).${REPLY:e};
  done;
  seen[$n]=1; map[$REPLY]=$n;
}
unset seen map; typeset -A seen map; zmv -nv -Q '*.*(+assign_new_name)' '$map[$f]'

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .