bash : If branch for two different loop - bash

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.

Related

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

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

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

How can I highlight given values in a generated numeric sequence?

I often receive unordered lists of document IDs. I can sort and print them easy enough, but I'd like to print a line for each available document and show an asterisk (or anything really, just to highlight) next to all values in the given list.
Such as ...
$ ./t.sh "1,4,3" 5
1*
2
3*
4*
5
$
The first parameter is the unordered list, and the second is the total number of documents.
If by "available document" you mean an "existing file on disk", then assuming you have 5 total files, and you are checking to see if you have 1, 4 and 3. The following script will produce sorted output.
#!/bin/bash
#Store the original IFS
ORGIFS=$IFS
#Now Set the Internal File Separater to a comma
IFS=","
###Identify which elements of the array we do have and store the results
### in a separate array
#Begin a loop to process each array element
for X in ${1} ; do
if [[ -f ${X} ]] ; then
vHAVE[$X]=YES
fi
done
#Now restore IFS
IFS=$ORGIFS
#Process the sequence of documents, starting at 1 and ending at $2.
for Y in $(seq 1 1 $2) ; do
#Check if the sequence exists in our inventoried array and mark accordingly.
if [[ ${vHAVE[$Y]} == YES ]] ; then
echo "$Y*"
else
echo "$Y"
fi
done
Returns the result:
rtcg#testserver:/temp/test# ls
rtcg#testserver:/temp/test# touch 1 3 4
rtcg#testserver:/temp/test# /usr/local/bin/t "1,4,3" 5
1*
2
3*
4*
5
The following code works for me on your example.
Generate a sequence of the length given by the user
Split the first argument of your script (it will gives you an array A for example)
Use the function contains to check if one element from A is in the sequence generated by the step one
I don't check the arguments length and you should do that to have a more proper script.
#!/bin/bash
function contains() {
local n=$#
local value=${!n}
for ((i=1;i < $#;i++)) {
if [ "${!i}" == "${value}" ]; then
echo "y"
return 0
fi
}
echo "n"
return 1
}
IFS=', ' read -a array <<< $1
for i in $(seq $2); do
if [ $(contains "${array[#]}" "${i}") == "y" ]; then
echo "${i}*"
else
echo "${i}"
fi
done
You can use parameter substitution to build an extended pattern that can be used to match document numbers to the list of documents to mark.
#!/bin/bash
# 1,4,3 -> 1|4|3
to_mark=${1//,/|}
for(( doc=1; doc <= $2; doc++)); do
# #(1|4|3) matches 1, 4 or 3
printf "%s%s\n" "$doc" "$( [[ $doc = #($to_mark) ]] && printf "*" )"
done

Ambigous redirect in a BASH script consisting 2 functions, taking 3 parameters

I found a bug in my bash script, and I have no idea how to fix it.
I have a main script in which at a certain point I'm calling the other script.
The other time_diff.sh script consists of 2 functions, and they look like this:
#!/bin/bash
function timeDiff() {
local time1="$(head -n 1 "$1")"
local time2="$(head -n 1 "$2")"
pc=$(( $time2 - $time1 ))
timediff=${pc#-}
pc=$(perl $BVTDIR/engine/math.pl $pc $time1)
pc=${pc#-}
echo "Prozentdiffertenz = "$pc"%"
echo "Laufzeitdiffertenz = "$timediff" milisekunden"
if (( "$timediff" < "$MAX_TIME_DIFF")) && (( "$pc" < "$MAX_PERC_DIFF" )); then
# the script is doing nothing, just echoing that everything is fine
elif (( "$timediff" > "$MAX_TIME_DIFF")) && (( "$pc" < "$MAX_PERC_DIFF" )); then
# the script is doing nothing, just echoing that everything is fine
elif (( "$timediff" < "$MAX_TIME_DIFF")) && (( "$pc" > "$MAX_PERC_DIFF" )); then
# the script is doing nothing, just echoing that everything is fine
elif (( "$timediff" > "$MAX_TIME_DIFF")) && (( "$pc" > "$MAX_PERC_DIFF" )); then
# the script is runing a diff and saving the output.
diff $1 $2 >> $3
fi
}
function recuDiff {
find $1 -maxdepth 1 -mindepth 1 -type f -name \*.TIME.TXT -printf '%P\n' |
while read each
do
if [ -e $2/"$each" ]
then
timeDiff {$1,$2}/"$each"
fi
done
}
recuDiff $1 $2
This script is called from the main script like this:
/bin/bash time_diff.sh $CURRENT_BUILD_DIR $PREVIOUS_BUILD_DIR $DIFF_DIR/DIFF_RUNTIMES.TXT
Until the timeDiff() function went to the parts of if-elif statements where the script was just doing nothing, printing out that everything is fine - everything WAS fine.
Today Ive found out that when the if-elif statement goes to the last section, where it should run the diff on the files and save it to the file, I get:
time_diff.sh: line 34: $3: ambiguous redirect
Line 34:
diff $1 $2 >> $3
And the file is never created.
What could be wrong? Should I touch DIFF_TIME.TXT before running the time_diff.sh script?
One more thing - it is VERY hard to reproduce this bug, it takes an hour to run throuhg the whole Jenkins job, and I have ABSOLUTELY no guarantee that one of the things this script is testing will run longer than it should, so I havent tried any solution yet.
It seems that the problem is simply that $3 is not defined.
Observe:
echo foo >> "$3"
bash: $3: ambiguous redirect
Note that positional parameters ($1 ... $9) are not shared or inherited. Each shell function has its own set of positional parameters.
You called your script with three arguments. But you called the function with only two arguments. That is why $3 is undefined inside the function.
Observe:
$ cat foo.sh
echo "$1" "$2" "$3"
func() {
echo "$1" "$2" "$3"
}
func "$1" "$2"
$ bash foo.sh a b c
a b c
a b
Also as a general rule: put quotes around all variable references.
Within a shell function, $1, $2, etc. refer to the arguments passed to the function, not the ones passed to the script. In your case, you are calling
timeDiff {$1,$2}/"$each"
which passes two arguments to timeDiff (outside the case where you might have spaces in the arguments - you might need to reconsider some of your quoting bits), yet timeDiff() is referring to $3, which will be undefined.

compound comparisons in bash

can anybody explain why the following bash code involving compound operators is not behaving as expected? basically, nothing enters the if statement inside the for loop but i am passing it correct parameters that should return something by running:
./my_bash_script 20100101 20120101
dates.txt is a list of all days since 2000
#!/bin/bash
old_IFS=$IFS
IFS=$'\n'
lines=($(cat dates.txt)) # array
IFS=$old_IFS
for (( i=1; i<${#lines[#]}; i++ ))
do
if [[ ${line[$i]} -ge $1 && ${line[$i]} -le $2 ]]; then
echo 0 > ${line[$i]} # redirect to file
echo ${line[$i]}
fi
done
The problem is that you've declared an array named lines, but then you try to access it as though it were named line. You need to change every occurrence of ${line[$i]} to ${lines[$i]}.
Better yet, you can dispense with the arithmetic for-loop, and write:
for line in "${lines[#]}" ; do
which will let you refer to the line as $line or "$line" rather than as ${lines[$i]}.
(By the way, how come you have that logic to modify $IFS? It seems like its default value would work just as well.)

Resources