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.

  • 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 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 at 15:35
  • Hi @JeffSchaller. I'm using bash. (echo $SHELL gives /usr/local/bin/bash) – Data2Dollars Mar 20 at 15:47
  • Thanks for the encouragement, @JeffSchaller. I just updated my post. – Data2Dollars Mar 20 at 15:56
  • You have in your code which is not ASCII double quote but something else. – Arkadiusz Drabczyk Mar 20 at 16:10
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.

| improve this answer | |
  • 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? – Data2Dollars Mar 20 at 17:53
  • 1. ok, I'll edit my post. 2. Umm, sorry, I forgot it, I've added it now. – Arkadiusz Drabczyk Mar 20 at 17:56
  • @Data2Dollars: see edited answer – Arkadiusz Drabczyk Mar 20 at 18:50
  • Thank you for the explanation. I really appreciate it! – Data2Dollars Mar 21 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! – Data2Dollars Mar 22 at 20:31
2

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

autoload zmv # best in ~/.zshrc
zmv '(* )([0-9].pdf)' '${1}0$2'
| improve this answer | |
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
| improve this answer | |

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.