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

5 Answers 5

5

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}' 
7
  • What's the second sh for? Commented Oct 20, 2018 at 18:59
  • This -exec {} \; calls a new shell per each file found Commented Oct 20, 2018 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. Commented Oct 20, 2018 at 19:19
  • Can it be omitted? Commented Oct 20, 2018 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? Commented Oct 20, 2018 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?

1
  • Did you give it a try? Worked for me, going through all the subdirectory levels. Commented Oct 20, 2018 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:

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.

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.