I have the following directory structure:

├── event-a
│   ├── album-a
│   │   ├── a.jpg
│   │   └── x.png
│   └── album-b
│       ├── a.jpg
│       ├── x.png
│       └── y.gif
└── event-b
    ├── album-x
    │   ├── a.jpg
    │   └── x.png
    └── album-y
        ├── a.jpg
        ├── x.png
        └── y.gif

For each second-level subfolder (named album-foo in the example), I want to sort the files by name and rename them to sequential padded numbers, regardless of their extension. The folders may contain JPG, PNG or GIF images, and any file extensions should be preserved.

So, in this case, I’d like to get this result:

├── event-a
│   ├── album-a
│   │   ├── 01.jpg
│   │   └── 02.png
│   └── album-b
│       ├── 01.jpg
│       ├── 02.png
│       └── 03.gif
└── event-b
    ├── album-x
    │   ├── 01.jpg
    │   └── 02.png
    └── album-y
        ├── 01.jpg
        ├── 02.png
        └── 03.gif

The rename utility may be used if that makes anything easier.

The main problem is figuring out the number each file should get. Do I need to use a nested for loop to iterate over the event-x and album-x subfolders, then for each inner loop, keep track of the number myself, or is there some clever solution that I’m missing?

4 Answers 4

for dir in */*; do           # loop over the directories
    (                        # run in a subshell ...
        cd "$dir"            # ... so we don't have to cd back
        files=(*)            # store the filenames in a zero-indexed array

        for index in "${!files[@]}"; do
            newname=$(printf "%02d.%s" $((index+1)) "$ext")
            mv "$file" "$newname"

Suppose you have a file with no extension. In that case it will have the same name except with leading numbers (e.g. my_file => 05.my_file)

All non-hidden directory entries will be renamed, including directories.

  • Thanks! FTR, a friend of mine wrote the following snippet which is very similar to your answer: gist.github.com/2471545 Apr 23, 2012 at 20:01
  • there is an open quote at ext=... Aug 26, 2014 at 20:25
  • We can safely ignore SC2164 on line 3?
    – Pablo A
    Mar 25, 2022 at 15:38

You have two sets of problems here:

  1. Recursing into directories.
  2. Restarting the numbering system within each separate directory.

This will handle both conditions smoothly, although you will want to modify the IMAGE_TYPES variable if you decide to include other extensions as well.


shopt -s extglob
shopt -s nocaseglob

IFS=$'\n' dirlist=(`find "$PWD" -type d`)

for dir in "${dirlist[@]}"; do
    cd "$dir"
    ls *.+($IMAGE_TYPES) > /dev/null 2>&1 || continue

    for file in *.+($IMAGE_TYPES); do
        printf -v newname "%.3d.%s" $((counter += 1)) "${file##*.}"
        mv --verbose "$file" "$newname"

I created the same tree and did your move build. Here's what I came up with:

n=0 IFS=.; set -f ./[e]vent*/*/*
for f do [ -n "${f##./\[*}" ] || break
    [ "$d" = "${f%/*}" ] || i=0 d=${f%/*}
    mv=': mv "${'$((n=$n+1))'}" "${'$n'%%/*}/'
    printf "$mv%.2d%.0s.%s\"\n" $((i=$i+1)) ${f##*/}
done | sh -sx -- "$@"

Without that last |pipe on the end there this is what it prints:

mv "${1}" "${1%/*}/01.jpg"
mv "${2}" "${2%/*}/02.png"
mv "${3}" "${3%/*}/01.jpg"
mv "${4}" "${4%/*}/02.png"
mv "${5}" "${5%/*}/03.gif"
mv "${6}" "${6%/*}/01.jpg"
mv "${7}" "${7%/*}/02.png"
mv "${8}" "${8%/*}/01.jpg"
mv "${9}" "${9%/*}/02.png"
mv "${10}" "${10%/*}/03.gif"

It doesn't look like much, I know, but what's important here is that you don't have to worry about any weird filenames or any of that - the intended destination for that output is a shell that shares a positional array with the for loop that generated it. I've got one such in the command there - and it's configured to provide debug output, which looks like this:

+ : mv ./event-a/album-a/a.jpg ./event-a/album-a/01.jpg
+ : mv ./event-a/album-a/x.png ./event-a/album-a/02.png
+ : mv ./event-a/album-b/a.jpg ./event-a/album-b/01.jpg
+ : mv ./event-a/album-b/x.png ./event-a/album-b/02.png
+ : mv ./event-a/album-b/y.gif ./event-a/album-b/03.gif
+ : mv ./event-b/album-x/a.jpg ./event-b/album-x/01.jpg
+ : mv ./event-b/album-x/x.png ./event-b/album-x/02.png
+ : mv ./event-b/album-y/a.jpg ./event-b/album-y/01.jpg
+ : mv ./event-b/album-y/x.png ./event-b/album-y/02.png
+ : mv ./event-b/album-y/y.gif ./event-b/album-y/03.gif

That's what it looks like when the : is the first character in the printf format string. When I remove it and execute the command:

ls ./event*/*/

01.jpg  02.png

01.jpg  02.png  03.gif

01.jpg  02.png

01.jpg  02.png  03.gif

That appears to be inline with your request. You can't beat results, I guess.


If you have the name of the file, you can get it's number by running:

ls -1 <your_dir> | grep <file_name> -B 1000 | wc -l
  • Parsing the output of ls is not a good idea. It'll mostly work here, but a wildcard-based solution will completely work, so why settle for second-best? Apr 23, 2012 at 23:16
  • "ls -1" give you only the file names if that's what you mean.
    – alexarsh
    Apr 24, 2012 at 16:46
  • In most cases, yes. In general, no. Try (in bash/ksh/zsh) touch junk precious $'junk\nprecious'; ls -1 | grep junk | xargs -l rm. You didn't really care about that precious file, did you? Not to mention that restriction to at most 1000 files, or the fact that you find 03.jpg when you're looking for 3.jpg. I retract my comment for second-best, this is really broken, and it's incomplete to boot. Apr 24, 2012 at 17:26

You must log in to answer this question.

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