0

I have a lot of files that are named as: MM-DD-YYYY.pdf. I want to rename them as YYYY-MM-DD.pdf I’m sure there is some bash magic to do this. What is it?

Nah
  • 1,690
  • 2
  • 26
  • 46
jlconlin
  • 14,206
  • 22
  • 72
  • 105
  • 5
    What did you try for yourself? We can assist you with your efforts, not provide a whole solution – Inian May 10 '18 at 08:58
  • @jlconlin: Have a safe flight and check out our answers after your arrival! ;-) – Allan May 10 '18 at 09:31

4 Answers4

4

For files in the current directory:

for name in ./??-??-????.pdf; do
    if [[ "$name" =~ (.*)/([0-9]{2})-([0-9]{2})-([0-9]{4})\.pdf ]]; then
        echo mv "$name" "${BASH_REMATCH[1]}/${BASH_REMATCH[4]}-${BASH_REMATCH[3]}-${BASH_REMATCH[2]}.pdf"
    fi
done

Recursively, in or under the current directory:

find . -type f -name '??-??-????.pdf' -exec bash -c '
    for name do
        if [[ "$name" =~ (.*)/([0-9]{2})-([0-9]{2})-([0-9]{4})\.pdf ]]; then
            echo mv "$name" "${BASH_REMATCH[1]}/${BASH_REMATCH[4]}-${BASH_REMATCH[3]}-${BASH_REMATCH[2]}.pdf"
        fi
    done' bash {} +

Enabling the globstar shell option in bash lets us do the following (will also, like the above solution, handle all files in or below the current directory):

shopt -s globstar

for name in **/??-??-????.pdf; do
    if [[ "$name" =~ (.*)/([0-9]{2})-([0-9]{2})-([0-9]{4})\.pdf ]]; then
        echo mv "$name" "${BASH_REMATCH[1]}/${BASH_REMATCH[4]}-${BASH_REMATCH[3]}-${BASH_REMATCH[2]}.pdf"
    fi
done

All three of these solutions uses a regular expression to pick out the relevant parts of the filenames, and then rearranges these parts into the new name. The only difference between them is how the list of pathnames is generated.

The code prefixes mv with echo for safety. To actually rename files, remove the echo (but run at least once with echo to see that it does what you want).

Kusalananda
  • 14,885
  • 3
  • 41
  • 52
  • 1
    @Inian Thanks! The glob is only there for selecting filenames. In the find variation, you could even delete the -name bit completely, while in the first variation, you might as well glob on *.pdf. I did as I did to show that the patterns were related. As for efficiency, I highly doubt it will make a noticeable difference. – Kusalananda May 10 '18 at 12:21
  • `for name in **/??-??-????.pdf` would be simpler than using `find` in this case, assuming the OP isn't stuck with `bash` 3.x. – chepner May 10 '18 at 12:38
  • @chepner With extglob enabled, yes. I might mention this later (on phone now). – Kusalananda May 10 '18 at 12:45
  • This is great! (Sorry for the slow response.) Unfortunately, I can't use `globstar`. Apparently my bash is too old on my Mac. So where does `BASH_REMATCH` know where to get the regex groups from? – jlconlin May 12 '18 at 14:47
1

A direct approach example from the command line:

$ ls
10-01-2018.pdf  11-01-2018.pdf  12-01-2018.pdf
$ ls [0-9]*-[0-9]*-[0-9]*.pdf|sed -r 'p;s/([0-9]{2})-([0-9]{2})-([0-9]{4})/\3-\1-\2/'|xargs -n2 mv
$ ls
2018-10-01.pdf  2018-11-01.pdf  2018-12-01.pdf

The ls output is piped to sed , then we use the p flag to print the argument without modifications, in other words, the original name of the file, and s to perform and output the conversion.

The ls + sed result is a combined output that consist of a sequence of old_file_name and new_file_name.

Finally we pipe the resulting feed through xargs to get the effective rename of the files.

From xargs man:

-n number Execute command using as many standard input arguments as possible, up to number arguments maximum.

Juan Diego Godoy Robles
  • 14,447
  • 2
  • 38
  • 52
0

You can use the following command very close to the one of klashxx:

for f in *.pdf; do echo "$f"; mv "$f" "$(echo "$f" | sed 's@\(..\)-\(..\)-\(....\)@\3-\2-\1@')"; done

before:

ls *.pdf
12-01-1998.pdf  12-03-2018.pdf

after:

ls *.pdf
1998-01-12.pdf  2018-03-12.pdf

Also if you have other pdf files that does not respect this format in your folder, what you can do is to select only the files that respect the format: MM-DD-YYYY.pdf to do so use the following command:

for f in `find . -maxdepth 1 -type f -regextype sed -regex './[0-9]\{2\}-[0-9]\{2\}-[0-9]\{4\}.pdf' | xargs -n1 basename`; do echo "$f"; mv "$f" "$(echo "$f" | sed 's@\(..\)-\(..\)-\(....\)@\3-\2-\1@')"; done

Explanations:

  • find . -maxdepth 1 -type f -regextype sed -regex './[0-9]\{2\}-[0-9]\{2\}-[0-9]\{4\}.pdf this find command will look only for files in the current working directory that respect your syntax and extract their basename (remove the ./ at the beginning, folders and other type of files that would have the same name are not taken into account, other *.pdf files are also ignored.

  • for each file you do a move and the resulting file name is computed using sed and back reference to the 3 groups for MM,DD and YYYY

Allan
  • 12,117
  • 3
  • 27
  • 51
0

For these simple filenames, using a more verbose pattern, you can simplify the body of the loop a bit:

twodigit=[[:digit:]][[:digit:]]
fourdigit="$twodigit$twodigit"
for f in $twodigit-$twodigit-$fourdigit.pdf; do
  IFS=- read month day year <<< "${f%.pdf}"
  mv "$f" "$year-$month-$day.pdf"
done

This is basically @Kusalananda's answer, but without the verbosity of regular-expression matching.

chepner
  • 497,756
  • 71
  • 530
  • 681