271

I am trying to use xargs to call a more complex function in parallel.

#!/bin/bash
echo_var(){
    echo $1
    return 0
}
seq -f "n%04g" 1 100 |xargs -n 1 -P 10 -i echo_var {} 
exit 0

This returns the error

xargs: echo_var: No such file or directory

Any ideas on how I can use xargs to accomplish this, or any other solution(s) would be welcome.

6
  • 3
    Danger, user1148366, Danger! Don't use bash for parallel programming- you will run into so many problems. Use C/C++ and pthreads, or Java threads, or anything that makes you think long and hard about what you're doing, because parallel programming takes a lot of thought to get right. Commented Jun 12, 2012 at 19:44
  • 42
    @DavidSouther If the tasks are independent, such as convert all these picture files to png, then don't worry. It is when you have synchronisation (beyond wait for all to finish) and communication that it gets messy. Commented Feb 26, 2014 at 15:41
  • 2
    @DavidSouther - I am a long time Java dev and I have been working in groovy of late. And I continue to tell people: Friends don't let friends write bash script. And yet, I find myself looking at this post/solution because (sad face :( ) I am engaged in parallel processing in bash. I could readily do it in groovy/java. Bad! Commented Feb 8, 2019 at 0:50
  • Also discussed in unix.stackexchange.com/questions/158564/… Commented Feb 28, 2019 at 17:38
  • 1
    @DavidSouther would love to hear some specific examples because bash is probably the only popular language that has sane parallel computation handling. Pipelines are so intuitive, people using them don't even know they're writing a parallel program. Meanwhile I couldn't imagine a worse language than C in which to do it.
    – hraban
    Commented Mar 24 at 14:33

6 Answers 6

255

Exporting the function should do it (untested):

export -f echo_var
seq -f "n%04g" 1 100 | xargs -n 1 -P 10 -I {} bash -c 'echo_var "$@"' _ {}

You can use the builtin printf instead of the external seq:

printf "n%04g\n" {1..100} | xargs -n 1 -P 10 -I {} bash -c 'echo_var "$@"' _ {}

Also, using return 0 and exit 0 like that masks any error value that might be produced by the command preceding it. Also, if there's no error, it's the default and thus somewhat redundant.

@phobic mentions that the Bash command could be simplified to

bash -c 'echo_var "{}"'

moving the {} directly inside it. But it's vulnerable to command injection as pointed out by @Sasha.

Here is an example why you should not use the embedded format:

$ echo '$(date)' | xargs -I {} bash -c 'echo_var "{}"'
Sun Aug 18 11:56:45 CDT 2019

Another example of why not:

echo '\"; date\"' | xargs -I {} bash -c 'echo_var "{}"'

This is what is output using the safe format:

$ echo '$(date)' | xargs -I {} bash -c 'echo_var "$@"' _ {}
$(date)

This is comparable to using parameterized SQL queries to avoid injection.

I'm using date in a command substitution or in escaped quotes here instead of the rm command used in Sasha's comment since it's non-destructive.

14
  • 18
    A bit more discussion: xargs executes a completely new instance of the process named. In this case, you provide the name echo_var, which is a function in this script, not a process (program) in your PATH. What Dennis' solution does is export the function for child bash processes to use, then forks to the subprocess and executes there. Commented Jun 12, 2012 at 19:39
  • 10
    what is the significance of _ and \ , without them it wasn't working for me
    – Hashbrown
    Commented Oct 19, 2013 at 8:16
  • 14
    @Hashbrown: The underscore (_) provides a place holder for argv[0] ($0) and almost anything could be used there. I think I added the backslash-semicolon (\;) because of its use in terminating the -exec clause in find, but it works for me without it here. In fact, if the function were to use $@ instead of $1 then it would see the semicolon as a parameter, so it should be omitted. Commented Oct 19, 2013 at 11:35
  • 4
    -i argument to xargs is since been deprecated. Use -I (capital i) instead.
    – Nicolai S
    Commented Jul 7, 2015 at 20:44
  • 13
    You can simplify this by including the argument from xargs in the command string for bash with bash -c 'echo_var "{}"'. So you do not need the _ {} at the end.
    – phobic
    Commented Jun 2, 2016 at 8:57
18

Something like this should work also:

function testing() { sleep $1 ; }
echo {1..10} | xargs -n 1 | xargs -I@ -P4 bash -c "$(declare -f testing) ; testing @ ; echo @ "
1
  • 1
    In general, this would break on shell special characters (e.g. |, #) and ignore white space in input. Instead of letting bash treat the input as code, I suggest letting xargs pass them as-is. echo {1..10} | xargs -n 1 -P4 bash -c "$(declare -f testing);"' testing "$@"; echo "$@";' argv0 Commented Oct 20, 2021 at 16:18
4

Seems I can't make comments :-(

I was wondering about the focus on

bash -c 'echo_var "$@"' _ {}
vs
bash -c 'echo_var "{}"'

The 1st substitutes the {} as an arg to bash while the 2nd as an arg to the function. The fact that example 1 doesn't expand the $(date) is simply a a side effect.

If you don't want the functions args expanded , use single single quotes rather than double. To avoid messy nesting , use double quote (expand args on the other one)

$ echo '$(date)' | xargs -0 -L1 -I {} bash -c 'printit "{}"'
Fri 11 Sep 17:02:24 BST 2020

$ echo '$(date)' | xargs -0 -L1 -I {} bash -c "printit '{}'"
$(date)
3

Maybe this is bad practice, but you if you are defining functions in a .bashrc or other script, you can wrap the file or at least the function definitions with a setting of allexport:

set -o allexport

function funcy_town {
  echo 'this is a function'
}
function func_rock {
  echo 'this is a function, but different'
}
function cyber_func {
  echo 'this function does important things'
}
function the_man_from_funcle {
  echo 'not gonna lie'
}
function funcle_wiggly {
  echo 'at this point I\'m doing it for the funny names'
}
function extreme_function {
  echo 'goodbye'
}

set +o allexport
2

@DennisWilliamson 's answer fails to use the -n 1 argument correctly and therefore fails when the input is a sequence on one line, e.g.:

DennisWilliamson's Method:

export -f echo_var 
echo 1 2 3 | xargs -n 1 -P 10 -I {} bash -c 'echo_var "$@"' _ {}

Output (Incorrect, ignores -n 1):

1 2 3

But I found a robust and simpler method which works with -n 1.

My Simpler/Robust Method (drops -I {} & trailing {}):

export -f echo_var;
echo 1 2 3 | xargs -n 1 -P 10 bash -c 'echo_var "$@"' _

Output (Correct):

1
2
3
1

You can create your own version of xargs that can handle functions.

usage example:

function my_echo() {
    echo "$@"
}
echo 1 2 3 | xargs my_echo
# xargs: my_echo: No such file or directory
echo 1 2 3 | xargs2 my_echo
# 1 2 3

xargs2 itself:

is_function() {
  if [[ $(type -t "${1?ensure_is_a_function: provide a command}") == "function" ]]; then
    return 0
  else
    return 1
  fi
}

function xargs2 {
  if is_function "$1"; then
    export -f "${1?}" # so subprocess bash can see it
  else
    echo "xargs2: use xargs" >&2
    return 1
  fi
  ARGS=$(printf "%q " "$@") # escape
  cat </dev/stdin | xargs bash -c "$ARGS \$@" _
}

Not the answer you're looking for? Browse other questions tagged or ask your own question.