I am using a nested function to partition and making the filesystem for drives attached to a new Linux box.
I am having a strange issue trying to break out of all loops.
I am keeping track of the nested loop index and using "break n".
When the user replies "n" to the question "Do you have any additional drives to partition?" i expect to break out of all nested loops and continue with the script, but what happens is that the question gets asked again.
Can you help me figure this out?
INIT_STARTED=0
chooseDisks()
{
INIT_STARTED=$((INIT_STARTED+1))
# Choosing which drive to work on
read -p "Please type the name of the disk you want to partition: " DISK
while true; do
read -p "Are you sure you want to continue ? y (partition)/n (choose another drive) /x (continue) " ynx
case $ynx in
[Yy]* )
containsElement "$DISK"
if [ $? == 1 ]; then
initializeDisk $DISK
# remove element from found disk to prevent trying to partition it again.
delete=($DISK)
FOUNDDISKS=( "${FOUNDDISKS[#]/$delete}" )
else
echo "${red}$DISK is not a valid choice, please select a valid disk.${reset}"
chooseDisks
fi
break;;
[Nn]* )
chooseDisks
break $((INIT_STARTED));;
[Xx]* )
return
break;;
* ) echo "Please answer y or n. x to continue the script.";;
esac
done
# Any additional disks to partition?
while true; do
read -p "Do you have any additional drives to partition ? y/n " yn
case $yn in
[Yy]* )
#chooseDisks $FOUNDDISKS
chooseDisks
break $((INIT_STARTED));;
[Nn]* )
return
break $((INIT_STARTED));;
* ) echo "Please answer y or n";;
esac
done
}
I expect this:
break $((INIT_STARTED));;
to end the nth loop and exiting the function.
Don't play with nested logic break, just use some variable like $userStop and instead of while true; do put
userStop = false
while[!${userStop}]
do
#...
# replace break $((INIT_STARTED));; by
# userStop = true
I ended up changing the code to avoid breaking within a loop.
Thanks guys for directing me the right way.
David
i expect to break out of all nested loops and continue with the script
You can run the function in a subshell and use exit.
chooseDisks()
{
if [ "$1" -eq 0 ]; then
echo "The user entered it all!"
exit 0
fi
echo "The user is still entering... $1"
( chooseDisks $(($1 - 1)) )
}
# Imagine the user 5 times enters something
( chooseDisks 5 )
But the best would be to refactor your code to just have a big while true; do loop in the beginning. There is no need to make this function recursive.
Related
I want to backup mikrotiks via scp. This script loops through the hosts from the hosts.txt. One by one, connects to each device from the list. Does backup and all manipulations. If at some stage it was not possible to connect to the device, then an empty backup is formed, which is then sent to the cloud.
I want to check. If it was not possible to connect to the host, then write this host into a variable, line by line, and go to the next device. Next, I will notify about all failed connections.
The problem is that only the first error is written to the variable, all subsequent ones are ignored.
Tell me who knows what.
#!/bin/bash
readarray -t hosts < hosts.txt
DATE=$(date +'%Y-%m-%d_%H-%M-%S')
ROS='<br>'
ERR=( )
#Get values from main list
for host in ${hosts[*]}
do
#Get values from sub list
hostname=($(echo ${host} | tr "_" " "))
echo ${hostname[0]} - ${hostname[1]}
#connect & backup & transfer & archive & rm old files & moove to cloud
if ssh backup#${hostname[0]} -C "/system backup save name=${hostname[1]}_$DATE"; then
scp backup#${hostname[0]}:"${hostname[1]}_$DATE.backup" ./
ssh backup#${hostname[0]} -C "rm ${hostname[1]}_$DATE.backup"
tar -czvf ./${hostname[1]}_$DATE.tar.gz ${hostname[1]}_$DATE.backup
scp ./${hostname[1]}_$DATE.tar.gz my#cloud.com:/var/www/my.cloud.com/backups/mikrotik/
rm ${hostname[1]}_$DATE.backup ${hostname[1]}_$DATE.tar.gz
ROS=$ROS${hostname[1]}"<br>"
else
ERR+=(${hosts[*]} "is not ready")
fi
done
hosts.txt
10.10.8.11_CAP-1
10.10.9.12_CAP-2
10.10.10.13_CAP-3
As I noted in the comments, you're misusing the array notation. Your line ERR=(${hosts[*]} "is not ready") should be ERR+=(${hosts[*]} "is not ready") and you should define ERR as an array, not a scalar: ERR=( ) for example, or declare -a ERR. Similarly with ROS.
Here's a test script that avoids all the ssh and scp work to demonstrate that lists of passing and failing hosts work — that the arrays hosts, ROS and ERR are handled correctly.
Note the use of "${ERR[#]}" with double quotes and # instead of no quotes and *. The difference matters because the values in the array contain spaces. Try the alternatives. Note, too, that printf always prints, even when there is no argument corresponding to the %s in the format string. Hence the check on the number of elements in the array before invoking printf.
#!/bin/bash
# Needs Bash 4.x - Bash 3.2 as found on Macs does not support readarray
# readarray -t hosts < hosts.txt
hosts=( passed-1 failed-2 passed-3 failed-4 passed-5 )
declare -a ERR
declare -a ROS
status=passed
for host in "${hosts[#]}"
do
if [ "$status" = "passed" ]
then ROS+=( "$host $status" ); status="failed"
else ERR+=( "$host $status" ); status="passed"
fi
done
# Brute force but handles empty lists
for passed in "${ROS[#]}"
do printf "== PASS == [%s]\n" "$passed"
done
for failed in "${ERR[#]}"
do printf "!! FAIL !! [%s]\n" "$failed"
done
# Alternative - better spread over multiple lines each
if [ "${#ROS}" -gt 0 ]; then printf "== PASS == [%s]\n" "${ROS[#]}"; fi
if [ "${#ERR}" -gt 0 ]; then printf "!! FAIL !! [%s]\n" "${ERR[#]}"; fi
Output:
== PASS == [passed-1 passed]
== PASS == [passed-3 passed]
== PASS == [passed-5 passed]
!! FAIL !! [failed-2 failed]
!! FAIL !! [failed-4 failed]
== PASS == [passed-1 passed]
== PASS == [passed-3 passed]
== PASS == [passed-5 passed]
!! FAIL !! [failed-2 failed]
!! FAIL !! [failed-4 failed]
I'm sorry there are so many failures to backup your data!
I have the following code to loop through a question session with a user:
#loop counter
COUNTER=1
# Initial question
echo ""
echo -n "Would you like a cup of tea? (y/n)" # Ask initial question
read answer # Check answer
while [ $COUNTER -le 5 ] # while counter is less than or equal to 5
do
if [ "$answer" != "${answer#[Yy]}" ]; then # if answer is 'y'
echo "" # skip a line for cleanliness
echo "Great, I'll make it now" # Satisfaction
echo ""
break # End the loop
else # if answer is anything other than 'y'
echo "" # skip a line for cleanliness
echo -n "Are you sure? (y/n)" # Ask again
read answer # Check the answer again
let COUNTER=COUNTER+1 # increment COUNTER
fi
done # finish
The code should act as follows:
Ask user if they would like tea.
If user says yes, then print out something, and end loop
If user says no, go for another 4 attempts before ending loop.
If user says yes, after saying no, print out something, and end loop.
The code works fine for points 1,2, and 4.
On point 3 - the code loops to a 5th loop before ending.
I can't seem to be able to identify the error.
Edit: Changed to COUNTER = 0, and $COUNTER -le 4. However, code doesn't implement point 4, on 4th attempt (i.e. 1 initial no, followed by 3 extra nos, before a yes).
You could simply limit counter to less than 5, instead of less than or equal to.
Use -lt instead of -le. That is [ $COUNTER -lt 5]
OR
You can say less than or equal to 4 and thus you can maintain [ $COUNTER -le 4]
Instead of having 2 lines,
echo -n "Are you sure? (y/n)" # Ask again
read answer # Check the answer again
you can shorten it to one:
read -p "Are you sure? (y/n): " answer
However, I'm not entirely sure by what you mean by point 4. Could you provide an example or a little more description?
I have created a bash script which is essentially a wizard with a number of questions, some are multiple choice. When a multiple choice question is presented, I want the user to be able to choose a number, where each number corresponds to a different answer. I want this answer to be the variable which can be later used in the script.
I realize this is what the 'select' function is used for, but I also have a requirement when the user simply hits [ENTER] a default value is assumed. As far as I know, the 'select' function assumes an empty value of "" when the [ENTER] key is pressed (an invalid option) and re-prompts the question.
The code below is me attempting to update a variable when a number is pressed. For example when the number '1' is pressed, I want the $hash variable to updated to 'sha224'.
Is there any to achieve this using a case statement without 'select'? If not what are my alternatives?
echo
echo "Select a hashing algorithm"
echo "1 - sha224"
echo "2 - sha256"
echo "3 - sha384"
echo "4 - sha512"
while true; do
read -p "Option: [sha256]:" -e hash
case $hash in
"") hash="sha256" break 2;;
1) hash="sha224" break 2;;
2) hash="sha256" break 2;;
3) hash="sha384" break 2;;
4) hash="sha512" break 2;;
*) echo "Invalid Response: Please enter [1-4] and hit [ENTER] or hit [ENTER] to select 'sha256'";;
esac
done
Insert ; before all break.
Replace $hash with ${hash:=2} to use a default value if $hash is empty.
"") hash="sha256" break 2;; can be removed.
I know how to check the status of the previously executed command using $?, and we can make that status using exit command. But for the loops in bash are always returning a status 0 and is there any way I can break the loop with some status.
#!/bin/bash
while true
do
if [ -f "/test" ] ; then
break ### Here I would like to exit with some status
fi
done
echo $? ## Here I want to check the status.
The status of the loop is the status of the last command that executes. You can use break to break out of the loop, but if the break is successful, then the status of the loop will be 0. However, you can use a subshell and exit instead of breaking. In other words:
for i in foo bar; do echo $i; false; break; done; echo $? # The loop succeeds
( for i in foo bar; do echo $i; false; exit; done ); echo $? # The loop fails
You could also put the loop in a function and return a value from it. eg:
in() { local c="$1"; shift; for i; do test "$i" = "$c" && return 0; done; return 1; }
Something like this?
while true; do
case $RANDOM in *0) exit 27 ;; esac
done
Or like this?
rc=0
for file in *; do
grep fnord "$file" || rc=$?
done
exit $rc
The real question is to decide whether the exit code of the loop should be success or failure if one iteration fails. There are scenarios where one make more sense than the other, and other where it's not at all clear cut.
The bash manual says:
while list-1; do list-2; done
until list-1; do list-2; done
[..]The exit status of the while and until commands is the exit status
of the last command executed in list-2, or zero if none was executed.[..]
The last command that is executed inside the loop is break. And the exit value of break is 0 (see: help break).
This is why your program keeps exiting with 0.
The break builtin for bash does allow you to accomplish what you are doing, just break with a negative value and the status returned by $? will be 1:
while true
do
if [ -f "./test" ] ; then
break -1
fi
done
echo $? ## You'll get 1 here..
Note, this is documented in the help for the break builtin:
help break
break: break [n] Exit for, while, or until loops.
Exit a FOR, WHILE or UNTIL loop. If N is specified, break N enclosing
loops.
Exit Status: The exit status is 0 unless N is not greater than or
equal to 1.
You can break out of n number of loops or send a negative value for breaking with a non zero return, ie, 1
I agree with #hagello as one option doing a sleep and changing the loop:
#!/bin/bash
timeout=120
waittime=0
sleepinterval=3
until [[ -f "./test" || ($waittime -eq $timeout) ]]
do
$(sleep $sleepinterval)
waittime=$((waittime + sleepinterval))
echo "waittime is $waittime"
done
if [ $waittime -lt $sleepinterval ]; then
echo "file already exists"
elif [ $waittime -lt $timeout ]; then
echo "waited between $((waittime-3)) and $waittime seconds for this to finish..."
else
echo "operation timed out..."
fi
I think what you should be asking is: How can I wait until a file or a directory (/test) gets created by another process?
What you are doing up to now is polling with full power. Your loop will allocate up to 100% of the processing power of one core. The keyword is "polling", which is ethically wrong by the standards of computer scientists.
There are two remedies:
insert a sleep statement in your loop; advantage: very simple; disadvantage: the delay will be an arbitrary trade-off between CPU load and responsiveness. ("Arbitrary" is ethically wrong, too).
use a notification mechanism like inotify (see: man inotify); advantage: no CPU load, great responsiveness, no delays, no arbitrary constants in your code; disadvantage: inotify is a kernel API – you need some code to access it: inotify-tools or some C/Perl/Python code. Have a look at inotify and bash!
I would like to submit an alternative solution which is simpler and I think more elegant:
(while true
do
if [ -f "test" ] ; then
break
fi
done
Of course of this is part of a script then you could user return 1 instead of exit 1
exit 1
)
echo "Exit status is: $?"
Git 2.27 (Q2 2020), offers a good illustration of the exit status in a loop, here within the context of aborting a failing test early (e.g. by exiting a loop), which is to say "return 1".
See commit 7cc112d (27 Mar 2020) by Junio C Hamano (gitster).
(Merged by Junio C Hamano -- gitster -- in commit b07c721, 28 Apr 2020)
t/README: suggest how to leave test early with failure
Helped-by: Jeff King
Over time, we added the support to our test framework to make it easy to leave a test early with failure, but it was not clearly documented in t/README to help developers writing new tests.
The documentation now includes:
Be careful when you loop
You may need to verify multiple things in a loop, but the following does not work correctly:
test_expect_success 'test three things' '
for i in one two three
do
test_something "$i"
done &&
test_something_else
'
Because the status of the loop itself is the exit status of the test_something in the last round, the loop does not fail when "test_something" for "one" or "two" fails.
This is not what you want.
Instead, you can break out of the loop immediately when you see a failure.
Because all test_expect_* snippets are executed inside a function, "return 1" can be used to fail the test immediately upon a failure:
test_expect_success 'test three things' '
for i in one two three
do
test_something "$i" || return 1
done &&
test_something_else
'
Note that we still &&-chain the loop to propagate failures from earlier commands.
Use artificial exit code 🙂
Before breaking the loop set a variable then check the variable as the status code of the loop, like this:
while true; do
if [ -f "/test" ] ; then
{broken=1 && break; };
fi
done
echo $broken #check the status with [[ -n $broken ]]
I have one requirement in my automation.
I need to pass values like 1, 2, 3 to MY_IMAGE in command line in Linux.
I had defines activities for all these inputs in other make file.
The code similar to below i wrote for my requirement. Issue was whenever I passes values like MY_IMAGE=1, MY_IMAGE=2, MY_IMAGE=3
it's printing only echo ACT_DO=XYZ;
It's not displaying the other info whenever I selected 2 or 3. Can anyone check and correct my code.
export MY_IMAGE
MY_IMAGE=$img_value;
if [ $img_value :="1" ]
then
echo ACT_DO=XYZ;
else
if [ $img_value :="2 ]
then
echo ACT_DO=ABC;
else
if [ $img_value :=3 ]
then
echo ACT_DO=ETC;
else
echo ""$img_value" is unsupported";
exit 1;
fi
fi
fi
Your code has a quote in the wrong place, and uses := which doesn't mean anything, as far as I know. It's also implemented confusingly.
Try this:
export MY_IMAGE
MY_IMAGE=$img_value
case "$img_value" in
1 ) echo ACT_DO=XYZ ;;
2 ) echo ACT_DO=ABC ;;
3 ) echo ACT_DO=ETC ;;
* )
echo "\"$img_value\" is unsupported"
exit 1
;;
esac
The first two lines are not required for this code, but I presume you wanted that for something else.