Circular iteration in bash - bash

I'd have a couple of possible (say string) values: a b c. I'd like a to switch from one to another in a circular way:
if [ "$value" == "a" ]; then value=b
elif [ "$value" == "b"]; then value=c
elif [ "$value" == "c"]; then value=a
fi
With method like this, Can this be done in a more efficient way, so that I could support many options, like a b c ... z?

It can be done like this:
get_next()
{
__current_i=${__current_i:- 0} # global variable that saves index of current value
[ $__current_i -ge $# ] && __current_i=0 # if we exceed the number of values, then reset to zero
shift $__current_i # move to current value
((__current_i++)) # increment index
echo $1
}
val="a b c d"
get_next $val
get_next $val
get_next $val
get_next $val
get_next $val

You can use bash arrays
vals=('a' 'b' 'c')
value=$1
max=$(( ${#vals[#]} - 1 ))
count=0
while [ "$value" != "${vals[count]}" ]
do
count=$(( $count + 1 ))
done
if [ $count -eq $max ]; then
count=0
else
count=$(( $count + 1 ))
fi
echo ${vals[count]}

Why so complicated?
This will do what you want:
while true; do
for x in a b c; do
echo "$x"
done
done

Related

How can I reference arrays with different numbers in their name in Bash?

(Apologies in advance if my english is bad)
So, I have started studying bash, and want to make a Connect 4 game in it.
However, I got stuck a bit. I have 7 arrays named column0 to column6, and I want to reference them later on.
I read in another post a few days ago, that i can use nameref to do so, but I think I'm doing something wrong, as the function runs, but doesn't do anything (doesn't check for winner). Here is a snippet form the code
declare -n nr="column$j"
for ((j=0 ; j < 6; j++ )); do
if [ "${nr[0]}" = $k ] && [ "${nr[1]}" = $k ] && [ "${nr[2]}" = $k ] && [ "${nr[3]}" = $k ]; then
(this is part of the function that will check for a winner, in case 4 characters in a row match. 'k' is just a color variable, it can be $k='Y' or $k='R')
Could someone point out what I'm doing wrong here? Or am I just dumb and Bash can't work with such solutions?
Thank you in advance for any help.
Given your specifications I tried to reproduce a minimal example.
You can try to do something like this:
#!/usr/bin/env bash
k="Y"
nr_0=("R" "R" "Y" "Y")
nr_1=("R" "Y" "Y" "Y")
nr_2=("Y" "R" "Y" "Y")
nr_3=("Y" "Y" "R" "Y")
nr_4=("Y" "Y" "Y" "R")
nr_5=("Y" "Y" "Y" "Y") # the winner
nr_6=("Y" "R" "Y" "Y")
for j in {0..6}; do
tmp_array_name="nr_$j[#]"
tmp_array=("${!tmp_array_name}")
if [ "${tmp_array[0]}" == "$k" ] && [ "${tmp_array[1]}" == "$k" ] && [ "${tmp_array[2]}" == "$k" ] && [ "${tmp_array[3]}" == "$k" ]; then
echo "The winner is $tmp_array_name - ${tmp_array[*]}"
fi
done
With this output:
The winner is nr_5[#] - Y Y Y Y
You need the nameref declare assignment to be within your loop or it gets assigned an empty string:
for ((j=0 ; j < 6; j++ )); do
declare -n nr="column$j"
if [ "${nr[0]}" = $k ] && [ "${nr[1]}" = $k ] && [ "${nr[2]}" = $k ] && [ "${nr[3]}" = $k ]; then
Another option to nameref which is as risky as the dynamic naming ${!dyn_name_var}, is to use Bash's associative arrays:
declare -A grid=()
for ((column=0 ; j < 6; column++ )); do
if [ "${grid[$column,0]}" == "$k" ] &&
[ "${grid[$column,1]}" == "$k" ] &&
[ "${grid[$column,2]}" == "$k" ] &&
[ "${grid[$column,3]}" == "$k" ]
then
: ...
fi
: ...
done
This will use an associative arrays of key formed by column,line.
Example of the first two lines of the grid associative array as declared statically.
declare -A grid=(
[0,0]="o" [0,1]="x" [0,2]=" ", [0,3]="o"
[1,0]=" " [1,1]="o" [1,2]="x", [1,3]="o"
)
See: How to declare 2D array in bash

Write a function in shell script to check if the two numbers are Palindromes

I'm writing a function in shell script to check if the two numbers are Palindromes but I am getting an error, It is showing error in line 18 command not found. Please help me how can I remove this error.
#!/bin/bash
echo "Enter two number:"
read a
read b
for num in $a $b;
do
x=$x$sep$num
sep=" "
done
y=$x
num1=$a
num2=$b
rem=""
rev=0
for word in $y;
do
checkPalindrome $word
if [ $? -eq 0 ]
then
echo "$word is palindrome"
fi
done
checkPalindrome() {
local s=$1
for i in $s ;
do
while [ $i -gt 0]
do
rem=$(($i%10));
rev=$(($rev*10+$rem));
i=$(($i / 10));
done
done
if [[ $rev -eq $num1 && $rev -eq $num2 ]]
then
return 0;
else
return 1;
fi
}
You need to provide your checkPalindrome() definition before you use it, as below:
#!/bin/bash
checkPalindrome() {
local s=$1
for i in $s
do
while [ "$i" -gt 0 ]
do
rem=$((i%10))
rev=$((rev*10+rem))
i=$((i / 10))
done
done
if [[ $rev -eq $num1 && $rev -eq $num2 ]]
then
return 0
else
return 1
fi
}
echo "Enter two number:"
read -r a
read -r b
for num in $a $b
do
x="$x$sep$num"
sep=" "
done
y="$x"
num1="$a"
num2="$b"
rem=""
rev=0
for word in $y;
do
if checkPalindrome "$word"
then
echo "$word is palindrome"
fi
done
You could consider the input as string (you don't have to restrict it to be an integer)
#!/bin/bash
is_palindrome() {
local arg=$1 i j
for ((i = 0, j = ${#arg} - 1; i < j; ++i, --j)); do
[[ ${arg:i:1} = "${arg:j:1}" ]] || return
done
}
read -r -p 'Enter two words: ' a b
for word in $a $b; do
is_palindrome "$word" && echo "$word is palindrome"
done

Values of an array not comparing to numbers correctly

Im trying to get an array from grades.txt, and determine what letter grade it should be assigned.
I either get
hw4part2.sh: line 26: [: : integer expression expected
If i use -ge or
hw4part2.sh: line 26: [: : unary operator expected
If i use >=
Below is the code im trying to get working
mapfile -t scores < grades.txt
numOScores=0
numOA=0
numOB=0
numOC=0
numOD=0
numOF=0
DoneWScores=0
A=90
B=80
C=70
D=60
F=59
while [ $DoneWScores -eq 0 ]
do
numOScores=$((numOScores + 1))
if [ "${scores[$numOScores]}" -ge "$A" ]
then
echo "A"
elif [ "${scores[$numOScores]}" -ge "$B" ]
then
echo "B"
elif [ "${scores[$numOScores]}" -ge "$C" ]
then
echo "C"
elif [ "${scores[$numOScores]}" -ge "$D" ]
then
echo "D"
elif [ "${scores[$numOScores]}" -le "$F" ]
then
echo "F"
else
echo "Done/error"
DoneWScores=1
fi
done
If anyone knows what my problem is, that'd be greatly appreciated
Consider this:
#!/usr/bin/env bash
if (( ${BASH_VERSINFO[0]} < 4 )); then
echo "Bash version 4+ is required. This is $BASH_VERSION" >&2
exit 1
fi
letterGrade() {
if (( $1 >= 90 )); then echo A
elif (( $1 >= 80 )); then echo B
elif (( $1 >= 70 )); then echo C
elif (( $1 >= 60 )); then echo D
else echo F
fi
}
declare -A num
while read -r score; do
if [[ $score == +([[:digit:]]) ]]; then
grade=$(letterGrade "$score")
(( num[$grade]++ ))
echo "$grade"
else
printf "invalid score: %q\n" "$score"
fi
done < grades.txt
for grade in "${!num[#]}"; do
echo "$grade: ${num[$grade]}"
done | sort

How to use multiple condition in if statement in bash?

Actually I am a new bash learner. I can use one condition in bash command. But how to use multiple condition in bash? I can use if statement like this:
read a
if [ $a = "y" ] ; then
echo "YES"
elif [ $a = "Y" ] ; then
echo "YES"
else
echo "NO"
fi
I am finding something like this:
read a b c
if [ $a -eq $b and $b -eq $c ] ; then
echo "EQUILATERAL"
elif [ $a -eq $b or $b -eq $c ] ; then
echo "ISOSCELES"
else
echo "SCALENE"
fi
I just want to know, what to use instead of and and or?
Use && for and (|| for or)
read a b c
if [ "$a" == "$b" ] && [ "$b" == "$c" ] ; then
echo "EQUILATERAL"
elif [ "$a" == "$b" ] || [ "$b" == "$c" ] ; then
echo "ISOSCELES"
else
echo "SCALENE"
fi
Use && and || to have multiple conditions. Additionally, change the square brackets to parentheses. Additionally change the -eq to == since you're comparing numbers and not strings. This works:
#!/bin/bash
read a b c
if (( $a == $b )) && (( $b == $c )); then
echo "EQUILATERAL"
elif (( $a == $b )) || (( $b == $c )) ; then
echo "ISOSCELES"
else
echo "SCALENE"
fi
In addition to the prior answers, the correct way to use compound expression in a single [ or test (they are the same) clause is to use -a (for and) and -o (for or).
(e.g. testing if both file1 and file2 are readable):
if [ -r "$file1" -a -r "$file2 ]
then
# do something with the files
fi
Using test itself:
if test -r "$file1" -a -r "$file2
then
# do something with the files
fi
The portable way of doing this inside test brackets is to use -a and -o. Beware however that -eq is a numeric comparison, so you need to make sure your variables are numeric before comparing them. Something like this:
#! /bin/sh
read a b c
expr "$a" : '[0-9][0-9]*$' \& "$b" : '[0-9][0-9]*$' \& "$c" : '[0-9][0-9]*$' >/dev/null || exit
if [ $a -eq $b -a $b -eq $c ] ; then
echo "EQUILATERAL"
elif [ $a -eq $b -o $b -eq $c ] ; then
echo "ISOSCELES"
else
echo "SCALENE"
fi

bash - increment variables that contain letters

I have a set of valid characters [0-9a-z_] and a variable that is assigned one of these characters. What I want to do is to be able to increment that variable to the next in the set.
If need be I can handle the "special" cases where it would increment from '9' to 'a' and 'z' to '_', but I can't figure out how to increment letters.
#!/bin/bash
y=b
echo $y # this shows 'b'
y=$((y+1))
echo $y # this shows '1', but I want it to be 'c'
y=b
echo "$y" # this shows 'b'
y=$(echo "$y" | tr "0-9a-z" "1-9a-z_")
echo "$y" # this shows 'c'
Note that this does not handle the case where $y = "_" (not sure what you want then, and in any case it'll probably require separate handling), and if $y is more than one character long it'll "increment" all of them (i.e. "10" -> "21", "09g" -> "1ah", etc).
Maybe this can be a solution:
a=({0..9} {a..z} _)
echo ${a[*]}
yc=11
echo ${a[yc]}
((++yc))
echo ${a[yc]}
echo ${a[++yc]}
#Alternative
declare -A h
# Fill the has point to the next character
for((i=0;((i+1))<${#a[*]};++i)) { h[${a[i]}]=${a[i+1]};}
y=b
echo $y, ${h[$y]}
Output:
0 1 2 3 4 5 6 7 8 9 a b c d e f g h i j k l m n o p q r s t u v w x y z _
b
c
d
b, c
for those who would like to print incremented letter by execution of a function:
ALPHA=( {A..Z} )
alpha_increment () { echo ${ALPHA[${i:-0}]}; ((i++)) ;}
alpha_increment
A
alpha_increment
B
alpha_increment
C
You can start with this:
echo 0x$(( $(printf "%x" "'b'") + 1)) | xxd -r
I wrote this for a project, it uses the chr and ord fucntions(also found here somewhere) and some pure bash(only external called in the function is tr), if you are doing more than 100 characters I would use something else, but for short strings in my testing its actually slightly faster than python.
Also this script lower cases any input, you will have to modify it for upper case.
after putting these functions in your script(or cut and paste into a shell) you can just do
inc_string abz9z9
and get back.
aca0a0
chr() {
[ "$1" -lt 256 ] || return 1
printf "\\$(printf '%03o' "$1")"
}
ord() {
LC_CTYPE=C printf '%d' "'$1"
}
inc_string ()
{
string="$1";
lcstring=$(echo $string | tr '[:upper:]' '[:lower:]');
for ((position=$((${#lcstring}-1));position>=0;position--));do
if [ "${lcstring:position:1}" = 'z' ]; then
if [ "$position" -eq "$((${#lcstring}-1))" ]; then
newstring="${lcstring:0:$(($position))}a";
lcstring="$newstring";
elif [ "$position" -eq "0" ]; then
newstring="a${lcstring:$((position+1))}";
echo $newstring;
break;
else
newstring="${lcstring:0:$(($position))}a${lcstring:$((position+1))}";
lcstring="$newstring";
fi
elif [ "${lcstring:position:1}" = '9' ]; then
if [ "$position" -eq "$((${#lcstring}-1))" ]; then
newstring="${lcstring:0:$(($position))}0";
lcstring="$newstring";
elif [ "$position" -eq "0" ]; then
newstring="0${lcstring:$((position+1))}";
echo $newstring;
break;
else
newstring="${lcstring:0:$(($position))}0${lcstring:$((position+1))}";
lcstring="$newstring";
fi
else
if [ "$position" -eq "$((${#lcstring}-1))" ]; then
newstring="${lcstring:0:$(($position))}$(chr $(($(ord ${lcstring:position})+1)))";
echo $newstring;
break;
elif [ "$position" -eq "0" ]; then
newstring="$(chr $(($(ord ${lcstring:position})+1)))${lcstring:$((position+1))}";
echo $newstring;
break;
else
newstring="${lcstring:0:$(($position))}$(chr $(($(ord ${lcstring:position})+1)))${lcstring:$(($position+1))}";
echo $newstring;
break;
fi
fi
done
}

Resources