26

I often need to pop the last positional argument of a bash function or script.

By "pop" I mean: "remove it from the list of positional arguments, and (optionally) assign it to a variable."

Given how frequently I need this operation, I am a bit surprised that best I have found is what is illustrated by the example below:

foo () {
    local argv=( "$@" )
    local last=${argv[$(( ${#argv[@]} - 1 ))]}
    argv=( ${argv[@]:0:$(( ${#argv[@]} - 1 ))} )
    echo "last: $last"
    echo "rest: ${argv[@]}"
}

In other words, an epic production featuring a cast of thousands...

Is there anything simpler, easier to read?

kjo
  • 15,339
  • 25
  • 73
  • 114
  • 10
    I find it interesting that you need to perform a remove-last operation that often. The reason there is a separate shift keyword is that removing the first element from the list of positional parameters is a common thing to want to do. Care to say something about your use cases? – Kusalananda Sep 27 '20 at 20:38
  • 1
    FYI all the $(( )) could be removed without impact, but Quasímodo has the actual answer. – bxm Sep 27 '20 at 20:45
  • 1
    Don't write your scripts and functions to need popping off the last positional parameter, and your life will be much easier, and your scripts especially will conform better to unix conventions, not to mention simpler by avoiding the shell's excessively ugly syntax around arrays. If you provide a more concrete example, I'd be happy to give a more detailed response. – Dale Hagglund Sep 29 '20 at 04:21
  • @DaleHagglund: I obviously don't know what kjo's use-case is, but both mv and cp have a "special" last parameter, so it's conceivable that kjo's use-case conforms to Unix conventions as well. – ruakh Sep 29 '20 at 16:19

5 Answers5

33

You can access the last element with ${argv[-1]} (bash 4.2 or above) and remove it from the array with the unset builtin (bash 4.3 or above):

last=${argv[-1]}
unset 'argv[-1]'

The quotes around argv[-1] are required as [...] is a glob operator, so argv[-1] unquoted could expand to argv- and/or argv1 if those files existed in the current directory (or to nothing or cause an error if they didn't with nullglob/failglob enabled).

Quasímodo
  • 18,865
  • 4
  • 36
  • 73
15

How about this:

foo () {
    local last="${!#}"
    local argv=("${@:1:$#-1}")
    echo "last: $last"
    echo "rest: ${argv[@]}"
}
  • That's interesting, I've never seen ${!#} used before, any idea where it's officially documented? Only a very small number of search results here: http://symbolhound.com/?q=%24%7B%21%23%7D. Nice answer BTW. – bxm Sep 28 '20 at 10:53
  • 2
    ! is for dereferencing and # is the number of arguments, so !# dereferences the last argument. – Xophmeister Sep 28 '20 at 14:44
6

You can simplify a little to make it easier on the eye, but the fundamental method is unchanged (this might be useful if you have a Bash version which doesn't support Quasímodo's offering):

foo () {
    local argv=( "$@" )
    local last="${argv[$# - 1]}"
    argv=( "${argv[@]:0:$# - 1}" )
    echo "last: $last"
    echo "rest: ${argv[@]}"
}

I concede that it's a bit cheeky use $# in this way, but it has the same value as ${#argv[@]} in this specific example and is more concise in code.

bxm
  • 4,855
3

For the record, in case switching to zsh is an option, in zsh, the array of positional parameters is the $argv array (zsh arrays are true arrays, and with indices starting at one like the positional parameters), so, there, it's just:

foo () {
    local last=$argv[-1]
    argv[-1]=()
    print -rC1 "last: $last" rest: ' - '$^argv
}

Though you could also do:

foo () {
    print -rC1 "last: $argv[-1]" rest: ' - '$^argv[1,-2]
}

And if the end goal is to loop over the positional parameters in reverse order:

for arg in "${(Oa)@}"; do
  something with "$arg"
done
2

For a simpler equivalent to bxm’s answer, use set:

pop() {
  last="${!#}"  # see man bash, search for "indirect expansion"
  set -- "${@:1:$#-1}"

printf 'not last: %s\n' "$@" printf 'last: %s\n' "${last}" }

pop 1 2 3 4

prints:

not last: 1

not last: 2

not last: 3

last: 4

Here, ${!#} is an indirect parameter expansion (search for “indirect expansion” in man bash), and set -- ARGS... sets the new values of the arguments ($1, etc.) to the given args.

Notably, ${!#} and the array slicing are bashisms, but set -- is portable to any POSIX shell.

wchargin
  • 1,091
  • 1
    That's essentially the same as Gordon's answer, except it modifies the positional parameters in place instead of an $argv copy. Note that ${var:start:length} is from ksh93 initially, not bash (and zsh had $var[first,last] long before ksh93 existed or bash had arrays). – Stéphane Chazelas Sep 30 '20 at 07:44
  • Right, yep. Since the OP asked to “remove it from the list of positional arguments”, I thought that this seemed appropriate. – wchargin Oct 01 '20 at 18:42
  • This works on bash5. The accepted answer doesn't. Thank you! – OyaMist Feb 08 '23 at 14:16