2

I have a script that parses command line arguments.

The intro to the loop to iterate over the argument array looks like this:

for arg in ${args[@]+"${args[@]}"}; do

Can someone explain this syntax? How does it work? Why would args[@] be used twice? I've been unable to locate anything online to explain [@]+

2
  • It even works with for arg in ${randomArray[@]+"${args[@]}"}; do where randomArray is an array containing at least one element. Commented yesterday
  • 1
    Useful link: gnu.org/software/bash/manual/html_node/… Commented yesterday

3 Answers 3

5

Your script contains a parameter expansion (${args[@]+"${args[@]}"}) that fits into the following definition:

${parameter:+word}

If parameter is null or unset, nothing is substituted, otherwise the expansion of word is substituted. The value of parameter is not used.

The + operator is much like the following JavaScript:

parameter == null ? null : word

The colon can be omitted:

[If] the colon is included, the operator tests for both parameter’s existence and that its value is not null; if the colon is omitted, the operator tests only for existence.

parameter is args[@], which is obviously not null or unset as it contains the entire array. This means that the following "${args[@]}" shall be substituted. This is, however, redundant as no matter what args[@] is, null, unset, or not, it will be substituted with that array or have nothing, always corresponding with the args array.

Therefore, the script is unnecessarily complicated and can be reduced to:

for arg in "${args[@]}"; do

Quotes need to be kept to prevent re-splitting the elements in the array.

3
  • 2
    Also, + doesn't correspond to ??, that would be -. Plus is the Alternate Value, i.e. it's used when the value is not nullish. Commented yesterday
  • It might be worth explicitly mentioning that omitting the colon in ${parameter:+word} changes the meaning. Now this sounds like it's saying it's just cosmetic, though the quote does explain the difference. (Consider bash -c 'var= ; echo ${var:+a} ${var+b}'). Commented 22 hours ago
  • also, in general, the point of ${foo+"$foo"} is to only produce a string if foo is set, but to produce nothing at all if it isn't. Plain "$foo" would produce an empty string even if foo was unset. Now, with arrays (and $@), just double-quoting the array expansion usually works right and produces nothing for an empty array but there are corner cases. Or it might be related to how shells of yore handled $@, where you had to use guards like that to avoid an empty string if there were no command-line arguments. Commented 22 hours ago
4

The [@] is an array subscript meaning "all the elements", the + means "Use Alternate Value".

In man bash, these are explained under "Arrays" and "Parameter Expansion", respectively. The + is hard to find, though, as it is shown as :+, but the colon can be omitted as noted in the same section:

When not performing substring expansion, using the forms documented below (e.g., :-), bash tests for a parameter that is unset or null. Omitting the colon results in a test only for a parameter that is unset.

In recent bash versions, this is not needed and you can use for arg in "${args[@]}" directly.

1
  • 1
    Good answer! I think this has been used because (according to the script notes), it's written for bash 3.2 compatibility only. Commented yesterday
3

TL;DR

${array[@]+"${array[@]}"} is what you need in some versions of some Korn-like shells, including bash-4.3 and older to avoid nounset (as enabled by set -o nounset or set -u) triggering when the array is empty.

It's useful to go back to the origins to understand where that comes from and why it may be needed.

In the very early 70s, Unix was born more as a toy project (though already revolutionary) and came with a rudimentary shell later referred to as the Thompson shell.

It had no variable but had positional parameters $1 to $9 but no way to refer to them as a whole other than $1 $2 $3 $4 $5 $6 $7 $8 $9.

The PWB shell aka Mashey shell from the mid 70s added $* to expand to all positional parameters separated by spaces (and a few single letter variables some of which with a special meaning).

Up to that point it was still rudimentary in that those parameters were expanded literally and the result again parsed again as shell code, similar to what would happen with variable expansion much later in MS-DOS' COMMAND.COM or I'm told still happens (!) in Microsoft Windows' CMD.EXE. For instance, with a script invoked at script 'foo' ';uname' that contains echo $1 $2 or echo $*, that would run echo foo and the uname command.

In the late 70s, the Bourne shell was written from scratch and first released in Unix version 7.

It kept some level of backward compatibility with its predecessors, including $1 to $9 positional parameters and $* to expand to all. It also introduced proper (scalar only) variable support.

Thankfully it fixed that design mentioned above in that the result of expansions was not re-interpreted, but it was still subject to globbing and it introduced that infamous $IFS-splitting I guess to provide some level of backward compatibility with its predecessors.

So $* there expanded to all the positional parameters, joined with spaces, but subject to $IFS-splitting and globbing unless quoted. When quoted though as cmd "$*", the list of positional parameters would be passed as a single argument to cmd. So $* could still not be used to pass the list of positional parameters (the arguments to the script) as-is to another command.

So Steve Bourne came up with the $@ special parameter which was the same as $* except that when used inside double quotes as in cmd "$@" it would result in all the positional parameters passed as separate arguments.

Except it was still a bit buggy in that it didn't work properly if $IFS was changed from its default value and in the case of an empty list of positional parameters cmd "$@" would pass one empty argument to cmd instead of none at all.

In hindsight, a much better design (and what several shells have done since) would have been to make variables lists (as lists of arguments is what shells primarily deal with). For instance to have $argv being the list of positional parameters and cmd $argv to pass that list as-is to cmd without that split+glob nonsense. It's interesting to see how (as is sadly often the case) it's backward compatibility considerations that resulted in such an awkward design.

