How to get the last argument to a /bin/sh function

What’s a better way to implement print_last_arg?

#!/bin/sh

print_last_arg () {
    eval "echo ${$#}"  # this hurts
}

print_last_arg foo bar baz
# baz

(If this were, say, #!/usr/bin/zsh instead of #!/bin/sh I’d know what to do. My problem is finding a reasonable way to implement this for #!/bin/sh.)

EDIT: The above is only a silly example. My goal is not to print the last argument, but rather to have a way to refer to the last argument within a shell function.


EDIT2: I apologize for such an unclearly worded question. I hope to get it right this time.

If this were /bin/zsh instead of /bin/sh, I could write something like this

#!/bin/zsh

print_last_arg () {
    local last_arg=$argv[$#]
    echo $last_arg
}

The expression $argv[$#] is an example of what I described in my first EDIT as a way to refer to the last argument within a shell function.

Therefore, I really should have written my original example like this:

print_last_arg () {
    local last_arg=$(eval "echo ${$#}")   # but this hurts even more
    echo $last_arg
}

…to make it clear that what I’m after is a less awful thing to put to the right of the assignment.

Note, however, that in all the examples, the last argument is accessed non-destructively. IOW, the accessing of the last argument leaves the positional arguments as a whole unaffected.

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

Although this question is just over 2 years old, I thought I’d share a somewhat compacter option.

print_last_arg () {
    echo "${@:${#@}:${#@}}"
}

Let’s run it

print_last_arg foo bar baz
baz

Bash shell parameter expansion.

Edit

Even more concise:
echo "${@: -1}"

(Mind the space)

Source

Tested on macOS 10.12.6 but should also return the last argument on most available *nix flavors…

Hurts much less ¯_(ツ)_/¯

Method 2

Here’s a simplistic way:

print_last_arg () {
  if [ "$#" -gt 0 ]
  then
    s=$(( $# - 1 ))
  else
    s=0
  fi
  shift "$s"
  echo "$1"
}

(updated based on @cuonglm’s point that the original failed when passed no arguments; this now echos a blank line — change that behavior in the else clause if desired)

Method 3

Given the example of the opening post (positional arguments without spaces):

print_last_arg foo bar baz

For the default IFS=' tn', how about:

args="$*" && printf '%sn' "${args##* }"

For a safer expansion of "$*", set IFS (per @StéphaneChazelas):

( IFS=' ' args="$*" && printf '%sn' "${args##* }" )

But the above will fail if your positional arguments can contain spaces. In that case, use this instead:

for a in "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f7d3b7">[email protected]</a>"; do : ; done && printf '%sn' "$a"

Note that these techniques avoid the use of eval and do not have side-effects.

Tested at shellcheck.net

Method 4

POSIXly:

while [ "$#" -gt 1 ]; do
  shift
done

printf '%sn' "$1"

(This approach also works in old Bourne shell)

With other standard tools:

awk 'BEGIN{print ARGV[ARGC-1]}' "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="ad89ed">[email protected]</a>"

(This won’t work with old awk, which did not have ARGV)

Method 5

This should work with any POSIX compliant shell and will work too with the pre POSIX legacy Solaris Bourne shell:

do=;for do do :;done;printf "%sn" "$do"

and here is a function based on the same approach:

print_last_arg()
  if [ "$*" ]; then
    for do do :;done
    printf "%sn" "$do"
  else
    echo
  fi

PS: don’t tell me I forgot the curly braces around the function body 😉

Method 6

From “Unix – Frequently Asked Questions”

(1)

unset last
if    [ $# -gt 0 ]
then  eval last=${$#}
fi
echo  "$last"

If the number of arguments could be zero, then argument zero $0 (usually the name of the script) will be assigned to $last. That’s the reason for the if.

(2)

unset last
for   last
do    :
done
echo  "$last"

(3)

for     i
do
        third_last=$second_last
        second_last=$last
        last=$i
done
echo    "$last"

To avoid printing an empty line when there are no arguments, replace the echo "$last" for:

${last+false} || echo "${last}"

A zero argument count is avoided by if [ $# -gt 0 ].

This is a not an exact copy of what is in the linked in the page, some improvements were added.

Method 7

eval printf %s${1+"'n' "the last arg is ${$#"}"}

…will either print the string the last arg is followed by a <space>, the value of the last argument, and a trailing <newline> if there is at least 1 argument, or else, for zero arguments, it will print nothing at all.

If you did:

eval ${1+"lastarg=${$#"}}

…then either you would assign the value of the last argument to the shell variable $lastarg if there is at least 1 argument, or you would do nothing at all. Either way, you would do it safely, and it ought to be portable even to ye Olde Bourne shell, I think.

Here’s another one that would work similarly, though it does require copying the whole arg array twice (and requires a printf in $PATH for the Bourne shell):

if   [ "${1+:}" ]
then set "$#" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="c9ed89">[email protected]</a>" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="153155">[email protected]</a>"
     shift    "$1"
     printf %s\n "$1"
     shift
fi

Method 8

This should work on all systems with perl installed (so most UNICES):

print_last_arg () {
    printf '%s' "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="86a2c6">[email protected]</a>" | perl -0ne 's///;$l=$_;}{print "$ln"'
}

The trick is to use printf to add a after each shell argument and then perl‘s -0 switch which sets its record separator to NULL. Then we iterate over the input, remove the and saving each NULL-terminated line as $l. The END block (that’s what the }{ is) will be executed after all input has been read so will print the last “line”: the last shell argument.

Method 9

Here’s a version using recursion. No idea how POSIX compliant this is though…

print_last_arg()
{
    if [ $# -gt 1 ] ; then
        shift
        echo $( print_last_arg "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="b793f7">[email protected]</a>" )
    else
        echo "$1"
    fi
}

Method 10

A simple concept, no arithmetic, no loop, no eval, just functions.
Remember that Bourne shell had no arithmetic (needed external expr). If wanting to get an arithmetic free, eval free choice, this is an option. Needing functions means SVR3 or above (no overwrite of parameters).
Look below for a more robust version with printf.

printlast(){
    shift "$1"
    echo "$1"
}

printlast "$#" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="654125">[email protected]</a>"          ### you may use ${1+"<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="2c086c">[email protected]</a>"} here to allow
                             ### a Bourne empty list of arguments,
                             ### but wait for a correct solution below.

This structure to call printlast is fixed, the arguments need to be set in the list of shell arguments $1, $2, etc. (the argument stack) and the call done as given.

If the list of arguments needs to be changed, just set them:

set -- o1e t2o t3r f4u
printlast "$#" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="644024">[email protected]</a>"

Or create an easier to use function (getlast) that could allow generic arguments (but not as fast, arguments are passed two times).

getlast(){ printlast "$#" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="5c781c">[email protected]</a>"; }
getlast o1e t2o t3r f4u

Please note that arguments (of getlast, or all included in [email protected] for printlast) could have spaces, newlines, etc. But not NUL.

Better

This version does not print 0 when the list of arguments is empty,
and use the more robust printf (fall back to echo if external printf is not available for old shells).

printlast(){ shift  "$1"; printf '%s' "$1"; }
getlast  ()  if     [ $# -gt 0 ]
             then   printlast "$#" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="88acc8">[email protected]</a>"
                    echo    ### optional if a trailing newline is wanted.
             fi
### The {} braces were removed on purpose.

getlast 1 2 3 4    # will print 4

Using EVAL.

If the Bourne shell is even older and there are no functions, or if for some reason using eval is the only option:

To print the value:

if    [ $# -gt 0 ]
then  eval printf "'1%sn'" "${$#}"
fi

To set the value in a variable:

if    [ $# -gt 0 ]
then  eval 'last_arg='"${$#}"
fi

If that needs to be done in a function, the arguments need to be copied to the function:

print_last_arg () {
    local last_arg                ### local only works on more modern shells.
    eval 'last_arg='"${$#}"
    echo "last_arg3=$last_arg"
}

print_last_arg "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="fdd9bd">[email protected]</a>"               ### Shell arguments are sent to the function.

Method 11

This comment suggested to use the very elegant:

echo "${@:$#}"


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x