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


85

我想浏览一堆目录,并将以_test.rb结尾的所有文件重命名为以_spec.rb结尾的文件。这是我从未真正弄清楚如何使用bash的事情,因此这一次我想我会花些力气将它钉牢。但是到目前为止,我的最大努力是:

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

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

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

mv original original

即用sed替代已丢失。诀窍是什么?


顺便说一句,我知道有一个重命名命令,但是我真的很想弄清楚如何使用sed来做它,以便将来我可以执行更强大的命令。
opsb 2011年


Answers:


32

发生这种情况是因为sed接收字符串{}作为输入,可以使用以下命令进行验证:

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

foofoo递归地为目录中的每个文件打印。出现这种现象的原因是,当管道扩展整个命令时,管道将由外壳执行一次。

由于无法通过外壳执行命令并且没有管道或反引号的概念,因此无法以对每个文件都执行sed管道的方式来引用管道。GNU findutils手册介绍了如何通过将管道放置在单独的shell脚本中来执行类似的任务:findfind

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

sh -c在一个命令中可能有一些错误的使用方式和大量的引号来完成所有这些操作,但是我不会尝试。)


27
对于那些想知道sh -c的错误用法的人,这里是:find spec -name“ * _test.rb” -exec sh -c'echo mv“ $ 1”“ $(echo” $ 1“ | sed s / test.rb \ $ / spec.rb /)“'_ {} \;
opsb,2011年

1
@opsb _到底是什么意思?伟大的解决方案-但我喜欢ramtam回答更多:)
iRaS

干杯! 救了我很多头。为了完整起见,这就是我将其通过管道传递给脚本的方法:find。-name“文件” -exec sh /path/to/script.sh {} \;
Sven M.

128

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


9
不幸的是,这存在空白问题。因此,使用名称中带有空格的文件夹将在xargs处将其破坏(使用-p进行详细/交互模式确认)
2014年

1
那正是我想要的。对于空白问题来说太糟糕了(不过我没有测试过)。但是对于我当前的需求,它是完美的。我建议先使用“ echo”而不是“ xvs”中的参数“ mv”对其进行测试。
Michele Dall'Agata,

5
如果您需要处理路径中的空格,并且使用的是GNU sed> = 4.2.2,则可以将该-z选项与finds-print0和xargs一起使用-0find -name '*._test.rb' -print0 | sed -ze "p;s/test/spec/" | xargs -0 -n2 mv
Evan Purkhiser

最佳解决方案。比find -exec快得多。谢谢
Miguel A. BaldiHörlle17年

如果test一个路径中有多个文件夹,则无法使用。sed将仅重命名第一个,并且mv命令将因No such file or directory错误而失败。
凯西

22

您可能想考虑其他方式,例如

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

看起来确实是个好方法。我真的很想破解这条线,以提高我的知识比什么都重要。
opsb 2011年

2
用于$(找到。-name“ * _test.rb”)中的文件;做回声MV $文件echo $file | sed s/_test.rb$/_spec.rb/; 做的是单线的,不是吗?
Bretticus

5
如果文件名带有空格,则无法使用。for会将它们拆分为单独的单词。您可以通过指示for循环仅在换行符上进行拆分来使其工作。有关示例,请参见cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html
onitake

我同意@onitake,尽管我希望使用-execfind中的选项。
ShellFish

18

我发现这个短

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 2011年

感谢您的建议,已修正
csg 2011年

感谢您清理问题-我之所以发表评论,是因为我花了一段时间思考_的含义,认为这可能是对$ _的一些欺骗性使用('_'在文档中很难搜索!)
agtb,2011年

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

sed如所接受的答案所解释,这不起作用(一个)。
阿里

@Ali,它确实有效-我在编写答案时亲自对其进行了测试。@larsman的解释不适用于for i in... ; do ... ; done,它通过外壳执行命令并且确实了解反引号。
韦恩·康拉德

9

您提到您正在bash用作外壳,在这种情况下,您实际上并不需要findsed实现了您想要的批处理重命名...

假设您将其bash用作外壳程序:

$ echo $SHELL
/bin/bash
$ _

...并假设您启用了所谓的globstarshell选项:

$ shopt -p globstar
shopt -s globstar
$ _

...并最终假设您已经安装了该rename实用程序(位于util-linux-ng软件包中)

$ which rename
/usr/bin/rename
$ _

...然后您可以在bash一线式中实现批量重命名,如下所示:

$ rename _test _spec **/*_test.rb

globstarshell选项将确保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 -bcat /proc/meminfo查找必须使用多少RAM。否则,请使用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")

这将find归档文件并重命名所有文件,_test.rb从末尾去除字符串并追加_spec.rb

在此步骤中,我们使用Shell参数扩展${var%string}从中删除最短的匹配模式“字符串” $var

$ 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'SO停止伤害

1

如果您有Ruby(1.9+)

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

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

1

我没有全心全意再做一次,但是我写这篇文章是为了回答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可能需要GNU使用sed和版本,find才能正确处理find printfandsed -z -e:;recursive regex test;tcall。如果您无法使用这些功能,则可以通过少量调整来复制该功能。

从头到尾,这应该可以做所有您想要的事情,而不必大惊小怪。我曾经fork使用过sed,但是我也在练习一些sed递归分支技术,所以这就是为什么我在这里。我想这有点像在理发学校打折理发。这是工作流程:

  • rm -rf ${UNNECESSARY}
    • 我故意忽略了可能删除或破坏任何类型数据的任何函数调用。您提到那./app可能是不必要的。事先删除它或将其移动到其他位置,或者,您可以构建一个\( -path PATTERN -exec rm -rf \{\} \)例程来find以编程方式进行操作,但这就是您的全部。
  • _mvnfind "${@}"
    • 声明其参数并调用worker函数。${sh_io}之所以特别重要,是因为它节省了函数的返回值。${sed_sep}紧随其后 这是用于引用sed函数中的递归的任意字符串。如果${sed_sep}将if设置为可能在您作用的任何路径名或文件名中都可能找到的值……那么,那就别让它如此。
  • mv -n $1 $2
    • 整个树从头开始移动。这将节省很多头痛;相信我。其余要做的事情-重命名-只是文件系统元数据的问题。例如,如果要将其从一个驱动器移动到另一个驱动器,或跨任何类型的文件系统边界,最好使用一个命令一次执行。也更安全。注意为-noclobber设置的选项mv;正如所写的,这个功能会不会把${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
    • 我们根据排序所有find的输出,%directory-depth以便首先处理与$ {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})确实包含了该字符串,并且后半部分被更改了多次,以擦除该字符串。 $ {OLD}mv的目标路径中的名称。
  • sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
    • -exec这里的两次通话很快就结束了fork。在第一,正如我们所看到的,我们修改了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}可以在函数的意志之外进行检查。

凉。

-麦克风


1

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

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

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

1

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

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

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

啊..i除了将逻辑放在shell脚本中并在exec中调用之外,不知道使用sed的方法。最初没有看到使用sed的要求
Damodharan R 2011年

0

您的问题似乎与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 .

如果不设置,如何在sed不逃避的情况下工作()-r选项,?
mikeserv 2013年

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

0

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



0

这是我的工作解决方案:

for FILE in {{FILE_PATTERN}}; do echo ${FILE} | mv ${FILE} $(sed 's/{{SOURCE_PATTERN}}/{{TARGET_PATTERN}}/g'); done
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.