I have a git clone of etckeeper, and I'm trying to rename all files and directories with etckeeper
in the name to usrkeeper
. For example, ./foo-etckeeper-bar
should be renamed to ./foo-usrkeeper-bar
.
Finding the relevant files is trivial:
% find . -path '*etckeeper*' -print
However, I can't figure out how to actually do the renaming. I tried combining xargs
with mv
:
% find . -path '*etckeeper*' -print0 | xargs -0 -n 1 -J % bash -c mv % '$(echo' % \| sed \"s/etckeeper/usrkeeper/\" \)
For readability, the non-escaped second half reads: xargs -0 -n 1 -J % bash -c mv % $(echo % | sed "s/etckeeper/usrkeeper/" )
. The idea behind it is that we use $()
to pipe the filename through sed
, which is used to do the replacement.
The issue here is that bash -c
requires the command to execute to be a single string. After that, it starts interpreting arguments as positional parameters. I could quote the whole thing:
% find . -path '*etckeeper*' -print0 | xargs -0 -n 1 -J % bash -c 'mv % $(echo % | sed "s/etckeeper/usrkeeper/g" )'
But now xargs
won't replace %
. How can I solve this? (Also, as a side note, the above will fail if there's a file containing etckeeper
in the name in a directory containing etckeeper
in the name, because the directory will be moved before the file.)
Best Answer
You can use the rename command (see edit 1).
Solution 1
For a reasonable number of files/directory, by setting bash 4 option globstar (not works on recursive name, see edit 3):
Solution 2
For a big number of files/directories using rename and find in two steps to prevent failed rename on files in just renamed directories (see edit 2):
EDIT 1:
There are two different rename commands. This answer uses the Perl-based rename command, available in Debian-based distros. To have it on a not Debian based distro you can install from cpan or grab it around.
EDIT 2:
As pointed out by Jeff Schaller in the comments the
-depth
find optionProcess each directory's contents before the directory itself
so only an "ordered" find by-depth
option would be enough:EDIT 3
Solution 1 doesn't work for recursive rename targets, (es.
etckeeper/etckeeper/etckeeper
) becasue outer levels are processed before inner levels and pointer to inner levels become useless. (After the first renameetckeeper/etckeeper/etckeeper
will be namedusrkeeper/etckeeper/etckeeper
so the rename foretckeeper/etckeeper/
andetckeeper/etckeeper/etckeeper
will fail). The same problem fixed infind
by-depth
options.EDIT4
As pointed out in the comments by cas, I'd use {} + rather than {} \; - forking a perl script like rename multiple times (once per file/dir) is expensive.