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

97

我想遍历一堆目录并将所有以_test.rb结尾的文件重命名为以_spec.rb结尾。这是我从未完全弄清楚如何在bash中完成的事情,所以这次我想付出一些努力来掌握它。到目前为止,我的最佳尝试是:

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

NB:在exec之后有一个额外的echo,这样命令就会被打印而不是被运行,我在测试它。

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

mv original original

即sed的替换已经丢失了,有什么技巧吗?


顺便说一下,我知道有一个重命名命令,但我真的想弄清楚如何使用sed来完成它,这样我将来可以执行更强大的命令。 - opsb
2
请不要交叉发布 - Dennis Williamson
20个回答

147

为了以最接近原问题的方式解决它,可能需要使用 xargs 的“每个命令行参数”选项:

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

该命令会递归在当前工作目录中查找文件,打印出原始文件名 (p),然后输出一个修改后的名称 (s/test/spec/) 并将所有文件名通过成对方式传递给 mv 命令 (xargs -n2)。需要注意的是,在这种情况下,路径本身不应包含字符串 test


10
很遗憾,这段代码存在空格问题。因此,如果文件夹名称中有空格,则在使用xargs时会出现错误(可以通过-p选项启用详细/交互模式进行确认)。 - cde
1
这正是我正在寻找的。白空格问题很遗憾(虽然我没有测试过)。但对于我的当前需求来说,它是完美的。我建议先使用“echo”而不是“mv”作为“xargs”参数进行测试。 - Michele Dall'Agata
8
如果你需要处理路径中的空格并且正在使用GNU sed >= 4.2.2,则可以使用 -z 选项以及 find-print0xargs-0find -name '*._test.rb' -print0 | sed -ze "p;s/test/spec/" | xargs -0 -n2 mv - Evan Purkhiser
最佳解决方案。比find -exec快得多。谢谢。 - Miguel A. Baldi Hörlle
如果一个路径中有多个test文件夹,这种方法就行不通了。sed只会重命名第一个文件夹,而mv命令则会因为“没有这样的文件或目录”错误而失败。 - Casey
你可以使用'find . -type f'来排除文件夹,只列出文件,或者反过来,如果你只想重命名文件夹,可以使用'find . -type d'。 - undefined

32

这是因为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 和大量引号来在一个命令中实现所有这些内容,但我不打算尝试。)


30
对于那些好奇 sh -c 的恶意用法的人,这里是解释:查找 spec 目录下所有名为 "*_test.rb" 的文件,并执行 sh -c 'echo mv "$1" "$(echo "$1" | sed s/test.rb$/spec.rb/)"' _ {} ; 命令。 - opsb
1
@opsb那个下划线是干嘛用的?很好的解决方案 - 但我更喜欢ramtam的答案 :) - iRaS
谢谢!省了我很多麻烦。为了完整起见,这是如何将其管道传输到脚本的方法:find . -name "file" -exec sh /path/to/script.sh {} ; - Sven M.

24

你可能希望考虑其他方式,比如

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

那看起来是一个不错的方法。但我真的想要破解这个一行代码,更多的是为了提高我的知识水平。 - opsb
2
“for file in $(find . -name "*_test.rb"); do echo mv $file echo $file | sed s/_test.rb$/_spec.rb/; done” 这不是一行代码吗? - Bretticus
5
如果你的文件名中有空格,这种方法将不起作用。“for”命令会把它们分成单独的单词。你可以通过指示“for”循环只在换行符上拆分来使其正常工作。请参考http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html查看示例。 - onitake
我同意@onitake的观点,尽管我更喜欢使用find命令中的-exec选项。 - ShellFish

21

我觉得这个更短

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

嗨,我认为 '_test.rb' 应该是 '_test.rb'(双引号改为单引号)。我可以问一下为什么您使用下划线来推动要定位到 $1 的参数,当我认为 find . -name '*_test.rb' -exec bash -c 'echo mv $0 ${0/test.rb/spec.rb}' {} \; 也可以工作吗?就像 find . -name '*_test.rb' -exec bash -c 'echo mv $1 ${1/test.rb/spec.rb}' iAmArgumentZero {} \; - agtb
感谢您的建议,已修复。 - csg
谢谢你澄清这个问题 - 我只是评论了一下,因为我花了一段时间思考 _ 的含义,可能认为它是一些 trick 使用 $_(在文档中很难搜索到“_”!) - agtb

9
如果您愿意,可以不使用sed来完成这个操作:
for i in `find -name '*_test.rb'` ; do mv $i ${i%%_test.rb}_spec.rb ; done

${var%%suffix}var的值中删除suffix

或者,使用sed执行此操作:

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

这个(sed)不起作用,正如被接受的答案所解释的那样。 - Ali
@Ali,它确实有效——我在回答时亲自测试过。@larsman的解释不适用于for i in...; do ...; done,它通过shell执行命令并且_确实_理解反引号。 - Wayne Conrad

9
您提到您正在使用bash作为您的shell,那么您实际上不需要使用find和sed来实现您想要的批量重命名...假设您正在使用bash作为您的shell:
$ echo $SHELL
/bin/bash
$ _

假设您已启用所谓的 globstar shell 选项,那么 ...
$ shopt -p globstar
shopt -s globstar
$ _

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

$ which rename
/usr/bin/rename
$ _

如果您使用的是bash,那么您可以通过以下一行命令来实现批量重命名:

$ rename _test _spec **/*_test.rb

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


7

最简单的方法:

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 -b或cat /proc/meminfo来查找可用的内存大小;否则,请使用top或系统活动监视器应用程序。假设您有1000000个字节的RAM可供使用,更安全的方法如下:
find . -name "*_test.rb" | xargs -s 1000000 rename s/_test/_spec/

2

当文件名中有空格时,以下方法对我很有效。下面的示例将所有 .dar 文件递归重命名为 .zip 文件:

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

2
对于这个问题,您不需要使用 sed。您可以使用一个 while 循环来处理 find 命令的结果,通过过程替换来实现。因此,如果您有一个选择所需文件的 find 表达式,则可以使用以下语法:
while IFS= read -r file; do
     echo "mv $file ${file%_test.rb}_spec.rb"  # remove "echo" when OK!
done < <(find -name "*_test.rb")

这将查找文件并重命名所有文件,从结尾处剥离字符串_test.rb并添加_spec.rb
在此步骤中,我们使用Shell参数扩展,其中${var%string}$var中删除最短匹配模式“string”。
$ 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 < <(find -type f -name "*.gz") - Vinay Vissh
1
@CasualCoder 很高兴看到这个 :) 注意你可以直接使用 find .... -exec mv ...。此外,要小心 $file,因为如果它包含空格,它将失败。最好使用引号 "$file" - fedorqui

1

如果你有 Ruby(1.9+)

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接