Determining if there is a next argument while iterating through the arguments - shell

While iterating through the arguments, how do you determine if there is a next argument?
The way I tried to approach this was to check if the next argument is not empty but I ran into some problems.
Here in this example I print the value of the current argument and if there is an argument that comes after that then print some message.
My approach:
use $i+1 where $i+1 will give you the value of the next index.
#!/bin/sh
for i in "$#"
do
echo $i
if ! [ ${i+1}="" ]; then
echo "test"
fi
done
sh test 1 2 3 4 5
but that didn't work. I also tried expr i + 1, but that didn't work as well.
If anyone could give me a hint on how to approach this problem that would be really appreciated.

#!/bin/sh
while [ $# -gt 0 ] ; do
echo $1
if [ -n "${2+x}" ]; then
echo another arg follows
fi
shift
done
$ ./test.sh 1 2 3
1
another arg follows
2
another arg follows
3
The trick here is that we use shift for consuming the argument list instead of iterating over it. The next argument is always $1, which we know exists because we only execute the loop if $# (the count of the positional arguments, not including $0) is positive. To check whether the argument after that, $2, exist, we can use the ${PARAM+WORD} expansion, which produces nothing if PARAM doesn't exist, otherwise produces WORD.
Of course, shift destroys the argument list. If you don't want that, move things into a function. The following example shows how we can process the same argument list twice by passing a copy into a function in which shift locally eats it:
#!/bin/sh
func() {
while [ $# -gt 0 ] ; do
echo $1
if [ -n "${2+x}" ]; then
echo another arg follows
fi
shift
done
}
func "$#"
func "$#"
$ ./test.sh 1 2 3
1
another arg follows
2
another arg follows
3
1
another arg follows
2
another arg follows
3

You can use a counter and check for $#:
n=1
for i in "$#"; do
echo "$i"
if [ $n -eq $# ]; then
echo "test"
fi
n=$(expr $n + 1)
done

Related

bash : If branch for two different loop

I am writing a for loop. But the loop is dependent on the content of positional argument.
If the positional arguments are seq 2 1 10, the loop is for i in $(seq 2 1 10)
If the positional arguments are purely numbers such as 1 2 5 7 10, then the loop is for i in 1 2 5 7 10.
I tried this, but it didn't work:
test () {
if [[ $1 == seq ]]
then
for i in $(seq $2 $3 $4)
else
for i in $#
fi
do
echo $i
done
}
I also tried this:
test2 () {
if [[ $1 == seq ]]
then
sss="for i in $(seq $2 $3 $4)"
else
sss="for i in $#"
fi
$sss
do
echo $i
done
}
also doesn't work.
So my questions are:
I know I could write explicit two loop inside if. But if loop content is large, this is a waste of code space. Is there any better way?
In my second attempt, why doesn't $sss expand to a for sentence and get parsed properly by bash?
Save the list of numbers in an array.
test () {
if [[ $1 == seq ]]
then
numbers=($(seq "$2" "$3" "$4"))
else
numbers=("$#")
fi
for i in "${numbers[#]}"
do
echo $i
done
}
In my second attempt, why doesn't $sss expand to a for sentence and get parsed properly by bash?
A variable can be expanded into a command to run, but not into a flow control construct like a for loop or if statement. Those need to be written out directly, they can't be stored in variables. If you try, bash will attempt to run a command named for--that is, it will look in /bin, /usr/bin, etc., for a binary named for.
An alternative to using arrays as in John Kugelman's answer is to use set -- to change the positional parameters:
test ()
{
if [[ $1 == seq ]]; then
set -- $(seq $2 $3 $4)
fi
for i; do
echo $i
done
}
Note that for i is equivalent to for i in "$#".
John already mentioned why it didn't work - variable interpolation and splitting happens after control flow is parsed.

Command line arguments in shell script

Following is the shell script which iterates over command line arguments and prints the values
for var in "$#"
do
echo $var
done
Now if i want to iterate from the second command line argument (the first argument being used for some other purpose), what is the command to exclude the first argument alone in iteration ?
Use shift:
#!/usr/bin/env bash
shift
for var in "$#"; do
echo "$var"
done
The first argument is in $1, so doing
var1=$1
will store it into var1
You can then use shift to "delete" the first arg and still use your for loop:
~$cat s.sh
var1=$1
shift
echo var1=$var1
for var in "$#"
do
echo $var
done
~$ ./s.sh 1 2 3
var1=1
2
3
From man bash:
shift [n]
The positional parameters from n+1 ... are renamed to $1 .... Parameters
represented by the numbers $# down to $#-n+1 are unset. n
must be a non-negative number less than or equal to $#. If n
is 0, no parameters are changed. If n is not given, it is assumed to be 1. If n is greater than $#, the positional parameters
are not changed. The return status is greater than zero if n is
greater than $# or less than zero; otherwise 0.
This example behaves more like a standard unix command line utility. Options are processed in any order and can have modifiers. It does not work for every situation (like modifiers with spaces in them), but I've found it to be very useful for scripts that I want to have some default behavior, but occasionally I want to modify one or more parameters.
#!/bin/bash
#defaults
VAR1="Yours"
VAR2="Mine"
VAR3="Ours"
SHARENICE="false"
while [ $# -gt 0 ]; do
case "$1" in
#single parameter
-s) SHARENICE="true"
shift
;;
#modified parameters
-1) VAR1="$2"
shift 2
;;
-2) VAR2="$2"
shift 2
;;
-3) VAR3="$2"
shift 2
;;
#you could put a 'usage' function here,
#or if your last parameter has no modifier,
#like a mandatory input, it will now be at
#$1
*)
break
;;
esac
done
echo -n "$VAR1, $VAR2 and $VAR3. "
if [ "$SHARENICE" != "true" ]; then
echo "Too bad we don't get along."
else
echo "Good thing we play nice!"
fi

