Rihad
Rihad

Reputation: 103

bourne shell whitespace intricacies

$ freebsd-version 
10.3-RELEASE-p17
$

portinstall() {
    port="$1"; shift
    env="$@"

    #1
    env "$@" printenv | grep -E '^WITH(OUT)?='
    #2
    env "$env" printenv | grep -E '^WITH(OUT)?='
    #3
    env "$*" printenv | grep -E '^WITH(OUT)?='
    #4
    env $@ printenv | grep -E '^WITH(OUT)?='
    #5
    env $* printenv | grep -E '^WITH(OUT)?='

}


portinstall foo/bar WITH='baz xyzzy' WITHOUT='quux'

Only #1 works, its output:

 WITHOUT=quux
 WITH=baz xyzzy

With #2,#3 (WITH is just one variable "baz xyzzy WITHOUT=quux"):

  WITH=baz xyzzy WITHOUT=quux

With #4,#5

 env: xyzzy: No such file or directory

My main question is why #1 works but the seemingly equivalent #2 doesn't?

Upvotes: 3

Views: 62

Answers (1)

Jonathan Leffler
Jonathan Leffler

Reputation: 754570

Using "$@" is most often correct, but maybe this analysis will help you understand why.

Your code is approximately:

env="$@"

#1
env "$@" printenv | grep -E '^WITH(OUT)?='
#2
env "$env" printenv | grep -E '^WITH(OUT)?='
#3
env "$*" printenv | grep -E '^WITH(OUT)?='
#4
env $@ printenv | grep -E '^WITH(OUT)?='
#5
env $* printenv | grep -E '^WITH(OUT)?='

and you're invoking the script with arguments WITH='baz xyzzy' WITHOUT='quux'. Yes, you've got a function and a spare argument as $1 adding to the complexity, but they're mostly irrelevant; this is the core.

In general, $* maps to the words in the arguments separated by spaces; $@ does the same. When enclosed in double quotes, they behave differently: "$@" expands to the set of arguments preserving internal spaces, while "$*" maps to a single string with internal spaces preserved and a single space between arguments.

However, in the context of env="$@", the assignment behaves like env="$*" — you end up with a single string in the variable. To preserve the separate arguments, in Bash, you'd use an array:

env=("$@")

and you could print them using:

printf '%s\n' "${env[@]}"

However, that's off at a tangent.

With #1, you end with an invocation of env with two valid, space-preserving assignments, which printenv duly prints and grep filters, giving you the two variable values. This is why "$@" is usually correct.

With #2 and #3, you end up with an invocation of env with a single string that starts WITH= (so it assigns to the variable WITH, and the remainder of the string is baz xyzzy WITHOUT=quux, which is why you see that as the output. There's a single environment variable — WITH — but its value contains spaces and an assignment.

With #4 and #5, you've run:

env WITH=baz xyzzy WITHOUT=quux printenv

Since xyzzy is not an assignment, it is treated as a command to be invoked with arguments WITHOUT=quux and printenv (and with WITH=baz added to the environment), but you don't have a program called xyzzy on your PATH, so env gives you the error. You could create a program xyzzy to show that's what's happening.

With the array env, you could run:

env "${env[@]}" printenv | …

and you'd get the same result as #1.

Upvotes: 2

Related Questions