使用find和sedrecursion重命名文件

我想通过一堆目录,并重命名以_test.rb结尾的所有文件,而不是以_spec.rb结尾。 这是我从来没有想过如何处理bash,所以这次我想我会付出一些努力来钉牢。 尽pipe我到目前为止已经尽力了,但我的最大努力是:

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

注意:在exec之后还有一个额外的回声,这样当我testing它时,命令就会被打印出来而不是运行。

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

 mv original original 

即sed的replace已经丢失。 有什么窍门?

发生这种情况是因为sed收到string{}作为input,可以通过以下方式进行validation:

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

以recursion方式为目录中的每个文件打印foofoo 。 这种行为的原因是pipe道在扩展整个命令时被shell执行一次。

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

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

(可能有一些不正当的方法使用sh -c和一大堆引用来完成所有这些命令,但我不会去尝试。)

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

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

它以recursion方式查找当前工作目录中的文件,回显原始文件名( p ),然后修改名称( s/test/spec/ ),并将它们全部成对( xargs -n2 )input到mv 。 请注意,在这种情况下,path本身不应包含stringtest

你可能要考虑其他方式

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

我觉得这个更短

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

你可以做到这一点,没有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 

你提到你使用bash作为你的shell,在这种情况下,你实际上并不需要findsed来实现批量重命名

假设你使用bash作为你的shell:

 $ echo $SHELL /bin/bash $ _ 

假设你已经启用了所谓的globstar shell选项:

 $ shopt -p globstar shopt -s globstar $ _ 

…最后假设你已经安装了rename工具(在util-linux-ng包中find)

 $ which rename /usr/bin/rename $ _ 

