Difference in assignment of x=( $@ ) and x="$@"

#!/bin/bash

if [ $# -gt 0 ]; then
    snum=( $@ ) 
    echo $snum
fi

When I run the script like ./testscript.sh 1234 4568
The output of echo command is only 1234, so I guess I am not building an array of all positional arguments?

#!/bin/bash

if [ $# -gt 0 ]; then
    snum="$@" 
    echo $snum
fi

and run ./testscript.sh 1234 4568
The output is 1234 4568

I am wondering why snum=( $@ ) is only taking the first positional argument?

Asked By: Cruise5

||
var=( values )

Is an array variable assignment

var=value

is a scalar variable assignment (and with var="$@", var is assigned the concatenation with space of the positional parameters as one string since its a scalar)

$var on an array expands to the element of index 0, same as ${var[0]}, not all the elements of the array for which you need ${var[@]}, a misdesign copied from the Korn shell.

Also note:

  • leaving parameter expansions unquoted in list context is the split+glob operator, you almost never want to do that. Ironically, the only place you put quotes above is where they were not needed.
  • echo can’t be used to output arbitrary data in bash.

Here, you want:

#! /bin/bash -
print_space_separated() {
  local IFS=' '
  printf '%sn' "$*"
}

if [ "$#" -gt 0 ]; then
  snum=( "$@" )
  print_space_separated "${snum[@]}"
fi

In zsh whose array design is closer to that of csh than of ksh, you could have done:

#! /bin/zsh -
if (( $# > 0 )) {
  snum=( $argv )
  print -r -- $snum
}

(the print -r itself is from ksh)

But with the caveat that even though there’s no split+glob upon parameter expansion in that shell, leaving array expansions unquoted like that still removes empty elements. To preserve them, you need the ksh syntax:

#! /bin/zsh -
if (( $# > 0 )) {
  snum=( "$@" )
  print -r -- "${snum[@]}"
}

(though the "${snum[@]}" can be shortened to "$snum[@]").

Note that in var="$@" or var="${array[@]}", var ends up containing a string made up of the positional parameters joined with the first character of $IFS in zsh and joined with spaces with bash. POSIX leaves the behaviour unspecified there, $@ is only meant to be used quoted and in list contexts. To get the positional parameters joined with the first character of $IFS portably, use "$*" (in either list or non-list context).

To join elements of an array with arbitrary strings (as opposed to the first character of $IFS), use the j parameter expansion flag in zsh: ${(j[that-string])array}. bash has no equivalent. fish has string join -- that-string $array.

Answered By: Stéphane Chazelas

Use "$@" (incl. double quotes) in a list context to get a list of positional parameters, individually quoted.

Use "$*" (incl. double quotes) in a scalar context to get the positional parameters concatenated into a single string with the first character of $IFS (usually a space) as the delimiter.

Using $@ unquoted (as in your first example) or using "$@" in a scalar context (as in your second example) rarely makes sense. In the bash shell, using "$@" in a scalar context is the same as using "$*" with the first character of $IFS set to a space.

When using snum=( "$@" ), you create the array snum. If you access the variable as $snum, you will get the first element of the array. It is, in effect, the same as accessing ${snum[0]}. Using "${snum[@]}" gives you a list of the individually quoted elements, in a similar manner as "$@" does. Using "${snum[*]}" gives you the equivalent of "$*", but for the array snum.

Assuming you want to create an array, snum, from the list of positional parameters and then print that array if it’s not empty, you may use

#!/bin/bash

snum=( "$@" )

if [ "${#snum[@]}" -gt 0 ]; then
    printf '%sn' "${snum[@]}"
fi

This prints the elements of snum on separate lines if the script was given arguments.

Example run:

bash-5.1$ ./script 1 2 3 "hello world" 4
1
2
3
hello world
4

Notice that the hello world argument is kept as a single argument, which would not be the case had you forgotten the quotes around $@.

To print the list of positional parameters at a single string, delimited by colons. The colons are inserted between the positional parameters by modifying the $IFS string.

#!/bin/bash

IFS=:
snum="$*"

if [ -n "$snum" ]; then
    printf '%sn' "$snum"
fi

The difference here is that snum is now a single string, not an array of elements. The script outputs the string if it is non-empty, i.e., if the script was given at least one non-empty argument.

Or, modifying our first example only slightly to keep using snum as an array,

#!/bin/bash

snum=( "$@" )

if [ "${#snum[@]}" -gt 0 ]; then
    IFS=:
    printf '%sn' "${snum[*]}"
fi

Example run:

bash-5.1$ ./script 1 2 3 "hello world" 4
1:2:3:hello world:4
Answered By: Kusalananda
Categories: Answers Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.