0

How to recursively rename all the files in several layers of subdirectories without changing their extensions?

Below's a toned down version (to save room) of what I've got. For argument's sake, I want all the files to have the same title, yet retain their original extension. There's never more than a single file per directory, so there's no chance of doubling up.

For simplicity, we'll just call them all foo, followed by their current extension.

So just to clarify:
Asset\ 1.pdf, Asset\ 1.png, Asset\ 1@4x.png, Asset\ 1.svg
Will become:
foo.pdf, foo.png, foo.png, foo.svg
And so on in that fashion.


I would typically use parameter expansion and a for loop, like:

for f in */*; do mv "$f" "${f%/*}/foo.${f##*.}"; done  

But it's not recursive. So I would prefer to use something with find..-exec or similar.


~/Desktop/Project/Graphics/
├── Huge
│   ├── PDF
│   │   └── Asset\ 1.pdf
│   ├── PNG
│   │   ├── 1x
│   │   │   └── Asset\ 1.png
│   │   └── 4x
│   │       └── Asset\ 1@4x.png
│   └── SVG
│       └── Asset\ 1.svg
├── Large
│   ├── PDF
│   │   └── ProjectAsset\ 2.pdf
│   ├── PNG
│   │   ├── 1x
│   │   │   └── ProjectAsset\ 2.png
│   │   └── 4x
│   │       └── ProjectAsset\ 2@4x.png
│   └── SVG
│       └── ProjectAsset\ 2.svg
├── Medium
│   ├── PDF
│   │   └── ProjectAsset\ 3.pdf
│   ├── PNG
│   │   ├── 1x
│   │   │   └── ProjectAsset\ 3.png
│   │   └── 4x
│   │       └── ProjectAsset\ 3@4x.png
│   └── SVG
│       └── ProjectAsset\ 3.svg
├── Small
│   ├── PDF
│   │   └── ProjectAsset\ 4.pdf
│   ├── PNG
│   │   ├── 1x
│   │   │   └── ProjectAsset\ 4.png
│   │   └── 4x
│   │       └── ProjectAsset\ 4@4x.png
│   └── SVG
│       └── ProjectAsset\ 4.svg
└── Tiny
    ├── PDF
    │   └── Asset\ 5.pdf
    ├── PNG
    │   ├── 1x
    │   │   └── Asset\ 5.png
    │   └── 4x
    │       └── Asset\ 5@4x.png
    └── SVG
        └── Asset\ 5.svg

30 directories, 20 files
  • You can use a combination of find and GNU mv like described in this answer. – feliks Oct 20 '18 at 13:35
  • @feliks This system has mostly BSD utilities. But I think it is basically the same. – tjt263 Oct 20 '18 at 16:01
  • @feliks useful link, but the challenge lies in getting find x -exec y {} \; to play nicely with "${parameter%/*}/foo.${expansion##*.}", etc. – tjt263 Oct 20 '18 at 16:19
3

It's very similar with find...-exec: invoke a shell so that you can use parameter expansion, extract the PARENT directory and the EXTENSION so that you can construct the new filename as PARENT/NAME.EXTENSION and then move/rename:

find target_dir -type f -exec sh -c '
h=${1%/*}; mv "$1" "${h}/NAME.${1##*.}"' sh {} \;

If you want to dry-run the above, insert an echo before the mv...


If you have access to zsh you could run:

autoload zmv
zmv -n '(**/)(*.*)' '${1}NAME.${2:e}'

remove the -n if you're happy with the result.

  • What's the second sh for? – tjt263 Oct 20 '18 at 18:59
  • This -exec {} \; calls a new shell per each file found – Isaac Oct 20 '18 at 19:18
  • @tjt263 The "second sh" is not a command, it is the name of the shell started by sh -c, change it to findsh if you wish. – Isaac Oct 20 '18 at 19:19
  • Can it be omitted? – tjt263 Oct 20 '18 at 19:50
  • @tjt263 - why ? Yes, you can but then you have to change $1s to $0s... See What does the x in "find ... -exec sh -c '...' x {}" mean? – don_crissti Oct 20 '18 at 19:52
1

If using bash, your idea could walk all directories:

$ shopt -s globstar
$ for f in ./Desktop/**/*; do [[ -f $f ]] && 
           mv -n "$f" "${f%/*}/foo.${f##*.}"; done

Added -n to mv to avoid overwriting existing files (if any).

That could also be done with find (in one call to the shell (faster than one shell per file)):

$ find ./Desktop -type f -exec sh -c '
       for f; do echo mv -n "$f" "${f%/*}/foo.${f##*.}"; done' findsh {} \+

Remove the echo if the command does what you need.

0

Maybe I'm oversimplifying, but, if you know the max depth of the directory hierarchy (here: 3), why not

for f in */* */*/* */*/*/*; do ... ; done 

adding some error checking?

  • Did you give it a try? Worked for me, going through all the subdirectory levels. – RudiC Oct 20 '18 at 13:25
0

Finding all regular files in or below the directory ~/Desktop/Project/Graphics whose names start with Asset 1 and replacing that part of the name with the string foo:

find ~/Desktop/Project/Graphics -type f -name 'Asset 1*' -exec sh -c '
    for pathname do
        dir=${pathname%/*}
        name=${pathname##*/Asset 1}
        mv -i "$pathname" "$dir/foo$name"
    done' sh {} +

In the above, $name will be the part of the original filename with Asset 1 removed from the start.

To replace the whole filename but keep the filename suffix (and also matching any file starting with Asset (with space after the word)):

find ~/Desktop/Project/Graphics -type f -name 'Asset *' -exec sh -c '
    for pathname do
        dir=${pathname%/*}
        name=${pathname##*.}
        mv -i "$pathname" "$dir/foo$name"
    done' sh {} +

Related:

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.