…然后你可以实现批量重命名在一个bash单行如下:

 $ rename _test _spec **/*_test.rb 

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

最简单的方法是

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

最快的方法 (假设你有4个处理器):

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

如果您有大量的文件需要处理,那么通过pipe道传递给xargs的文件名列表可能会导致生成的命令行超出允许的最大长度。

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

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

一个更安全的方法 (假设你有100万字节的RAM工作):

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

如果你有Ruby(1.9+)

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

在我喜欢的ramtam的答案中,查找部分工作正常,但如果path中有空格,其余部分不起作用。 我不太熟悉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 

我再也没有这样做过,但是我在回答Commandline Find Sed Exec时写了这个。 在那里,提问者想知道如何移动整个树,可能不包括一个或两个目录,并将包含string“OLD”的所有文件和目录重命名为包含“NEW”

除了下面详细描述细节之外 ,这种方法也可以是独特的,因为它包含了内置的debuggingfunction。 它基本上不做任何事情,除了编译和保存到一个variables,它认为它应该做的所有命令,以执行所要求的工作。

它也尽可能避免循环 。 除了sedrecursionsearch模式的多个匹配之外,据我所知没有其他recursion。

最后,这是完全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可能需要sed GNU版本,并find正确处理find printfsed -z -e:;recursive regex test;t调用。 如果这些function不适用于您,则可能会通过一些小的调整来重复function。

这应该做你想要的一切,从一开始就完成,很less大惊小怪。 我做了sed fork ,但是我也在练习一些sedrecursion分支技术,所以我就来这里。 这就像是在理发店打理折扣,我想。 这是工作stream程:

  • rm -rf ${UNNECESSARY}
    • 我故意排除了任何可能删除或销毁任何types数据的function调用。 你提到./app可能是不需要的。 事先删除它或将它移动到别处,或者,也可以在一个\( -path PATTERN -exec rm -rf \{\} \)例程中构build,以编程方式执行它,但这是你的全部。
  • _mvnfind "${@}"
    • 声明它的参数并调用worker函数。 ${sh_io}特别重要,因为它节省了函数的返回。 ${sed_sep} ; 这是一个用来引用sed在函数中recursion的任意string。 如果将${sed_sep}设置为一个可能在您的任何path或文件名中find的值,那么请不要这么做。
  • mv -n $1 $2
    • 整棵树从一开始就移动。 这将节省很多头痛; 相信我。 你想做的其他事情 – 重命名 – 只是文件系统元数据的问题。 例如,如果你是从一个驱动器移动到另一个驱动器,或者跨越任何types的文件系统边界,那么你最好用一个命令立即执行。 这也更安全。 请注意为mv设置的-noclobber选项; 正如所写,这个函数不会把${SRC_DIR}放在一个${TGT_DIR}已经存在的地方。
  • read -R SED <<HEREDOC
    • 我在这里find了sed的所有命令,以节省逃避的麻烦,并将它们读入一个variables,以供给sed。 下面的解释。
  • find . -name ${OLD} -printf
    • 我们开始find过程。 使用find我们只search任何需要重命名的东西,因为我们已经用函数的第一个命令完成了所有的地方到地方的mv操作。 例如,我们不是直接使用find命令来exec ,而是使用-printfdynamic构build命令行。
  • %dir-depth :tab: 'mv '%path-to-${SRC}' '${sed_sep}'%path-again :null delimiter:'
    • findfind我们需要的文件后,直接构build并打印( 大部分 )我们需要处理重命名的命令。 添加到每行开头的%dir-depth将有助于确保我们不试图用尚未重命名的父对象重命名树中的文件或目录。 find使用各种优化技术来走你的文件系统树,这不是一个肯定的事情,它会返回我们需要的数据在一个安全的操作顺序。 这就是为什么我们接下来…
  • sort -general-numerical -zero-delimited
    • 我们根据%directory-depth对所有find的输出进行sorting,以便与$ {SRC}关系最近的path首先被处理。 这样可以避免将文件mv到不存在的位置时可能出现的错误,并最大限度地减less了recursion循环的需要。 ( 事实上,你可能很难find一个循环
  • sed -ex :rcrs;srch|(save${sep}*til)${OLD}|\saved${SUBSTNEW}|;til ${OLD=0}
    • 我认为这是整个脚本中唯一的循环,只有当它包含多个可能需要replace的$ {OLD}值时,它才会循环遍历为每个string打印的第二个%Path 。 我想到的所有其他解决scheme都涉及到第二个sed进程,虽然短暂的循环可能并不理想,但它肯定会影响产卵和分叉整个进程。
    • 所以基本上sed在这里search$ {sed_sep},然后find它,保存它和它遇到的所有字符,直到find$ {old},然后用$ {NEW}replace它。 然后返回$ {sed_sep}并再次查找$ {OLD},以防string中出现多次。 如果没有find它,它将修改后的string打印到stdout (然后再次捕获),并结束循环。
    • 这就避免了必须parsing整个string,并且确保当前需要包含$ {OLD}的mv命令string的前半部分确实包括了它,并且后​​半部分被擦除了必要的次数mv目标path中的$ {OLD}名称。
  • sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
    • 这两个-exec调用在这里没有第二个fork 。 在第一个-printf ,我们已经看到,我们修改了mv命令,按照find-printf函数提供的命令,将$ {OLD}的所有引用正确地改为$ {NEW},但是为了这么做,我们必须使用一些不应包含在最终输出中的任意参考点。 因此,一旦sed完成了所有的工作,我们就会指示它从保持缓冲区中清除它的参考点,然后再传递它。

现在我们回来了

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

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

它会将它read${msg}作为${sh_io} ,可以在函数外部随意检查。

凉。

-麦克风

按照onitakebuild议的例子,我可以用空格处理文件名。

如果path包含空格或stringtest 则不会中断:

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

这是一个适用于所有情况的例子。 运行recursiveley,只需要shell,并用空格支持文件名。

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

你的问题似乎是关于sed,但为了实现recursion重命名的目标,我build议以下,从我给这里的另一个答案无耻撕裂: recursion重命名在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 . 

使用find utils和sed正则expression式types进行重命名更安全的方法:

  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:]]+$//'`' {} + 

这是一个不错的手段。 sed不能处理这个权利,尤其是如果多个variables通过xargs传递与-n 2.一个bash的replace将处理这个很容易,如:

 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将处理path中的空白空间。

当文件名中有空格的时候,这就是我的工作。 以下示例recursion地将所有.dar文件重命名为.zip文件:

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

为此,你不需要sed 。 你可以完全独立,通过一个进程替代 find的结果。

所以如果你有一个findexpression式来select需要的文件,那么使用下面的语法:

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

这将find文件并将其全部重命名为从末尾剥离string_test.rb并追加_spec.rb

对于这一步,我们使用Shell Parameter Expansion ,其中${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