2

I have a folder with several files formatted as follows:

C Block Scan copy 1.pdf
C Block Scan copy 2.pdf
C Block Scan copy 3.pdf
C Block Scan copy 4.pdf
.
.
.

One issue I'm having is that when I run ls, Unix lists them as

C Block Scan copy 1.pdf
C Block Scan copy 10.pdf
C Block Scan copy 11.pdf
C Block Scan copy 12.pdf
.
.
.

I'd like to pad the numbers so that Unix (hopefully) lists them in order as

C Block Scan copy 01.pdf
C Block Scan copy 02.pdf
C Block Scan copy 03.pdf
.
.
.
C Block Scan copy 09.pdf
C Block Scan copy 10.pdf
C Block Scan copy 11.pdf
C Block Scan copy 12.pdf
.
.
.

I found a post with a similar problem, Padding a number in a filename to a fixed length, but I tried applying their solution to my case and I couldn't get it to work. Here's what I tried:

for f in *.pdf; do
    int=`basename $f .pdf | cut -d '.' -f 2`
    new_name=`printf "file.%0.2i.pdf\n” $int`
    [ ! -f $new_name ] && mv $f $new_name
done

I admit that this is a blind adaptation of their solution to my situation, which I don't like because I don't really understand the underlying syntax, and I don't have a high enough rank on StackExchange to comment on that post yet.

I'm new to shell scripting, so an explanation of what the syntax means would be helpful and appreciated.

If it helps, I'm using the macOS Mojave 10.14.6, and I have Brew installed.

Thanks in advance.

6
  • Didn't some new version of OSX convert to using zsh instead of bash? Are you convinced that bash is your shell and not zsh?
    – Jeff Schaller
    Mar 20, 2020 at 15:34
  • You did the right thing, by the way, by asking your own question. This gives you the benefit of explaining your exact situation. What error(s) do you get when trying other the answers on the other post? List the commands and the results by editing your post. Thank you!
    – Jeff Schaller
    Mar 20, 2020 at 15:35
  • Hi @JeffSchaller. I'm using bash. (echo $SHELL gives /usr/local/bin/bash) Mar 20, 2020 at 15:47
  • Thanks for the encouragement, @JeffSchaller. I just updated my post. Mar 20, 2020 at 15:56
  • You have in your code which is not ASCII double quote but something else. Mar 20, 2020 at 16:10

3 Answers 3

2

With zsh (now the default user shell on macOS):

autoload zmv # best in ~/.zshrc
zmv '(* )([0-9].pdf)' '${1}0$2'
1

This will find and rename all "*.pdf" files in the current directory whose names follow the pattern described in the question to the same name but with number padded to 2 digits:

#!/usr/bin/env bash

shopt -s nullglob

for f in *[[:space:]][0-9].pdf; do
    int="$(basename "$f" | rev | cut -d ' ' -f1 | rev | cut -d . -f1)"
    name="$(basename "$f" | rev | cut -d ' ' -f2- | rev)"
    padded_int="$(printf "%02d\n" "$int")"

    echo mv "$f" "$(printf "%s %s.pdf" "$name" "$padded_int")"

done

Remove echo before mv to actually run mv but first run it as is to make sure that it does what you want it to do. Example usage with the above script saved to pdf.sh:

$ for i in {1..10}; do touch "C Block Scan copy $i.pdf"; done
$ ./pdf.sh
mv C Block Scan copy 1.pdf C Block Scan copy 01.pdf
mv C Block Scan copy 2.pdf C Block Scan copy 02.pdf
mv C Block Scan copy 3.pdf C Block Scan copy 03.pdf
mv C Block Scan copy 4.pdf C Block Scan copy 04.pdf
mv C Block Scan copy 5.pdf C Block Scan copy 05.pdf
mv C Block Scan copy 6.pdf C Block Scan copy 06.pdf
mv C Block Scan copy 7.pdf C Block Scan copy 07.pdf
mv C Block Scan copy 8.pdf C Block Scan copy 08.pdf
mv C Block Scan copy 9.pdf C Block Scan copy 09.pdf

