Restart loop from the previous state if a condition is not met - bash

The problem statement is that if a condition is not true, it should "stop" the for loop or should not progress unless that value is met.
Lets say that there is an input statement of true/false or a boolean statement in a for loop that prints to 1 to 10.
If the condition goes false in the the 5th iteration, it should stay at 5th iteration until the boolean returns true.
for i in {1..10}
do
read jobstatus
# echo $i
if [ "$jobstatus" = true ] ; then
echo 'Okay lets progress'
else
echo 'Nop, cant progress'
((i--))
# continue
#print $i
fi
echo $i
done
I have tried making codes such as this, also checked it via making files that were acting as "flags" But I cant seem to "stop" the progression. I dont want to break the loop, I just want to make it stay at that specific iteration until and unless the state changes.
Like if the job status goes falses at i=5, i should stay at 5 unless jobstatus gets to true. ( I intend to check the jobstatus at each input)

You could structure the loops something like:
#!/bin/bash
i=0
while test "$i" -lt 10; do
while read jobstatus && test "$jobstatus" != true; do
echo "can't progress. jobstatus = $jobstatus" >&2
done
if test "$jobstatus" = true; then
echo "Okay lets progress to step $((++i))"
else
echo Unexpected End of input >&2
exit 1
fi
echo "$i"
done

Use a C-style for-loop:
#!/bin/bash
for ((i=1; i<=10; ++i)) ; do
read jobstatus
if [ "$jobstatus" = true ] ; then
echo 'Okay lets progress'
else
echo 'Nop, cant progress'
((i--))
fi
echo $i
done

The if test should to be a loop of some sort.
For example:
for i in {1..10}
do
while
read jobstatus
[ "$jobstatus" != true ]
do
echo 'Nop, cant progress'
done
echo 'Okay lets progress'
echo $i
done
If you want to repeat the other for commands (ie. the echo $i statement here) until the test succeeds, they should go inside the loop too. For example:
for i in {1..10}
do
while
echo $i
read jobstatus
[ "$jobstatus" != true ]
do
echo 'Nop, cant progress'
done
echo 'Okay lets progress'
done

This may be what you are looking for:
for i in {1..10}; do
read jobstatus
if [ "$jobstatus" != true ]; then
echo 'Nop, cant progress'
fi
while [ "$jobstatus" != "true" ]; do
read jobstatus
sleep 1
# echo statements here if needed
done
echo 'Okay lets progress'
echo $i
done
No need to decrement i , this also avoids checking the status of jobstatus too often potentially consuming an unnecessary amount of resources.

With an interactive user input, Maybe something like this:
#!/bin/sh
i=0
while [ "$i" -lt 10 ] && printf 'Enter status: '; do
IFS= read -r jobstatus
case "$jobstatus" in
true)
i=$((i+1))
printf 'Okay lets progress to %s' "$i";;
*)
printf >&2 'Nop, cant progress because jobstatus is: %s' "${jobstatus:-empty}"
esac
printf '\n'
done
Using a text file for the input, to test the script.
The file.txt
true
true
true
true
true
false
foo
bar
baz
true
true
true
true
qux
true
false
The script:
#!/bin/sh
i=0
while [ "$i" -lt 10 ] && IFS= read -r jobstatus; do
case "$jobstatus" in
true)
i=$((i+1))
printf 'Okay lets progress to %s' "$i";;
*)
printf >&2 'Nop, cant progress because jobstatus is: %s' "${jobstatus:-empty}";;
esac
printf '\n'
done < file.txt
Output:
Okay lets progress to 1
Okay lets progress to 2
Okay lets progress to 3
Okay lets progress to 4
Okay lets progress to 5
Nop, cant progress because jobstatus is: false
Nop, cant progress because jobstatus is: foo
Nop, cant progress because jobstatus is: bar
Nop, cant progress because jobstatus is: baz
Nop, cant progress because jobstatus is: empty
Okay lets progress to 6
Okay lets progress to 7
Okay lets progress to 8
Okay lets progress to 9
Nop, cant progress because jobstatus is: qux
Nop, cant progress because jobstatus is: empty
Okay lets progress to 10

