使用 find 和 sed 递归地重命名文件

Posted

技术标签:

【中文标题】使用 find 和 sed 递归地重命名文件【英文标题】:Recursively rename files using find and sed 【发布时间】:2011-06-15 04:47:37 【问题描述】:

我想浏览一堆目录并将所有以 _test.rb 结尾的文件重命名为以 _spec.rb 结尾。这是我从来没有完全弄清楚如何用 bash 做的事情,所以这次我想我会付出一些努力来搞定它。到目前为止,我还不够,我最大的努力是:

find spec -name "*_test.rb" -exec echo mv  `echo  | sed s/test/spec/` \;

注意:在 exec 之后有一个额外的回显,以便在我测试它时打印而不是运行命令。

当我运行它时,每个匹配的文件名的输出是:

mv original original

即sed 的替换已丢失。有什么诀窍?

【问题讨论】:

顺便说一句,我知道有一个重命名命令,但我真的很想弄清楚如何使用 sed 来执行此操作,以便将来可以执行更强大的命令。 请不要cross-post。 Rename multiple files in Unix、https://***.com/q/6911301/608639、Rename multiple files by replacing a particular pattern in the filenames using a shell script、Find directories with names matching pattern and move them、Rename multiple files shell 等可能重复 【参考方案1】:

要以最接近原始问题的方式解决它,可能会使用 xargs "args per command line" 选项:

find . -name "*_test.rb" | sed -e "p;s/test/spec/" | xargs -n2 mv

它递归地查找当前工作目录中的文件,回显原始文件名 (p) 和修改后的文件名 (s/test/spec/),并将其全部提供给 mv 成对 (xargs -n2)。请注意,在这种情况下,路径本身不应包含字符串 test

【讨论】:

不幸的是,这有空白问题。因此,使用名称中包含空格的文件夹会在 xargs 处破坏它(使用 -p 确认详细/交互模式) 这正是我想要的。对于空白问题来说太糟糕了(不过我没有测试它)。但对于我目前的需求,它是完美的。我建议先用“echo”而不是“mv”作为“xargs”中的参数对其进行测试。 如果您需要处理路径中的空格并且您使用的是 GNU sed >= 4.2.2,那么您可以使用 -z 选项以及 finds -print0 和 xargs -0find -name '*._test.rb' -print0 | sed -ze "p;s/test/spec/" | xargs -0 -n2 mv 最佳解决方案。比 find -exec 快得多。谢谢 如果一个路径中有多个 test 文件夹,这将不起作用。 sed 只会重命名第一个,mv 命令将因No such file or directory 错误而失败。【参考方案2】:

这是我的工作解决方案:

for FILE in FILE_PATTERN; do echo $FILE | mv $FILE $(sed 's/SOURCE_PATTERN/TARGET_PATTERN/g'); done

【讨论】:

【参考方案3】:

我分享这篇文章,因为它与问题有点相关。很抱歉没有提供更多细节。希望它可以帮助别人。 http://www.peteryu.ca/tutorials/shellscripting/batch_rename

【讨论】:

【参考方案4】:

为此,您不需要sed。通过process substitution 输入find 的结果的while 循环,您可以完美地独处。

因此,如果您有一个选择所需文件的find 表达式,请使用以下语法:

while IFS= read -r file; do
     echo "mv $file $file%_test.rb_spec.rb"  # remove "echo" when OK!
done < <(find -name "*_test.rb")

这将 find 文件并重命名所有文件,从末尾删除字符串 _test.rb 并附加 _spec.rb

对于这一步,我们使用Shell Parameter Expansion,其中$var%string 会从$var 中删除最短匹配模式“字符串”。

$ file="HELLOa_test.rbBYE_test.rb"
$ echo "$file%_test.rb"          # remove _test.rb from the end
HELLOa_test.rbBYE
$ echo "$file%_test.rb_spec.rb"  # remove _test.rb and append _spec.rb
HELLOa_test.rbBYE_spec.rb

看一个例子:

$ tree
.
├── ab_testArb
├── a_test.rb
├── a_test.rb_test.rb
├── b_test.rb
├── c_test.hello
├── c_test.rb
└── mydir
    └── d_test.rb

$ while IFS= read -r file; do echo "mv $file $file/_test.rb/_spec.rb"; done < <(find -name "*_test.rb")
mv ./b_test.rb ./b_spec.rb
mv ./mydir/d_test.rb ./mydir/d_spec.rb
mv ./a_test.rb ./a_spec.rb
mv ./c_test.rb ./c_spec.rb

