5

(NB: the motivation for this question is only to improve my bash programming know-how.)

I want to know the bash equivalent of zsh expressions such as $@[2,$#], which directly address a range of a shell function's (or script's) command-line arguments array ($@).

(All solutions I've found online for addressing ranges of $@ in bash can be described as "indirect addressing", since they all require first assigning $@ to an intermediate variable. See example below.)

For the sake of concreteness: what would be the bash-equivalent of the following zsh test function?

testfn () {
  printf '>%s<\n' $@[2,$#]
}

% testfn a b c d
>b<
>c<
>d<

Below is the closest I've managed to come up with; as mentioned above, it requires the assignment of $@ to an intermediate variable:

testfn () {
  holdargs=( $@ )
  printf '>%s<\n' "${holdargs[@]:1}"
}

All my attempts at something not requiring an intermediate variable fail with a bad substitution error.

  • 3
    Isn't that simply ${@:2}? – manatwork Aug 10 '13 at 12:07
  • testfn() { printf '>%s<\n' "${@:2}" ;}; testfn a b c d – Valentin Bajrami Aug 10 '13 at 12:20
  • 1
    Except that "${@:2}" should always be quoted to prevent word splitting and path name expansion. – Valentin Bajrami Aug 10 '13 at 12:32
5

This worked for me:

testfn () {
   printf '>%s<\n' "${@:2}"
}

Example:

$ testfn a b c d
>b<
>c<
>d<

From Bash bEginner's Guide

$@     Expands to the positional parameters, starting from one. When the 
       expansion occurs within double quotes, each parameter expands to a
       separate word.

Take a look at the Positional Parameters section of the Bash Hacker's Wiki as well as the Mass Usage section for more examples and details.

In general when you double quote forms of $@ like this, "$@" or "${@:2}" the results returned will be double quotes results.

Elements of arrays like $@ can be accessed using this notation:

"${@:START:COUNT}"

excerpt from Mass Usage section

This will expand COUNT number of positional parameters starting at START. COUNT can be omitted (${@:START}), in this case all positional parameters beginning at START are expanded.

Empty elements?

Others have stated that the above isn't equivalent because it can't handle empty elements. I'm on Bash 4.1.7 and it seems as though it does.

Examples:

$ testfn "" a "" c d e
>a<
><
>c<
>d<
>e<

$ testfn '' a '*' c d e
>a<
>*<
>c<
>d<
>e<

$ testfn "" a " " c d e
>a<
> <
>c<
>d<
>e<
  • 1
    That's not equivalent if there are empty elements. – Stéphane Chazelas Aug 10 '13 at 15:21
  • This seems to work: testfn "" a "" c d e. I think I'm not understanding the issue though. – slm Aug 10 '13 at 15:31
  • 3
    @slm: What Stephane Chazelas is saying is that in zsh's $@[2,$#], empty arguments will disappear. In Bash as in zsh, the only way to get empty arguments to disappear is to leave $@ unquoted; but in Bash unlike in zsh, that will have other effects, such as filename expansion and word splitting. If you want to disable those effects, you have to set IFS to the empty string (disabling word splitting) and run set -f (disabling filename expansion); but once you do that, although $@ will still expand to all arguments, ${@:2} will expand to a string with all arguments concatenated. – ruakh Aug 10 '13 at 17:07
  • 2
    (In other words, yes: your Bash function works, and is probably what the OP wants, and +1. Stephane Chazelas is just pointing out a case where zsh behaves differently, and is taking the OP's request for an "equivalent" rather strictly.) – ruakh Aug 10 '13 at 17:08
  • @ruakh - thanks for the details. Understood. – slm Aug 10 '13 at 17:12
3

In zsh,

printf '<%s>\n' ${@[2,$#]}

same as

printf '<%s>\n' $@[2,-1]

prints the non-empty positional elements 2 to the last one. That's the same as writing (if $# == 5):

printf '<%s>\n' $2 $3 $4 $5

So:

$ set 1 2 '' '*' 5
$ printf '<%s>\n' $@[2,-1]
<2>
<*>
<5>

To get the equivalent in bash, you'd need:

$ set 1 2 '' '*' 5
$ a=("$@")
$ IFS=; set -f
$ printf '<%s>\n' ${a[@]:1}
<2>
<*>
<5>

You need an intermediary array because ${@:2} doesn't work that way (at least not with 4.2.45).

Of course, if you wanted all the elements but the first one regardless of whether they're empty or not, you'd have to write it:

$ printf '<%s>\n' "$@[2,-1]"
<2>
<>
<*>
<5>

in zsh and

$ printf '<%s>\n' "${@:2}"
<2>
<>
<*>
<5>

in bash.

Note that zsh eventually included the ${array:first:n} syntax (only when it doesn't conflict with csh style modifiers), so the above bash (actually ksh) code will also work in newer versions of zsh.

As to the reason for the discrepancy between ${a[@]:1} versus ${@:2}, you have to bear in mind that in bash, contrary to zsh or csh or rc, but like ksh where bash copied its arrays from, arrays are sparse and indices start at 0.

${a[@]:4:5} returns the 5 first elements whose indice is greater or equal to 4. The first element in $@ has indice 1 ($1), while an array defined as a=(...) has its elements set with indices starting at 0.

Well, that's not exactly true. In bash, "$@" expands like "$1" "$2" "$3" "$4" "$5", while "${@:0:1}" is like "$0" if and only if $# > 0. "$@" seems to be like "${@:1}" while $@ (without the quotes) is only like ${@:1} if $IFS is unset or non-empty. That sounds like bugs to me. The behaviour in ksh is different and for once more consistent.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.