繁体   English   中英

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

[英]Recursively rename files using find and sed

我想浏览一堆目录并将所有以 _test.rb 结尾的文件重命名为以 _spec.rb 结尾。 这是我从来没有想过如何处理 bash 的事情,所以这次我想我会付出一些努力来解决它。 到目前为止,我还没有完成,但我最大的努力是:

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

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

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

mv original original

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

要以最接近原始问题的方式解决它,可能会使用 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

发生这种情况是因为sed接收字符串{}作为输入,可以通过以下方式进行验证:

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

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

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

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

(可能有一些使用sh -c和大量引号在一个命令中完成所有这些的反常方法,但我不打算尝试。)

你可能想考虑其他方式

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}去掉suffix从值var

或者,使用 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包中找到)

$ which rename
/usr/bin/rename
$ _

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

$ rename _test _spec **/*_test.rb

globstar shell 选项将确保 bash 找到所有匹配的*_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/

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

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

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

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

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

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

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

为此,您不需要sed 您可以完全独立于通过进程替换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 参数扩展,其中${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

在我喜欢的 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

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

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

它还明确地尽可能地避免循环 除了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可能需要sedfind GNU版本才能正确处理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 <<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} 关系最近的路径。 这避免了可能涉及的错误mv荷兰国际集团文件到不存在的位置,以及它最大限度地减少需要递归循环。 事实上,你可能很难找到一个循环
  • 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} ,可以在函数之外随意检查。

凉爽的。

-麦克风

通过遵循onitake建议的示例,我能够处理带空格的文件名。

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

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

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

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

如果你有 Ruby (1.9+)

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

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

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

$ 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 有关,但为了实现递归重命名的目标,我建议以下内容,从我在这里给出的另一个答案中无耻地撕下: 在 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 .

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

这是我的工作解决方案:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM