How can I rename files by padding numbers anywhere within the filename?

You can use perl-rename (which should be installable with sudo apt install rename on Debian-based systems). Using these files as input:

$ ls -1
'anotherFile.m4a'    
'file 1 with 12 many 100 numbers 3.mp4'
'SL Benfica - Match 101 vs FC Porto.mp4'
'SL Benfica - Match 20 vs FC Porto.mp4'
'SL Benfica - Match 2 vs FC Porto.mp4'

You can run:

rename 's/(\d+)(?=.*\.)/sprintf("%03d",$1)/eg' *

Which will rename them to:

'anotherFile.m4a'
'file 001 with 012 many 100 numbers 003.mp4'
'SL Benfica - Match 002 vs FC Porto.mp4'
'SL Benfica - Match 020 vs FC Porto.mp4'
'SL Benfica - Match 101 vs FC Porto.mp4'

Note how the 100 was left unchanged, and the rest were padded. Also note how the 4 in .m4a and mp4 was not affected.

IMPORTANT: run the command with -n first to see what will happen before actually renaming the files:

rename -n 's/(\d+)(?=.*\.)/sprintf("%03d",$1)/eg' *

The regular expression looks for stretches one or more digits (\d+) that come before at least one . (?=.*\.). This is to avoid changing a number found in the extension. However, that will not work if your file names have no extension. If you have files like that, use this instead to just pad all numbers:

rename 's/(\d+)/sprintf("%03d",$1)/eg' *

The s/old/new/ is the substitution operator which will replace old with new. Here, because the (\d+) is in parentheses, whatever is matched will be captured and will then be available as $1 on the right hand side of the substitution. We therefore replace the digits with themselves 0 padded ( sprintf("%03d", $number) will print $number padded with 0s until its length is 3). Finally, the flag e lets us use expressions (here, sprintf) and g makes the substitution global, for all matches on the input line (file name).

Note that if you have a number that is already 0-padded with more than 3 0s, this will trim it to 3-0 padded. So file 00000001.mp3 will become file 001.mp3.


With zsh:

autoload -Uz zmv # best in ~/.zshrc
zmv -n '(*).mp4' '${1//(#m)<->/${(l[3][0])MATCH}}.mp4'

Remove the -n (dry-run) if happy.

  • zmv renames files based on zsh glob patterns. Here (*).mp4 matches filenames that end in .mp4, the part before .mp4 is captured so it can be used as $1 in the replacement.
  • ${1//pattern/replacement}: ksh operator to perform replacement on a parameter (here $1) expansion.
  • Here the pattern is (#m)<->, a zsh extendedglob pattern. (#m) causes the matched text to be available as $MATCH in the replacement, <->, simplest form of <x-y> number matching operator, matches any sequence of 1 or more decimal digits. Same as [0-9]##.
  • ${(l[3][0])param} uses the l[length][padstring] parameter expansion flag to left-pad the expansion of $MATCH with 0s to length 3 (note that it would also truncate numbers to length 3).

Avoiding truncation is possible, though it gets a bit more convoluted:

zmv -n '(*).mp4' '${1//(#m)<->/${(l[$#MATCH > 3 ? $#MATCH : 3][0])MATCH}}.mp4'

Instead of padding+truncating to length 3, we pad+truncate to a length which is the maximum of 3 and the length of the string to pad. Alternatively you could use the max() math function from the autoloadable zmathfunc function:

autoload -Uz zmv zmathfunc; zmathfunc
zmv -n '(*).mp4' '${1//(#m)<->/${(l[max(3, $#MATCH)][0])MATCH}}.mp4'

With GNU sed, you can do (even with no extension):

sed -E \
-e 'h;s/(.*)(\.[^.]*$)/\2/;ta;z;:a;x;s//\1/' \
-e 's/([0-9]{1,})/\n000\1\n/g' \
-e 's/\n[0-9]*([0-9]{3})\n/\1/g' \
-e 'G;s/\n//g' file

No loops only regexes.

The first regex will match

  • 'h;s/(.*)(\.[^.]*$)/\2/;ta;z;:a;x;s//\1/' place the extension in the hold space (if there was one). Leave the rest in the pattern space.
  • 's/([0-9]{1,})/\n000\1\n/g' Place each stream of numbers in separate lines prepending them with 3 zeros.

After each number to modify has been extended to have leading zeros and being in their own line, do:

  • 's/\n[0-9]*([0-9]{3})\n/\1/g' Extract the last 3 digits from each line of digits.
  • 'G;s/\n//' re-attach the extension captured at the start.

If the source list is:

➤ cat file
1- 23.m2
ATestFile.mp4
SomeFile.m4a
AFileWithNumbers23inside.mp4
File 1 with 12 some 100 numbers 3.mp4
SL Benfica - Match 1 vs FC Porto.mp4
SL Benfica - Match 2 vs FC Porto.mp4
SL Benfica - Match 20 vs FC Porto.mp4
SL Benfica - Match 101 vs FC Porto.mp4

The result will be:

001- 023.m2
ATestFile.mp4
SomeFile.m4a
AFileWithNumbers023inside.mp4
File 001 with 012 some 100 numbers 003.mp4
SL Benfica - Match 001 vs FC Porto.mp4
SL Benfica - Match 002 vs FC Porto.mp4
SL Benfica - Match 020 vs FC Porto.mp4
SL Benfica - Match 101 vs FC Porto.mp4

So, to rename files (filenames should not contain newlines and the extension should follow a dot):

#!/bin/bash

for old in *; do
    new=$(  printf '%s\n' "$old" | 
                sed -E \
                    -e 'h;s/(.*)(\.[^.]*$)/\2/;ta;z;:a;x;s//\1/' \
                    -e 's/([0-9]{1,})/\n000\1\n/g' \
                    -e 's/\n[0-9]*([0-9]{3})\n/\1/g' \
                    -e 'G;s/\n//'
         );
    if [[ ! -f $new ]]; then
        echo \
            mv "$old" "$new";
    fi
done

Remove the echo after you are satisfied with the script to actually change the files.

Tags:

Shell

Bash

Rename