0

I have created the following two minimal shell scripts...

test1

#!/usr/bin/env bash letterList=$1 for letter in $letterList do echo $letter done 

and a second

test2

#!/usr/bin/env bash letterList=$1 test1 $letterList 

When I execute test1 'A B C' I get the desired output, A, B and then C.

But when I execute test2 'A B C' I expect test1 'A B C' to happen, but the actual output is only A.

So it appears as though the nested variable list is not somehow being read – in other words, what is being executed is test1 A.

What's going on and how can I achieve the desired result?

P.S.: I'm using OS X, if that's relevant.

2
  • In test2 replace last line with ./test1 "$letterlist"
    – kaylum
    CommentedJan 12, 2020 at 10:40
  • @kaylum Many thanks! The addition of " " does the trick. The addition of `.` threw me an error. I assume that it is not necessary since without it it works.
    – Geoff
    CommentedJan 12, 2020 at 10:46

3 Answers 3

3

You have no list variable in your code. You have a string.

The call to test1 uses the expansion $letterList unquoted. This will make the shell split its value up into multiple words on spaces, tabs and newline characters (by default), and each word will undergo filename globbing (if they contain filename globbing characters). The resulting words are passed to test1 as separate arguments.

In test1, you only put the very first of these arguments into letterList.

If you want to pass the string in $letterList as it is to test1 from test2, you need to double quote its expansion in the call:

test1 "$letterList" 

Incidentally, you rely on the shell performing the splitting of the $letterList value in your loop in test1 (the same mechanics that cause your issue), but you never protect the code from accidentally performing filename globbing (test your script with e.g. test2 "A B C *" to see what I mean).


If you want to pass a list rather than a single string, then call test2 with a list rather than with a string:

test2 A B C "Hello World" "* *" 

Then, in test2, pass the arguments to test1:

#!/bin/sh test1 "$@" 

Here, "$@" will expand to the list of positional parameters (the arguments to the script), with each individual parameter being quoted. This means that an argument like Hello World will not be split into multiple arguments in the call to test1, and the string * * will also not get split up into two and no filename globbing will occur.

In test1, iterate over the arguments:

#!/bin/sh for arg in "$@"; do printf '%s\n' "$arg" done 

or

#!/bin/sh for arg do printf '%s\n' "$arg" done 

... or, if you just want to print the arguments, just

#!/bin/sh printf '%s\n' "$@" 

I've used /bin/sh as the shell throughout as there is nothing here that is specific to bash.

    1

    When ./test2 'a b c' is run we get:

    $1=a b c 

    So the last line becomes:

    test1 a b c 

    And when test1 runs we get:

    $1=a $2=b $3=c 

    That is, the args give to test1 are not enclosed in quotes and hence they become three args and not one.

    To pass test1 a single arg just enclose the variable in double quotes. That is, replace the last line in test2 with:

    ./test1 "$letterlist" 

    Note, I have also added ./ to give the path of the executable as it is not good practice to have . in $PATH.

    1
    • It is possible that the OP has placed test1 and test2 in /usr/bin, /usr/local/bin, $HOME/bin, or some other directory that is in his PATH. In that case, prepending ./ is not only not constructive, but rather, harmful (as the OP reports). Of course I agree that . should not be in $PATH.CommentedJan 26, 2020 at 20:26
    0

    It’s not clear what your actual objective is.  The only thing you have said clearly is that you want test2 'A B C' to output A, B and C on three separate lines.

    Here is a different solution that will give you that result.  In general, it seems to give the same output as kaylum’s answer.

    • Keep test2 the same.
    • Change test1 as follows:

      #!/bin/sh for letter in $* do printf '%s\n' "$letter" done 

      This, naturally, changes the behavior of test1.  It now looks at all its arguments, not just $1.  So test1 'A B C' 'D E F' will output A, B, C, D, E and F on six separate lines (instead of just A, B and C).

    We normally discourage the use of $* because it breaks strings that contain spaces into separate words.  For example, "lazy dog" would become two words (arguments).  But that seems to be exactly the behavior you want here.

    printf is slightly safer than echo in the case that a filename begins with - or contains \.  That’s been discussed at length on this site; if you want more information, just search for it.

      You must log in to answer this question.

      Start asking to get answers

      Find the answer to your question by asking.

      Ask question

      Explore related questions

      See similar questions with these tags.