bash - 使用find和sed递归重命名文件
我想浏览一堆目录并将所有以_test.rb结尾的文件重命名为_spec.rb结尾。 这是我从未想过如何处理bash的事情所以这次我认为我会付出一些努力来实现它。 到目前为止,我做得很短,我最大的努力是:
find spec -name "*_test.rb" -exec echo mv {} `echo {} | sed s/test/spec/` \;
注意:在exec之后有一个额外的回声,所以在我测试时打印命令而不是运行。
当我运行它时,每个匹配文件名的输出是:
mv original original
即sed的替换已经丢失。 有什么诀窍?
18个解决方案
106 votes
要以最接近原始问题的方式解决它可能会使用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。
ramtam answered 2019-05-07T00:38:21Z
31 votes
发生这种情况是因为sh -c收到字符串find作为输入,可以通过以下方式验证:
find . -exec echo `echo "{}" | sed 's/./foo/g'` \;
它以递归方式为目录中的每个文件打印sh -c。 这种行为的原因是,当它扩展整个命令时,shell会执行一次管道。
没有办法引用sh -c管道,以便find将为每个文件执行它,因为find不通过shell执行命令,并且没有管道或反引号的概念。 GNU findutils手册解释了如何通过将管道放在单独的shell脚本中来执行类似的任务:
#!/bin/sh
echo "$1" | sed 's/_test.rb$/_spec.rb/'
(在一个命令中使用sh -c和大量引号可能会有一些不正常的方法,但我不会尝试。)
Fred Foo answered 2019-05-07T00:37:48Z
22 votes
你可能想要考虑其他方式
for file in $(find . -name "*_test.rb")
do
echo mv $file `echo $file | sed s/_test.rb$/_spec.rb/`
done
ajreal answered 2019-05-07T00:38:46Z
17 votes
我发现这个更短
find . -name '*_test.rb' -exec bash -c 'echo mv $0 ${0/test.rb/spec.rb}' {} \;
csg answered 2019-05-07T00:39:11Z
9 votes
如果你愿意,你可以在没有sed的情况下完成:
for i in `find -name '*_test.rb'` ; do mv $i ${i%%_test.rb}_spec.rb ; done
${var%%suffix} strip suffix from value of var。
或者,使用sed来做:
for i in `find -name '*_test.rb'` ; do mv $i `echo $i | sed 's/test/spec/'` ; done
Wayne Conrad answered 2019-05-07T00:39:50Z
9 votes
你提到你使用globstar作为你的shell,在这种情况下你实际上并不需要*_test.rb和help shopt来实现批量重命名你之后...
假设您使用globstar作为您的shell:
$ echo $SHELL
/bin/bash
$ _
...并假设您已启用所谓的globstar shell选项:
$ shopt -p globstar
shopt -s globstar
$ _
...最后假设您已经安装了globstar实用程序(可在*_test.rb包中找到)
$ which rename
/usr/bin/rename
$ _
...然后你可以在bash one-liner中实现批量重命名,如下所示:
$ rename _test _spec **/*_test.rb
(globstar shell选项将确保bash找到所有匹配的*_test.rb文件,无论它们嵌套在目录层次结构中有多深...使用help shopt查找如何设置选项)
pvandenberk answered 2019-05-07T00:40:50Z
5 votes
最简单的方法:
find . -name "*_test.rb" | xargs rename s/_test/_spec/
最快的方法(假设您有4个处理器):
find . -name "*_test.rb" | xargs -P 4 rename s/_test/_spec/
如果要处理大量文件,则通过管道传输到xargs的文件名列表可能会导致生成的命令行超过允许的最大长度。
您可以使用free -b检查系统的限制
在大多数Linux系统上,您可以使用free -b或cat /proc/meminfo来查找您需要使用多少RAM; 否则,请使用top或您的系统活动监视器应用程序。
一种更安全的方式(假设您有1000000字节的ram可以使用):
find . -name "*_test.rb" | xargs -s 1000000 rename s/_test/_spec/
l3x answered 2019-05-07T00:41:45Z
2 votes
为此你不需要${var%string}.你可以通过流程替换以_spec.rb循环馈送$var循环。
因此,如果您有一个${var%string}表达式来选择所需的文件,那么请使用以下语法:
while IFS= read -r file; do
echo "mv $file ${file%_test.rb}_spec.rb" # remove "echo" when OK!
done <
这将是${var%string}文件,并重命名所有这些文件从末尾剥离字符串$var并附加_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 <
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
fedorqui answered 2019-05-07T00:42:40Z
1 votes
如果你有Ruby(1.9+)
ruby -e 'Dir["**/*._test.rb"].each{|x|test(?f,x) and File.rename(x,x.gsub(/_test/,"_spec") ) }'
kurumi answered 2019-05-07T00:43:07Z
1 votes
在我喜欢的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
dzs0000 answered 2019-05-07T00:43:42Z
1 votes
我没有心再重复一遍,但我在回答Commandline Find Sed Exec时写了这个。 在那里,提问者想要知道如何移动整个树,可能不包括一两个目录,并将包含字符串“OLD”的所有文件和目录重命名为“NEW”。
除了描述下面的详细冗长之外,这种方法也可以是独特的,因为它包含了内置的调试功能。 它基本上没有做任何事情,除了编译和保存到变量它认为它应该做的所有命令,以执行所请求的工作。
它还尽可能明确地避免循环。 除了function递归搜索模式的多个匹配之外,据我所知没有其他递归。
最后,这完全是function分隔 - 除了GNU之外,它不会跳过任何文件名中的任何字符。我认为你不应该这样。
顺便说一句,这真的很快。 看:
% _mvnfind() { mv -n "${1}" "${2}" && cd "${2}"
> read -r 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 <
> Prepared commands saved in variable: ${6}
> To view do: printf ${6} | tr "\000" "\n"
> To run do: sh <
> $(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 )
0.06s user 0.03s system 106% cpu 0.090 total
Lines Words Bytes
115 362 20691 -
mv .config/replacement_word-chrome-beta/Default/.../googlestars \
.config/replacement_word-chrome-beta/Default/.../replacement_wordstars
注意:以上function可能需要GNU版本sed和find才能正确处理read和${msg}以及${sh_io}。 如果您无法使用这些功能,则可能会通过一些小的调整来复制功能。
这应该从头到尾完成您想要的一切,并且非常小心。 我做了read与${msg},但我也练习了一些${sh_io}递归分支技术,这就是为什么我在这里。 我觉得这有点像在理发学校打折。 这是工作流程:
read我故意遗漏任何可能删除或破坏任何类型数据的函数调用。 你提到read可能不需要。 删除它或事先将其移动到其他地方,或者,您可以构建${msg}例程${sh_io}以编程方式执行此操作,但那一个都是您的。
read声明其参数并调用worker函数。 read特别重要的是它保存了函数的返回。 ${msg}紧随其后; 这是一个任意字符串,用于在函数中引用${sh_io}的递归。 如果将find设置为可能在您的任何路径或文件名中找到的值...好吧,就是不要让它成为。
read整棵树从一开始就被移动了。 它会省去很多头痛; 相信我。 您想要做的其余部分 - 重命名 - 只是文件系统元数据的问题。 例如,如果您将其从一个驱动器移动到另一个驱动器,或者跨越任何类型的文件系统边界,那么最好使用一个命令立即执行此操作。 它也更安全。 注意read选项设置为${msg}; 如上所述,此功能不会放${sh_io},其中find已经存在。
read我在这里找到所有sed的命令以节省逃避麻烦并将它们读入变量以供给下面的sed。 说明如下。
read我们开始read进程。 使用${msg},我们只搜索需要重命名的任何内容,因为我们已经使用函数的第一个命令执行了所有的地方到位${sh_io}操作。 例如,我们不是像-printf那样采取任何直接行动,而是使用它来动态构建命令行sed。
readread找到我们需要的文件之后直接构建并打印出(大部分)我们处理重命名所需的命令。 添加到每行开头的${msg}将有助于确保我们不会尝试使用尚未重命名的父对象重命名树中的文件或目录。 ${sh_io}使用各种优化技术来遍历您的文件系统树,并且它不能确保它将以安全的操作顺序返回我们需要的数据。 这就是为什么我们接下来...
read我们根据${msg}对read的所有输出进行排序,以便最先处理与$ {SRC}关系最近的路径。 这避免了将文件涉及${sh_io}34文件到不存在的位置的可能错误,并且它最小化了递归循环的需要。 (事实上,你可能很难找到一个循环)
read我认为这是整个脚本中唯一的循环,它只循环遍历为每个字符串打印的第二个read,以防它包含多个可能需要替换的$ {OLD}值。 我想象的所有其他解决方案都涉及第二个${msg}过程,虽然可能不需要短循环,但它肯定会产生并分叉整个过程。
所以基本上read在这里搜索$ {sed_sep},然后,找到它,保存它和它遇到的所有字符,直到它找到$ {OLD},然后用$ {NEW}替换它。 然后返回$ {sed_sep}并再次查找$ {OLD},以防它在字符串中出现多次。 如果未找到,则将修改后的字符串打印到${msg}(然后再次捕获)并结束循环。
这避免了必须解析整个字符串,并确保read命令字符串的前半部分(当然需要包含$ {OLD})确实包含它,并且后半部分被更改为擦除所需的次数 $ {OLD}名称来自${msg}的目的地路径。
read这里的两个read调用没有第二个${msg}。在第一个,正如我们所见,我们修改find的-printf函数命令所提供的${sh_io}命令,以正确地将$ {OLD}的所有引用更改为$ {NEW} ,但为了做到这一点,我们不得不使用一些不应包含在最终输出中的任意参考点。 因此,一旦sed完成所有需要的操作,我们会指示它在传递它之前从保持缓冲区中清除它的参考点。
现在我们回来了
read将收到如下所示的命令:
% mv /path2/$SRC/$OLD_DIR/$OLD_FILE /same/path_w/$NEW_DIR/$NEW_FILE \000
它将read改为${msg}作为${sh_io},可随意检查该功能。
凉。
-麦克风
mikeserv answered 2019-05-07T00:47:16Z
1 votes
我可以按照onitake建议的示例来处理带空格的文件名。
如果路径包含空格或字符串test,则不会中断:
find . -name "*_test.rb" -print0 | while read -d $'\0' file
do
echo mv "$file" "$(echo $file | sed s/test/spec/)"
done
James answered 2019-05-07T00:47:53Z
1 votes
这是一个应该适用于所有情况的示例。Works recursiveley,需要shell,并支持带空格的文件名。
find spec -name "*_test.rb" -print0 | while read -d $'\0' file; do mv "$file" "`echo $file | sed s/test/spec/`"; done
eldy answered 2019-05-07T00:48:24Z
1 votes
当文件名中包含空格时,这对我有用。 下面的示例以递归方式将所有.dar文件重命名为.zip文件:
find . -name "*.dar" -exec bash -c 'mv "$0" "`echo \"$0\" | sed s/.dar/.zip/`"' {} \;
rskengineer answered 2019-05-07T00:48:54Z
0 votes
$ 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
Damodharan R answered 2019-05-07T00:49:17Z
0 votes
你的问题似乎是关于sed,但为了实现你的递归重命名的目标,我建议以下,从我在这里给出的另一个答案无耻地撕掉:在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 .
dreynold answered 2019-05-07T00:49:48Z
0 votes
使用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:]]+$//'`' {} +
Sathish answered 2019-05-07T00:50:33Z
0 votes
这是一个很好的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将处理路径中的空白空间。
deajan answered 2019-05-07T00:51:10Z