Extract parameters before last parameter in "$@"

Viewed 34634

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?

16 Answers

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 :(.

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

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

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

Array without last parameter:

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

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

#!/bin/bash

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

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

Try:

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

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;}'`
#!/bin/sh

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

echo $list $last

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))]}"

This works for me, with sh and bash:

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