Getting the last argument passed to a shell script - shell

$1 is the first argument.
$# is all of them.
How can I find the last argument passed to a shell
script?

This is Bash-only:
echo "${#: -1}"

This is a bit of a hack:
for last; do true; done
echo $last
This one is also pretty portable (again, should work with bash, ksh and sh) and it doesn't shift the arguments, which could be nice.
It uses the fact that for implicitly loops over the arguments if you don't tell it what to loop over, and the fact that for loop variables aren't scoped: they keep the last value they were set to.

$ set quick brown fox jumps
$ echo ${*: -1:1} # last argument
jumps
$ echo ${*: -1} # or simply
jumps
$ echo ${*: -2:1} # next to last
fox
The space is necessary so that it doesn't get interpreted as a default value.
Note that this is bash-only.

The simplest answer for bash 3.0 or greater is
_last=${!#} # *indirect reference* to the $# variable
# or
_last=$BASH_ARGV # official built-in (but takes more typing :)
That's it.
$ cat lastarg
#!/bin/bash
# echo the last arg given:
_last=${!#}
echo $_last
_last=$BASH_ARGV
echo $_last
for x; do
echo $x
done
Output is:
$ lastarg 1 2 3 4 "5 6 7"
5 6 7
5 6 7
1
2
3
4
5 6 7

The following will work for you.
# is for array of arguments.
: means at
$# is the length of the array of arguments.
So the result is the last element:
${#:$#}
Example:
function afunction{
echo ${#:$#}
}
afunction -d -o local 50
#Outputs 50
Note that this is bash-only.

Use indexing combined with length of:
echo ${#:${##}}
Note that this is bash-only.

Found this when looking to separate the last argument from all the previous one(s).
Whilst some of the answers do get the last argument, they're not much help if you need all the other args as well. This works much better:
heads=${#:1:$#-1}
tail=${#:$#}
Note that this is bash-only.

This works in all POSIX-compatible shells:
eval last=\${$#}
Source: http://www.faqs.org/faqs/unix-faq/faq/part2/section-12.html

Here is mine solution:
pretty portable (all POSIX sh, bash, ksh, zsh) should work
does not shift original arguments (shifts a copy).
does not use evil eval
does not iterate through the whole list
does not use external tools
Code:
ntharg() {
shift $1
printf '%s\n' "$1"
}
LAST_ARG=`ntharg $# "$#"`

From oldest to newer solutions:
The most portable solution, even older sh (works with spaces and glob characters) (no loop, faster):
eval printf "'%s\n'" "\"\${$#}\""
Since version 2.01 of bash
$ set -- The quick brown fox jumps over the lazy dog
$ printf '%s\n' "${!#} ${#:(-1)} ${#: -1} ${#:~0} ${!#}"
dog dog dog dog dog
For ksh, zsh and bash:
$ printf '%s\n' "${#: -1} ${#:~0}" # the space beetwen `:`
# and `-1` is a must.
dog dog
And for "next to last":
$ printf '%s\n' "${#:~1:1}"
lazy
Using printf to workaround any issues with arguments that start with a dash (like -n).
For all shells and for older sh (works with spaces and glob characters) is:
$ set -- The quick brown fox jumps over the lazy dog "the * last argument"
$ eval printf "'%s\n'" "\"\${$#}\""
The last * argument
Or, if you want to set a last var:
$ eval last=\${$#}; printf '%s\n' "$last"
The last * argument
And for "next to last":
$ eval printf "'%s\n'" "\"\${$(($#-1))}\""
dog

If you are using Bash >= 3.0
echo ${BASH_ARGV[0]}

For bash, this comment suggested the very elegant:
echo "${#:$#}"
To silence shellcheck, use:
echo ${*:$#}
As a bonus, both also work in zsh.

shift `expr $# - 1`
echo "$1"
This shifts the arguments by the number of arguments minus 1, and returns the first (and only) remaining argument, which will be the last one.
I only tested in bash, but it should work in sh and ksh as well.

I found #AgileZebra's answer (plus #starfry's comment) the most useful, but it sets heads to a scalar. An array is probably more useful:
heads=( "${#: 1: $# - 1}" )
tail=${#:${##}}
Note that this is bash-only.
Edit: Removed unnecessary $(( )) according to #f-hauri's comment.

A solution using eval:
last=$(eval "echo \$$#")
echo $last

If you want to do it in a non-destructive way, one way is to pass all the arguments to a function and return the last one:
#!/bin/bash
last() {
if [[ $# -ne 0 ]] ; then
shift $(expr $# - 1)
echo "$1"
#else
#do something when no arguments
fi
}
lastvar=$(last "$#")
echo $lastvar
echo "$#"
pax> ./qq.sh 1 2 3 a b
b
1 2 3 a b
If you don't actually care about keeping the other arguments, you don't need it in a function but I have a hard time thinking of a situation where you would never want to keep the other arguments unless they've already been processed, in which case I'd use the process/shift/process/shift/... method of sequentially processing them.
I'm assuming here that you want to keep them because you haven't followed the sequential method. This method also handles the case where there's no arguments, returning "". You could easily adjust that behavior by inserting the commented-out else clause.

For tcsh:
set X = `echo $* | awk -F " " '{print $NF}'`
somecommand "$X"
I'm quite sure this would be a portable solution, except for the assignment.

After reading the answers above I wrote a Q&D shell script (should work on sh and bash) to run g++ on PGM.cpp to produce executable image PGM. It assumes that the last argument on the command line is the file name (.cpp is optional) and all other arguments are options.
#!/bin/sh
if [ $# -lt 1 ]
then
echo "Usage: `basename $0` [opt] pgm runs g++ to compile pgm[.cpp] into pgm"
exit 2
fi
OPT=
PGM=
# PGM is the last argument, all others are considered options
for F; do OPT="$OPT $PGM"; PGM=$F; done
DIR=`dirname $PGM`
PGM=`basename $PGM .cpp`
# put -o first so it can be overridden by -o specified in OPT
set -x
g++ -o $DIR/$PGM $OPT $DIR/$PGM.cpp

The following will set LAST to last argument without changing current environment:
LAST=$({
shift $(($#-1))
echo $1
})
echo $LAST
If other arguments are no longer needed and can be shifted it can be simplified to:
shift $(($#-1))
echo $1
For portability reasons following:
shift $(($#-1));
can be replaced with:
shift `expr $# - 1`
Replacing also $() with backquotes we get:
LAST=`{
shift \`expr $# - 1\`
echo $1
}`
echo $LAST

echo $argv[$#argv]
Now I just need to add some text because my answer was too short to post. I need to add more text to edit.

This is part of my copy function:
eval echo $(echo '$'"$#")
To use in scripts, do this:
a=$(eval echo $(echo '$'"$#"))
Explanation (most nested first):
$(echo '$'"$#") returns $[nr] where [nr] is the number of parameters. E.g. the string $123 (unexpanded).
echo $123 returns the value of 123rd parameter, when evaluated.
eval just expands $123 to the value of the parameter, e.g. last_arg. This is interpreted as a string and returned.
Works with Bash as of mid 2015.

To return the last argument of the most recently used command use the special parameter:
$_
In this instance it will work if it is used within the script before another command has been invoked.

#! /bin/sh
next=$1
while [ -n "${next}" ] ; do
last=$next
shift
next=$1
done
echo $last

Try the below script to find last argument
# cat arguments.sh
#!/bin/bash
if [ $# -eq 0 ]
then
echo "No Arguments supplied"
else
echo $* > .ags
sed -e 's/ /\n/g' .ags | tac | head -n1 > .ga
echo "Last Argument is: `cat .ga`"
fi
Output:
# ./arguments.sh
No Arguments supplied
# ./arguments.sh testing for the last argument value
Last Argument is: value
Thanks.

There is a much more concise way to do this. Arguments to a bash script can be brought into an array, which makes dealing with the elements much simpler. The script below will always print the last argument passed to a script.
argArray=( "$#" ) # Add all script arguments to argArray
arrayLength=${#argArray[#]} # Get the length of the array
lastArg=$((arrayLength - 1)) # Arrays are zero based, so last arg is -1
echo ${argArray[$lastArg]}
Sample output
$ ./lastarg.sh 1 2 buckle my shoe
shoe

Using parameter expansion (delete matched beginning):
args="$#"
last=${args##* }
It's also easy to get all before last:
prelast=${args% *}

$ echo "${*: -1}"
That will print the last argument

With GNU bash version >= 3.0:
num=$# # get number of arguments
echo "${!num}" # print last argument

Just use !$.
$ mkdir folder
$ cd !$ # will run: cd folder

Related

Unix Shell equivalency to Java .hasNext()?

Or anything in shell script to implement the same thing?
I was doing an assignment that requires us to write a Bourne shell script that shows the last argument of a bunch, e.g.:
lastarg arg1 arg2 arg3 ..... argN
which would show:
argN
I was not sure if there's any equivalencies to hasNext in Java as it's easy to implement.
Sorry if I was rude and unclear.
#!/bin/bash
all=($#)
# to make things short:
# you can use what's in a variable as a variable name
last=$(( $# )) # get number of arguments
echo ${!last} # use that to get the last argument. notice the !
# while the number of arguments is not 0
# put what is in argument $1 into next
# move all arguments to the left
# $1=foo $2=bar $4=moo
# shift
# $1=bar $2=moo
while [ $# -ne 0 ]; do
next=$1
shift
echo $next
done
# but the problem was the last argument...
# all=($#): put all arguments into an array
# ${all[n]}: get argument number n
# $(( 1+2 )): do math
# ${#all[#]}: get the count of element in an array
echo -e "all:\t ${all[#]}"
echo -e "second:\t ${all[1]}"
echo -e "fifth:\t ${all[4]}"
echo -e "# of elements:\t ${#all[#]}"
echo -e "last element:\t ${all[ (( ${#all[#]} -1 )) ]}"
ok, last edit (omg :p)
$ sh unix-java-hasnext.sh one two three seventyfour sixtyeight
sixtyeight
one
two
three
seventyfour
sixtyeight
all: one two three seventyfour sixtyeight
second: two
fifth: sixtyeight
# of elements: 5
last element: sixtyeight
POSIX-based shell languages don't implement iterators.
The only things you have are for V in words ; do ... ; done or implementing the loop with while and manual stuff to update and test the loop variable.
This is not place to make wild guesses, still: Bash provide shift operator, for loop and more.
(If this is for argument processing you have getopt library. More info in Using getopts in bash shell script to get long and short command line options )

Bash empty array expansion with `set -u`

I'm writing a bash script which has set -u, and I have a problem with empty array expansion: bash appears to treat an empty array as an unset variable during expansion:
$ set -u
$ arr=()
$ echo "foo: '${arr[#]}'"
bash: arr[#]: unbound variable
(declare -a arr doesn't help either.)
A common solution to this is to use ${arr[#]-} instead, thus substituting an empty string instead of the ("undefined") empty array. However this is not a good solution, since now you can't discern between an array with a single empty string in it and an empty array. (#-expansion is special in bash, it expands "${arr[#]}" into "${arr[0]}" "${arr[1]}" …, which makes it a perfect tool for building command lines.)
$ countArgs() { echo $#; }
$ countArgs a b c
3
$ countArgs
0
$ countArgs ""
1
$ brr=("")
$ countArgs "${brr[#]}"
1
$ countArgs "${arr[#]-}"
1
$ countArgs "${arr[#]}"
bash: arr[#]: unbound variable
$ set +u
$ countArgs "${arr[#]}"
0
So is there a way around that problem, other than checking the length of an array in an if (see code sample below), or turning off -u setting for that short piece?
if [ "${#arr[#]}" = 0 ]; then
veryLongCommandLine
else
veryLongCommandLine "${arr[#]}"
fi
Update: Removed bugs tag due to explanation by ikegami.
According to the documentation,
An array variable is considered set if a subscript has been assigned a value. The null string is a valid value.
No subscript has been assigned a value, so the array isn't set.
But while the documentation suggests an error is appropriate here, this is no longer the case since 4.4.
$ bash --version | head -n 1
GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu)
$ set -u
$ arr=()
$ echo "foo: '${arr[#]}'"
foo: ''
There is a conditional you can use inline to achieve what you want in older versions: Use ${arr[#]+"${arr[#]}"} instead of "${arr[#]}".
$ function args { perl -E'say 0+#ARGV; say "$_: $ARGV[$_]" for 0..$#ARGV' -- "$#" ; }
$ set -u
$ arr=()
$ args "${arr[#]}"
-bash: arr[#]: unbound variable
$ args ${arr[#]+"${arr[#]}"}
0
$ arr=("")
$ args ${arr[#]+"${arr[#]}"}
1
0:
$ arr=(a b c)
$ args ${arr[#]+"${arr[#]}"}
3
0: a
1: b
2: c
Tested with bash 4.2.25 and 4.3.11.
The only safe idiom is ${arr[#]+"${arr[#]}"}
Unless you only care about Bash 4.4+, but you wouldn't be looking at this question if that were the case :)
This is already the recommendation in ikegami's answer, but there's a lot of misinformation and guesswork in this thread. Other patterns, such as ${arr[#]-} or ${arr[#]:0}, are not safe across all major versions of Bash.
As the table below shows, the only expansion that is reliable across all modern-ish Bash versions is ${arr[#]+"${arr[#]}"} (column +"). Of note, several other expansions fail in Bash 4.2, including (unfortunately) the shorter ${arr[#]:0} idiom, which doesn't just produce an incorrect result but actually fails. If you need to support versions prior to 4.4, and in particular 4.2, this is the only working idiom.
Unfortunately other + expansions that, at a glance, look the same do indeed emit different behavior. Using :+ instead of + (:+" in the table), for example, does not work because :-expansion treats an array with a single empty element (('')) as "null" and thus doesn't (consistently) expand to the same result.
Quoting the full expansion instead of the nested array ("${arr[#]+${arr[#]}}", "+ in the table), which I would have expected to be roughly equivalent, is similarly unsafe in 4.2.
You can see the code that generated this data along with results for several additional version of bash in this gist.
#ikegami's accepted answer is subtly wrong! The correct incantation is ${arr[#]+"${arr[#]}"}:
$ countArgs () { echo "$#"; }
$ arr=('')
$ countArgs "${arr[#]:+${arr[#]}}"
0 # WRONG
$ countArgs ${arr[#]+"${arr[#]}"}
1 # RIGHT
$ arr=()
$ countArgs ${arr[#]+"${arr[#]}"}
0 # Let's make sure it still works for the other case...
Turns out array handling has been changed in recently released (2016/09/16) bash 4.4 (available in Debian stretch, for example).
$ bash --version | head -n1
bash --version | head -n1
GNU bash, version 4.4.0(1)-release (x86_64-pc-linux-gnu)
Now empty arrays expansion does not emits warning
$ set -u
$ arr=()
$ echo "${arr[#]}"
$ # everything is fine
this may be another option for those who prefer not to duplicate arr[#] and are okay to have an empty string
echo "foo: '${arr[#]:-}'"
to test:
set -u
arr=()
echo a "${arr[#]:-}" b # note two spaces between a and b
for f in a "${arr[#]:-}" b; do echo $f; done # note blank line between a and b
arr=(1 2)
echo a "${arr[#]:-}" b
for f in a "${arr[#]:-}" b; do echo $f; done
#ikegami's answer is correct, but I consider the syntax ${arr[#]+"${arr[#]}"} dreadful. If you use long array variable names, it starts to looks spaghetti-ish quicker than usual.
Try this instead:
$ set -u
$ count() { echo $# ; } ; count x y z
3
$ count() { echo $# ; } ; arr=() ; count "${arr[#]}"
-bash: abc[#]: unbound variable
$ count() { echo $# ; } ; arr=() ; count "${arr[#]:0}"
0
$ count() { echo $# ; } ; arr=(x y z) ; count "${arr[#]:0}"
3
It looks like the Bash array slice operator is very forgiving.
So why did Bash make handling the edge case of arrays so difficult? Sigh. I cannot guarantee you version will allow such abuse of the array slice operator, but it works dandy for me.
Caveat: I am using GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu)
Your mileage may vary.
"Interesting" inconsistency indeed.
Furthermore,
$ set -u
$ echo $#
0
$ echo "$1"
bash: $1: unbound variable # makes sense (I didn't set any)
$ echo "$#" | cat -e
$ # blank line, no error
While I agree that the current behavior may not be a bug in the sense that #ikegami explains, IMO we could say the bug is in the definition (of "set") itself, and/or the fact that it's inconsistently applied. The preceding paragraph in the man page says
... ${name[#]} expands each element of name to a separate word. When there are no array members, ${name[#]} expands to nothing.
which is entirely consistent with what it says about the expansion of positional parameters in "$#". Not that there aren't other inconsistencies in the behaviors of arrays and positional parameters... but to me there's no hint that this detail should be inconsistent between the two.
Continuing,
$ arr=()
$ echo "${arr[#]}"
bash: arr[#]: unbound variable # as we've observed. BUT...
$ echo "${#arr[#]}"
0 # no error
$ echo "${!arr[#]}" | cat -e
$ # no error
So arr[] isn't so unbound that we can't get a count of its elements (0), or a (empty) list of its keys? To me these are sensible, and useful -- the only outlier seems to be the ${arr[#]} (and ${arr[*]}) expansion.
I am complementing on #ikegami's (accepted) and #kevinarpe's (also good) answers.
You can do "${arr[#]:+${arr[#]}}" to workaround the problem. The right-hand-side (i.e., after :+) provides an expression that will be used in case the left-hand-side is not defined/null.
The syntax is arcane. Note that the right hand side of the expression will undergo parameter expansion, so extra attention should be paid to having consistent quoting.
: example copy arr into arr_copy
arr=( "1 2" "3" )
arr_copy=( "${arr[#]:+${arr[#]}}" ) # good. same quoting.
# preserves spaces
arr_copy=( ${arr[#]:+"${arr[#]}"} ) # bad. quoting only on RHS.
# copy will have ["1","2","3"],
# instead of ["1 2", "3"]
Like #kevinarpe mentions, a less arcane syntax is to use the array slice notation ${arr[#]:0} (on Bash versions >= 4.4), which expands to all the parameters, starting from index 0. It also doesn't require as much repetition. This expansion works regardless of set -u, so you can use this at all times. The man page says (under Parameter Expansion):
${parameter:offset}
${parameter:offset:length}
...
If parameter is an indexed array name subscripted by # or *, the result is the length members of the array beginning with ${parameter[offset]}. A negative offset is taken relative to one
greater than the maximum index of the specified array. It is an
expansion error if length evaluates to a number less than zero.
This is the example provided by #kevinarpe, with alternate formatting to place the output in evidence:
set -u
function count() { echo $# ; };
(
count x y z
)
: prints "3"
(
arr=()
count "${arr[#]}"
)
: prints "-bash: arr[#]: unbound variable"
(
arr=()
count "${arr[#]:0}"
)
: prints "0"
(
arr=(x y z)
count "${arr[#]:0}"
)
: prints "3"
This behaviour varies with versions of Bash. You may also have noticed that the length operator ${#arr[#]} will always evaluate to 0 for empty arrays, regardless of set -u, without causing an 'unbound variable error'.
Here are a couple of ways to do something like this, one using sentinels
and another using conditional appends:
#!/bin/bash
set -o nounset -o errexit -o pipefail
countArgs () { echo "$#"; }
arrA=( sentinel )
arrB=( sentinel "{1..5}" "./*" "with spaces" )
arrC=( sentinel '$PWD' )
cmnd=( countArgs "${arrA[#]:1}" "${arrB[#]:1}" "${arrC[#]:1}" )
echo "${cmnd[#]}"
"${cmnd[#]}"
arrA=( )
arrB=( "{1..5}" "./*" "with spaces" )
arrC=( '$PWD' )
cmnd=( countArgs )
# Checks expansion of indices.
[[ ! ${!arrA[#]} ]] || cmnd+=( "${arrA[#]}" )
[[ ! ${!arrB[#]} ]] || cmnd+=( "${arrB[#]}" )
[[ ! ${!arrC[#]} ]] || cmnd+=( "${arrC[#]}" )
echo "${cmnd[#]}"
"${cmnd[#]}"
Now, as technically right the "${arr[#]+"${arr[#]}"}" version is, you never want to use this syntax for appending to an array, ever!
This is, as this syntax actually expands the array and then appends.
And that means that there is a lot going on computational- and memory-wise!
To show this, I made a simple comparison:
# cat array_perftest_expansion.sh
#! /usr/bin/bash
set -e
set -u
loops=$1
arr=()
i=0
while [ $i -lt $loops ] ; do
arr=( ${arr[#]+"${arr[#]}"} "${i}" )
#arr=arr[${#arr[#]}]="${i}"
i=$(( i + 1 ))
done
exit 0
And then:
# timex ./array_perftest_expansion.sh 1000
real 1.86
user 1.84
sys 0.01
But with the second line enabled instead, just setting the last entry directly:
arr=arr[${#arr[#]}]="${i}"
# timex ./array_perftest_last.sh 1000
real 0.03
user 0.02
sys 0.00
If that is not enough, things get much worse, when you try to add more entries!
When using 4000 instead of 1000 loops:
# timex ./array_perftest_expansion.sh 4000
real 33.13
user 32.90
sys 0.22
Just setting the last entry:
# timex ./array_perftest_last.sh 4000
real 0.10
user 0.09
sys 0.00
And this gets worse and worse ... I could not wait for the expansion version to finish a loop of 10000!
With the last element instead:
# timex ./array_perftest_last.sh 10000
real 0.26
user 0.25
sys 0.01
Never use such an array expansion for any reason.
Interesting inconsistency; this lets you define something which is "not considered set" yet shows up in the output of declare -p
arr=()
set -o nounset
echo ${arr[#]}
=> -bash: arr[#]: unbound variable
declare -p arr
=> declare -a arr='()'
UPDATE: as others mentioned, fixed in 4.4 released after this answer was posted.
The most simple and compatible way seems to be:
$ set -u
$ arr=()
$ echo "foo: '${arr[#]-}'"

How to handle "--" in the shell script arguments?

This question has 3 parts, and each alone is easy, but combined together is not trivial (at least for me) :)
Need write a script what should take as its arguments:
one name of another command
several arguments for the command
list of files
Examples:
./my_script head -100 a.txt b.txt ./xxx/*.txt
./my_script sed -n 's/xxx/aaa/' *.txt
and so on.
Inside my script for some reason I need distinguish
what is the command
what are the arguments for the command
what are the files
so probably the most standard way write the above examples is:
./my_script head -100 -- a.txt b.txt ./xxx/*.txt
./my_script sed -n 's/xxx/aaa/' -- *.txt
Question1: Is here any better solution?
Processing in ./my_script (first attempt):
command="$1";shift
args=`echo $* | sed 's/--.*//'`
filenames=`echo $* | sed 's/.*--//'`
#... some additional processing ...
"$command" "$args" $filenames #execute the command with args and files
This solution will fail when the filenames will contain spaces and/or '--', e.g.
/some--path/to/more/idiotic file name.txt
Question2: How properly get $command its $args and $filenames for the later execution?
Question3: - how to achieve the following style of execution?
echo $filenames | $command $args #but want one filename = one line (like ls -1)
Is here nice shell solution, or need to use for example perl?
First of all, it sounds like you're trying to write a script that takes a command and a list of filenames and runs the command on each filename in turn. This can be done in one line in bash:
$ for file in a.txt b.txt ./xxx/*.txt;do head -100 "$file";done
$ for file in *.txt; do sed -n 's/xxx/aaa/' "$file";done
However, maybe I've misinterpreted your intent so let me answer your questions individually.
Instead of using "--" (which already has a different meaning), the following syntax feels more natural to me:
./my_script -c "head -100" a.txt b.txt ./xxx/*.txt
./my_script -c "sed -n 's/xxx/aaa/'" *.txt
To extract the arguments in bash, use getopts:
SCRIPT=$0
while getopts "c:" opt; do
case $opt in
c)
command=$OPTARG
;;
esac
done
shift $((OPTIND-1))
if [ -z "$command" ] || [ -z "$*" ]; then
echo "Usage: $SCRIPT -c <command> file [file..]"
exit
fi
If you want to run a command for each of the remaining arguments, it would look like this:
for target in "$#";do
eval $command \"$target\"
done
If you want to read the filenames from STDIN, it would look more like this:
while read target; do
eval $command \"$target\"
done
The $# variable, when quoted will be able to group parameters as they should be:
for parameter in "$#"
do
echo "The parameter is '$parameter'"
done
If given:
head -100 test this "File name" out
Will print
the parameter is 'head'
the parameter is '-100'
the parameter is 'test'
the parameter is 'this'
the parameter is 'File name'
the parameter is 'out'
Now, all you have to do is parse the loop out. You can use some very simple rules:
The first parameter is always the file name
The parameters that follow that start with a dash are parameters
After the "--" or once one doesn't start with a "-", the rest are all file names.
You can check to see if the first character in the parameter is a dash by using this:
if [[ "x${parameter}" == "x${parameter#-}" ]]
If you haven't seen this syntax before, it's a left filter. The # divides the two parts of the variable name. The first part is the name of the variable, and the second is the glob filter (not regular expression) to cut off. In this case, it's a single dash. As long as this statement isn't true, you know you have a parameter. BTW, the x may or may not be needed in this case. When you run a test, and you have a string with a dash in it, the test might mistake it for a parameter of the test and not the value.
Put it together would be something like this:
parameterFlag=""
for parameter in "$#" #Quotes are important!
do
if [[ "x${parameter}" == "x${parameter#-}" ]]
then
parameterFlag="Tripped!"
fi
if [[ "x${parameter}" == "x--" ]]
then
print "Parameter \"$parameter\" ends the parameter list"
parameterFlag="TRIPPED!"
fi
if [ -n $parameterFlag ]
then
print "\"$parameter\" is a file"
else
echo "The parameter \"$parameter\" is a parameter"
fi
done
Question 1
I don't think so, at least not if you need to do this for arbitrary commands.
Question 3
command=$1
shift
while [ $1 != '--' ]; do
args="$args $1"
shift
done
shift
while [ -n "$1" ]; do
echo "$1"
shift
done | $command $args
Question 2
How does that differ from question 3?

Extract parameters before last parameter in "$#"

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?
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 :(.
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.
last_arg="${!#}"
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
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
)
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
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
Try:
if [ "$#" -gt '0' ]; then
/bin/echo "${!#}" "${#:1:$(($# - 1))}
fi
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
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 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
This works for me, with sh and bash:
last=${*##* }
others=${*%${*##* }}

number of tokens in bash variable

how can I know the number of tokens in a bash variable (whitespace-separated tokens) - or at least, wether it is one or there are more.
The $# expansion will tell you the number of elements in a variable / array. If you're working with a bash version greater than 2.05 or so you can:
VAR='some string with words'
VAR=( $VAR )
echo ${#VAR[#]}
This effectively splits the string into an array along whitespace (which is the default delimiter), and then counts the members of the array.
EDIT:
Of course, this recasts the variable as an array. If you don't want that, use a different variable name or recast the variable back into a string:
VAR="${VAR[*]}"
I can't understand why people are using those overcomplicated bashisms all the time. There's almost always a straight-forward, no-bashism solution.
howmany() { echo $#; }
myvar="I am your var"
howmany $myvar
This uses the tokenizer built-in to the shell, so there's no discrepancy.
Here's one related gotcha:
myvar='*'
echo $myvar
echo "$myvar"
set -f
echo $myvar
echo "$myvar"
Note that the solution from #guns using bash array has the same gotcha.
The following is a (supposedly) super-robust version to work around the gotcha:
howmany() ( set -f; set -- $1; echo $# )
If we want to avoid the subshell, things start to get ugly
howmany() {
case $- in *f*) set -- $1;; *) set -f; set -- $1; set +f;; esac
echo $#
}
These two must be used WITH quotes, e.g. howmany "one two three" returns 3
set VAR='hello world'
echo $VAR | wc -w
here is how you can check.
if [ `echo $VAR | wc -w` -gt 1 ]
then
echo "Hello"
fi
Simple method:
$ VAR="a b c d"
$ set $VAR
$ echo $#
4
To count:
sentence="This is a sentence, please count the words in me."
words="${sentence//[^\ ]} "
echo ${#words}
To check:
sentence1="Two words"
sentence2="One"
[[ "$sentence1" =~ [\ ] ]] && echo "sentence1 has more than one word"
[[ "$sentence2" =~ [\ ] ]] && echo "sentence2 has more than one word"
For a robust, portable sh solution, see #JoSo's functions using set -f.
(Simple bash-only solution for answering (only) the "Is there at least 1 whitespace?" question; note: will also match leading and trailing whitespace, unlike the awk solution below:
[[ $v =~ [[:space:]] ]] && echo "\$v has at least 1 whitespace char."
)
Here's a robust awk-based bash solution (less efficient due to invocation of an external utility, but probably won't matter in many real-world scenarios):
# Functions - pass in a quoted variable reference as the only argument.
# Takes advantage of `awk` splitting each input line into individual tokens by
# whitespace; `NF` represents the number of tokens.
# `-v RS=$'\3'` ensures that even multiline input is treated as a single input
# string.
countTokens() { awk -v RS=$'\3' '{print NF}' <<<"$1"; }
hasMultipleTokens() { awk -v RS=$'\3' '{if(NF>1) ec=0; else ec=1; exit ec}' <<<"$1"; }
# Example: Note the use of glob `*` to demonstrate that it is not
# accidentally expanded.
v='I am *'
echo "\$v has $(countTokens "$v") token(s)."
if hasMultipleTokens "$v"; then
echo "\$v has multiple tokens."
else
echo "\$v has just 1 token."
fi
Not sure if this is exactly what you meant but:
$# = Number of arguments passed to the bash script
Otherwise you might be looking for something like man wc

Resources