3 minute read

Like most shells, zsh support arrays.

ary=(foo bar baz)
typeset -p ary
# typeset -a ary=( foo bar baz )

With such an array, you can access elements via subscripting like so:

echo $ary[1]
# foo

Users usually coming from bash or ksh will quickly notice that arrays in zsh are 1 based, this drums up a sort of debate among software developers about the proper way indexing should happen. With zero-based indexing being the most common among programming languages, so clearly zsh is weird in choosing 1-based indexing and to write off zsh’s subscripting as lesser than in someway.

This debate is lost upon me being the glue code writing sysadmin that i am, which prioritizes functionally. Functioningly zsh’s subscripting is more powerful than the shells that i am aware of.

For the curious though, the reason why zsh’s indexes start at 1 is most likely due to one of it’s early design goals of appealing to csh users which was a lot more common at the time. csh, released in 1979 had arrays seemingly since it’s original release, which was 1-based. Making it the earliest that I am aware of to support arrays.

# earliest man page that i can find personally.
man =(curl -sL https://github.com/llua/2bsd/raw/master/2bsd.tar.gz | tar -zOxf- man/csh.u)

csh itself most likely being a product of it’s time too since awk (1977-ish), arrays were 1-based

awk 'BEGIN { split("foo bar baz", ary); print ary[1] }'
# foo

along with shell positional parameters $1...$n, which later made up $@ and $* (foreshadowing).

So, anyway, subscripting. zsh’s evalutes the contents of a subscript inside a arithmetic context, so anything you would usually do inside of (()) or $(()) is possible, the resulting number is indexed.

print ${ary[1+1]}
# bar

which isn’t unique but mentioned for completeness.

Something that is less common among bourne-like shells is specifying a range.

print ${ary[2,3]}
# bar baz
ksh93 -c 'ary=(foo bar baz); print "${ary[1..2]}"'
# bar baz

If you are mostly a user of bash, you may mention the expansion ${var:offset:length} as an alternative. But really it isn’t, it is a specifc form of parameter expansion; which prevents the use of other forms of expansions on a range of elements.

print ${ary[2,3]#ba}
# r z
ksh93 -c 'ary=(foo bar baz); print "${ary[1..2]#ba}"'
# r z
bash -c 'ary=(foo bar baz); for arg in "${ary[@]:1:2}"; do printf "%s\n" "${arg#ba}"; done'
# r
# z

zsh, did adopt the ${var:offset:length} expansion for feature parity but unlike ksh and bash allows parameter expansions to nest, which makes it doable, though awkward.

print "${(@)${ary[@]:1:2}#ba}"
# r z

In addition to specifying ranges, zsh subscripts have flags to change the meaning how the subscript indexes. (see zshparam(1))

makes subscripting index the words of a scalar parameter

str='Lorem ipsum dolor sit amet, consectetur adipiscing elit'
print $str[3]
# r
print $str[(w)3]
# dolor
print $str[(w)3,(w)-1]
# dolor sit amet, consectetur adipiscing elit

treat the subscript as a pattern to match against keys, selecting the keys of the associate array.

print $userdirs[(I)l*]
# llua lxdm

Or as a pattern matched against values, then using a parameter expansion flag to get the keys of those values.

# prints the username of users with those homedirs
print ${(k)userdirs[(R)/(nonexistent)#]}
# tty games nobody kmem news cyrus proxy pop messagebus bin operator www git_daemon bind

Other than ranges and flags, the fact that indexes start at 1 in zsh avoids weird edge cases like in other shells.

ary=(foo bar baz); set -- "${ary[@]}"
printf '%s\n' "${ary[*]:0:2}" "${*:0:2}"
# foo bar
# bash foo

The second line is probably not what one would expect to happen. Since when performing expansions with $@ or $* you usually get

printf '%s\n' "$*"
# foo bar baz # argv[0]/$0 is missing

I imagine since positional parameters starts at 1, bash is added to index 0 to keep positioning consisent in a different, more likely unexpected way. But it is inconsisent with how one does "$@" or "$*".

That isn’t the case with zsh’s normal way of subscripting.

print "$ary[1,2]"
# foo bar
print "$@[1,2]"
# foo bar
print "${ary[@]:0:2}"
# foo bar
# whether or not zsh not being index 0 is a bug depends...

So yeah, it is possibly a weird choice to do 1-based indexing, but zsh’s subscripting is still better than most bourne-like shells.

Comments