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?

| improve this question | | | | |
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.

| improve this answer | | | | |

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"
| 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.

| 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
| 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 'SO- stop being evil' 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 'SO- stop being evil' Apr 24 '12 at 17:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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