Struggling with while loop with two conditions and a for loop - bash

I'm trying to learn how to use arrays in bash. I'm writing a script that asks the user for three numbers and figures out what the biggest number is. I want the script to force the user to enter only numeric values. Furthermore, I want the three numbers should be different. I'm using an array to store user input. This is what I have so far:
## declare variables
order=("first" "second" "third")
answers=()
RED='\033[0;31m'
NC='\033[0m' # No Color
numbersonly() {
if [[ ! $1 =~ ^[0-9]+$ ]]; then
echo "${RED}$1 is not a valid number.${NC}"
else
answers+=("$input")
break
fi
}
inarray(){
for e in ${answers[#]}; do
if [[ $1 == $e ]]; then
echo "${RED}Warning.${NC}"
fi
done
}
readnumber(){
for i in {1..3}; do
j=$(awk "BEGIN { print $i-1 }")
while read -p "Enter the ${order[$j]} number: " input ; do
inarray $input
numbersonly $input
done
done
}
displayanswers(){
echo "Your numbers are: ${answers[#]}"
}
biggestnumber(){
if (( ${answers[0]} >= ${answers[1]} )); then
biggest=${answers[0]}
else
biggest=${answers[1]}
fi
if (( $biggest <= ${answers[2]} )); then
biggest=${answers[2]}
fi
echo "The biggest number is: $biggest"
}
main(){
readnumber
displayanswers
biggestnumber
}
main
Right now, I can get the script to display a warning when the user enters a number that was previously entered, but I can't seem to find the proper syntax to stay in the while loop if the user input has already been entered. Thoughts?

I found a way around it. My problem was twofold: 1) I didn't realize that if you have a for loop nested in a while loop, you'll need two break statements to exit the while loop; 2) having two functions within the same while loop made it hard to control what was happening. By merging inarray() and numbersonly() into a new function, I solved the double conditional issue. The new function looks like this:
testing(){
for item in ${answers[*]}
do
test "$item" == "$1" && { inlist="yes"; break; } || inlist="no"
done
if [[ $inlist == "yes" ]]; then
echo "${RED}$1 is already in list.${NC}"
else
if [[ ! $1 =~ ^[0-9]+$ ]]; then
echo "${RED}$1 is not a valid number.${NC}"
else
answers+=("$input")
break
fi
fi
}

