0

I would like to rename multiple files that are located in different folders. More specifically, I would like to add zeroes to the middle of the filename, so that all filenames have three digits (and therefore show up in a logical order).

To be more specific, I have 130+ folders with 400+ .nii files in each folder. Each .nii file has the following pattern:

swu_run1_P_139_Vol_1.nii

  • P_### ranges from P76 to P277 (participant number)
  • Vol_### ranges from 1 till 405 (volume number)

As the volume ranges from 1 till 405, it means that any list starts with 100, rather than going 1-405 (e.g. it starts: 100 - 101 - 102 [..] - 109 - 10 - 110 - 111 etc). One way to solve this, is to add zeroes to the filename and make everything three digits, e.g:

swu_run1_P_277_Vol_1.nii -> swu_run1_P_277_Vol_001.nii
swu_run1_P_277_Vol_2.nii -> swu_run1_P_277_Vol_002.nii
swu_run1_P_277_Vol_10.nii -> swu_run1_P_277_Vol_010.nii
swu_run1_P_277_Vol_120.nii -> swu_run1_P_277_Vol_120.nii

I have little experience with unix/linux systems, but using previous threads, I managed to come up with the following code. It has two parts to it:

1. Rename multiple filesnames, adding zeroes to the middle of the filename:

rename Vol_ Vol_0 *Vol_[0-9].nii

If I run this in a subfolder, I get the following error message:

Error: rename: swu_run1_P_275_Vol_1.nii: rename to swu_run1_P_275_Vol_01.nii failed: No such file or directory

Strangely enough, it is adding the zero to Vol_1-9. However, it does not add a zero to any number which already has two or three digits:

swu_run1_P_277_Vol_1.txt -> swu_run1_P_277_Vol_01.nii
swu_run1_P_277_Vol_10.txt -> swu_run1_P_277_Vol_10.nii
swu_run1_P_277_Vol_100.txt -> swu_run1_P_277_Vol_100.nii

It seems like there is some kind of weird loop going on with the expression (it tries to change the new Vol_01, giving the error message)? And why is it not adding an zero to the two/three digits?

2. Find all .nii files in the relevant subfolders, and then do a batch renaming:

find . -iname "*.nii" -execdir rename Vol_ Vol_0 *Vol_[0-9].nii '{}' \;

My understanding of this code is as follows:

  • find . -iname "*.nii" searches for all .nii files, both in the current folder and the subfolders

  • -execdir tells it to apply the following expression to the current folder and the subfolders, i.e. adding a zero

  • rename Vol_ Vol_0 *Vol_[0-9].nii adds that zero (using the format of fromtext totext filelist)

  • '{}' is there for the path name of the file

  • \; is there to end the -execdir expression

If I try to run the code on more folders, I get the following error message:

Error: rename: *Vol_[0-9].nii: rename to *Vol_0[0-9].nii failed: No such file or directory

I think that I am getting the error message because the -execdir is not being executed in the subfolders, but I can't work out how to solve this.

I would rather not go into each subfolder manually to run the shell, so do you have any suggestions how to improve this code (and make it work)? And why am I getting the error message of "No such file or directory"?

  • Are the .txt filename suffixes a typo? – Kusalananda Apr 15 '20 at 19:26
  • Yes, sorry, that's a typo: everything should be listed as .nii (there is no .txt) – fidodido Apr 16 '20 at 8:07
2

If we had the name swu_run1_P_277_Vol_1.nii in the variable name, then ${name##*_} would be 1.nii (the longest prefix string matching *_ is removed).

Taking that and removing .nii leaves us with the number that we want to zero-fill to three character's width:

name=swu_run1_P_277_Vol_1.nii

number=${name##*_}
number=${number%.nii}

In the bash shell, the easiest way (possibly) to zero-fill the number is with printf:

printf '%.3d' "$number"    # would print 001 (with no newline)

We can construct the new name at the same time:

printf '%s_%.3d.nii' "${name%_*.nii}" "$number"

That printf command would print swu_run1_P_277_Vol_001.nii by removing the suffix string that matches _*.nii from the original name, adding _ and the zero-filled number, and then the .nii suffix string.

To top this off, we may print the resulting string directly into a new variable with printf -v newname ....

Putting this together for a single name:

name=swu_run1_P_277_Vol_1.nii

number=${name##*_}
number=${number%.nii}

printf -v newname '%s_%.3d.nii' "${name%_*.nii}" "$number"

Then it's just a matter of mv "$name" "$newname".

Ok, so how to do it for all relevant files?

Let's assume that all relevant files matches the globbing pattern *Vol_*.nii, then, with find,

find . -type f -name '*Vol_*.nii' -exec bash -c '
    for pathname do
        dirpath=${pathname%/*}  # or: dirpath=$(dirname "$pathname")
        name=${pathname##*/}    # or: name=$(basename "$pathname")

        number=${name##*_}
        number=${number%.nii}

        printf -v newname "%s_%.3d.nii" "${name%_*.nii}" "$number"

        printf "Would rename %s --> %s\n" "$pathname" "$dirpath/$newname"
        # mv "$pathname" "$dirpath/$newname"
    done' bash {} +

The inline bash -c script here gets called by find with batches of found pathnames that fulfill the -type f (is a regular file) and -name (has a particular name) criteria. The script loops over these pathnames and pulls out the filename into name and the directory pathname into dirpath.

It then does the same operations as we did before to arrive at a new name, which gets stored in newname, and then it renames the file.

Well, I've commented out the actual mv command for safety. You should run this once an see that the output is correct first. If you use GNU tools, you may also want to use mv -b to make backups of files if there are name collisions.


As a side note, in the zsh shell, the globbing pattern

./**/*Vol_*.nii(n)

would expand to all those names in numerical order (and recursively down int subdirectories):

$ print -rC1 ./**/*Vol_*.nii(n)
./swu_run1_P_277_Vol_1.nii
./swu_run1_P_277_Vol_2.nii
./swu_run1_P_277_Vol_10.nii
./swu_run1_P_277_Vol_120.nii
  • Amazing, it seems to work like a charm (and thanks for the clear explanations)! I have one question, if you don't mind? If I were to want to zero-fill with 4 digits, would I then change %s_%.3d.nii to %s_%.4d.nii? Does it work like that? – fidodido Apr 16 '20 at 9:33
  • @fidodido Correct. But run with the mv commented out first so that you see that it would work. – Kusalananda Apr 16 '20 at 9:35
  • @Kusalananda where does the second 'bash' in ... -exec bash -c '...' bash {} + come from? man find only mentions '-exec command {} +' whereby I guess 'command' would only be 'bash -c '...' What am I missing?? – bey0nd Apr 16 '20 at 19:32
  • @bey0nd The second bash is an argument, a string that will go into $0 within the in-line script. $0 in not part of the positional parameters, and will not be part of the loop. It will be used by the bash -c script if it needs to generate any diagnostic messages (error messages). It's an arbitrary string, but it makes sense to use bash. – Kusalananda Apr 16 '20 at 19:36
  • 1
    @Kusalananda Thanks a lot for the feetback. I missed the fact with $0. – bey0nd Apr 16 '20 at 21:31

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.