Problem with function arguments and for loop in bash - bash

Why doesn't this print all the passed arguments, in bash?
function abc() {
echo "$1" #prints the correct argument
for x in `seq 1 $#`; do
echo "$x" #doesn't print the 1st, 2nd, etc arguments, but instead 1, 2, ..
done
}
It is printing
1
2
3
4
...
instead.

I'll just add a couple more options to what everyone else has given. The closest to the way you're trying to write this is to use bash indirect expansion:
function abc() {
for x in `seq 1 $#`; do
echo "${!x}" # the ! adds a level of indirection
done
}
...another option if you want to operate on only some of the arguments, is to use array slicing with $#:
function def() {
for arg in "${#:2:3}"; do # arguments 2 through 4 (i.e. 3 args starting at number 2)
echo "$arg"
done
}
similarly, "${#:2}" will give you all arguments starting at number 2, "${#:$start:$((end-start+1))}" will give you arguments $start through $end (the $(( expression calculates how many arguments there are between $start and $end), etc...

Actually there is a special short-hand for this case:
function abc() {
for arg ; do
echo "$arg"
done
}
That is, if the in ... part is omitted, arg loops over the function's argument $#.
Incidentally if you have for arg ; ... outside of a function, it will iterate over the arguments given on the command line.

The seq command returns all numbers from start to stop. What you are calling here is seq 1 <number_of_arguments_to_abc>. For example, if you call abc alpha beta gamma, then the arguments would be seq 1 3, thus you get the numbers 1, 2 and 3.
If you want the arguments to abc instead, the expression is for x in "$#".

If you want to print all arguments try this
function abc() {
for arg in $#; do
echo "$arg"
done
}

The variable X holds the literal numbers. You're trying to do indirection - substitute $1 where there's a $x. Indirection warps the brain. $# provides a simpler mechanism for looping over the arguments - without any adverse effects on your psyche.
for x in "$#"; do
echo $x
done
See the bash man page for more details on $#.

You should use the for arg form that others have shown. However, to address some things in your question and comments, see the following:
In Bash, it's not necessary to use seq. You can use C-style for loops:
for ((i = 2; i <= $#; i++))
do
echo "${#:i:1}"
done
Which demonstrates array slicing which is another technique you can use in addition to direct iteration (for arg) or using shift.
An advantage of using either version of for is that the argument array is left intact, while shift modifies it. Also, with the C-style form with array slicing, you could skip any arguments you like. This is usually not done to the extent shown below, because it would rely on the arguments following a strict pattern.
for ((i = 2; i < $# - 2; i+=2))
That bit of craziness would start at the second argument, process every other one and stop before the last two or three (depending on whether $# is odd or even).

Related

Why doesn't ${*// /*} work to replace blanks with * in command line arguments?

I want my script to perform the product of all its integer arguments. Instead of performing a loop I tried to replace blanks with * and then compute the operation. But I got the following result which I don't understand:
#!/bin/bash
# product.sh
echo $(( ${*// /*} )) # syntax error with ./product.sh 2 3 4
args=$*
echo $(( ${args// /*} )) # ./product.sh 2 3 4 => outputs 24
How is it that the first one produces an error while using an intermediate variable works fine?
How is it that the first one produces an error:
From the Bash Reference Manual:
If parameter is ‘#’ or ‘*’, the substitution operation is applied to each positional parameter in turn
(emphasis mine)
That is, the expression ${*// /*} replaces spaces inside positional parameters, not the spaces separating positional parameters. That expression expands to 2 3 4 (which gives a syntax error when used in an arithmetic context), since the parameters itself don't contain a space. Try with
./product '2 ' '3 ' 4
and you will see the difference.
In your example, the value $* does not actually contain any literal spaces, so ${*// /*} does not do anything.
If it did, those asterisks would be subject to wildcard expansion, so the idea of performing a substitution would seem to be rather brittle even if it worked.
I would simply create a function to process the arguments, instead of rely on trickery with substitutions -- these tend to have icky corner cases when one of the arguments is a variable or etc.
mul () {
case $# in
[01]) echo "$#";;
*) local n=$1; shift; echo $((n * $(mul "$#")));;
esac
}
You may utilize IFS:
#!/bin/bash
# product.sh
# set IFS to *
IFS='*'
# use IFS in output
echo "$*"
# perform arithmetic
echo "$(( $* ))";
Output:
2*3*4
24
Or use printf, like this:
echo $(( $(printf '%s*' $*)1 ))

executing variable substitution/String manipultion N times

Repeating same command n times. Question has been asked before
But methods in that question are not working for variable assignments
Eg.
var='abc,xyz,mnx,duid'
for f in `seq 3`; do var=${var%,*}; done
Above works but using it in function as described in other question does't work
Eg.
repeat() { num="$"; shift; for f in $(seq $num); do $1; done; }
repeat 3 'var=${var%,*}'
repeat 3 "var=${var%,*}"
Doesn't work
Based on your sample data
var='abc,xyz,mnx,duid'
you can also have the same effect by concatenating the search terms multiple times
var=${var%,*,*,*}
That said, you could also do things like
var=${var%$(printf ',*%.0s' {1..3})}
or
n=3; var=${var%$(printf ',*%.0s' $(seq $n))}
Variable expansions and assignments aren't processed after expanding variables. You need to use eval to re-execute it as a shell command.
You also have a typo, "$" should be "$1".
repeat() { num="$1"; shift; for f in $(seq $num); do eval "$1"; done; }
repeat 3 'var=${var%,*}'
echo "$var" # output = abc
Note that the argument needs to be in single quotes, otherwise the variable expansion will be processed once before calling the function.

Passing an array to a function as one argument and other arguments after it

I want to pass an array to a function and the loop thru it.
is_node ${nodes[#]}
if I try to loop
function is_node(){
for role in "${1[#]}"
do
I get the following error:
bad substitution
If I first try to check the number of arguments, I notice there are more than one.
function is_node(){
if [[ $# -ne 1 ]] then
echo "Error - The number of arguments is not correct. 1 argument(a role name) needed"
I want to pass the array, just as one argument, and pass other arguments after
is_node array status limit
then inside the function loop thru it.
The question is perfectly valid and don't think its a duplicate of Passing arrays as parameters in bash.
The problem with passing the array as argument to the function as "${nodes[#]}" or ${nodes[#]} in this case would be at the receiving side, the array contents are not kept intact, because the contents of the array is expanded before the function is called. So when the arguments are unpacked at the receiver, they are split at $1, $2 till the size of the array. You could see it from this simple example,
set -x
newf() { echo "$1"; echo "$2"; echo "$3"; }
arr=(1 2 3)
newf "${arr[#]}"
+ newf 1 2 3
+ echo 1
1
+ echo 2
2
+ echo 3
3
as you can see the array arr is expanded to the list of positional arguments while the intention was to use an array.
So given this problem and with your claim that you have additional argument flags after the array, you need to identify in the receiver side, how to start processing arguments after the array. The best way would be to pass the array expansion using *, so that the elements quoted as a whole.
So assuming your function expects 3 arguments to it, you can define it as below. The read command on the receiver will split the whole string of array content to individual elements and store it in the array arrayArgs and you can parse it as you wish.
is_node(){
(( $# < 3 )) && { printf 'insufficient args provided' >&2; return 1; }
read -ra arrayArgs <<<"$1"
printf 'Printing the array content \n'
for element in "${arrayArgs[#]}"; do
printf '%s\n' "$element"
done
printf '2nd arg=%s 3rd arg=%s\n' "$2" "$3"
}
and pass the array as
list=(1 2 3)
is_node "${list[*]}" 4 5
I assume that you want to write function with both arguments - array and traditional "single" ones. If I am mistaken please let me know.
My solution:
#!/bin/bash
function_with_array_and_single_argument () {
declare -a _array1=("${!1}")
echo "${_array1[#]}"
echo $2
}
array="a
b
c"
function_with_array_and_single_argument "array[#]" "Szczerba"
Output:
$ ./script.sh
a
b
c
Szczerba
You can pass in a list of arguments any way you like. The arguments to the function are simply "$#".
is_node(){
for role in "$#"; do
: something with "$role"
done
}
is_node "${nodes[#]}"
Notice also the proper use of quoting, and the omission of the (gratuitous, here) Bash-only keyword function.
More tangentially, the shell assumes in "$#" if you don't pass an explicit list of tokens, so this can (slightly obscurely) be simplified to for role; do
If you have a fixed number of other arguments, just put them before the variable-length list of arguments.
Nice Szczerba!
Your solution works perfectly, the contents of the array can change without changing the relative position of other variables, and that sets your answer appart. Here is an example backup script that can handle different sub directory's based on your solution.
#!/bin/bash
#Logging
logpath="/tmp/ram1"
eDate=$( date '+%Y%m%d_%H%M%S' )
ext="log"
#Backup Source/Destination drives and folder
src1="/mymedia"
subs1=(video audio)
lbl1="M_ResQ1"
dest1="/mnt/media/SG_ResQ1/Archive"
src2="/mymedia"
subs2=(TVSeries _In Test pic Theater)
lbl2="M_ResQ2"
dest2="/mnt/media/SG_ResQ2/Archive"
opt="-va --partial --del"
#-q quite
#-n dry run
#-P is = --partial --progress
Arc (){ # $1 subs $2 from $3 lbl $4 dest
declare -a subs=("${!1}")
from="$2"
lbl=$3
dest=$4
if [ -d "$dest" ]; then
for i in "${subs[#]}"; do
#logto=${logpath}/${eDate}_${lbl}_${i}.${ext}
logto=${logpath}/${eDate}_${lbl}.${ext}
echo $logto $lbl $dest
echo -e "\n\nStarting:\n\t${i}\tinto\t${lbl}\n\t${eDate}\n\t${opt}\n\n" | tee -a ${logto}
rsync ${opt} ${from}/${i} ${dest}/ | tee -a ${logto}
done
echo $( date '+Done %Y%m%d_%H%M%S' ) | tee -a ${logto}
cp ${logto} ${dest}/
else
echo -e "Not mounted or wrong drive"
fi
}
Arc "subs1[#]" $src1 $lbl1 $dest1
Arc "subs2[#]" $src2 $lbl2 $dest2

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 SCRIPT : How to use a variable inside another variable

I need to store command line arguments passed in an array
my Command is
./test1.sh 2 4 6
Now i need to store 2 4 6 in an array and im using..
s1=$#
"it tells how many arguments are passed."
for (( c=1; c<=$s1; c++ ))
do
a[$c]}=${$c}
I have written ${$c} for taking the arguments value but it is showing bad substition.
This will give you an array with the arguments: args=("$#")
And you can call them like this: echo ${args[0]} ${args[1]} ${args[2]}
bash variable can be indirectly referenced by \$$VARNAME or by ${!VARNAME} in version 2
so your assignment statement must be :
a[$c]=${!c}
or
eval a[$c]=\$$c
You can always pick the first argument and drop it. Use $1 and shift.
c=1
while [ $# -gt 0 ]; do
a[$c]=$1
c=$((c+1))
shift
done

Resources