Without much study here is what leapt off the screen to me follows. Beware I haven't actually tested it... debugging is an exercise left to the student.
Recommend using newer "function" definitions as you can declare local variables. () definitions do not allow localized variables.
function inarray
{
local e; #don't muck up any variable e in caller
...
}
To calculate values avoid extra awk and use j=$(( i - 1 ));
biggestnumber should likely use a loop.
Overall comment:
nummax=3; #maximum value defined in just one place
# loop this way... showing optional {} trick for marking larger loops
for (( n = 0; n < nummax; ++n )); do
{
nx=$(( 1 + n )); #1 based index
} done;
Hint: should stop input loop once all input present. Could also add:
if [ "" == "${input:-}" ]; then break;
for (( a = 0; a < ${#answer[*]}; ++a )); do
Note the extensive use of double quotes to avoid syntax errors if the variable value is empty or contains many shell metacharacters, like spaces. I can't tell you how many bug reports I've fixed by adding the quotes to existing code.
[[ ... ]] expressions use file name tests, not regular expressions. The closest you can get to [[ ! $1 =~ ^[0-9]+$ ]]; is using [[ "$1" != [0-9]* ]] && [[ "$1" != *[^0-9]* ]].
But I suspect ! expr >/dev/null "$i" : '[0-9][0-9]*$'; is more what you want as "expr" does use regular expressions. Don't enclose in []s. Used [0-9][0-9]* rather than [0-9]+ as "+" has given me mixed successes across all dialects of UNIX regular expressions.

Related

Bash script with multiline variable

Here is my code
vmname="$1"
EXCEPTLIST="desktop-01|desktop-02|desktop-03|desktop-04"
if [[ $vmname != #(${EXCEPTLIST}) ]]; then
echo "${vmname}"
else
echo "Its in the exceptlist"
fi
The above code works perfectly but my question is , the EXCEPTLIST can be a long line, say 100 server names. In that case its hard to put all that names in one line. In that situation is there any way to make the variable EXCEPTLIST to be a multiline variable ? something like as follows:
EXCEPTLIST="desktop-01|desktop-02|desktop-03| \n
desktop-04|desktop-05|desktop-06| \n
desktop-07|desktop-08"
I am not sure but was thinking of possibilities.
Apparently I would like to know the terminology of using #(${})- Is this called variable expansion or what ? Does anyone know the documentation/explain to me about how this works in bash. ?
One can declare an array if the data/string is long/large. Use IFS and printf for the format string, something like:
#!/usr/bin/env bash
exceptlist=(
desktop-01
desktop-02
desktop-03
desktop-04
desktop-05
desktop-06
)
pattern=$(IFS='|'; printf '#(%s)' "${exceptlist[*]}")
[[ "$vmname" != $pattern ]] && echo good
In that situation is there any way to make the variable EXCEPTLIST to be a multiline variable ?
With your given input/data an array is also a best option, something like:
exceptlist=(
'desktop-01|desktop-02|desktop-03'
'desktop-04|desktop-05|desktop-06'
'desktop-07|desktop-08'
)
Check what is the value of $pattern variable one way is:
declare -p pattern
Output:
declare -- pattern="#(desktop-01|desktop-02|desktop-03|desktop-04|desktop-05|desktop-06)"
Need to test/check if $vmname is an empty string too, since it will always be true.
On a side note, don't use all upper case variables for purely internal purposes.
The $(...) is called Command Substitution.
See LESS=+'/\ *Command Substitution' man bash
In addition to what was mentioned in the comments about pattern matching
See LESS=+/'(pattern-list)' man bash
See LESS=+/' *\[\[ expression' man bash
s there any way to make the variable EXCEPTLIST to be a multiline variable ?
I see no reason to use matching. Use a bash array and just compare.
exceptlist=(
desktop-01
desktop-02
desktop-03
desktop-04
desktop-05
desktop-06
)
is_in_list() {
local i
for i in "${#:2}"; do
if [[ "$1" = "$i" ]]; then
return 0
fi
done
return 1
}
if is_in_list "$vmname" "${EXCEPTLIST[#]}"; then
echo "is in exception list ${vmname}"
fi
#(${})- Is this called variable expansion or what ? Does anyone know the documentation/explain to me about how this works in bash. ?
${var} is a variable expansion.
#(...) are just characters # ( ).
From man bash in Compund commands:
[[ expression ]]
When the == and != operators are used, the string to the right of the operator is considered a pattern and matched according to the rules
described below under Pattern Matching, as if the extglob shell option were enabled. ...
From Pattern Matching in man bash:
#(pattern-list)
Matches one of the given patterns
[[ command receives the #(a|b|c) string and then matches the arguments.
There is absolutely no need to use Bash specific regex or arrays and loop for a match, if using grep for raw string on word boundary.
The exception list can be multi-line, it will work as well:
#!/usr/bin/sh
exceptlist='
desktop-01|desktop-02|desktop-03|
deskop-04|desktop-05|desktop-06|
desktop-07|deskop-08'
if printf %s "$exceptlist" | grep -qwF "$1"; then
printf '%s is in the exceptlist\n' "$1"
fi
I wouldn't bother with multiple lines of text. This is would be just fine:
EXCEPTLIST='desktop-01|desktop-02|desktop-03|'
EXCEPTLIST+='desktop-04|desktop-05|desktop-06|'
EXCEPTLIST+='desktop-07|desktop-08'
The #(...) construct is called extended globbing pattern and what it does is an extension of what you probably already know -- wildcards:
VAR='foobar'
if [[ "$VAR" == fo?b* ]]; then
echo "Yes!"
else
echo "No!"
fi
A quick walkthrough on extended globbing examples: https://www.linuxjournal.com/content/bash-extended-globbing
#!/bin/bash
set +o posix
shopt -s extglob
vmname=$1
EXCEPTLIST=(
desktop-01 desktop-02 desktop-03
...
)
if IFS='|' eval '[[ ${vmname} == #(${EXCEPTLIST[*]}) ]]'; then
...
Here's one way to load a multiline string into a variable:
fn() {
cat <<EOF
desktop-01|desktop-02|desktop-03|
desktop-04|desktop-05|desktop-06|
desktop-07|desktop-08
EOF
}
exceptlist="$(fn)"
echo $exceptlist
As to solving your specific problem, I can think of a variety of approaches.
Solution 1, since all the desktop has the same desktop-0 prefix and only differ in the last letter, we can make use of {,} or {..} expansion as follows:
vmname="$1"
found=0
for d in desktop-{01..08}
do
if [[ "$vmname" == $d ]]; then
echo "It's in the exceptlist"
found=1
break
fi
done
if (( !found )); then
echo "Not found"
fi
Solution 2, sometimes, it is good to provide a list in a maintainable clear text list. We can use a while loop and iterate through the list
vmname="$1"
found=0
while IFS= read -r d
do
if [[ "$vmname" == $d ]]; then
echo "It's in the exceptlist"
found=1
break
fi
done <<EOF
desktop-01
desktop-02
desktop-03
desktop-04
desktop-05
desktop-06
desktop-07
desktop-08
EOF
if (( !found )); then
echo "Not found"
fi
Solution 3, we can desktop the servers using regular expressions:
vmname="$1"
if [[ "$vmname" =~ ^desktop-0[1-8]$ ]]; then
echo "It's in the exceptlist"
else
echo "Not found"
fi
Solution 4, we populate an array, then iterate through an array:
vmname="$1"
exceptlist=()
exceptlist+=(desktop-01 desktop-02 desktop-03 deskop-04)
exceptlist+=(desktop-05 desktop-06 desktop-07 deskop-08)
found=0
for d in ${exceptlist[#]}
do
if [[ "$vmname" == "$d" ]]; then
echo "It's in the exceptlist"
found=1
break;
fi
done
if (( !found )); then
echo "Not found"
fi

How to tokenise string and call a function on each token in bash?

I have a text file with comma delimiter like following
for example str_data.txt
aaa,111,bbb
ccc,222,ddd
eee,333,fff
I have a bash function to validate each token (i.e. if each token is following some rule or not based on that function will echo true or false. (can leave it like [[ XYZ == "$1"]] also, instead of returning echo) )
for example
function validate_token {
local _rule = XYZ
if [[ XYZ == "$1" ]]; then
echo "true"
else
echo "false"
fi
}
I want to write a bash script (one-liner or multi-line) to validate all these tokens separately (i.e. validate_token "aaa" then validate_token "111") and finally answer "true" or "false" based on ANDing of each token's results.
Would yo please try the following:
validate_token() {
local rule="???" # matches a three-chraracter string
if [[ $1 == $rule ]]; then
echo 1
else
echo 0
fi
}
final=1 # final result
while IFS=',' read -ra ary; do
for i in "${ary[#]}"; do
final=$(( final & $(validate_token "$i") ))
# take AND with the individual test result
done
done < "str_data.txt"
(( $final )) && echo "true" || echo "false"
I've also modified your function due to several reasons.
When defining a bash function, the form name() { .. } is preferred.
It is not recommended to start the user's variable name with an underscore.
You have localized it and don't have to care about the variable name
collision.
When evaluating the conditional expression by using == or = operator
within [[ .. ]], it will be better to place the pattern or rule to the right of the
operator.
It will be convenient to return 1 or 0 rather than true or false for further calculation.
Hope this helps.
You can try the below, reading line by line and storing the values into an array, then iterating the array calling the function for each value :
IFS=","
while read line
do
read -ra lineValues <<< "$line"
for value in "${lineValues[#]}"
do
validate_token "$value"
done
done < your_txt_file

Removing a block of a certain character in pure bash

Hey so lets say you have a string "aabbaabbbaab". As you can see you have 3 blocks of "b". For example, how do I remove the 2nd block of b , so "bbb"? It should turn the string into: "aabbaaaab". I have tried looking everywhere but I just couldnt think of a right syntax for my specific question. I need to do this in pure bash so no awk, sed etc.
Here's pure bash: it iterates over the string, character by character. When it detects it's in the n'th block of the specified char, we know that the entire string up til here is the first part of the output we want. When we get to the end of the n'th block, we know that the rest of the string is wanted.
remove_nth_block () {
local str=$1 char=$2 n=$3
local i count=0 prev prefix
for ((i=0; i<${#str}; i++)); do
if [[ ${str:i:1} = $char && $prev != $char ]]; then
((++count == n)) && prefix=${str:0:i}
else
if [[ ${str:i:1} != $char && $prev = $char && $count -eq $n ]]; then
echo "$prefix${str:i}"
return
fi
fi
prev=${str:i:1}
done
}
Then
$ remove_nth_block aabbaabbbaab b 2
aabbaaaab
$ remove_nth_block aabbaabbbaab a 2
aabbbbbaab
This should print myString after replacing all occurrences of bbb with nothing. For some really useful tips and examples of string manipulation in bash, check out this site.
myString="aabbaabbbaab"
echo ${myString//bbb/}

Shell script for word in $sentece How to change the value of word?

I need some help from this awesome community.
I'm trying to write a script that loops through each word of a sentence stored in a variable (for example SENTENCE).
Example:
for WORD in $SENTENCE
do
echo do something
done
The problem that I'm facing is that I need to change the value of WORD to restart the loop if a certain condition is true inside the loop.
Example:
for WORD in $SENTENCE
do
echo do something
if [[ $SOMETHING_HAPPENED == TRUE ]]; then
WORD=$FIRST_WORD_IN_SENTENCE
fi
done
Basically, I need to restart the loop if certain conditions are met (SOMETHING_HAPPENED), but I don't know how to do this properly.
If this was a normal C loop I would do it like this:
for (i=0;i<10;i++){
do_something();
if (SOMETHING_HAPPENED == TRUE){
i=0;
}
}
How do I do this in shell script?
Thank you.
You could use a loop around your for loop that executes the for loop until the something doesn't happen:
repeat=yes
while [ "$repeat" = yes ]; do
repeat=no
for WORD in $SENTENCE; do
# do something
if [[ $SOMETHING_HAPPENED == TRUE ]]; then
repeat=yes
break
fi
done
done
while [[ 1 ]]; do # infinite loop
for word in $sentence; do
…
if [[ $condition ]]; then
continue 2 # go to the next iteration of the *outer* loop
fi
done
break # escape the outer loop
done
To restart the loop you can use BASH arrays and run your loop like this:
arr=( $sentence )
for ((i=0; i<${#arr[#]}; i++)); do
word="${arr[$i]}"
# evaluate condition
if [[ "$SOMETHING_HAPPENED" == TRUE ]]; then
i=0
continue
fi
echo "$word"
done

Bash script to input a group of numbers and pass them to other functions

This program is supposed to call the first function, read-series and then pass the input of every iteration of the while loop to the even-odds function which would tell if the number was even or odd and make VARSUMODDS=the value of VARSUMODDS+input if it was odd or make VARPRODUCTEVENS=the value of VARSUMEVENS*input. Then it would print them out. I'm sure there are a thousand syntax errors here, so please, be brutal. Keep in mind that I just started learning this language and I just came to it knowing only C++ and Java a few days ago, so don't expect me to understand complex answers. Thanks!
#! /bin/bash
TMPDIR=${HOME}/tmpdir
echo "Enter an integer: "
VARSUMODDS=0
VARPRODUCTEVENS=0
function read-series() {
while read numbers ; do
echo "Enter an integer: "
even-odds $numbers
done
echo numbers > $TMPDIR/$$.temp
return 0;
}
function even-odds() {
evenp=$(( $1 % 2 ))
if [ $evenp -eq 0 ] ; then
$VARPRODUCTEVENS=$(($VARPRODUCTEVENS * $1))
return 0;
else
$VARSUMODDS=$(($VARSUMODDS + $1))
return 1;
fi
}
function reduce () {
echo -n "Sum of odds: "
echo VARSUMODDS
echo -n "Product of evens: "
echo VARPRODUCTEVENS
return 0;
}
read-series
#! /bin/bash
tmpdir=${HOME}/tmpdir
mkdir -p $tmpdir
odd_sum=0
even_product=1
numbers=()
read-series() {
while read -p "Enter an integer (q to quit): " number ; do
[[ $number == [Qq]* ]] && break
even-odds $number
numbers+=($number)
done
printf "%d\n" "${numbers[#]}" > $tmpdir/$$.temp
}
even-odds() {
if (( $1 % 2 == 0 )) ; then
(( even_product *= $1 ))
else
(( odd_sum += $1 ))
fi
}
reduce () {
echo "Sum of odds: $odd_sum"
echo "Product of evens: $even_product"
}
read-series
reduce
Notes:
make sure your tmpdir exists
no good seeding your product variable with 0
use an array to store the list of numbers
provide a way to break out of the input loop
in bash, the == operator within [[ ... ]] is a pattern matching operator.
you don't actually use your function return values anywhere, so I removed them
to declare a function, you don't need to use both the "function" keyword and the parentheses
use read -p to provide the prompt
use arithmetic expressions more widely
to assign a variable, don't use $ on the left-hand side.
to get the value, you must use $
let the system use upper-case variable names, you don't want to accidentally overwrite PATH for example.
avoid writing temp files unless you really need them (for logging or auditing)
printf re-uses the format string until all the arguments are consumed. this is very handy for printing the contents of an array.
semi-colons are optional
You should initialize VARPRODUCTEVENS to 1, because multiplying anything by 0 produces 0.
$ should not be put before the variable being assigned in an assignment statement.
You can use the -p option to read to specify a prompt
You're writing to $$.temp after the loop is done. numbers will be empty then, so you're not writing anything to the file. If you want to record all the numbers that were entered, you must do that inside the loop, and use >> to append to the file instead of overwriting it.
There's no reason to use return in your functions -- nothing tests the exit status. And non-zero is usually used to mean there was an error.
You defined a function reduce to print the results, but never called it
You need to put $ before the variable names on all your echo lines.
Don't put function before function definitions; it's allowed, but not required, and not portable (it's a bash extension).
#! /bin/bash
TMPDIR=${HOME}/tmpdir
VARSUMODDS=0
VARPRODUCTEVENS=1
read-series() {
while read -p "Enter an integer: " numbers ; do
even-odds $numbers
echo $numbers >> $TMPDIR/$$.temp
done
}
even-odds() {
evenp=$(( $1 % 2 ))
if [ $evenp -eq 0 ] ; then
VARPRODUCTEVENS=$(($VARPRODUCTEVENS * $1))
else
VARSUMODDS=$(($VARSUMODDS + $1))
fi
}
reduce () {
echo ''
echo -n "Sum of odds: "
echo $VARSUMODDS
echo -n "Product of evens: "
echo $VARPRODUCTEVENS
}
read-series
reduce
If you run this script stand-alone, you have to end your input with CTRL-d. Here are the problems:
VARPRODUCTEVENS=0
has to be
VARPRODUCTEVENS=1
or your product will always be zero.
echo numbers > $TMPDIR/$$.temp
Seems to have no useful purpose. You are putting the string "numbers" into the file. If you use $numbers it still appears to have no purpose. You would be putting the single last number from the read into the file. From the use "number" may be a better name than "numbers"
$VARPRODUCTEVENS=$(($VARPRODUCTEVENS * $1))
and
$VARSUMODDS=$(($VARSUMODDS + $1))
has to be
VARPRODUCTEVENS=$(($VARPRODUCTEVENS * $1))
and
VARSUMODDS=$(($VARSUMODDS + $1))
Having $VARSUMODDS on the left of the assignment will try to assign to the variable named "1" (the value of $VARSUMODDS).
There is no call to reduce, so you see no results. I assume you want that at the end.
Your return statements are unnecessary, and probably not doing what you intended. You are basically setting the exit status, and non-zero implies failure.

Resources