The trick is to double quote it as in "$@".
foo(){
printf ' ->%s\n' "$@";
}
foo "a b" c "d e"
is equivalent to:
printf ' ->%s\n' "a b" c "d e"
If the $@ in the above is not double-quoted, then you'll get:
printf ' ->%s\n' a b c d e
due to word splitting on $IFS characters ( $IFS defaults to ' '$'\t'$'\n', i.e. a space, a tab, and a newline )
$@ vs. $*
Double quotes work like this for any @-expansion on any array:
$ foo=( "a b" c "d e" )
$ printf ' ->%s\n' "${foo[@]}"
->a b
->c
->d e
In contrast, *-expansions (e.g., $*, ${foo[*]}) will use the first character of $IFS to join the items of an array into a single string:
$ foo=( "a b" c "d e" )
$ ( IFS=:; printf ' ->%s\n' "${foo[*]}" )
->a b:c:d e
which, if left unquoted, will again split on this very IFS character:
$ foo=( "a b" c "d e:f:g" )
$ ( IFS=:; printf ' ->%s\n' ${foo[*]} )
->a b
->c
->d e
->f
->g
Trick for iterating over $@ in for-loops:
The "$@" array is special. If you want to iterate over "$@" in a for loop, then you can abbreviate
for variable_name in "$@"; do
...
done
as
for variable_name; do
done
as skipping the in something part of a for loop implies in "$@".
This works even in POSIX-only shells too (dash, bourne shell) which don't have array variables but support "$@" and "$*.