tags:

views:

1461

answers:

6

I'm looking to write a Bash one-liner that calls a function once for each item in a list. For example, given the list

foo bar baz
and the program "cowsay", it would produce:

 _____
< foo >
 -----
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
 _____
< bar >
 -----
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
 _____
< baz >
 -----
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

(Maybe with additional text in between, doesn't really matter)

I know I can do this with a bash script:

#!/bin/sh

for w in $@; do
  cowsay $w
done

But I can't imagine that there isn't another way to do it.

EDIT: I wasn't very clear in my initial question, I think. I want to be able to do something like this without writing a bash script:

locate foo | sed s/bar/baz/ | [other-processing] | [insert-magic-here] cowsay

The point is that I'm trying to avoid having to write a script, so that I can just add it to my pipe chain and not think about it.

+1  A: 

In one line:

for i in foo bar baz; do cowsay $i; done

Or more clearly:

foobar="foo bar baz"

for i in $foobar
do
    cowsay $i
done
Kimmo Puputti
Well, yes. But is there a way to do this without the for loop? I guess that's the real question.
perimosocordiae
A: 

You could write a function that turns this into a liner:

function foreach
{
    func=$1; shift
    for arg in $@; do
        ${func} ${arg}
    done
}

And then invoke it like:

foreach cosway foo bar baz
R Samuel Klatchko
This comes close, but still requires writing a script. Is there a built-in way to do this?
perimosocordiae
A: 

If you are not interested in using a "for" loop, then your other options to iterate over a list of strings is to use a "while" or "until" loop.

Is it the case that you are wanting to write a bash script that can be executed as follows?

$ cowsay-loop foo bar buzz

jkndrkn
Sorry, I wasn't clear (again). It's not the for loop that I'm trying to avoid, but the script itself. I've edited my question to hopefully make more sense.
perimosocordiae
+9  A: 

Sounds like you want to use xargs then.

$ echo foo bar | xargs -n 1 cowsay
 _____
< foo >
 -----
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
 _____
< bar >
 -----
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
Nicholas Riley
+4  A: 

You want xargs. Without a for, while, or until loop structure, xargs is about the only thing that will do what you ask.

Use -n1 if you need xargs to execute your command for each input, instead of executing with many inputs as separate arguments. Your example becomes:

$ locate foo | sed s/bar/baz/ | [other-processing] | xargs -n1 cowsay
quack quixote
Aha! I knew about xargs, but not the -n flag. Awesome, thanks.
perimosocordiae
+1  A: 

As other's have said, xargs(1) is what you want, but it is not always suitable. Most often when it has failed for me, it was when I wanted to run a shell function. xargs runs an executable command.

You can put a loop in your pipeline without needing to put it in a shell script:

$ locate foo | sed s/bar/baz/ | [other-processing] | while read line ; do cowsay "$line" ; done

If cowsay was a shell function, this pipeline would work, but not with xargs.

camh
Interesting. I don't see myself needing to use this with shell functions often, but that's good to know. Hopefully it'll prevent some hair-pulling and Googling down the road.
perimosocordiae