【讨论】:

非常感谢!它帮助我轻松地以递归方式从所有文件名中删除尾随 .gz。 while IFS= read -r file; do mv $file $file%.gz; done &lt; &lt;(find -type f -name "*.gz") @CasualCoder 很高兴读到 :) 请注意,您可以直接说 find .... -exec mv ...。另外,请注意$file,因为如果它包含空格,它将失败。更好地使用引号"$file"【参考方案5】:

当文件名中有空格时,这对我有用。下面的示例递归地将所有 .dar 文件重命名为 .zip 文件:

find . -name "*.dar" -exec bash -c 'mv "$0" "`echo \"$0\" | sed s/.dar/.zip/`"'  \;

【讨论】:

【参考方案6】:

这是一个很好的 oneliner 可以解决问题。 Sed 无法正确处理此问题,尤其是当 xargs 使用 -n 2 传递多个变量时。 一个 bash 替换可以很容易地处理这个问题:

find ./spec -type f -name "*_test.rb" -print0 | xargs -0 -I  sh -c 'export file=; mv $file $file/_test.rb/_spec.rb'

添加 -type -f 将限制移动操作仅限于文件, -print 0 将处理路径中的空格。

【讨论】:

【参考方案7】:

这是一个适用于所有情况的示例。 递归工作,只需要 shell,并支持带空格的文件名。

find spec -name "*_test.rb" -print0 | while read -d $'\0' file; do mv "$file" "`echo $file | sed s/test/spec/`"; done

【讨论】:

【参考方案8】:

最简单的方法

find . -name "*_test.rb" | xargs rename s/_test/_spec/

最快的方式(假设您有 4 个处理器):

find . -name "*_test.rb" | xargs -P 4 rename s/_test/_spec/

如果您要处理大量文件,通过管道传输到 xargs 的文件名列表可能会导致生成的命令行超出允许的最大长度。

您可以使用getconf ARG_MAX检查系统的限制

在大多数 linux 系统上,您可以使用 free -bcat /proc/meminfo 来查找您需要使用多少 RAM;否则,请使用 top 或您的系统活动监视器应用程序。

更安全的方式(假设您有 1000000 字节的 ram 可以使用):

find . -name "*_test.rb" | xargs -s 1000000 rename s/_test/_spec/

【讨论】:

【参考方案9】:

按照 onitake 建议的 the examples,我能够处理带空格的文件名。

如果路径包含空格或字符串test,则此不会中断:

find . -name "*_test.rb" -print0 | while read -d $'\0' file
do
    echo mv "$file" "$(echo $file | sed s/test/spec/)"
done

【讨论】:

【参考方案10】:

使用 find utils 和 sed 正则表达式类型进行更安全的重命名:

  mkdir ~/practice

  cd ~/practice

  touch classic.txt.txt

  touch folk.txt.txt

如下删除“.txt.txt”扩展名-

  cd ~/practice

  find . -name "*txt" -execdir sh -c 'mv "$0" `echo "$0" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'`'  \;

如果您使用 + 代替 ;为了在批处理模式下工作,上面的命令将只重命名第一个匹配的文件,而不是“find”匹配的整个文件列表。

  find . -name "*txt" -execdir sh -c 'mv "$0" `echo "$0" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'`'  +

【讨论】:

【参考方案11】:

我不忍心再做一遍,但我写这篇文章是为了回复Commandline Find Sed Exec。提问者想知道如何移动整个树,可能不包括一两个目录,并将所有包含字符串 "OLD" 的文件和目录重命名为包含 "NEW" em>。

除了在下面详细描述如何之外,这种方法也可能是独一无二的,因为它结合了内置调试。它基本上不做任何写的事情,除了编译并将所有它认为应该执行的命令保存到一个变量中以执行请求的工作。

它还尽可能明确地避免循环。除了 sed 递归搜索多个 pattern 匹配项,据我所知,没有其他递归。

最后,这完全是由null 分隔的——除了null 之外,它不会触发任何文件名中的任何字符。我认为你不应该这样。

顺便说一句,这真的快。看:

