Bash script unexpected result for while loop - linux

I have following bash script to stop Apache server. I am not getting error but an unexpected result.
#!/bin/bash
/etc/init.d/httpd stop
if [[ $? -ne 0 ]]
then
exit 1
fi
RTN=10
while [[ $RTN -gt 0 ]]
echo "Before" $RTN
do
sleep 5
RTN=`ps -C httpd | wc -l`
RTN=$(( RTN - 1 ))
echo "After" $RTN
done
exit 0
I got the following answer which I do not expect to be, with an infinite printing:
Before 10
After 0
Before 0
After 0
Before 0
After 0
I expect to print:
Before 10
After -1
#and exit here
Could anybody please tell me what is happening?

This doesn't work the way you seem to think it does:
while [[ $RTN -gt 0 ]]
echo "Before" $RTN
do
You want the echo to come after the do. With it before the do, it's part of the list-1 condition rather than the list-2 body. And, as per the bash docs (my bold):
The while command continuously executes the list list-2 as long as the last command in the list list-1 returns an exit status of zero.
You can see the difference between the following script, similar to yours:
#!/usr/bin/bash
RTN=2
while [[ $RTN -gt 0 ]]
echo "Before" $RTN
do
sleep 1
RTN=$(( RTN - 1 ))
echo "After" $RTN
done
which outputs (ad infinitum):
Before 2
After 1
Before 1
After 0
Before 0
After -1
Before -1
After -2
Before -2
When you move the echo to inside the body:
#!/usr/bin/bash
RTN=2
while [[ $RTN -gt 0 ]]
do
echo "Before" $RTN
sleep 1
RTN=$(( RTN - 1 ))
echo "After" $RTN
done
it then terminates properly:
Before 2
After 1
Before 1
After 0
<returns to prompt>
Once that change is made, the loop terminates correctly, given the values being generated by your ps command.
Additionally, if you want to find out what's in the process list (and probably causing an result of zero rather than negative one), output the process list before checking the count:
:
ps -C httpd # Add this line temporarily.
RTN=`ps -C httpd | wc -l`
:

Your ps -C httpd command always returns a line: PID TTY TIME CMD (basically the heading of the ps output) which is counted by wc -l as 1 when there is no process running. If httpd was running the line count would be greater than 1.
Thus $((RTN -1)) becomes 0.
Edit:
I notice, you have an error in your while loop:
while [[ $RTN -gt 0 ]]
echo "Before" $RTN
do
Change it to:
while [[ $RTN -gt 0 ]]
do
echo "Before" $RTN

Related

unexpected return value 123

