I have a bunch of files with the keyword 'A' in them. They are nested to look something like this (simplified)
.
└── key_A
├── data_A
│ ├── d0_A.txt
│ └── d1_A.txt
├── image_A.txt
└── text_A.txt
I would like to rename all 'A' to 'B'.
I tried with the rename command
find . -name '*A*' -exec rename 's/A/B/g' '{}' ';'
But the lowest level directory key_A
is renamed first and the following renames don't know what's happening:
find: ‘./key_A’: No such file or directory
I can run it multiple times from top down with the -mindepth
and replacing only the last part, i.e.,
find . -mindepth 3 -name '*A*' -exec rename 's/(.*)A/\1B/' '{}' ';'
but it takes many command line calls.
Is there an easy solution that is specific for this nested directory problem?
I have a bunch of files with the keyword 'A' in them. They are nested to look something like this (simplified)
.
└── key_A
├── data_A
│ ├── d0_A.txt
│ └── d1_A.txt
├── image_A.txt
└── text_A.txt
I would like to rename all 'A' to 'B'.
I tried with the rename command
find . -name '*A*' -exec rename 's/A/B/g' '{}' ';'
But the lowest level directory key_A
is renamed first and the following renames don't know what's happening:
find: ‘./key_A’: No such file or directory
I can run it multiple times from top down with the -mindepth
and replacing only the last part, i.e.,
find . -mindepth 3 -name '*A*' -exec rename 's/(.*)A/\1B/' '{}' ';'
but it takes many command line calls.
Is there an easy solution that is specific for this nested directory problem?
Share edited Mar 7 at 15:16 Mofi 49.3k19 gold badges87 silver badges154 bronze badges asked Mar 6 at 19:32 Paweł WójcikPaweł Wójcik 3562 silver badges11 bronze badges 6 | Show 1 more comment3 Answers
Reset to default 1find -depth
is the right tool to get the files and directories in the right order, but you only want to change the last instance of _A
in each line. With -depth
that will do the files first, then any subdirectories, and so on up the chain.
I'm partial to visually pre-testing. I like to see what is going to happen before I execute, so I usually do something with simple-ish string parsing and/or debug mode.
$: find key_? # show me what's there now
key_A
key_A/data_A
key_A/data_A/d0_A.txt
key_A/data_A/d1_A.txt
key_A/image_A.txt
key_A/text_A.txt
$: find key_A -depth | # get files in sensible order, echo commands below to confirm
> while read -r f; do post="${f##*_A}"; pre="${f%_A$post}"; echo mv "$f" "${pre}_B$post"; done
mv key_A/data_A/d0_A.txt key_A/data_A/d0_B.txt
mv key_A/data_A/d1_A.txt key_A/data_A/d1_B.txt
mv key_A/data_A key_A/data_B
mv key_A/image_A.txt key_A/image_B.txt
mv key_A/text_A.txt key_A/text_B.txt
mv key_A key_B
$: set -x; find key_A -depth | # remove the echo to make it happen
while read -r f; do post="${f##*_A}"; pre="${f%_A$post}"; mv "$f" "${pre}_B$post"; done; set +x
+ find key_A -depth
+ read -r f
+ post=.txt
+ pre=key_A/data_A/d0
+ mv key_A/data_A/d0_A.txt key_A/data_A/d0_B.txt
+ read -r f
+ post=.txt
+ pre=key_A/data_A/d1
+ mv key_A/data_A/d1_A.txt key_A/data_A/d1_B.txt
+ read -r f
+ post=
+ pre=key_A/data
+ mv key_A/data_A key_A/data_B
+ read -r f
+ post=.txt
+ pre=key_A/image
+ mv key_A/image_A.txt key_A/image_B.txt
+ read -r f
+ post=.txt
+ pre=key_A/text
+ mv key_A/text_A.txt key_A/text_B.txt
+ read -r f
+ post=
+ pre=key
+ mv key_A key_B
+ read -r f
+ set +x
$: find key_A # gone
find: ‘key_A’: No such file or directory
$: find key_?
key_B
key_B/data_B
key_B/data_B/d0_B.txt
key_B/data_B/d1_B.txt
key_B/image_B.txt
key_B/text_B.txt
This does still leave the possibility of breaking on files with newlines embedded in the name. See BashFAQ: How can I find and safely handle file names containing newlines, spaces or both?
Addendum
As a follow-up, it's certainly possible to use the same basic tools for files with multiple occurrences of the key value, and/or odd embedded characters like newlines. While I'd recommend more error checking, here's a stripped-down but functional rewrite:
$: find ./key_A -depth -print0 |
> while read -r -d '' p; do f="${p##*/}"; mv "$p" "${p%/*}/${f//_A/_B}"; done
There are several possibly non-obvious optimizations here to avoid sometimes subtle errors, such as adding a dot-slash (./
) to the beginning of the target directory and leaving off the trailing slash, but it does work.
$: shopt -s globstar; printf "[%s]\n" key_?/**
[key_A]
[key_A/data_A]
[key_A/data_A/d0_A-and-another_A.txt]
[key_A/data_A/d1_A.txt]
[key_A/image_A.txt]
[key_A/text_A.txt]
[key_A/with_A
and a newline, and spaces, and another_A.txt]
$: find ./key_? -depth -print0 | while read -r -d '' p
> do f="${p##*/}"; mv "$p" "${p%/*}/${f//_A/_B}"; done
$: printf "[%s]\n" key_?/**
[key_B]
[key_B/data_B]
[key_B/data_B/d0_B-and-another_B.txt]
[key_B/data_B/d1_B.txt]
[key_B/image_B.txt]
[key_B/text_B.txt]
[key_B/with_B
and a newline, and spaces, and another_B.txt]
The rename
utility is problematic because different systems have one of two radically different versions of it, and some systems don't have it at all. See Why is the rename utility on Debian/Ubuntu different than the one on other distributions, like CentOS?.
This solution depends on features of GNU find
and bash
:
find . -depth -name '*A*' -execdir bash -c 'mv -v -- "$1" "${1//A/B}"' bash {} \;
- The
-depth
option ensures that directories are not renamed until after all files and directories below them are renamed. - The
-execdir
option ensures that replacing 'A' with 'B' is done only on file (or directory) names, not paths. - See Bash Pitfalls #52 (find . -exec sh -c 'echo {}' ;) for an explanation of the syntax used to invoke a Bash shell to run a command on every file that needs to be renamed.
- See Substituting part of a string (BashFAQ/100 (How do I do string manipulation in bash?)) for an explanation of
${1//A/B}
.
How about going to zsh for this problem?
zsh -c 'autoload zmv; zmv '(**/*)A(*)' '$1B$2'
should do the trick. Run it first with zmv -n
, which just shows what it would do, without actually renaming. If it works, do it again without the -n
.
-type f
? – Benjamin W. Commented Mar 6 at 19:37-depth
which would rename the files first, but I think you don't want to change the directory name at all. – Benjamin W. Commented Mar 6 at 19:37find . -depth -name '*A*' -execdir rename 's/A/B/g' {} ';'
– pjh Commented Mar 7 at 0:22