在 xargs 中修改替换字符串

在使用 xargs时,有时不需要显式地使用替换字符串:

find . -name "*.txt" | xargs rm -rf

在其他情况下,我希望指定替换字符串,以便执行以下操作:

find . -name "*.txt" | xargs -I '{}' mv '{}' /foo/'{}'.bar

之前的命令会将工作目录下的所有文本文件移动到 /foo中,并将扩展名 bar附加到所有文件中。

如果不是向替换字符串添加一些文本,而是修改该字符串,以便可以在文件的名称和扩展名之间插入一些文本,那么如何做到这一点呢?例如,假设我想执行与前面示例相同的操作,但是文件应该从 <name>.txt重命名/移动到 /foo/<name>.bar.txt(而不是 /foo/<name>.txt.bar)。

更新 : 我设法找到了一个解决方案:

find . -name "*.txt" | xargs -I{} \
sh -c 'base=$(basename $1) ; name=${base%.*} ; ext=${base##*.} ; \
mv "$1" "foo/${name}.bar.${ext}"' -- {}

但我想知道是否有一个更短/更好的解决方案。

76378 次浏览

If you're allowed to use something other than bash/sh, AND this is just for a fancy "mv"... you might try the venerable "rename.pl" script. I use it on Linux and cygwin on windows all the time.

http://people.sc.fsu.edu/~jburkardt/pl_src/rename/rename.html

rename.pl 's/^(.*?)\.(.*)$/\1-new_stuff_here.\2/' list_of_files_or_glob

You can also use a "-p" parameter to rename.pl to have it tell you what it WOULD HAVE DONE, without actually doing it.

I just tried the following in my c:/bin (cygwin/windows environment). I used the "-p" so it spit out what it would have done. This example just splits the base and extension, and adds a string in between them.

perl c:/bin/rename.pl -p 's/^(.*?)\.(.*)$/\1-new_stuff_here.\2/' *.bat


rename "here.bat" => "here-new_stuff_here.bat"
rename "htmldecode.bat" => "htmldecode-new_stuff_here.bat"
rename "htmlencode.bat" => "htmlencode-new_stuff_here.bat"
rename "sdiff.bat" => "sdiff-new_stuff_here.bat"
rename "widvars.bat" => "widvars-new_stuff_here.bat"

In cases like this, a while loop would be more readable:

find . -name "*.txt" | while IFS= read -r pathname; do
base=$(basename "$pathname"); name=${base%.*}; ext=${base##*.}
mv "$pathname" "foo/${name}.bar.${ext}"
done

Note that you may find files with the same name in different subdirectories. Are you OK with duplicates being over-written by mv?

If you have GNU Parallel http://www.gnu.org/software/parallel/ installed you can do this:

find . -name "*.txt" | parallel 'ext={/} ; mv -- {} foo/{/.}.bar."${ext##*.}"'

Watch the intro videos for GNU Parallel to learn more: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

The following command constructs the move command with xargs, replaces the second occurrence of '.' with '.bar.', then executes the commands with bash, working on mac OSX.

ls *.txt | xargs -I {} echo mv {} foo/{} | sed 's/\./.bar./2' | bash

It is possible to do this in one pass (tested in GNU) avoiding the use of the temporary variable assignments

find . -name "*.txt" | xargs -I{} sh -c 'mv "$1" "foo/$(basename ${1%.*}).new.${1##*.}"' -- {}

Inspired by an answer by @justaname above, this command which incorporates Perl one-liner will do it:

find ./ -name \*.txt | perl -p -e 's/^(.*\/(.*)\.txt)$/mv $1 .\/foo\/$2.bar.txt/' | bash

the files should be renamed/moved from <name>.txt to /foo/<name>.bar.txt

You can use rename utility, e.g.:

rename s/\.txt$/\.txt\.bar/g *.txt

Hint: The subsitution syntax is similar to sed or vim.

Then move the files to some target directory by using mv:

mkdir /some/path
mv *.bar /some/path

To do rename files into subdirectories based on some part of their name, check for:

-p/--mkpath/--make-dirs Create any non-existent directories in the target path.


Testing:

$ touch {1..5}.txt
$ rename --dry-run "s/.txt$/.txt.bar/g" *.txt
'1.txt' would be renamed to '1.txt.bar'
'2.txt' would be renamed to '2.txt.bar'
'3.txt' would be renamed to '3.txt.bar'
'4.txt' would be renamed to '4.txt.bar'
'5.txt' would be renamed to '5.txt.bar'

Adding on that the wikipedia article is surprisingly informative

for example:

Shell trick

Another way to achieve a similar effect is to use a shell as the launched command, and deal with the complexity in that shell, for example:

$ mkdir ~/backups
$ find /path -type f -name '*~' -print0 | xargs -0 bash -c 'for filename; do cp -a "$filename" ~/backups; done' bash