9
function tail() {
    shift 1
    echo $@
}
% tail one two three
two three

How can I write the opposite of this simple function?

I want to achieve

% init one two three
one two

and get all but the last of the arguments in the $@ variable, so the function can be written as

function init() {
    # ???
    echo $@
}

I have tried unset '@[-1]', unset '[@:-1]', from https://unix.stackexchange.com/a/611717/696135 but these don't work

I have tried set -- "${@:1:$(($#-1))}", unset "@[${#@[@]}-1]", set -- "${@:1:$#-1}" from https://stackoverflow.com/questions/20398499/remove-last-argument-from-argument-list-of-shell-script-bash but these don't work

An example of a script that doesn't work:

function init() {
    set -- ${@:1:$#-1}
    echo $@
}

init one two three
3
  • By experiment: set -- "${@:1:#-1}". LIke Don Adams you were 'this close'! Commented Jan 5 at 23:39
  • 1
    @dave_thompson_085 thanks, that does work. You could post it as an answer? Commented Jan 5 at 23:58
  • 2
    In zsh the positional parameters can be accessed as the argv array, so methods for updating arrays should work. Try argv[-1]=() or set -- "${(@)argv[1,-2]}". Also, typeset -p argv is a good way to view the results. More in this answer (part of one of your links). Commented Jan 6 at 2:34

2 Answers 2

15

shift -p will remove the last array element, as per man zshbuiltins:

shift [ -p ] [ n ] [ name ... ]

The positional parameters ${n+1} ... are renamed to $1 ..., where n is an arithmetic expression that defaults to 1. If any names are given then the arrays with these names are shifted instead of the positional parameters.

If the option -p is given arguments are instead removed (popped) from the end rather than the start of the array.

Your desired init function can be:

function init {
    shift -p
    print -r -- "$@"
}
1
  • 4
    👍 TIL. Note that like shift without -p, that returns an error if the list of positional parameters is empty. Commented Jan 6 at 14:42
9

As @Gairfowl said in comments, in zsh, you'd do:

argv[-1]=()

To remove the last argument (replace it with an empty list).

Even

argv[1]=()

Or even:

1=()

May sometimes be preferable to shift (itself short for shift 1, itself short for shift 1 argv, the latter being zsh-specific), as it doesn't complain if $argv has no element.

Note that your:

function tail() {
    shift 1
    echo $@
}

Should rather be written:

tail() {
  (( $# == 0 )) || shift
  echo -E - "$@"
}

Or Korn-compatible:

function tail {
  (( $# == 0 )) || shift
  print -r - "$@"
}

If the point is to print the remaining arguments space separated and followed by a newline.

Without the quotes, empty arguments would be skipped, without -, that wouldn't work properly is the first argument started with -, without -E/-r, it wouldn't work properly for arguments containing backslashes.

For your init:

init() {
  argv[-1]=()
  print -r -- "$@"
}

Though you could also do¹:

init() print -r -- "$@[1,-2]"

Zsh also recently added the ${array:offset:length} alternative form of array slicing for compatibility with ksh93, so a ksh93-compatible variant could be:

function init {
  print -r -- "${@:1:${#}-1}"
}

Note the brace around # without which in zsh (and unless in sh or ksh emulation), $#-1 would be taken as ${#-}1 that is the length of the $- parameter (using csh-style $#param operator) followed by 1. ${@:1:$# - 1} would also work.

zsh also supports ${var:offset:-length} à la bash, so you could also use print -r -- "${@:1:-1}".

Beware that those report an error if the list of arguments is empty, same as in ksh93 or bash.

${@:1:#-1} also works in current versions of zsh, just like echo $(( # )) outputs the same as echo $(( $# )), but I would avoid that as it's not documented, and # is used in several operators in arithmetic expressions. In particular $(( #var )), expands to the character value of the first character in $var (similar to $(( '$var' )) in ksh), so one might argue that $(( #- )) should expand to the character value of the first character in $-.

unset 'array[i]' does work in zsh for compatibility with ksh, but in ksh, arrays are not real arrays but more like associative arrays with keys limited to positive integers, while in zsh they are normal arrays like in all other non-Korn-like shells, so to emulate the ksh behaviour unset 'array[i]' doesn't unset the element (which wouldn't make sense for normal arrays), but sets it to the empty string, even if that's the last element in the array.

In zsh:

$ a=({1..5})
$ unset 'a[3]' 'a[-1]'
$ print -r - $a[4]
4
$ typeset -p a
typeset -a a=( 1 2 '' 4 '' )

$a[4] is still 4, the unset elements have been replaced with the empty string.

Comparing with ksh93 or bash with their peculiar array design:

$ a=({0..5})
$ unset 'a[3]' 'a[-1]'
$ print -r - "${a[4]}"
4
$ typeset -p a
typeset -a a=([0]=0 [1]=1 [2]=2 [4]=4)

That (and the fact that array indices start at 0 instead of 1) is one reason why in ksh and its clones (such as bash), the positional parameters cannot be mapped to an array (like most other shells such as csh, fish, rc, zsh do), and I suspect that the fact that one can't unset 'argv[i]' in zsh except for the last element (though it still doesn't unset it but replaces with the empty string) might have something to do with ksh compatibility.

array[first,last]=(new values)

(if ,last is omitted, it's the same as array[first,first]) is itself an array slice assignment, similar to what you can achieve in perl with its splice function for instance, it's not limited to unsetting the last element (like perl's pop), you can also do:

  • array[1,0]=(new elements) to insert elements at the front (like perl's unshift)
  • array[3,5]=() to remove elements in the middle.
  • array+=(more) or array[3]+=(more) to insert elements at the end or after a given index.

What's missing compared to the perl equivalents is the ability to retrieve the elements that are removed at the same time they are removed.

For example, perl's @last2 = splice @array, -2 to pop the last two elements into @last2 has to be written: last2=( "${(A@)array[-2,-1]}" ); array[-2,-1]=() (which can be shortened to last2=( $array[-2,-1] ); array[-2,-1]=() if there's no empty element that needs to be preserved).

But I digress...


¹ Beware "$@[-2,-1]" expands to a list of one empty elements instead of an empty list. That makes no difference for print -r -- which prints an empty line in either case, but in the general case, that could be undesirable and would be worked around by using "${(A)@[-2,-1]}". If there's no empty element that needs to be preserved, $@[-2,-1] (without the quotes) is enough.

2
  • "${@:1:${#}-1}" is also compatible with bash Commented Jan 6 at 16:27
  • 2
    @Fravadona, yes, that was implied, bash copied its array design from ksh. Commented Jan 6 at 16:32

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.