3

I have a bunch of pictures, some are .jpg, some are .png. any given picture could have an entirely random name (possibly including spaces). in any given directory, how can I rename all of them, while keeping the extension as is? the input, for example:

001.jpg
02.png
05.png
zzzz.jpg

I would like to completely replace their names with four-digit numbers, starting with 0001, and going up to 9999 (if necessary). thus, the output might look like:

0001.jpg
0002.png
0003.png
0004.jpg

the order of these files is important to me, but they are already in sequential order if I were to sort them by filename alone.

I have checked out the perl rename, and its usage is a mystery to me in this example.

4

Perl Solution

perl -e '
    @files = sort glob "*.png *.jpg";
    for(@files){ 
        ($extension = $_) =~ s/.*\.(.*)/$1/;
        $new_name = sprintf("%04d",++$counter).".$extension";
        die "File $new_name already exists and would be clobbered\n" if -e $new_name;
        rename $_ => sprintf("%04d",++$counter).".$extension"
     }'

Bash Solution

export COUNT=0 
find . -maxdepth 1 -name "*.jpg" -o -name "*.png" -print0 | xargs -0 sort | while read file do\
extension=${file##*.}\
COUNT=$((COUNT+1))\
mv -- "$file" $(printf "%04d" $COUNT).$extension\
done

Note

  • Both solutions are Bash solution is untested: try them it on dummy files first.
  • The Bash solution may break if you have file names with newlines in them.
  • Danger: As pointed out by Gilles in the comments below, if one of the original files has a name that follows the target naming convention, it will at best be renamed to another "number" and at worst be clobbered by another file. Which one happens will depend on how it sorts relative to other files. I have added a provision for this in the Perl solution since this is the one the OP seems to favor.
  • The Perl solution (with the provision from the above point implemented) is non-idempotent.
  • 1
    You need to use %04d in printf for padding, otherwise it gets space-padded toward the right (and the spaces subsequently ignored). – a CVn Aug 2 '13 at 11:35
  • I definitely like the Perl solution better, myself; it should be safe even in the face of weird file names. I'm not sure but I get the feeling that the bash solution centered around while read is still vulnerable to IFS issues. – a CVn Aug 2 '13 at 11:37
  • @MichaelKjörling Do you happen to have an edge case in mind? – Joseph R. Aug 2 '13 at 11:38
  • 1
    Since in the Perl solution you're putting the file names into an array which you then iterate over, that one should be safe as long as glob does its job properly. – a CVn Aug 2 '13 at 11:42
  • 2
    Warning: if any of the source files matches the target pattern, it may get erased in the process. – Gilles Aug 2 '13 at 23:31
2

(bash solution)

Using printf to format the new names and awk to pick out the filename extension...

$ ls
a.b  c.d  e.f  g.h  i.j  k.l  m.n  o.p  q.r  s.t  this file.y  u.v  w.x
$ COUNTER=0
$ for i in *; do COUNTER=$(($COUNTER + 1)); mv -v "$i" $(printf "%04g" $COUNTER).$(echo $i | awk -F'.' '{print $NF}'); done
`a.b' -> `0001.b'
`c.d' -> `0002.d'
`e.f' -> `0003.f'
`g.h' -> `0004.h'
`i.j' -> `0005.j'
`k.l' -> `0006.l'
`m.n' -> `0007.n'
`o.p' -> `0008.p'
`q.r' -> `0009.r'
`s.t' -> `0010.t'
`this file.y' -> `0011.y'
`u.v' -> `0012.v'
`w.x' -> `0013.x'

Remember to surround the input $i in quotes to escape spaces in filenames, as you have mentioned.

  • Nice, but you could replace the $(echo $i | awk -F'.' '{print $NF}') with ${i##*.} and keep the whole thing bash (since printf is a builtin IIRC). Also, you should either use mv -- or prepend a ./ to the glob to avoid problems with files starting with a -. – evilsoup Aug 2 '13 at 16:37
  • Warning: if any of the source files matches the target pattern, it may get erased in the process. – Gilles Aug 2 '13 at 23:32
  • @evilsoup valid point, however: h.j.k.'s original version with echo and awk would also work on older and real old bash versions, considering that a statement like ${i##*.}, though very elegant, was still on the users' wishlist in bash 2.x times (if I remember correctly). – syntaxerror Dec 3 '14 at 21:26
0

Save this script:

#!/bin/bash

NUM=0
NUMBER_LENGTH=4

cd $1;

ls . -1 | sort | while read file; do
    NUM=$(( $NUM + 1 ));
    PREFIX=$NUM;
    while [ ${#PREFIX} != $NUMBER_LENGTH ]; do
        PREFIX="0${PREFIX}";
    done;
    NEW_NAME=$PREFIX.`echo $file | sed 's|[^.]\+\.\([\A-Za-z0-9]\+\)|\1|g'`;
    mv "$file" "$NEW_NAME";
done

and execute it as:

./_scriptname_ _dirWhereYourStuffIs_

CAVEAT: don't put the script in the same dir where your stuff is, because the script itself would be renamed.

If you're insane enough, you can do it all in a line (without using the script):

NUM=0; NUMBER_LENGTH=4; ls . -1 | sort | while read file; do NUM=$(( $NUM + 1 )); PREFIX=$NUM; while [ ${#PREFIX} != $NUMBER_LENGTH ]; do PREFIX="0${PREFIX}"; done; NEW_NAME=$PREFIX.`echo $file | sed 's|[^.]\+\.\([\A-Za-z0-9]\+\)|\1|g'`; mv "$file" "$NEW_NAME"; done

CAVEAT #2: for this single line, I'm assuming you're using BASH

  • 4
    Generally, don't parse the output of ls. And you can replace your inner while loop with PREFIX=$(printf '%04d' $NUM). – a CVn Aug 2 '13 at 11:34
  • @MichaelKjörling Though I agree with you that parsing ls is an epic fail, I have to state that the perfect, OS-independent line is one that new users will not always be able to think of so easily: while read file; do (...); done < <(find . -maxdepth 1 -type f) -- the greatest difficulty of which is the "reverse thinking" that the find output must be somewhat "post-injected" at the very end of the loop. I didn't find it too logical either when I was a beginner in this TBH. – syntaxerror Dec 3 '14 at 21:37
  • @syntaxerror I believe find . -maxdepth 1 -type f | while read file; do (...); done works just as well, and is more intuitive. I used a script for a good while that used exactly that construct (actually, with a sort thrown in for good measure); it was eventually retired when I rewrote it (ironically, with one loop using just about the exact construct you mention) because I needed the script to do things rather differently compared to what it did, making it easier to rewrite than adapt in place. – a CVn Dec 4 '14 at 7:29
  • However, parsing the output of find does suffer from some of the same problems as parsing the output of ls. Since the question title explicitly says unknown names and extensions this is potentially a problem. A good solution then will be one that can handle unusual file names as well. – a CVn Dec 4 '14 at 7:33
  • But I think we can agree that it's not in such an extreme way as ls, Michael. This is the difference. ls output (especially ls -(a)l output) may vary hugely due to different locale settings etc, hence the wrongest thing to do is piping awk to ls -l output (on system #1, field name $12 may correspond to field $15 on system #2). -- Whereas the differences in find output seem marginal to me in comparison, if existing at all. – syntaxerror Dec 4 '14 at 10:52

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.