Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

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?

share|improve this question
up vote 7 down vote accepted
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.

share|improve this answer
Thanks! FTR, a friend of mine wrote the following snippet which is very similar to your answer: gist.github.com/2471545 – Mathias Bynens Apr 23 '12 at 20:01
there is an open quote at ext=... – thumbmunkeys Aug 26 '14 at 20:25

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"
share|improve this answer

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.

share|improve this answer

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

Please tell me if it helps you or not. :)

share|improve this answer
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? – Gilles Apr 23 '12 at 23:16
"ls -1" give you only the file names if that's what you mean. – alexarsh Apr 24 '12 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. – Gilles Apr 24 '12 at 17:26

Your Answer


By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.