-1

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\ [email protected], 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\ [email protected]
│   └── SVG
│       └── Asset\ 1.svg
├── Large
│   ├── PDF
│   │   └── ProjectAsset\ 2.pdf
│   ├── PNG
│   │   ├── 1x
│   │   │   └── ProjectAsset\ 2.png
│   │   └── 4x
│   │       └── ProjectAsset\ [email protected]
│   └── SVG
│       └── ProjectAsset\ 2.svg
├── Medium
│   ├── PDF
│   │   └── ProjectAsset\ 3.pdf
│   ├── PNG
│   │   ├── 1x
│   │   │   └── ProjectAsset\ 3.png
│   │   └── 4x
│   │       └── ProjectAsset\ [email protected]
│   └── SVG
│       └── ProjectAsset\ 3.svg
├── Small
│   ├── PDF
│   │   └── ProjectAsset\ 4.pdf
│   ├── PNG
│   │   ├── 1x
│   │   │   └── ProjectAsset\ 4.png
│   │   └── 4x
│   │       └── ProjectAsset\ [email protected]
│   └── SVG
│       └── ProjectAsset\ 4.svg
└── Tiny
    ├── PDF
    │   └── Asset\ 5.pdf
    ├── PNG
    │   ├── 1x
    │   │   └── Asset\ 5.png
    │   └── 4x
    │       └── Asset\ [email protected]
    └── SVG
        └── Asset\ 5.svg

30 directories, 20 files
don_crissti
  • 79,330
  • 30
  • 216
  • 245
voices
  • 1,252
  • 3
  • 15
  • 30
  • You can use a combination of `find` and GNU `mv` like described in [this answer](https://unix.stackexchange.com/a/154819/316810). – Felix Oct 20 '18 at 13:35
  • @feliks This system has mostly BSD utilities. But I think it is basically the same. – voices 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. – voices Oct 20 '18 at 16:19

5 Answers5

4

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 -name '?*.*' -type f -exec sh -c '
  ret=0
  for file do
    head=${file%/*}
    mv -- "$file" "$head/NAME.${file##*.}" || ret=$?
  done
  exit "$ret"' sh {} +

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

Beware it also processes hidden files.


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.

Hidden files are skipped by default, but you can use the (#qD) glob qualifier if you want them processed:

autoload zmv
zmv -n '(**/)(?*.*)(#qD)' '${1}NAME.${2:e}'
Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
don_crissti
  • 79,330
  • 30
  • 216
  • 245
  • What's the second `sh` for? – voices Oct 20 '18 at 18:59
  • This `-exec {} \;` calls a new shell per each file found –  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. –  Oct 20 '18 at 19:19
  • Can it be omitted? – voices Oct 20 '18 at 19:50
  • @tjt263 - why ? Yes, you can but then you have to change `$1`s to `$0`s... See [What does the x in "find ... -exec sh -c '...' x {}" mean?](https://www.in-ulm.de/~mascheck/various/find/#shell) – don_crissti Oct 20 '18 at 19:52
  • @don_crissti Well, my question to you is, why bother/what's the point? It makes a complicated command line slightly more complicated, with no benefit that I can see. I'm happy to be corrected though. – voices Oct 23 '18 at 17:03
  • @tjt263 - the benefit is explained in the link in my comment. I don't have anything to add, really... Feel free to not use it but then, as I said, you'll have to adjust the parameters accordingly... – don_crissti Oct 23 '18 at 17:04
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?

RudiC
  • 8,889
  • 2
  • 10
  • 22
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:

Kusalananda
  • 320,670
  • 36
  • 633
  • 936
0
find ~/Desktop/Project/Graphics/ -type f -exec sh -c 'f={}; mv $f ${f%/*}/foo.${f##*.}' \;

sh means invoking default shell. you can change it to bash, zsh and so on.