5

I have a Bash function that does some string manipulation on its arguments as a whole ("$@") by putting it in a local variable, something like this:

#!/bin/sh

my_func() {
    local args="$@"
    echo "args: <$args>"
}

my_func "$@"

When I run this in Bash, args contains all of the arguments that were passed:

$ bash foo.sh foo bar baz
foo bar baz

However, if I run it in Dash, only the first argument is stored:

$ dash test.sh foo bar baz
args: <foo>

Reading the section on local in the Ubuntu Wiki's "Dash as /bin/sh" page, it seems that Dash is expanding the local args="$@" line like so:

local args=foo bar baz

and therefore only putting "foo" in args and declaring bar and baz as (local?) variables. In fact, if I add echo "bar: $bar" to my_func and run it with an = in the arguments it seems to confirm that I am adding variables:

$ foo.sh foo bar=baz
args: <foo>
bar: baz

All this to say, is there a way to get the Bash-like behaviour (of $args containing "foo bar baz") in Dash?

Harry Cutts
  • 232
  • 1
  • 9

1 Answers1

4

The expansion of $@ in local args="$@" is unspecified by the POSIX standard. The bash shell will create a single space-delimited string containing all the positional parameters as the value for the args variable, while dash will try to execute local args="$1" "$2" "$3" (etc.)

The zsh and ksh shells behave like bash (creating a single string out of the positional parameters, although zsh would use the first character of $IFS for the delimiter) while the yash shell behaves like dash, at least in their default configurations.

In your case, you should use

my_func () {
    local args
    args="$*"

    printf 'args: <%s>\n' "$args"
}

or

my_func () {
    local args="$*"

    printf 'args: <%s>\n' "$args"
}

I'm using $* here to make it obvious that I'm constructing a single string from a list of values. The string will contain the values of the positional parameters, delimited by the first character of $IFS (a space by default).

I'm also using printf to be sure to get the correct output of the user-supplied values (see Why is printf better than echo?).

Also, your script should use #!/bin/dash as the first line rather than #!/bin/sh as local is an extension to the standard sh syntax.

Kusalananda
  • 320,670
  • 36
  • 633
  • 936
  • In which versions of `dash`? `local foo=bar` works in both dash-0.5.8 and the related busybox ash. –  Jul 25 '19 at 20:28
  • @mosvy I noticed. However, the manual does not mention that it is possible explicitly, and I'm now wondering whether this is a bug related to the expansion of `"$@"`. – Kusalananda Jul 25 '19 at 20:29
  • Yep, that works, thank you! Yes, I guess it should be `#!/bin/dash`, but I hadn't put that much thought into it since it's just an example script, similarly for `echo` use. – Harry Cutts Jul 25 '19 at 20:33
  • The expansion of `"$@"` is unspecified in `foo="$@"`, according to the standard. –  Jul 25 '19 at 20:34
  • @mosvy Yes, you are correct. Better to use `"$*"` when creating a single string out of the positional parameters. – Kusalananda Jul 25 '19 at 20:35
  • It's not true that `zsh` behaves like `bash`: Try `foo(){ local IFS=/; local foo=$@; echo "{$foo}"; }; foo 1 2 3` in `zsh` and `bash`. Basically, `zsh`, `yash` and `dash` work the same, different from `bash` if not tripping on the non/badly implemented `local` feature. They will treat `foo=$@` exactly like `foo=$*`. –  Jul 26 '19 at 15:07
  • @mosvy Huh? If your code shows anything, it shows that not any shell treats the expansion as any other shell when modifying `$IFS`. What I meant was that `zsh` and `bash` both creates a single string out of all positional parameters (yes, `zsh` will use `$IFS` and `bash` does not), while the others does the expansion totally differently, leading to a potential error in `dash` and to just seeing `$1` in `yash`. – Kusalananda Jul 26 '19 at 15:25
  • I've re-read my comment and my code shows _exactly_ what I said. **All** the shells will join the positional arguments into a single string in `foo=$@`, some of them using the 1st char of `IFS` (_exactly_ as `foo=$*`), and some of them using a space. Not all shells support `local` and some support it badly, an issue that is orthogonal to this. –  Jul 26 '19 at 15:39
  • @mosvy Your code returns `{1}` for `yash`, showing that it does not create a string out of all positional parameters. Likewise, `dash` emits an error because it ends up executing `local foo=1 2 3` and declaring `2` local makes no sense to it. – Kusalananda Jul 26 '19 at 15:46
  • You're mixing up two separate issues **a)** how does `foo=$@` work in different shells and **b)** how well is `local` supported. If you want a testcase for **a**) have it here: `foo(){ IFS=/; foo=$@; echo "$foo"; }; foo 1 2 3`. If you want a testcase for **b**): `foo(){ local foo=$1; echo "$foo"; }; foo "1 2 3"`. –  Jul 26 '19 at 15:59
  • @mosvy Well, the only thing I've touched upon in the answer is the expansion of `$@`. `local` is not supported in `ksh93`, so one would simply use `typeset` instead. That is not the issue I'm addressing, and that is not the issue cause a problem for this user. – Kusalananda Jul 26 '19 at 16:02