0

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_Ais 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?

4
  • Ignore directories with -type f? Commented Mar 6 at 19:37
  • 2
    There is also -depth which would rename the files first, but I think you don't want to change the directory name at all. Commented Mar 6 at 19:37
  • 1
    do you want to update just files or do you also want to update directories? Commented Mar 6 at 20:33
  • 2
    One possibility (I don't have time to test it so I'm not posting it as an answer) is: find . -depth -name '*A*' -execdir rename 's/A/B/g' {} ';' Commented Mar 7 at 0:22

3 Answers 3

1

find -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]
Sign up to request clarification or add additional context in comments.

2 Comments

Note that this doesn't account for the possibility that the key string might exist in the filename more than once. If that's a possibility you'll need to handle it.
This is really helpful and talks about important edge cases. The edge case that is relevant for me is unfortunately missing from my original question. My edge case is that some directories contain files that miss the 'key_A' in their names, but appear in the listing as their parent directories have the 'key_A' in their names. Using the -name flag seems to handle it.
1

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 {} \;

Comments

0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.