0

How can this code be changed so as not to issue an error, in other words not execute the code after xargs if stdout is empty.

Terminal:

$ cat << EOF > dummy.sh
#! /usr/bin/env bash
[[ -f "\$1" ]] &&  echo "file=\$1" || { echo "error"; exit 1; } 
shift 
(( \$# == 0 )) || "\$0" "\$@" 
EOF
$ chmod +x dummy.sh
$ mkdir trash.later && touch trash.later/foo
$ find trash.later -type f | xargs -n 1 ./dummy.sh 
file=trash.later/foo
$ find trash.later -type f | grep 'bar' | xargs -n 1 ./dummy.sh 
error
Erwann
  • 677

1 Answers1

3

Don't use xargs on the output of find. xargs by default expects the arguments on input to be delimited in a format that no command outputs, certainly not find. The output of find -print is also not post-processable reliably.

The only way to use find with xargs sort-of-reliably is by using the -print0 and -0 non-standard extensions (initially from GNU find/xargs, but nowadays found in many other implementations).

GNU xargs also has a -r / --no-run-if-empty extension to not run the command if the input is empty (some xargs implementations such as the one on NetBSD do that by default).

So here, with GNU find/xargs/grep or compatible, you could do:

find trash.later -type f -print0 |
  grep -z bar |
  xargs -r -0 -n 1 ./dummy.sh

But that has no advantage over the standard:

find trash.later -path '*bar*' -type f -exec ./dummy.sh {} ';'

(where -path '*bar*' looks for bar anywhere in the file path like your grep bar attempted to do. Replace with -name '*bar*' to look for bar in the file name only (the last component of its path)).

More reading on that at Why is looping over find's output bad practice?


As a more general answer to your question, for xargs implementations that do not support -r / --no-run-if-empty, the work around is to use a sh wrapper.

Instead of:

... | xargs -n1 cmd

do:

... | xargs sh -c 'for file do cmd "$file"; done' sh

That still runs sh if there's no input, but since the list of arguments is empty, there will be no pass in the loop, so cmd won't be run.

See also the ifne command from moreutils which only runs the command if the input is non-empty

... | ifne xargs -n1 cmd

However note that if the input is not empty but only contains blank lines, ifne will run xargs but xargs will still consider its input has no argument and still run cmd once without argument (except on NetBSD).

For completeness

... | xargs -I {} cmd {}

will also not run cmd if the input is empty / blank. In that case, the splitting of the input into arguments is different: leading blanks are removed, quotes are still processed (in xargs's very own unique way), but otherwise arguments are whole lines instead of blank separated words.

  • "xargs sh -c 'for file [in $1;] do cmd "$file"; done' sh" I tried both way, it works. – Erwann Mar 15 '22 at 09:34
  • @Erwann, for file in $1 would be wrong. That's applying split+glob on the contents of the first positional parameter. Possibly you meant xargs -n1 sh -c '[ "$#" -eq 0 ] || cmd "$1"' sh, but it's wasting resource to run one sh per argument when sh is well able to process more than one at once. – Stéphane Chazelas Mar 15 '22 at 10:20
  • 1
    Just to reiterate, find ... | xargs sh -c 'for file do cmd "$file"; done' sh is not what you want to do, it will fail if file names contain blanks or newlines or quotes or backslashes... IMO the find ... | xargs idiom should be banned (at least when not using NUL-delimited records). – Stéphane Chazelas Mar 15 '22 at 10:25
  • "when not using NUL-delimited records" or xargs -I {} ..., right? – Erwann Mar 15 '22 at 17:11
  • 1
    @Erwann, no, as I said xargs -I {} will still not work with filenames containing quotes, backslashes or newline (or that start with blanks, though that doesn't apply to your case where file paths start with trash.later/) – Stéphane Chazelas Mar 15 '22 at 18:01