% _mvnfind()  mv -n "$1" "$2" && cd "$2"
> read -r SED <<SED
> :;s|$3\(.*/[^/]*$5\)|$4\1|;t;:;s|\($5.*\)$3|\1$4|;t;s|^[0-9]*[\t]\(mv.*\)$5|\1|p
> SED
> find . -name "*$3*" -printf "%d\tmv %P $5 %P\000" |
> sort -zg | sed -nz $SED | read -r $6
> echo <<EOF
> Prepared commands saved in variable: $6
> To view do: printf $6 | tr "\000" "\n"
> To run do: sh <<EORUN
> $(printf $6 | tr "\000" "\n")
> EORUN
> EOF
> 
% rm -rf "$UNNECESSARY:=/any/dirs/you/dont/want/moved"
% time ( _mvnfind $SRC=./test_tree $TGT=./mv_tree \
> $OLD=google $NEW=replacement_word $sed_sep=SsEeDd \
> $sh_io:=sh_io ; printf %b\\000 "$sh_io" | tr "\000" "\n" \
> | wc - ; echo $sh_io | tr "\000" "\n" |  tail -n 2 )

   <actual process time used:>
    0.06s user 0.03s system 106% cpu 0.090 total

   <output from wc:>

    Lines  Words  Bytes
    115     362   20691 -

    <output from tail:>

    mv .config/replacement_word-chrome-beta/Default/.../googlestars \
    .config/replacement_word-chrome-beta/Default/.../replacement_wordstars        

注意:上述function 可能需要GNU 版本的sedfind 才能正确处理find printfsed -z -e:;recursive regex test;t 调用。如果您无法使用这些功能,则可能会通过一些小的调整来复制功能。

这应该可以从头到尾完成您想做的所有事情,而且很少大惊小怪。我用sed 做了fork,但我也在练习一些sed 递归分支技术,所以这就是我在这里的原因。我猜这有点像在理发学校打折理发。这是工作流程:

