Extract parameters before last parameter in "$@"

BashParameters

Bash Problem Overview


I'm trying to create a Bash script that will extract the last parameter given from the command line into a variable to be used elsewhere. Here's the script I'm working on:

#!/bin/bash
# compact - archive and compact file/folder(s)

eval LAST=\$$#

FILES="$@"
NAME=$LAST

# Usage - display usage if no parameters are given
if [[ -z $NAME ]]; then
  echo "compact <file> <folder>... <compressed-name>.tar.gz"
  exit
fi

# Check if an archive name has been given
if [[ -f $NAME ]]; then
  echo "File exists or you forgot to enter a filename.  Exiting."
  exit
fi

tar -czvpf "$NAME".tar.gz $FILES

Since the first parameters could be of any number, I have to find a way to extract the last parameter, (e.g. compact file.a file.b file.d files-a-b-d.tar.gz). As it is now the archive name will be included in the files to compact. Is there a way to do this?

Bash Solutions


Solution 1 - Bash

To remove the last item from the array you could use something like this:

#!/bin/bash

length=$(($#-1))
array=${@:1:$length}
echo $array

Even shorter way:

array=${@:1:$#-1}

But arays are a Bashism, try avoid using them :(.

Solution 2 - Bash

Portable and compact solutions

This is how I do in my scripts

last=${@:$#} # last parameter 
other=${*%${!#}} # all parameters except the last

EDIT
According to some comments (see below), this solution is more portable than others.
Please read Michael Dimmitt's commentary for an explanation of how it works.

Solution 3 - Bash

last_arg="${!#}" 

Solution 4 - Bash

Several solutions have already been posted; however I would advise restructuring your script so that the archive name is the first parameter rather than the last. Then it's really simple, since you can use the shift builtin to remove the first parameter:

ARCHIVENAME="$1"
shift
# Now "$@" contains all of the arguments except for the first

Solution 5 - Bash

Thanks guys, got it done, heres the final bash script:

#!/bin/bash
# compact - archive and compress file/folder(s)

# Extract archive filename for variable
ARCHIVENAME="${!#}"

# Remove archive filename for file/folder list to backup
length=$(($#-1))
FILES=${@:1:$length} 

# Usage - display usage if no parameters are given
if [[ -z $@ ]]; then
  echo "compact <file> <folder>... <compressed-name>.tar.gz"
  exit
fi

# Tar the files, name archive after last file/folder if no name given
if [[ ! -f $ARCHIVENAME ]]; then
  tar -czvpf "$ARCHIVENAME".tar.gz $FILES; else
  tar -czvpf "$ARCHIVENAME".tar.gz "$@"
fi

Solution 6 - Bash

Just dropping the length variable used in Krzysztof Klimonda's solution:

(
set -- 1 2 3 4 5
echo "${@:1:($#-1)}"       # 1 2 3 4
echo "${@:(-$#):($#-1)}"   # 1 2 3 4
)

Solution 7 - Bash

I would add this as a comment, but don't have enough reputation and the answer got a bit longer anyway. Hope it doesn't mind.

As @func stated:

> last_arg="${!#}"

How it works:

${!PARAM} indicates level of indirection. You are not referencing PARAM itself, but the value stored in PARAM ( think of PARAM as pointer to value ).
${#} expands to the number of parameters (Note: $0 - the script name - is not counted here).

Consider following execution:

$./myscript.sh p1 p2 p3

And in the myscript.sh

#!/bin/bash

echo "Number of params: ${#}"  # 3
echo "Last parameter using '\${!#}': ${!#}"  # p3
echo "Last parameter by evaluating positional value: $(eval LASTP='$'${#} ; echo $LASTP)"  # p3

Hence you can think of ${!#} as a shortcut for the above eval usage, which does exactly the approach described above - evaluates the value stored in the given parameter, here the parameter is 3 and holds the positional argument $3

Now if you want all the params except the last one, you can use substring removal ${PARAM%PATTERN} where % sign means 'remove the shortest matching pattern from the end of the string'.

Hence in our script:

echo "Every parameter except the last one: ${*%${!#}}"


You can read something in here: Parameter expansion

Solution 8 - Bash

Are you sure this fancy script is any better than a simple alias to tar?

alias compact="tar -czvpf"

Usage is:

compact ARCHIVENAME FILES...

Where FILES can be file1 file2 or globs like *.html

Solution 9 - Bash

Array without last parameter:

array=${@:1:$#-1}

But it's a bashism :(. Proper solutions would involve shift and adding into variable as others use.

Solution 10 - Bash

#!/bin/bash

lastidx=$#
lastidx=`expr $lastidx - 1`

eval last='$'{$lastidx}
echo $last

Solution 11 - Bash

Try:

if [ "$#" -gt '0' ]; then
    /bin/echo "${!#}" "${@:1:$(($# - 1))}
fi

Solution 12 - Bash

Alternative way to pull the last parameter out of the argument list:

eval last="\$$#"
eval set -- `awk 'BEGIN{for(i=1;i<'$#';i++) printf " \"$%d\"",i;}'`

Solution 13 - Bash

#!/bin/sh

eval last='$'$# while test $# -gt 1; do list="$list $1" shift done

echo $list $last

Solution 14 - Bash

I can't find a way to use array-subscript notation on $@, so this is the best I can do:

#!/bin/bash

args=("$@")
echo "${args[$(($#-1))]}"

Solution 15 - Bash

This script may work for you - it returns a subrange of the arguments, and can be called from another script.

Examples of it running:
$ args_get_range 2 -2 y a b "c 1" d e f g                          
'b' 'c 1' 'd' 'e'

$ args_get_range 1 2 n arg1 arg2                                   
arg1 arg2

$ args_get_range 2 -2 y arg1 arg2 arg3 "arg 4" arg5                
'arg2' 'arg3'

$ args_get_range 2 -1 y arg1 arg2 arg3 "arg 4" arg5                
'arg2' 'arg3' 'arg 4'

# You could use this in another script of course 
# by calling it like so, which puts all
# args except the last one into a new variable
# called NEW_ARGS

NEW_ARGS=$(args_get_range 1 -1 y "$@")
args_get_range.sh
#!/usr/bin/env bash

function show_help()
{
  IT="
  Extracts a range of arguments from passed in args
  and returns them quoted or not quoted.

  usage: START END QUOTED ARG1 {ARG2} ...

  e.g. 

  # extract args 2-3 
  $ args_get_range.sh 2 3 n arg1 arg2 arg3
  arg2 arg3

  # extract all args from 2 to one before the last argument 
  $ args_get_range.sh 2 -1 n arg1 arg2 arg3 arg4 arg5
  arg2 arg3 arg4

  # extract all args from 2 to 3, quoting them in the response
  $ args_get_range.sh 2 3 y arg1 arg2 arg3 arg4 arg5
  'arg2' 'arg3'

  # You could use this in another script of course 
  # by calling it like so, which puts all
  # args except the last one into a new variable
  # called NEW_ARGS

  NEW_ARGS=\$(args_get_range.sh 1 -1 \"\$@\")

  "
  echo "$IT"
  exit
}

if [ "$1" == "help" ]
then
  show_help
fi
if [ $# -lt 3 ]
then
  show_help
fi

START=$1
END=$2
QUOTED=$3
shift;
shift;
shift;

if [ $# -eq 0 ]
then
  echo "Please supply a folder name"
  exit;
fi

# If end is a negative, it means relative
# to the last argument.
if [ $END -lt 0 ]
then
  END=$(($#+$END))
fi

ARGS=""

COUNT=$(($START-1))
for i in "${@:$START}"
do
  COUNT=$((COUNT+1))

  if [ "$QUOTED" == "y" ]
  then
    ARGS="$ARGS '$i'"
  else
    ARGS="$ARGS $i"
  fi

  if [ $COUNT -eq $END ]
  then
    echo $ARGS
    exit;
  fi
done
echo $ARGS

Solution 16 - Bash

This works for me, with sh and bash:

last=${*##* }
others=${*%${*##* }}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser148813View Question on Stackoverflow
Solution 1 - BashKrzysztof KlimondaView Answer on Stackoverflow
Solution 2 - BashePi272314View Answer on Stackoverflow
Solution 3 - BashfuncView Answer on Stackoverflow
Solution 4 - BashAdam RosenfieldView Answer on Stackoverflow
Solution 5 - Bashuser148813View Answer on Stackoverflow
Solution 6 - BashbashistView Answer on Stackoverflow
Solution 7 - BashCermakMView Answer on Stackoverflow
Solution 8 - BashMestreLionView Answer on Stackoverflow
Solution 9 - BashpevikView Answer on Stackoverflow
Solution 10 - BashjsightView Answer on Stackoverflow
Solution 11 - BashzorroView Answer on Stackoverflow
Solution 12 - BashAndersView Answer on Stackoverflow
Solution 13 - BashWilliam PursellView Answer on Stackoverflow
Solution 14 - BashNorman RamseyView Answer on Stackoverflow
Solution 15 - BashBrad ParksView Answer on Stackoverflow
Solution 16 - BashChristian TostaView Answer on Stackoverflow