我有一个 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 在评论中指出的-depth
find 选项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 -0
为find -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
!
我们只替换最后的路径名组件。我们可以通过使用basename
and来做到这一点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 上)传递-x
to 来xargs
告诉它立即失败。
答案3
另一个解决方案是使用一个小脚本,for
对查找结果进行循环,并对mv
找到的文件进行 bash 字符串替换:
IFS=$'\n'
for files in $(find . -name "*etckeeper*");
do
mv "$files ${files/etckeeper/usrkeeper}"
done
如果您不在脚本中使用它,那么最好保存原始 IFS 并在循环结束时恢复它。