5

I've got a shell script and I want to use the last argument:

#!/bin/bash
echo My last param is #somehow put it here

Here is what I've tried:

echo $$#
echo ${$#}
echo ${(($#))}

Unfortunately it did not work.

I'm specifically want to understand why my options did not work, I want to do something like double expansion.

In a broader sense, I would like to know how to access the Nth argument from the end. How do I achieve that?

YardenST
  • 247
  • 3
  • 9

6 Answers6

5

To get any argument from bash script, you can use slicing:

#!/bin/bash

# Get 3rd element from the end
from_end1=3
# Get last element
from_end2=1

# Get slice of array [end - from_end1 : end ] of length 1
echo "${@: -$from_end1: 1}"
echo "${@: -$from_end2: 1}"

You can also use this to get Nth element:

# Get 2nd element
from_beginning=2

echo "${@: $from_beginning: 1}"

Remember to check for length; this might return your program's name or an empty string.

MatthewRock
  • 6,986
4

In bash (release 4.3+), you may assign the passed parameters to an array and access the last one by the index -1:

#!/bin/bash

params=( "$@" )
printf 'The last parameter given to the script is %s\n' "${params[-1]}"

foo () {
    params=( "$@" )
    printf 'The last parameter given to the function is %s\n' "${params[-1]}"
}

In general, negative indexes into arrays accesses the elements from the end of the array.

Kusalananda
  • 333,661
  • A negative index might work on bash 4.2+ depending on the patch level. But an slice ${@: -1:1} works with negative indexes since bash 3.0. –  Dec 08 '18 at 03:12
  • @Isaac That's good if you are code golfing, but not if you're writing code that others need to understand and maintain. – Kusalananda Dec 08 '18 at 08:19
3

In addition (to Kusalananda's answer):

#! /bin/bash 

echo "(bash/ksh): ${@: -1}"
echo "(bash 3.x+): ${!#}"
echo "(bash 3.x+): $BASH_ARGV"
echo "(bash 3.x+/ksh): ${@:$#}"
echo "(bash 3.x+): ${BASH_ARGV[0]}"

and if you worry about portability:

#!/bin/bash

penultimate=''
ultimate=''

for p in "$@" ; do
    penultimate="$ultimate"
    ultimate="$p"
done

echo "penultimate=$penultimate"
echo "ultimate=$ultimate"
Kusalananda
  • 333,661
Ljm Dullaart
  • 4,643
  • to Kusalananda's answer. – Ljm Dullaart Dec 07 '18 at 20:24
  • Yes, you're right; I added it in the answer. – Ljm Dullaart Dec 07 '18 at 20:26
  • can you please explain about this notation? ${!#} – YardenST Dec 07 '18 at 21:22
  • @YardenST From the bash manpage: "If the first character of parameter is an exclamation point (!), and parameter is not a nameref, it introduces a level of variable indirection. Bash uses the value of the variable formed from the rest of parameter as the name of the variable; this variable is then expanded and that value is used in the rest of the substitution, rather than the value of parameter itself. This is known as indirect expansion." – JoL Dec 07 '18 at 22:29
  • @YardenST In other words, bash takes #, expands it like it would $#, and then the result (let's say 5) is put through variable expansion again like it would $5. – JoL Dec 07 '18 at 22:38
  • From the manual The shell sets BASH_ARGV only when in extended debugging mode. So, that parameter is not always present. –  Dec 08 '18 at 01:30
  • The ${@:$#} works on bash (since at least 2.01), ksh and zsh. –  Dec 08 '18 at 01:42
  • @JoL awesome, this is exactly the thing i was trying to workout! – YardenST Dec 08 '18 at 08:14
  • 1
    @YardenST Yes. Honestly, I think your attempts show what would have been the most obvious, intuitive syntax. I can only imagine ${!x} was chosen to intentionally limit the syntax to only allow 1 level of indirection instead of multiple by nesting, maybe to ease the implementation. There's probably other subtle reasons, though, like arguments about consistency and how it implies that other thing could also be nested. – JoL Dec 09 '18 at 05:20
3

Portable sh, not bash-specific, and without O(n) loops:

eval x=\$$(($#-1))

The -1 yields the penultimate argument; replace it with the position you want relative to the end, or drop it entirely if you want the very last one.

1

The clasic solutions for POSIX shells (which also work on ksh, zsh or bash) are:

 for last do :;done; echo "last=$last"
 eval "last=\$$#"; echo "last=$last"

For newer shells (ksh93,zsh,bash):

 echo "last=${@: -1}"
 echo "last=${@:(-1)}"
 echo "last=${@:~0}"
 echo "last=${@:$#}"

Only for:

echo "last=${!#}"
echo "last=$BASH_ARGV"
echo "last=${@[-1]}"
echo "last=${@[#]}"

For the penultimate argument:

 for arg do penultimate=$ultimate; ultimate=$arg; done; echo "$penultimate"
 eval penultimate=\$$((#-1))
 echo "${@:$((#-1)):1}"
 echo "${@: -2:1}"
 echo "${@:~1:1}"
0

In zsh, the most natural thing, I think, would be:

echo "ultimate: ${@[$#]}"
echo "ultimate: ${@[-1]}"
echo "penultimate: ${@[-2]}"
echo "penultimate: ${@[$(($# - 1))]}"
JoL
  • 4,735