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

97

我想遍历一堆目录并将所有以_test.rb结尾的文件重命名为以_spec.rb结尾。这是我从未完全弄清楚如何在bash中完成的事情,所以这次我想付出一些努力来掌握它。到目前为止,我的最佳尝试是:

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

NB:在exec之后有一个额外的echo,这样命令就会被打印而不是被运行,我在测试它。

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

mv original original

即sed的替换已经丢失了,有什么技巧吗?


顺便说一下,我知道有一个重命名命令,但我真的想弄清楚如何使用sed来完成它,这样我将来可以执行更强大的命令。 - opsb
2
请不要交叉发布 - Dennis Williamson
20个回答

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

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"
除了在下面详细描述how的方法之外,此方法还可能独具匠心,因为它结合了内置调试。实际上,它所编译并保存到变量中的所有命令都不会执行任何操作,只是为了执行所请求的工作而编译和保存。
它还尽可能地显式避免循环。除了sed递归搜索pattern的多个匹配项之外,据我所知,没有其他递归。
最后,这完全是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版本的sedfind才能正确处理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操作。而不是采取任何直接行动,例如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}关系最近的路径首先处理。这避免了可能涉及将文件移动

      现在我们回到原点了

      read将会接收到一个命令,看起来像这样:

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

      它将把它读入${msg}作为${sh_io},可以在函数外随意检查。
      很酷。
      -Mike

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

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

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 .

如果您没有设置-r选项,sed如何在不转义()的情况下工作? - mikeserv

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

0

使用查找工具和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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接