如何递归替换文件和目录名称中的字符串?

如何递归替换文件和目录名称中的字符串?

我有一个 etckeeper 的 git 克隆,我正在尝试将etckeeper名称中的所有文件和目录重命名为usrkeeper.例如,./foo-etckeeper-bar应重命名为./foo-usrkeeper-bar.

查找相关文件很简单:

% find . -path '*etckeeper*' -print

但是,我不知道如何实际进行重命名。我尝试xargs与以下结合mv

% find . -path '*etckeeper*' -print0 | xargs -0 -n 1 -J % bash -c mv % '$(echo' % \| sed \"s/etckeeper/usrkeeper/\" \)

为了便于阅读,非转义的后半部分显示为:xargs -0 -n 1 -J % bash -c mv % $(echo % | sed "s/etckeeper/usrkeeper/" )。其背后的想法是我们使用$()管道传输文件名sed,用于进行替换。

这里的问题是bash -c要求执行的命令是单个字符串。之后,它开始将参数解释为位置参数。我可以引用整件事:

% find . -path '*etckeeper*' -print0 | xargs -0 -n 1 -J % bash -c 'mv % $(echo % | sed "s/etckeeper/usrkeeper/g" )'

但现在xargs不会取代%。我该如何解决这个问题? (另外,作为旁注,如果etckeeper在包含名称的目录中存在包含名称的文件etckeeper,则上述操作将失败,因为该目录将被移动到文件之前。)

答案1

您可以使用重命名命令(请参阅编辑1)。

解决方案1

对于合理数量的文件/目录,通过设置 bash 4 选项环球星(不适用于递归名称,请参阅编辑3):

shopt -s globstar
rename -n 's/etckeeper/userkeeper/g' **

解决方案2

对于大量文件/目录,使用重命名和查找分两个步骤来防止对刚刚重命名的目录中的文件重命名失败(请参阅编辑2):

find . -type f -exec rename 's/etckeeper/userkeeper/g' {} \;
find . -type d -exec rename 's/etckeeper/userkeeper/g' {} \;

编辑1:

有两种不同的重命名命令。这个答案使用基于 Perl 的重命名命令,该命令在基于 Debian 的发行版中可用。要将其安装在非基于 Debian 的发行版上,您需要可以从cpan安装或抓住它。

编辑2:

正如 Jeff Schaller 在评论中指出的-depthfind 选项Process each directory's contents before the directory itself,因此只有按选项“有序”查找-depth就足够了:

find . -depth -exec rename 's/etckeeper/userkeeper/g' {} \;

编辑3

解决方案 1 不适用于递归重命名目标,(例如etckeeper/etckeeper/etckeeper)因为外部级别在内部级别之前被处理,并且指向内部级别的指针变得无用。 (第一次重命名后将etckeeper/etckeeper/etckeeper被命名,usrkeeper/etckeeper/etckeeper因此重命名为etckeeper/etckeeper/etckeeper/etckeeper/etckeeper将会失败)。选项find解决了同样的问题-depth

编辑4

正如 cas 评论中指出的那样,我会使用 {} + 而不是 {} \; - 分叉 Perl 脚本,例如多次重命名(每个文件/目录一次)是昂贵的。

答案2

你原来的问题实际上很容易回答:(xargs至少在 OS X 上)-I也有一个选项,它确实不是要求替换字符串是唯一的参数。因此,调用就变成了:

% find . -path '*etckeeper*' -print0 | xargs -0 -n 1 -I % bash -c 'echo mv % $(echo % | sed "s/etckeeper/usrkeeper/g" )'

十分简单。现在,让我们按正确的顺序重命名。事实证明,find将首先打印目录(因为它必须在下降到目录之前处理它们),所以我们需要做的就是反转文件名的顺序:

% find . -path '*etckeeper*' -print | tail -r | xargs -n 1 -I % bash -c 'echo mv % $(echo % | sed "s/etckeeper/usrkeeper/g" )'

请注意,我已切换find -print0 | xargs -0find -print | xargs.为什么?因为tail -r反转基于换行符,而不是空字符。如果您没有切换它,tail -r则会打印出您通过管道输入的相同内容!所以脚本现在更正确了,但它也会在包含换行符的文件名上中断。

请注意,如果您没有tail -r,您应该尝试一下tac。看:如何反转文件中的行顺序?在堆栈溢出上。

现在的问题是该sed命令过于激进。例如,有一棵树:

.
├── etckeeper-foo
│   └── etckeeper-bar.md
└── some-other-directory
    └── etckeeper-baz.md

您将得到mv如下所示的调用:

mv ./some-other-directory/etckeeper-baz.md ./some-other-directory/usrkeeper-baz.md
mv ./etckeeper-foo/etckeeper-bar.md ./usrkeeper-foo/usrkeeper-bar.md
mv ./etckeeper-foo ./usrkeeper-foo

第一个很好,但第二个显然不行——因为我们还没有完成mv ./etckeeper-foo ./usrkeeper-foo

我们只替换最后的路径名组件。我们可以通过使用basenameand来做到这一点dirname

% find . -name '*etckeeper*' -print | tail -r | xargs -n 1 -I % bash -c 'echo mv % $(dirname %)/$(basename % | sed "s/etckeeper/usrkeeper/g" )'

请注意,我所做的另一个更改是使用find -name,而不是find -path

这会产生正确的mv调用:

mv ./some-other-directory/etckeeper-baz.md ./some-other-directory/usrkeeper-baz.md
mv ./etckeeper-foo/etckeeper-bar.md ./etckeeper-foo/usrkeeper-bar.md
mv ./etckeeper-foo ./usrkeeper-foo

最后。再次记住,这将失败关于深奥的文件名。另请注意,如果您的文件名特别长,xargs则将无法正常工作,因为参数mv变得太长。如果是这种情况,您可以(至少在 OS X 上)传递-xto 来xargs告诉它立即失败。

答案3

另一个解决方案是使用一个小脚本,for对查找结果进行循环,并对mv找到的文件进行 bash 字符串替换:

IFS=$'\n'
for files in $(find . -name "*etckeeper*"); 
do 
  mv "$files ${files/etckeeper/usrkeeper}"
done

如果您不在脚本中使用它,那么最好保存原始 IFS 并在循环结束时恢复它。

相关内容