The first line:

#!/usr/bin/env bash

is a shebang. Additionally, I use env for its features.

This line

shopt -s nullglob

sets nullglob shell option which is also explained on the linked site:

nullglob

If set, Bash allows filename patterns which match no files to
expand to a null string, rather than themselves.

It's needed to prevent for loop from starting if there are no files that match the pattern criteria:

for f in *[[:space:]][0-9].pdf; do

*[[:space:]][0-9].pdf is a shell pattern that means look for files that have names composed of any number of characters followed by a whitespace followed by a single digit and end with .pdf. Inside the loop $f will hold a name of the processed .pdf file.

Here

int="$(basename "$f" | rev | cut -d ' ' -f1 | rev | cut -d . -f1)"

we use command substitution to assign integer part of the given file name to a variable. You can see what basename does in man basename and reproduce the pipeline in the terminal:

$ f='C Block Scan copy 1.pdf'
$ echo basename "$f" | rev | cut -d ' ' -f1 | rev | cut -d . -f1
1
$ f='C Block Scan copy 5.pdf'
$ echo basename "$f" | rev | cut -d ' ' -f1 | rev | cut -d . -f1
5

(Notice that $ here is a command line prompt used to indicate start of a new line, not a part of the command).

In the next line

name="$(basename "$f" | rev | cut -d ' ' -f2- | rev)"

we extract name part of the .pdf, that is everything before the number.

In the next line

padded_int="$(printf "%02d\n" "$int")"

we use Bash built-in printf command to pad $int and save it to a variable called padded_int. In the last line in the loop

echo mv "$f" "$(printf "%s %s.pdf" "$name" "$padded_int")"

we run (without echo of course) the actual renaming using printf and command substitution again. printf is used with 2 %s format specifiers and 2 corresponding arguments, similarly as in C.

The last line

done

closes the loop.

8
  • Worked like a charm, thanks! A couple of questions: 1) Any chance I could get an explanation of how the code works? I blindly copied and pasted it into a file like you said, but I'd like to understand what's going on. If you could recommend any good resources to learn it myself, that would be fine too. 2) You said to "Remove echo before mv", but I don't see echo anywhere here. Did I miss something? Mar 20, 2020 at 17:53
  • 1. ok, I'll edit my post. 2. Umm, sorry, I forgot it, I've added it now. Mar 20, 2020 at 17:56
  • @Data2Dollars: see edited answer Mar 20, 2020 at 18:50
  • Thank you for the explanation. I really appreciate it! Mar 21, 2020 at 16:52
  • That's a great explanation. I have a few more questions, if that's OK: 1) ./pdf.sh didn't work for me. I had to do source pdf.sh. Do you know why? 2) In *[[:space:]][0-9].pdf, how does precedence work? It seems like *[[:space:]] gets "greedily" matched before [0-9].pdf gets matched. Does "greedy" precedence work left to right? How does it work? 3) In name="$(basename "$f" | rev | cut -d ' ' -f2- | rev)", why is there a dash before and after f2 in -f2-? I see the difference in the outputs, but what does that syntax mean? Thanks again! Mar 22, 2020 at 20:31
1

Using P.E. parameter expansion. Only mv for external tools from the bash shell.

#!/usr/bin/env bash

shopt -s nullglob

for f in *[[:space:]][0-9].pdf; do
  n=${f##* }  ##: Remain only 1.pdf, 2.pdf etc.
  n=0${n%.*}  ##: Remain only 1 , 2 etc. and pad 0 so it will be 01, 02 ..
  echo mv -v "$f" "${f/[0-9]/"$n"}"  ##: Replace all [0-9] with the value of "$n"
done

You must log in to answer this question.

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