Try this Shellcheck-clean Bash code:
#! /bin/bash -p
for ((truecount=0; truecount<10;)); do
read -r jobstatus
if [[ $jobstatus == true ]]; then
echo "Okay let's progress"
(( ++truecount ))
else
echo "Nope, can't progress"
fi
printf '%d\n' "$truecount"
done
The major change from the original code is that it advances only on 'true' status instead of always advancing and stepping back on non-'true' status. In general, it's best if you can modify a loop variable in only one place.
Since the code isn't just stepping through a sequence, I replaced the meaningless i with the descriptive truecount.
See the accepted, and excellent, answer to Why is printf better than echo? for an explanation of why I used printf instead of echo to print the count value. (echo "$truecount" would probably be OK in this specific case, but echo "$var" doesn't work in general so I never use it. echo $var is bad. See Bash Pitfalls #14 (echo $foo).)

Related

What is the best way to accept a 2nd user input from options defined by the 1st user input?

My background is in SQL but I've been learning Bash to create tools to help non-Linux users find what they need from my Linux system - I am pretty green with Bash, I apologize if this looks a bit dumb.
The goal of the script is to essentially display all directories within the current directory to the user, and allow them to input 1-9 to navigate to lower directories.
My sticking point is that I'm trying to use arrays to define potential filepaths, since in practice new directories will be added over time and it is not practical to edit the script each time a filepath is added.
Here's my prototype so far, currently it navigates into Test1, Test2, or Test3 then echos pwd to prove it is there.
#Global Variables
DIR_MAIN='/home/admin/Testhome'
#Potential Filepaths
#/home/admin/Testhome/Test1/Test1-1/
#/home/admin/Testhome/Test1/Test1-2/
#/home/admin/Testhome/Test2/Test2-1/
#/home/admin/Testhome/Test2/Test2-2/
#/home/admin/Testhome/Test3/Test3-1/
#/home/admin/Testhome/Test3/Test3-2/
#Defining Array for first user input
arr=($(ls $DIR_MAIN))
#System to count total number of directories in filepath, then present to user for numbered selection
cnt=0
for i in ${arr[#]}
do
cnt=$(($cnt+1))
echo "$cnt) $i"
done
read -p "Select a folder from the list: " answer
case $answer in
1)
cd $DIR_MAIN/${arr[0]}
echo "Welcome to $(pwd)"
;;
2)
cd $DIR_MAIN/${arr[1]}
echo "Welcome to $(pwd)"
;;
3)
cd $DIR_MAIN/${arr[2]}
echo "Welcome to $(pwd)"
;;
esac
I've tried the following, but it doesn't like the syntax (to someone experienced I'm sure these case statements look like a grenade went off in vim).
I'm beginning to wonder if the SELECT CASE road I'm going down is appropriate, or if there is an entirely better way.
#User Input Start
echo "What is the secret number?"
while :
do
read STRING1
case $STRING1 in
1)
echo "Enter the number matching the directory you want and I will go there"
echo "1 - ${arr[0]}"
echo "2 - ${arr[1]}"
echo "3 - ${arr[2]}"
read STRING2
case $STRING2 in
1)
cd $DIR_MAIN/${arr[0]}
echo "Welcome to" $(pwd)
2)
cd $DIR_MAIN/${arr[1]}
echo "Welcome to" $(pwd)
3)
cd $DIR_MAIN/${arr[2]}
echo "Welcome to" $(pwd)
*)
echo "Thats not an option and you know it"
*)
echo "1 is the secret number, enter 1 or nothing will happen"
;;
esac
#break needs to be down here somewhere
done
Ultimately I know I'll need to variabilize a local array once I'm in Test2 for example (since in practice, this could descend as far as /Test2/Test2-9 and there would be tons of redundant code to account for this manually).
For now, I'm just looking for the best way to present the /Test2-1 and /Test2-2 filepaths to the user and allow them to make that selection after navigating to /Test2/
This might do what you wanted.
#!/usr/bin/env bash
shopt -s nullglob
n=1
for i in /home/admin/Testhome/Test[0-9]*/*; do
printf '%d) %s\n' "$n" "$i"
array[n]="$i"
((n++))
done
(( ${#array[*]} )) || {
printf 'It looks like there is/are no directory listed!\n' >&2
printf 'Please check if the directories in question exists!\n' >&2
return 1
}
dir_pattern_indices=$(IFS='|'; printf '%s' "#(${!array[*]})")
printf '\n'
read -rp "Select a folder from the list: " answer
if [[ -z $answer ]]; then
printf 'Please select a number and try again!' >&2
exit 1
elif [[ $answer != $dir_pattern_indices ]]; then
printf 'Invalid option %s\n' "$answer" >&2
exit 1
fi
for j in "${!array[#]}"; do
if [[ $answer == "$j" ]]; then
cd "${array[j]}" || exit
printf 'Welcome to %s\n' "$(pwd)"
break
fi
done
The script needs to be sourced e.g.
source ./myscript
because of the cd command. See Why can't I change directory using a script.
Using a function instead of a script.
Let's just name the function list_dir
list_dir() {
shopt -s nullglob
declare -a array
local answer dir_pattern_indices i j n
n=1
for i in /home/admin/Testhome/Test[0-9]*/*; do
printf '%d) %s\n' "$n" "$i"
array[n]="$i"
((n++))
done
(( ${#array[*]} )) || {
printf 'It looks like there is/are no directory listed!\n' >&2
printf 'Please check if the directories in question exists!\n' >&2
return 1
}
dir_pattern_indices=$(IFS='|'; printf '%s' "#(${!array[*]})")
printf '\n'
read -rp "Select a folder from the list: " answer
if [[ -z $answer ]]; then
printf 'Please select a number and try again!' >&2
return 1
elif [[ $answer != $dir_pattern_indices ]]; then
printf 'Invalid option %s\n' "$answer" >&2
return 1
fi
for j in "${!array[#]}"; do
if [[ $answer == "$j" ]]; then
cd "${array[j]}" || return
printf 'Welcome to %s\n' "$(pwd)"
break
fi
done
}
All of the array names and variables are declared local to the function in order not to pollute the interactive/enviromental shell variables.
Put that somewhere in your shellrc file, like say in ~/.bashrc then source it again after you have edited that shellrc file.
source ~/.bashrc
Then just call the function name.
list_dir
I took what #Jetchisel wrote and ran with it - I see they updated their code as well.
Between that code and what I hacked together piggybacking off what he wrote, I'm hoping future viewers will have what they need to solve this problem!
My code includes a generic logging function (can write to a log file if you define it and uncomment those logging lines, for a script this size I just use it to output debugging messages), everything below is the sequence used.
As he mentioned the "0" element needs to be removed from the array for this to behave as expected, as a quick hack I ended up assigning array element 0 as null and adding logic to ignore null.
This will also pull pretty much anything in the filepath, not just directories, so more tweaking may be required for future uses but this serves the role I need it for!
Thank you again #Jetchisel !
#hopt -s nullglob
DIR_MAIN='/home/admin/Testhome'
Dir_Cur="$DIR_MAIN"
LOG_LEVEL=1
array=(NULL $(ls $DIR_MAIN))
########FUNCTION LIST#########
####Generic Logging Function
Log_Message()
{
local logLevel=$1
local logMessage=$2
local logDebug=$3
local dt=$(date "+%Y-%m-%d %T")
##Check log level
if [ "$logLevel" == 5 ]
then local logLabel='INFO'
elif [ "$logLevel" == 1 ]
then local logLabel='DEBUG'
elif [ "$logLevel" == 2 ]
then local logLabel='INFO'
elif [ "$logLevel" == 3 ]
then local logLabel='WARN'
elif [ "$logLevel" == 4 ]
then local logLabel='ERROR'
fi
##Check conf log level
if [ "$LOG_LEVEL" == 1 ]
then #echo "$dt [$logLabel] $logMessage" >> $LOG_FILE ##Log Message
echo "$dt [$logLabel] $logMessage" ##Echo Message to Terminal
##Check if Debug Empty
if [ "$logDebug" != "" ]
then #echo "$dt [DEBUG] $logDebug" >> $LOG_FILE ##Extra Debug Info
echo "$dt [DEBUG] $logDebug" ##Extra Debug Info
fi
elif [ "$logLevel" -ge "$LOG_LEVEL" ]
then #echo "$dt [$logLabel] $logMessage" >> "$LOG_FILE" ##Log Message
echo "$dt [$logLabel] $logMessage"
fi
}
####
####Function_One
##Removes 0 position in array by marking it null, generates [1-X] list with further filepaths
Function_One()
{
Log_Message "1" "entered Function_One"
local local_array=("$#")
Log_Message "1" "${local_array[*]}"
n=1
for i in "${local_array[#]}"; do
if [ "$i" != "NULL" ]
then
printf '%d) %s\n' "$n" "$i"
array[n]="$i"
((n++))
fi
done
printf '\n'
read -rp "Select a folder from the list: " answer
for j in "${!local_array[#]}"; do
if [[ $answer == "$j" ]]; then
cd "$Dir_Cur/${local_array[j]}" || exit
printf 'Welcome to %s\n' "$(pwd)"
break
fi
done
}
####
########FUNCTION LIST END#########
########MAIN SEQUENCE########
echo "Script start"
Function_One "${array[#]}"
Dir_Cur="$(pwd)"
array2=(NULL $(ls $Dir_Cur))
Function_One "${array2[#]}"
Dir_Cur="$(pwd)"
$Dir_Cur/test_success.sh
echo "Script end"
########

Extract value from file for input into IF Statement

I have a config.txt file where the first line has a number 1, I need to read this and do certain tasks based on whether the number is 1 or 2 or 3 etc
The problem is I cannot test that config value, none of the if statements below work, and I tried many more variances.
config=$(head -n 1 /mnt/writable/config.txt) #grabs vale from file
echo $config #prints 1
But the following if statements do not echo anything.
if [[ "$config" = "1" ]];
then
echo "is a 1";
fi
if [[ "$config" == "1" ]];
then
echo "is a 1";
fi
if [[ "$config" = 1 ]];
then
echo "is a 1";
fi
if [ $config = 1 ];
then
echo "is a 1";
fi
I also tried "declare -i config" at the top but that didnt work either. Spent a day and no luck so far.
Because you have space or tab in your first line( I think )
So if you print using
echo $config
it will print 1 correctly but when you use it in if condition, it may not work as your wish ...
so I suggest that you should try following in your script,
config=$(head -n 1 /mnt/writable/config.txt | tr -d '[:space:]')
and all your if condition may work.
Integer equality can be tested by using -eq option.
Do man test for further details about if condition.
if [ $config -eq 1 ]; then echo "config is 1"; else echo "config is not 1"; fi

Bash loop until a certain command stops failing

I would like to write a loop in bash which executes until a certain command stops failing (returning non-zero exit code), like so:
while ! my_command; do
# do something
done
But inside this loop I need to check which exit code my_command returned, so I tried this:
while ! my_command; do
if [ $? -eq 5 ]; then
echo "Error was 5"
else
echo "Error was not 5"
fi
# potentially, other code follows...
done
But then the special variable ? becomes 0 inside the loop body.
The obvious solution is:
while true; do
my_command
EC=$?
if [ $EC -eq 0 ]; then
break
fi
some_code_dependent_on_exit_code $EC
done
How can I check the exit code of my_command (called in loop header) inside loop body without rewriting this example using a while true loop with a break condition as shown above?
In addition to the well-known while loop, POSIX provides an until loop that eliminates the need to negate the exit status of my_command.
# To demonstrate
my_command () { read number; return $number; }
until my_command; do
if [ $? -eq 5 ]; then
echo "Error was 5"
else
echo "Error was not 5"
fi
# potentially, other code follows...
done
If true command hurt your sensibility, you could write:
while my_command ; ret=$? ; [ $ret -ne 0 ];do
echo do something with $ret
done
This could be simplified:
while my_command ; ((ret=$?)) ;do
echo do something with $ret
done
But if you don't need ResultCode, you could simply:
while my_command ; [ $? -ne 0 ];do
echo Loop on my_command
done
or
while my_command ; (($?)) ;do
echo Loop on my_command
done
And maybe, why not?
while ! my_command ;do
echo Loop on my_command
done
But from there you could better use until as chepner suggest
You can get the status of a negated command from the PIPESTATUS built-in variable:
while ! my_command ; do
some_code_dependent_on_exit_code "${PIPESTATUS[0]}"
done
chepner's solution is better in this case, but PIPESTATUS is sometimes useful for similar problems.
So in my case I also need to ignore some exit codes and want to provide some useful output to the user so I wrote this up:
retrycmd(){
MSG=$1
IGNORE=$2
shift 2
local SLEEP_T=5
local L_CNT=5
local C_CNT=0
while ((C_CNT++ < ${L_CNT})) && ! $#;do
RET=${PIPESTATUS[0]}
#echo "RET: ${RET}"
for I in ${IGNORE//,/ };do # bashism: replace(/) all(/) match(,) with(/) value(<space>)
if ((${RET} == ${I}));then
#echo "${RET} = ${I}"
break 2
fi
done
echo "${MSG} failure ${C_CNT}"
sleep ${SLEEP_T}
done
if ((${C_CNT} > ${L_CNT}));then
echo "${MSG} failed"
poweroff
fi
}
#retrycmd "Doing task" "IGNORE,CSV" <CMD>
retrycmd "Ping google" "0" ping www.google.com

How can I loop If Statements in Bash

Here is my code:
#!/bin/bash
echo "Letter:"
read a
if [ $a = "a" ]
then
echo "LOL"
fi
if [ $a = "b" ]
then
echo "ROFL"
fi
Is there a way for me to loop this so that, after displaying either LOL or ROFL, I would be asked for a letter again?
Yes.
Oh, you want to know how?
while true; do
echo "Letter:"
read a
if [ $a = "a" ]
then
echo "LOL"
elif [ $a = "b" ]
then
echo "ROFL"
fi
done
Of course, you probably want some way to get out of that infinite loop. The command to run in that case is break. I would write the whole thing like this:
while read -p Letter: a; do
case "$a" in
a) echo LOL;;
b) echo ROFL;;
q) break;;
esac
done
which lets you exit the loop either by entering 'q' or generating end-of-file (control-D).
Don't forget that you always want -r flag with read.
Also there is a quoting error on that line:
if [ $a = "a" ] # this will fail if a='*'
So here is a bit better version(I've also limited the user to input only 1 character):
#!/bin/bash
while true; do
read -rn1 -p 'Letter: ' a
echo
if [[ $a = 'a' ]]; then
echo "LOL"
elif [[ $a = 'b' ]]; then
echo "ROFL"
else
break
fi
done
Or with switch statement:
#!/bin/bash
while read -rn1 -p 'Letter: ' a; do
echo
case $a in
a) echo LOL;;
b) echo ROFL;;
*) break;;
esac
done