I'm trying to do a script that is used with two arguments - a file and an integer. It should check if the arguments are valid, otherwise exit with 1. Then it should either return 0 if the file is smaller than second argument, or echo size of the file to stdout. The script keeps returning value 123 instead of 1 or 0. Where is the problem? Thanks.
#!/bin/bash
if [ $# -eq 2 ];
then
if test $2 -eq $2 > /dev/null 2>&1
then
if [ -f $1 ];
then
if [ $(stat -c %s $1) -ge $2 ];
then
echo $(stat -c %s $1)
else
exit 0
fi
else
exit 1
fi
else
exit 1
fi
else
echo 042f9
exit 1
fi
I do not know where the "123" output comes from, but I would do it like this:
#!/bin/bash
# Must have 2 arguments
if [[ $# -ne 2 ]]
then
printf "042f9\n"
exit 1
fi
# File must exist
if [[ ! -f "$1" ]]
then
exit 1
fi
# File size > $2 check
filesize=$(stat -c %s "$1")
if [[ $filesize -ge $2 ]]
then
printf "%d" "$filesize"
else
exit 1
fi
A couple notes for your scripts (IMHO):
Like Mat mentioned in the comments, test 1 condition and exit right away. When I read your script, I had to go to the end to see what happens if the number of arguments is wrong. Logically there is nothing wrong with your code, it is just making it easier to read.
For bash, use [[ ]] to test if conditions.
I try never to call a function or command twice. That is why I stored the result of the stat command in a variable. If you use it more than once, store it, do not call the command again.
No need for ; since you put your then on the next line anyway.
Always double-quote your variables, especially if they are filenames. Weird filenames break so many scripts!
Finally use printf instead of echo. For simple cases, its the same, but echo does have some issues (https://unix.stackexchange.com/questions/65803/why-is-printf-better-than-echo).
Possible return values:
the size of the file, and the exit value is 0 ($?). The file is larger than argument 2 value.
"042f9", and the exit value is 1 ($?). Arguments error.
nothing, and the exit value is 1 ($?). Missing file error, or the file is smaller than argument 2 value.

Bash exit in if statement in a for loop

I am trying to do a for loop in Bash and exit on an if statement but I realised it will break the code before finishing the loop.
#!/bin/bash
for node in $(ps -ef | awk <something>);
do
var=<command>
echo "node ${node}"
if [[ ${var} -gt 300000 ]]; then
echo "WARNING!";
exit 1;
elif [[ ${var} -gt 1000000 ]]; then
echo "CRITICAL!";
exit 2;
else
echo "OK!";
exit 0;
fi
done
My second option is to set a variable instead of the exit outside the loop but then I realised it will override each node status:
#!/bin/bash
for node in $(ps -ef | awk <command>);
do
var=<command>
echo "node ${node}"
if [[ ${var} -gt 300000 ]]; then
echo "WARNING!";
status="warning"
elif [[ ${var} -gt 1000000 ]]; then
echo "CRITICAL!";
status="critical"
else
echo "OK!";
status="ok"
fi
done
if [[ status == "warning" ]]; then
exit 1;
elif [[ status == "critical" ]]; then
exit 2;
elif [[ status == "ok" ]]; then
exit 0;
fi
How do I exit properly on each node?
Here is an alternative. It counts the results and and creates an exit status depending on the counters. I changed the semantic, because your script never reaches the CRITICAL path. Instead the WARNING path was entered for values >1000000:
#!/bin/bash
let ok_count=0 warn_count=0 critical_count=0
for node in $(ps -ef | awk <command>);
do
var=<command>
echo "node ${node}"
# >1000000 must be checked before >300000
if [[ ${var} -gt 1000000 ]]; then
echo "CRITICAL!";
let critical_count++
elif [[ ${var} -gt 300000 ]]; then
echo "WARNING!";
let warn_count++
else
echo "OK!";
let ok_count++
fi
done
if ((critical_count)); then
exit 2
elif ((warn_count)); then
exit 1
else
exit 0
fi
This script can be optimized, if only the exit status is needed:
CRITICAL is the highest warn level. So counting is not necessary.
OK is the fallback. So counting is not necessary.
#!/bin/bash
let warn_count=0
for node in $(ps -ef | awk <command>);
do
var=<command>
echo "node ${node}"
if [[ ${var} -gt 1000000 ]]; then
echo "CRITICAL! -> abort";
exit 2 # no more analysis needed!
elif [[ ${var} -gt 300000 ]]; then
echo "WARNING!";
let warn_count++
fi
done
exit $(( warn_count > 0 ))
Use continue operator, from man bash
...
continue [n]
Resume the next iteration of the enclosing for, while, until, or select loop. If n is specified, resume at the nth enclosing loop. n must be
≥ 1. If n is greater than the number of enclosing loops, the last enclosing loop (the ``top-level'' loop) is resumed. The return value is 0
unless n is not greater than or equal to 1.
Or break if you need to exit from loop
...
break [n]
Exit from within a for, while, until, or select loop. If n is specified, break n levels. n must be ≥ 1. If n is greater than the number of
enclosing loops, all enclosing loops are exited. The return value is 0 unless n is not greater than or equal to 1.

Bash continue only if ping is down

I'm trying to create a script which will only continue when pinging is unresponsive.
I'm running into 2 main issues. One being that it will require 2 CTL-C commands to end the script, and the other issue being that the script will never end and will need killing.
Here are my attempts;
rc=0
until [ $rc -gt 0 ]
do
ping 69.69.69.69 > /dev/null 2>&1
rc=$?
done
## Here will be the rest of code to be executed
I feel this one above is very close as it requires the 2 CTL-C commands and continues.
Here is something I found on SO but can't get to work at all.
counter=0
while [ true ]
do
ping -c 1 69.69.69.69 > /dev/null 2>&1
if [ $? -ne 0 ] then
let "counter +=1"
else
let "counter = 0"
fi
if [ $counter -eq 10 ] then
echo "Here should be executed once pinging is down"
fi
done
Any assistance is greatly appreciated, Thank you.
Try this:
while ping -c 1 -W 20 "$host" >& /dev/null
do
echo "Host reachable"
sleep 10 # do not ping too often
done
echo "Host unreachable within 20 seconds"
There's a few issues with that:
Firstly the if statements are wrong
Use this format
if [ $? -ne 0 ] then;
or this other format
if [ $? -ne 0 ]
then
Secondly I suspect that your pings are not timing out
ping -c 1 -w 2 69.69.69.69 > /dev/null 2>&1
might be helpfull
Thirdly your loop will continue incrementing counter even after 10. You might want to quit after 10
while [ $counter -le 10 ]
If you would be happy with executing something if ping times out after x seconds without any response it could be all condensed (ten seconds in the example below):
ping -c 1 -w 10 69.69.69.69 >/dev/null 2>&1 || echo "run if ping timedout without any response"

Why is the value of "$?" changed in the "if" condition?

I'm confused by the the value of "$?" in my shell script. I wrote a demo script as follows:
#!/bin/bash
echo "good"
if [ $? -ne 0 ];then
echo "$? not equal 0"
else
echo "$? equal 0"
fi
echo "good"
ret=$?
if [ $ret -ne 0 ];then
echo "$ret not equal 0"
else
echo "$ret equal 0"
fi
I know,"$?" is used to save the return value of the last function or command, but why is the output as below?
good
1 equal 0
good
0 equal 0
Does it mean that [ $? -eq 0 ] is also a command or function? I'm puzzled!
Your original code could be rewritten as:
$ if echo "good"; then echo "$? equal 0"; fi
good
0 equal 0
Since your code is only checking the exit value of the echo command, it's almost always going to return 0 (e.g. "true") unless its file descriptor is closed. For example:
$ echo foo; echo $?
foo
0
$ echo bar >&- ; echo $?
-bash: echo: write error: Bad file descriptor
1

How to do string comparison properly in shell script?

I am executing my url through shell script and storing the response in a variable.
http://hostname.domain.com:8080/beat
After hitting the above url, I will be getting the below response which I need to parse it and extract value of state from it
num_retries_allowed: 3 count: 30 count_behind: 100 state: POST_INIT num_rounds: 60 hour_col: 2 day_col: 0
Now I am extracting value of state variable from the above string using grep.
#send the request, put response in variable
DATA=$(wget -O - -q -t 1 http://hostname.domain.com:8080/beat)
#grep $DATA for state
state=$(grep -oP 'state: \K\S+' <<< "$DATA")
[[ -z "$state" ]] && state=0
echo $state
Also if in $DATA variable state: string is not there by any chance, then I want to assign 0 to state variable. After that I want to verify the conditionals and exit out of the script depending on that.
If state is equal to POST_INIT then exit successfully out of the shell script or if state is equal to 0, then exit successfully as well.
if [[ $state -eq "POST_INIT" || $state -eq "0" ]]; then exit 0; fi
So my above if condition is not working somehow. Since what I have noticed is if my state variable value is IN_INIT, then also it is exiting out of the shell script? Is there anything wrong I am doing here in the string comparison?
-eq is for comparing numbers. = is for comparing strings.
If you were using [ instead of [[ you would be getting an error for a value of POST_INIT and IN_INIT.
$ state=POST_INIT
$ [ $state -eq 0 ]
-bash: [: POST_INIT: integer expression expected
$ echo $?
2
What I believe [[ is doing is actually being more clever and more annoying.
I believe it is expanding the variable and then using the expanded value in an arithmetic context (in which variables are expanded even from bare words) and since the variable POST_INIT doesn't have a value that gets expanded to 0 by default and your check passes.
$ state=POST_INIT
$ [[ $state -eq 0 ]]; echo $?
0
$ POST_INIT=5
$ [[ $state -eq 0 ]]; echo $?
1
$ POST_INIT=0
$ [[ $state -eq 0 ]]; echo $?
0
trying this
if [[ "$state" == "POST_INIT" || "$state" == "0" ]];
will help because if you use something like [ $state == "POST_INIT" ] , it ignores $state if it is null and would rather read the statement as [ == "POST_INIT". including " " ,prevents that case.

Resources