最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

bash - Rename multiple files in nested directories - Stack Overflow

programmeradmin2浏览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?

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?

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
  • Ignore directories with -type f? – Benjamin W. 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. – Benjamin W. Commented Mar 6 at 19:37
  • 1 do you want to update just files or do you also want to update directories? – markp-fuso Commented Mar 6 at 20:33
  • 2 Please edit your question to show the expected output for the input you posted. – Ed Morton Commented Mar 6 at 20:39
  • 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' {} ';' – pjh Commented Mar 7 at 0:22
 |  Show 1 more comment

3 Answers 3

Reset to default 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]

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.

发布评论

评论列表(0)

  1. 暂无评论