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

时间:2011-01-25 13:08:52

标签: bash scripting sed find replace

我想浏览一堆目录并将所有以_test.rb结尾的文件重命名为_spec.rb结尾。这是我从未想过如何处理bash的事情所以这次我认为我会付出一些努力来实现它。到目前为止,我做得很短,我的最大努力是:

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

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

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

mv original original

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

20 个答案:

答案 0 :(得分:111)

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

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

以递归方式查找当前工作目录中的文件,回显原始文件名(p),然后回复修改后的名称(s/test/spec/)并将其全部反馈到mvxargs -n2)。请注意,在这种情况下,路径本身不应包含字符串test

答案 1 :(得分: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和大量引号可能有一些不正常的方式来做所有这些,但我不会尝试。)

答案 2 :(得分:23)

你可能想要考虑其他方式,如

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

答案 3 :(得分:17)

我觉得这个更短

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

答案 4 :(得分:9)

如果你愿意,你可以在没有sed的情况下完成:

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

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

或者,使用sed:

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

答案 5 :(得分:9)

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

假设您使用bash作为shell:

$ 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了解如何设置选项)

答案 6 :(得分:5)

最简单的方法

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或您的系统活动监控应用。

更安全的方式(假设你有100万字节的ram可以使用):

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

答案 7 :(得分:2)

为此,您不需要sed。使用while结果find通过process substitution进行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并附加${var%string}

对于此步骤,我们使用Shell Parameter Expansion $var删除最短匹配模式&#34; string&#34;来自$ 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

查看示例:

{{1}}

答案 8 :(得分:2)

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

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

答案 9 :(得分:1)

我没有心再重复一次,但我在回答Commandline Find Sed Exec时写了这个。在那里,提问者想要知道如何移动整个树,可能不包括一两个目录,并重命名包含字符串&#34; OLD&#34; 的所有文件和目录,而不是包含 &#34; NEW&#34;

除了在下面描述如何具有煞费苦心的详细程度之外,这种方法也可能是唯一的,因为它包含内置调试。它基本上没有做任何事情,除了编译和保存到变量它认为应该做的所有命令以执行所请求的工作。

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

最后,这完全是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 sedfindfind printf来正确处理sed -z -e:;recursive regex test;tfork来电。如果您无法使用这些功能,则可能会通过一些小的调整来复制功能。

这应该从头到尾做你想做的一切,而且很少。我与sed进行了sed,但我也在练习一些rm -rf ${UNNECESSARY}递归分支技术,这就是我在这里的原因。我想,这有点像在理发学校打折。这是工作流程:

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

现在我们重新回到

% mv /path2/$SRC/$OLD_DIR/$OLD_FILE /same/path_w/$NEW_DIR/$NEW_FILE \000 将收到如下命令:

read

${msg}将其${sh_io}改为{{1}}作为{{1}},可以在函数外部随意查看。

冷却。

-Mike

答案 10 :(得分:1)

这是一个应该适用于所有情况的示例。 使用recursiveley,只需要shell,并支持带空格的文件名。

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

答案 11 :(得分:1)

我可以按照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

答案 12 :(得分:1)

在我喜欢的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 :(得分:1)

如果你有Ruby(1.9 +)

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

答案 14 :(得分:0)

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

答案 15 :(得分:0)

这是一个很好的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将处理路径中的空白区域。

答案 16 :(得分:0)

你的问题似乎是关于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 .

答案 17 :(得分:0)

$ 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

答案 18 :(得分:0)

我分享了此帖子,因为它与问题有点相关。抱歉,没有提供更多详细信息。希望它可以帮助别人。 http://www.peteryu.ca/tutorials/shellscripting/batch_rename

答案 19 :(得分:0)

这是我的工作解决方案:

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