adelphus
adelphus

Reputation: 10326

Can xargs execute a subshell command for each argument?

I have a command which is attempting to generate UUIDs for files:

find -printf "%P\n"|sort|xargs -L 1 echo $(uuid)

But in the result, xargs is only executing the $(uuid) subshell once:

8aa9e7cc-d3b2-11e4-83a6-1ff1acc22a7e file1
8aa9e7cc-d3b2-11e4-83a6-1ff1acc22a7e file2
8aa9e7cc-d3b2-11e4-83a6-1ff1acc22a7e file3

Is there a one-liner (i.e not a function) to get xargs to execute a subshell command on each input?

Upvotes: 22

Views: 21274

Answers (3)

hek2mgl
hek2mgl

Reputation: 158010

This is because the $(uuid) gets expanded in the current shell. You could explicitly call a shell:

find -printf "%P\n"| sort | xargs -I '{}' bash -c 'echo $(uuid) {}'

Btw, I would use the following command:

find -exec bash -c 'echo "$(uuid) ${1#./}"' -- '{}' \;

without xargs.

Upvotes: 25

mklement0
mklement0

Reputation: 438008

hek2mgl's answer explains the problem well and his solution works well; this answer looks at performance.

The accepted answer is a tad slow, because it creates a bash process for every input line.

While xargs is generally preferable to and faster than a shell-code loop, in this particular case the roles are reversed, because shell functionality is needed in each iteration.

The following alternative solution uses a while loop to process the input lines, and, on my machine, is about twice as fast as the xargs solution.

find . -printf "%P\n" | sort | while IFS= read -r f; do echo "$(uuid) $f"; done

If you're concerned about filenames with embedded newlines (very rare) and use GNU utilities, you could use NUL bytes as separators:

find . -printf "%P\0" | sort -z | while IFS= read -d '' -r f; do echo "$(uuid) $f"; done

Update: The fastest approach is to not use a shell loop at all, as evidenced by ᴳᵁᴵᴰᴼ's clever answer. See below for a portable version of his answer.


Compatibility note:

The OP's find command implies the use of GNU find (Linux), and uses features (-printf) that may not work on other platforms.

Here's a portable version of ᴳᵁᴵᴰᴼ's answer that uses only POSIX-compliant features of find (and awk).
Note, however, that uuid is not a POSIX utility; since Linux and BSD-like systems (including OSX) have a uuidgen utility, the command uses that instead:

 find . -exec printf '%s\t' {} \; -exec uuidgen \; | 
   awk -F '\t' '{ sub(/.+\//,"", $1); print $2, $1 }' | sort -k2

Upvotes: 6

guido
guido

Reputation: 19194

With a for loop:

for i in $(find -printf "%P\n" | sort) ; do echo "$(uuid) $i";  done

Edit: another way to do this:

find -printf "%P\0" -exec uuid -v 4 \; | sort | awk -F'\0' '{ print $2 " " $1}'

this outputs the filename followed by the uuid (no subshell required) for letting the sort to happen, then swaps the two columns separated by null.

Upvotes: 4

Related Questions