13

I want to get the second last item given to a shell program. Currently I do it like this:

file1_tmp="${@: -2}"
oldIFS=$IFS
IFS=" "
count=0
for value in $file1; do
  if [[ count -e 0 ]]; then
    file1=$value
  fi
    count=1
done
oldIFS=$IFS 

I'm sure that there is a much easier way to do this. So how can I get the second last argument from a shell script input in as few lines as possible?

0

4 Answers 4

27
set -- "first argument" "second argument" \
       "third argument" "fourth argument" \
       "fifth argument"
second_to_last="${@:(-2):1}"
echo "$second_to_last"

Note the quoting, which ensures that arguments with whitespace stick together -- which your original solution doesn't do.

4
  • The differences between $@ and $* don't matter when you're only retrieving one item, because they control how subsequent items are joined together (or, as the case may be, not). I still recommend $@ as more clear to a reader that the desired output is an individual element with no joining operations taking place. Or did you have a more specific question? Commented Jan 6, 2019 at 18:00
  • ...as for (-2) vs a space before the -, the point is just to be unambiguous to the parser that we don't want :- to be parsed as a single token. Either approach accomplishes it. Commented Jan 6, 2019 at 18:02
  • I found (-2) this confusing as it is also used for arrays and sub shell. I guess that is not how it is used here.
    – Porcupine
    Commented Jan 6, 2019 at 18:33
  • 1
    Determining array indices is an operation with a mathematical result, so bash's parser operates in an arithmetic context (the same kind of context that $(( )) creates); so parens have the same meaning they have in any other mathematical operation, as grouping / precedence / order-of-operations with no implied subshell. Commented Jan 6, 2019 at 18:35
6

In bash/ksh/zsh you can simply ${@: -2:1}

$ set a b c d 
$ echo ${@: -1:1}
c

In POSIX sh you can use eval:

$ set a b c d 
$ echo $(eval "echo \$$(($#-2))")
c
4
  • The quoting in this answer is correct for zsh, but incorrect in any POSIX-compliant shell (such as bash or ksh). Commented Apr 12, 2013 at 2:36
  • This can be done in Bourne without eval -- for instance, by shifting args inside a function: penultimate() { while [ "$#" -gt 2 ]; do shift; done; printf '%s\n' "$1"; }; penultimate "$@" Commented Jun 26, 2013 at 12:38
  • 1
    (This is where I kick my former self for being sloppy about naming -- that's not valid [1970s] Bourne, which doesn't support $(( )); rather, it's valid [1990s] POSIX sh). Commented Sep 14, 2017 at 14:40
  • When I run your first snippet, $ echo ${@: -1:1} prints d. Don't you mean $ echo ${@: -2:1} ?
    – user3064538
    Commented Apr 6, 2021 at 5:14
5
n=$(($#-1))
second_to_last=${!n}
echo "$second_to_last"
5

There are some options for all bash versions:

$ set -- aa bb cc dd ee ff
$ echo "${@: -2:1}   ${@:(-2):1}   ${@:(~1):1}   ${@:~1:1}   ${@:$#-1:1}"
ee   ee   ee   ee   ee

The (~) is the bitwise negation operator (search in the ARITHMETIC EVALUATION section).
It means flip all bits.

The selection even could be done with (integer) variables:

$ a=1  ; b=-a; echo "${@:b-1:1}   ${@:(b-1):1}   ${@:(~a):1}   ${@:~a:1}   ${@:$#-a:1}"
ee   ee   ee   ee   ee

$ a=2  ; b=-a; echo "${@:b-1:1}   ${@:(b-1):1}   ${@:(~a):1}   ${@:~a:1}   ${@:$#-a:1}"
dd   dd   dd   dd   dd

For really old shells, you must use eval:

eval "printf \"%s\n\" \"\$$(($#-1))\""

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