rm -rf $UNNECESSARY 我故意遗漏了任何可能删除或破坏任何类型数据的函数调用。你提到./app 可能是不需要的。事先将其删除或将其移至其他位置,或者,您可以在\( -path PATTERN -exec rm -rf \\ \) 的例程中构建find 以编程方式执行此操作,但那是您的全部。 _mvnfind "$@" 声明其参数并调用工作函数。 $sh_io 尤其重要,因为它保存了函数的返回。 $sed_sep 紧随其后;这是一个任意字符串,用于在函数中引用sed 的递归。如果$sed_sep 设置为可能在您的任何路径或文件名中找到的值......好吧,不要让它成为。 mv -n $1 $2 从头开始移动整个树。它会省去很多麻烦;相信我。您想要做的其余事情 - 重命名 - 只是文件系统元数据的问题。例如,如果您要将它从一个驱动器移动到另一个驱动器,或者跨越任何类型的文件系统边界,那么您最好使用一个命令立即执行此操作。它也更安全。注意为mv 设置的-noclobber 选项;如所写,此函数不会将 $SRC_DIR 放在已存在 $TGT_DIR 的位置。 read -R SED &lt;&lt;HEREDOC 我在这里找到了所有 sed 的命令,以节省逃避麻烦并将它们读入一个变量以提供给下面的 sed。解释如下。 find . -name $OLD -printf 我们开始find 进程。使用find,我们只搜索需要重命名的任何内容,因为我们已经使用函数的第一个命令完成了所有的位置到位置mv 操作。与其对find 采取任何直接操作,例如调用exec,不如使用它通过-printf 动态构建命令行。 %dir-depth :tab: 'mv '%path-to-$SRC' '$sed_sep'%path-again :null delimiter:' find 找到我们需要的文件后,它会直接构建并打印(大部分)我们需要处理您的重命名的命令。在每一行的开头添加%dir-depth 将有助于确保我们不会尝试使用尚未重命名的父对象重命名树中的文件或目录。 find 使用各种优化技术来遍历您的文件系统树,并且不确定它是否会以操作安全的顺序返回我们需要的数据。这就是为什么我们接下来... sort -general-numerical -zero-delimited 我们根据%directory-depthfind 的所有输出进行排序,以便首先处理与$SRC 关系最近的路径。这避免了涉及mving 文件到不存在的位置的可能错误,并且最大限度地减少了递归循环的需要。 (事实上,你可能很难找到一个循环sed -ex :rcrs;srch|(save$sep*til)$OLD|\saved$SUBSTNEW|;til $OLD=0 我认为这是整个脚本中唯一的循环,它只循环为每个字符串打印的第二个%Path,以防它包含多个可能需要替换的 $OLD 值。我想象的所有其他解决方案都涉及第二个sed 进程,虽然可能不希望短循环,但它肯定优于生成和分叉整个进程。 所以基本上sed 在这里所做的就是搜索$sed_sep,然后,找到它,保存它以及它遇到的所有字符,直到它找到$OLD,然后用$NEW 替换它。然后它返回到 $sed_sep 并再次查找 $OLD,以防它在字符串中多次出现。如果没有找到,它将修改后的字符串打印到stdout(然后它会再次捕获它)并结束循环。 这避免了必须解析整个字符串,并确保mv 命令字符串的前半部分(当然需要包含 $OLD)确实包含它,并且后​​半部分被多次更改根据需要从mv 的目标路径中擦除 $OLD 名称。 sed -ex...-ex search|%dir_depth(save*)$sed_sep|(only_saved)|out 这里的两个-exec 调用没有第二个fork 发生。首先,正如我们所见,我们根据需要修改find-printf 函数命令提供的mv 命令,以正确地将$OLD 的所有引用更改为$NEW,但是在为此,我们必须使用一些不应包含在最终输出中的任意参考点。因此,一旦sed 完成了它需要做的所有事情,我们就会指示它在传递它之前从保持缓冲区中清除它的参考点。

现在我们回来了

read 将收到如下所示的命令:

% mv /path2/$SRC/$OLD_DIR/$OLD_FILE /same/path_w/$NEW_DIR/$NEW_FILE \000

它将read它转换为$msg作为$sh_io,可以在函数之外随意检查。

酷。

-迈克

【讨论】:

【参考方案12】:

在我喜欢的 ramtam 的答案中,查找部分可以正常工作,但如果路径有空格,其余部分则不能。我对 sed 不太熟悉,但我能够将答案修改为:

find . -name "*_test.rb" | perl -pe 's/^((.*_)test.rb)$/"\1" "\2spec.rb"/' | xargs -n2 mv

我真的需要这样的改变,因为在我的用例中,最终命令看起来更像

find . -name "olddir" | perl -pe 's/^((.*)olddir)$/"\1" "\2new directory"/' | xargs -n2 mv

【讨论】:

【参考方案13】:

您的问题似乎是关于 sed,但为了实现递归重命名的目标,我建议以下内容,无耻地从我在这里给出的另一个答案中撕下:recursive rename in bash

#!/bin/bash
IFS=$'\n'
function RecurseDirs

for f in "$@"
do
  newf=echo "$f" | sed -e 's/^(.*_)test.rb$/\1spec.rb/g'
    echo "$f" "$newf"
    mv "$f" "$newf"
    f="$newf"
  if [[ -d "$f" ]]; then
    cd "$f"
    RecurseDirs $(ls -1 ".")
  fi
done
cd ..

RecurseDirs .

【讨论】:

如果不设置-r 选项,sed 如何在不转义() 的情况下工作?【参考方案14】:

我觉得这个更短

find . -name '*_test.rb' -exec bash -c 'echo mv $0 $0/test.rb/spec.rb'  \;

【讨论】:

嗨,我认为'_test.rb'应该是'_test.rb'(双引号到单引号)。请问您为什么使用下划线当我认为find . -name '*_test.rb' -exec bash -c 'echo mv $0 $0/test.rb/spec.rb' \; 有效时,推动你想要定位 $1 的论点?find . -name '*_test.rb' -exec bash -c 'echo mv $1 $1/test.rb/spec.rb' iAmArgumentZero \; 也一样 感谢您的建议,已修复 感谢您澄清这一点 - 我之所以发表评论,是因为我花了一段时间思考 _ 的含义,认为这可能是 $_ 的一些技巧使用('_' 在文档中很难搜索! )【参考方案15】:

您提到您使用bash 作为您的外壳,在这种情况下,您实际上不需要findsed 来实现您所追求的批量重命名......

假设您使用bash 作为您的外壳:

$ echo $SHELL
/bin/bash
$ _

...假设您启用了所谓的globstar shell 选项:

$ shopt -p globstar
shopt -s globstar
$ _

...最后假设您已经安装了rename 实用程序(在util-linux-ng 包中找到)

$ which rename
/usr/bin/rename
$ _

...那么您可以在bash one-liner中实现批量重命名,如下所示:

$ rename _test _spec **/*_test.rb

globstar shell 选项将确保 bash 找到所有匹配的 *_test.rb 文件,无论它们在目录层次结构中嵌套多深...使用 help shopt 了解如何设置该选项)

【讨论】:

【参考方案16】:

您可能想考虑其他方式,例如

for file in $(find . -name "*_test.rb")
do 
  echo mv $file `echo $file | sed s/_test.rb$/_spec.rb/`
done

【讨论】:

这看起来确实是个不错的方法。不过,我真的很想打破一条线,以提高我的知识比其他任何事情都重要。 对于 $(find . -name "*_test.rb") 中的文件;做 echo mv $file echo $file | sed s/_test.rb$/_spec.rb/; done 是单行的,不是吗? 如果您的文件名带有空格,这将不起作用。 for 会将它们拆分为单独的单词。您可以通过指示 for 循环仅在换行符处拆分来使其工作。有关示例,请参阅cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html 我同意@onitake,尽管我更喜欢使用 find 中的-exec 选项。【参考方案17】:

如果你愿意,你可以不用 sed 来完成:

for i in `find -name '*_test.rb'` ; do mv $i $i%%_test.rb_spec.rb ; done

$var%%suffixvar 的值中去除suffix

或者,使用 sed:

for i in `find -name '*_test.rb'` ; do mv $i `echo $i | sed 's/test/spec/'` ; done

【讨论】:

这不起作用(sed 之一),正如接受的答案所解释的那样。 @Ali,它确实有效——我在写答案时自己测试了它。 @larsman 的解释不适用于 for i in... ; do ... ; done,它通过 shell 执行命令并且确实理解反引号。【参考方案18】:

这是因为sed 接收字符串 作为输入,可以通过以下方式验证:

find . -exec echo `echo "" | sed 's/./foo/g'` \;

为目录中的每个文件递归打印foofoo。这种行为的原因是管道在 shell 展开整个命令时执行一次。

没有办法引用sed 管道以使find 将为每个文件执行它,因为find 不通过shell 执行命令并且没有管道或反引号的概念。 GNU findutils 手册解释了如何通过将管道放在单独的 shell 脚本中来执行类似的任务:

#!/bin/sh
echo "$1" | sed 's/_test.rb$/_spec.rb/'

(可能有一些不正当的方式使用sh -c 和大量引号在一个命令中完成所有这些操作,但我不会尝试。)

【讨论】:

对于那些想知道 sh -c 的不正当用法的人,这里是:find spec -name "*_test.rb" -exec sh -c 'echo mv "$1" "$(echo "$1 " | sed s/test.rb\$/spec.rb/)"' _ \; @opsb _ 到底是干什么用的?很好的解决方案 - 但我更喜欢 ramtam 的回答 :) 干杯!让我省了很多麻烦。为了完整起见,这就是我将它传递到脚本的方式: find . -name "文件" -exec sh /path/to/script.sh \;【参考方案19】:

如果你有 Ruby (1.9+)

ruby -e 'Dir["**/*._test.rb"].each|x|test(?f,x) and File.rename(x,x.gsub(/_test/,"_spec") ) '

【讨论】:

【参考方案20】:
$ find spec -name "*_test.rb"
spec/dir2/a_test.rb
spec/dir1/a_test.rb

$ find spec -name "*_test.rb" | xargs -n 1 /usr/bin/perl -e '($new=$ARGV[0]) =~ s/test/spec/; system(qq(mv),qq(-v), $ARGV[0], $new);'
`spec/dir2/a_test.rb' -> `spec/dir2/a_spec.rb'
`spec/dir1/a_test.rb' -> `spec/dir1/a_spec.rb'

$ find spec -name "*_spec.rb"
spec/dir2/b_spec.rb
spec/dir2/a_spec.rb
spec/dir1/a_spec.rb
spec/dir1/c_spec.rb

【讨论】:

啊..除了将逻辑放在 shell 脚本中并在 exec 中调用之外,我不知道使用 sed 的方法。最初没有看到使用 sed 的要求

以上是关于使用 find 和 sed 递归地重命名文件的主要内容,如果未能解决你的问题,请参考以下文章

递归地重命名字典列表中的字典键

Bash 重命名扩展递归

递归地复制和重命名文件

Linux在目录之前重命名文件

文件重命名示例

递归重命名文件 Mac OSX