Mimicking the Python "for-else" construct

Python has a handy language feature called "for-else" (similarly, "while-else"), which looks like this:
for obj in my_list:
if obj == target:
break
else: # note: this else is attached to the for, not the if
print "nothing matched", target, "in the list"
Essentially, the else is skipped if the loop breaks, but runs if the loop exited via a condition failure (for while) or the end of iteration (for for).
Is there a way to do this in bash? The closest I can think of is to use a flag variable:
flag=false
for i in x y z; do
if [ condition $i ]; then
flag=true
break
fi
done
if ! $flag; then
echo "nothing in the list fulfilled the condition"
fi
which is rather more verbose.
Using a subshell:
( for i in x y z; do
[ condition $i ] && echo "Condition $i true" && exit;
done ) && echo "Found a match" || echo "Didn't find a match"
You could put a sentinel value in the loop list:
for i in x y z 'end-of-loop'; do
if [ condition $i ]; then
# loop code goes here
break
fi
if [ $i == 'end-of-loop' ]; then
# your else code goes here
fi
done
Something very hacky to introduce similar syntax:
#!/bin/bash
shopt -s expand_aliases
alias for='_broken=0; for'
alias break='{ _broken=1; break; }'
alias forelse='done; while ((_broken==0)); do _broken=1;'
for x in a b c; do
[ "$x" = "$1" ] && break
forelse
echo "nothing matched"
done
 
$ ./t.sh a
$ ./t.sh d
nothing matched
You can do this but I personally find it hard to read:
while :;
do for i in x y z; do
if [[ condition ]]; then
# do something
break 2
done
echo Nothing matched the condition
break
done
I also enjoy devnull's answer, but this is even more pythonic:
for i in x y z; do
[ condition $i ] && break #and do stuff prior to break maybe?
done || echo "nothing matched"
This will only echo "nothing matched" if the loop did not break.
You can change this
if ! $flag; then
echo "nothing in the list fulfilled the condition"
fi
to something simpler like this
"$flag" || echo "nothing in the list fulfilled the condition"
if you only have one statement after it, although that's not really going to help much.

Resources