2

I was trying to create a function that loops over inputs and executes a command - regardless of how they are delimited.

function loop {
    # Args
    #     1: Command
    #     2: Inputs
    for input in "$2" ; do                     
        $1 $input
    done
}
declare -a arr=("1" "2" "3")

$ loop echo "$arr[@]"
1

$ loop echo 1 2 3
1

$ loop echo $arr
1

However as per this answer, for .. in .. works for arrays:

for item in "${arr[@]}" ; do
   echo "$item"
done

It also works for space separated values:

for item in 1 2 3 ; do
   echo "$item"
done

In a nutshell, how do I get the effect of "${arr[@]}" and 1 2 3 while passing it an argument.

Also would it be possible to extend this notion of looping to any kind of delimited items for example \n separated contents like a file? In Python we have a concept of iterators, is there something similar in bash?

  • There is no iteration or looping tag unix.stackexchange.com; should I move this to Stackoverflow? – Nishant Jun 22 '19 at 17:05
  • Your example showing loop echo $arr is not how it actually works, or if it does work that way for you, you have a bug in your system. Similarly loop echo "$arr[@]" is also not how it works – jesse_b Jun 22 '19 at 17:18
  • Sorry that was loop echo "$arr" from zsh, corrected. – Nishant Jun 22 '19 at 17:19
  • 3
    Are you trying to re-invent xargs? – Kusalananda Jun 22 '19 at 17:43
  • @Kusalananda, thanks. Here is from the official docs: xargs reads items from the standard input, delimited by blanks (which can be protected with double or single quotes or a backslash) or newlines, and executes the command (default is /bin/echo) one or more times with any initial-arguments followed by items read from standard input. Blank lines on the standard input are ignored. – Nishant Jun 23 '19 at 7:21
4

You have not called the array properly. $arr will only expand to the first element in the array and $arr[@] will expand to the first element with the literal string [@] appended to it.

To call all elements of an array use: "${arr[@]}"

The other issue you have is that $2 only contains the second positional parameter, where you are trying to iterate through the 3rd, 4th, 5th, etc. They will all be stored in $@.

To accomplish your goal you could do something like:

function loop {
    local command=$1
    shift
    for i in "$@"; do
        "$command" "$i"
    done
}

This will set command to the first positional parameter and then shift so that $@ can be used to loop through the remaining ones. Then you just need to call the array properly:

$ declare -a arr=("element1" "element2" "element3")
$ loop echo "${arr[@]}"
element1
element2
element3
$ loop printf 'hello ' 'world\n'
hello world
$ loop touch file1 file2 file3
$ ls
file1  file2  file3

If you want this function to be able to accept various delimiters you could do something like:

function loop {
    local command=$1
    local delim=$2
    shift 2
    set -- $(tr "$delim" ' ' <<<"$@")
    for i in "$@"; do
        "$command" "$i"
    done
}

This means you have to specify what delimiter is being used via the second parameter though, like:

$ loop echo '|' 'one|two|three'
one
two
three
$ loop echo '\n' "$(printf '%s\n' 'one' 'two' 'three')"
one
two
three

However this has some bugs (If you specify a custom delimiter it will still also delimit by whitespace)

  • So it now works items separated by space or array but not for \n separated items right? – Nishant Jun 22 '19 at 17:26
  • How would you provide multiple arguments separated by newlines as command line arguments? – jesse_b Jun 22 '19 at 17:28
  • Oh, I wasn't thinking about interactive use. My first program does understand newlines magically? I want to write one for loop in my program for all data-structures. For example I have a \n separated commits that I want to cherry-pick? – Nishant Jun 22 '19 at 17:29
  • 1
    @Nishant you do say "My current code works for a string separated by \n character i.e newline character but not for an array", but you haven't provided an example of it working. – muru Jun 22 '19 at 17:31
  • 1
    Fun fact, in "$@" is implied. You can leave it off. – wjandrea Jun 23 '19 at 1:19
1

You have a syntax error in your code. In the call to your loop function, use "${arr[@]}" to expand the arr array to the list of each of its elements, individually quoted. This is what you do in the for loop that you show, and this is also what you should do when calling the function:

loop echo "${arr[@]}"

Note also that your function needs to pick out the name of the command that you pass, and that it then needs to loop over its remaining arguments:

loop () {
    local cmd=$1; shift
    for arg do
        "$cmd" "$arg"
    done
}

Here, we assign the first argument to the variable cmd (this is the command), then we shift this argument off of the list of arguments. The list of arguments now contains only the strings that you want to loop over.

The loop then calls the command for each remaining argument in turn.

This replicates the purpose of the xargs utility in a limited way, and the function can be re-implemented using this utility (unless you expect that one of the arguments will contain an embedded newline, in which case you will have to tweak the options to xargs):

loop () {
    local cmd=$1; shift
    printf '%s\n' "$@" | xargs -L 1 "$cmd"
}

Since the xargs utility expects data to be delivered over standard input, we arrange with this using printf and print each argument given to loop on a line of its own (while telling xargs to call the given utility once for each newline-delimited argument using -L 1).

This would allow us to use other features of some implementations of xargs, like starting parallel processes using -P n where n is some number of processes to run in parallel.

Testing:

$ cat script.sh
loop () {
    local cmd=$1; shift
    printf '%s\n' "$@" | xargs -L 1 "$cmd"
}

arr1=(1 2 3)
arr2=("hello world" "home sweet home")

loop echo "${arr1[@]}"
loop echo "${arr2[@]}"
$ bash script.sh
1
2
3
hello world
home sweet home
1

Your loop is very similar to GNU Parallel:

declare -a arr=("1 a" "2 b" "3 c")
var1="1 a,2 b,3 c"
var2="1-a 2-b 3-c"
parallel -j1 echo ::: "${arr[@]}"
parallel -j1 -d , echo ::: "$var1"
parallel -j1 echo ::: $var2

-j1 forces running a single jobs at a time.

  • Thanks for the pointer, @Ole Tange. I will have a look into GNU Parallel. – Nishant Jun 26 '19 at 9:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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