How to fetch last argument and stop before last arguments in shell script?

I want to merge all files into one. Here, the last argument is the destination file name.
I want to take last argument and then in loop stop before last arguments.
Here code is given that I want to implement:
echo "No. of Argument : $#"
for i in $* - 1
do
echo $i
cat $i >> last argument(file)
done
How to achieve that?
Using bash:
fname=${!#}
for a in "${#:1:$# - 1}"
do
echo "$a"
cat "$a" >>"$fname"
done
In bash, the last argument to a script is ${!#}. So, that is where we get the file name.
bash also allows selecting elements from an array. To start with a simple example, observe:
$ set -- a b c d e f
$ echo "${#}"
a b c d e f
$ echo "${#:2:4}"
b c d e
In our case, we want to select elements from the first to the second to last. The first is number 1. The last is number $#. We want to select all but the last. WE thus want $# - 1 elements of the array. Therefore, to select the arguments from the first to the second to last, we use:
${#:1:$# - 1}
A POSIX-compliant method:
eval last_arg=\$$#
while [ $# -ne 1 ]; do
echo "$1"
cat "$1" >> "$last_arg"
shift
done
Here, eval is safe, because you are only expanding a read-only parameter in the string that eval will execute. If you don't want to unset the positional parameters via shift, you can iterate over them, using a counter to break out of the loop early.
eval last_arg=\$$#
i=1
for arg in "$#"; do
echo "$arg"
cat "$arg" >> "$last_arg"
i=$((i+1))
if [ "$i" = "$#" ]; then
break
fi
done

For loop to find out if directory exists in unix

I want to use a script that checks whether a list of directories exists or not and at the same time it should print some custom message that I am sending.
For example:
I have a script that validates if directory exists or not:
**check.sh**
for i in $*
if [ -d "$i" ]; then
echo Found <msg-i> directory.
else
echo <msg-i> directory not found.
Now I want to call this script like this:
./check.sh $DIR1 msg1 $Dir2 msg2 $Dir3 msg3
So if DIR1 doesn't exist then I want to display message as "msg1 directory not found", similarly for DIR2 I want to show "msg2 directory not found". Here msg1 and msg2 are something I want to pass as string. How to achieve this? I am using bash shell.
Try this:
while [ -n "$1" ]
do
dir="$1"
msg="$2"
if [ -d "$dir" ]; then
echo "$msg dir FOUND"
else
echo "$msg dir NOT FOUND"
fi
shift 2
done
shift <n> command simply shifts left positional parameters passed to the script of n positions.
For example if you call a script with:
./myscript 1 2 3 4
$1 is "1" and $2 is "2"
but if you shift 2 then $1 is "3" and $2 is "4".
In this way the loop consumes 2 parameters per cycle until $1 parameter is an empty string ( -n "$1").
while condition can be written more elegantly as:
while (( $# ))
obtaining the same result.
You can also check for the second parameter (while [ -n "$2" ]) but this changes the behavior when user provides an odd number of parameters:
in the first case last directory will be checked but you'll have a strange message because $msg il empty
il the second case you'll not have strange messages, but last directory will silently not be checked
Better test parameters at the beginning:
if (( $# % 2 ))
then
echo "Provide an even number of parameters"
exit 1
fi
Chepner Says:
The while condition can simply be (( $# )) (test if the number of positional parameters is non-zero).
Chaitanya Says:
Hi Chepner, thanks for providing alternate solution, can you please tell me how the while condition should actually look like in order to use $# , I tried different ways but it is not working for me.
Here's a quick sample:
while (( $# ))
do
dir=$1
msg=$2
shift 2
[...]
done
The while (( $# )) will be true as long as there are any command line arguments. Doing the shift twice removes arguments from the list. When no more arguments, the while loop ends.
#Zac has the correct answer.
One tip for the message: use a printf format string:
./check.sh dir1 "can't locate %s directory"
and in the script:
if [[ ! -d "$dir" ]]; then
printf "$msg" "$dir"

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=${*%${*##* }}

Resources