Modifying replace string in xargs

BashXargs

Bash Problem Overview


When I am using xargs sometimes I do not need to explicitly use the replacing string:

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

In other cases, I want to specify the replacing string in order to do things like:

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

The previous command would move all the text files under the current directory into /foo and it will append the extension bar to all the files.

If instead of appending some text to the replace string, I wanted to modify that string such that I could insert some text between the name and extension of the files, how could I do that? For instance, let's say I want to do the same as in the previous example, but the files should be renamed/moved from <name>.txt to /foo/<name>.bar.txt (instead of /foo/<name>.txt.bar).

UPDATE: I manage to find a solution:

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

But I wonder if there is a shorter/better solution.

Bash Solutions


Solution 1 - Bash

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

Solution 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##*.}"' -- {}

Solution 3 - Bash

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?

Solution 4 - Bash

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

Solution 5 - Bash

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"

Solution 6 - 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'

Solution 7 - Bash

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

Solution 8 - Bash

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionbetabandidoView Question on Stackoverflow
Solution 1 - BashfairidoxView Answer on Stackoverflow
Solution 2 - BashSantrixView Answer on Stackoverflow
Solution 3 - Bashglenn jackmanView Answer on Stackoverflow
Solution 4 - BashOle TangeView Answer on Stackoverflow
Solution 5 - BashMichael CampbellView Answer on Stackoverflow
Solution 6 - BashkenorbView Answer on Stackoverflow
Solution 7 - BashMike GrafView Answer on Stackoverflow
Solution 8 - BashVasiliyView Answer on Stackoverflow