The Bourne shell also introduced a number of parameter expansion operators, including ${var+value} which expands to value if $var is set and nothing otherwise.

That operator helped to work around that bug mentioned above in that you could write cmd ${1+"$@"} so no argument was passed to cmd if $1 was unset (and $1 is unset iff $# == 0).

The Bourne shell could also be passed a -u option which caused parameter expansions (other than the ${parma+x}/${param-x}...) to result in an error if they were for an unset parameter (something the C shell does by default). ${1+"$@"} would also prevent that error to trigger when the -u option was enabled and there was no positional parameter.

In the early 80s, David Korn extended the Bourne shell to support some form of arrays, in a shell that would be called the Korn shell or ksh.

It was quite an unusual array design, and was more like giving alternative dimensions to variables. $var from the Bourne shell became the same as ${var[0]}, but you could add more dimensions by assigning to var[12] for instance, or assign several elements at once with set -A array or read -A array.

The $var variable was considered unset if dimension 0 was unset regardless of other dimensions. An empty array was (and would be undistinguishable from) the same as a variable with all its dimensions unset.

Those arrays were quite alien to the Bourne shell's list of positional parameters in that they were sparse and started with at index 0.

Still, ksh added ${array[*]} and ${array[@]} expanding to all the set elements of the array in the same way $* and $@ did in the Bourne shell. ksh also fixed many bugs of the Bourne shell and changed $IFS handling quite significantly.

It also gave long names to many of the Bourne shell's single letter options. -u could also be referred as -o nounset (passed to ksh or the set builtin like for Bourne's -u).

Now, with that nounset option enabled "${array[@]}" would cause an error if all the dimensions of the variables were unset. You could not do ${array+"${array[@]}"} which is the same as ${array[0]+"${array[@]}"} to avoid it because contrary to $@, ksh arrays can be sparse, that is dimension 0 be unset but other dimensions be set, hence the need for ${array[@]+"${array[@]}"} instead to avoid the effect of nounset when passing all the elements of an array to a command.

The bash shell, from the late 80s (though array support was not added until 2.0 in the mid 90s) copied the Korn shell (ksh88) array design almost verbatim.

Main difference is that assignment as a whole was with array=(...) (initially from zsh) instead of set -A (and read -A was changed to read -a), and by some aspects, contrary to ksh, array variables were considered a different type from scalar variables.

In bash, contrary to ksh88, array=() was not the equivalent of unsetting all the dimensions of the variable. From the output of declare -p array, you could see it was an array set to an empty list, not an unset variable.

Still, even after array=(), "${array[@]}" would trigger nounset and you'd need ${array[@]+"${array[@]}"} to work around it.

The purpose of nounset is to help spot coding errors when variables are used uninitialised like as a result of a typo in the variable name. Triggering in the case above is unwanted.

That was "fixed" in the case of bash in version 4.4, but one might argue that the fix is incorrect and defeats the intent of the nounset option.

With bash 4.4 and newer, ${array[@]+"${array[@]}"} is no longer necessary to work around the problem whereby that triggers nounset if $array is set to an empty list, but ${arry[@]} now fails to trigger nounset if you mistyped $array.

$ bash-3.2.57 -uc 'a=(); echo "${a[@]}"'
bash-3.2.57: a[@]: unbound variable
$ bash-5.3.3 -uc 'echo "${a[@]}"'

Possibly bash "fixed" it that way because that's what ksh93 also did in ksh93u from 2011, though it made more sense there as in ksh, empty arrays are undistinguishable from unset variables.

Other Bourne-like shells with better or less unusual array design than that of ksh such as zsh or yash (though they still recognise ${array[@]} and ${array[*]}) do it properly:

$ zsh -uc 'array=(); echo array: "${array[@]}"'
array:
$ zsh -uc 'array=(); echo array: "${arry[@]}"'
zsh:1: arry[@]: parameter not set
$ yash -uc 'array=(); echo array: "${array[@]}"'
array:
$ yash -uc 'array=(); echo array: "${arry[@]}"'
yash: parameter `arry' is not set

(no error for empty arrays, error for misspelled array)

As that's normal arrays there (non-sparse, and with index starting at 1 like for $@ and $array doing more what you'd expect) with a distinct type from scalars, if you want to avoid the effect of nounset for the expansion of the variable, you can use ${array+"${array[@]}"}, no need for ${array[@]+"${array[@]}"}.

$ yash -uc 'unset array; echo array: ${array+"${array[@]}"}'
array:
$ zsh -uc 'unset array; echo array: ${array+"${array[@]}"}'
array:

In mksh (itself derived from pdksh, a public domain clone of ksh88, but with additional support for zsh-style array=(values) assignment), ${array[@]} still triggers nounset if $array is set to an empty list. But that makes sense there as, like in the original ksh, empty arrays are indistinguishable from unset ones.

$ mksh -c 'a=(); typeset -p a'
$ mksh -c 'typeset -p a'
$ mksh -uc 'a=(); echo "${a[@]}"'
E: mksh: a[@]: parameter not set

So that's one shell where you do still need the ${array[@]+"${array[@]}"} work around to avoid the effect of nounset for empty arrays.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.