I have a text file called file.txt that has below entries :-
healthy
healthy
healthy
healthy
healthy
unhealthy
initial
healthy
initial
healthy
Now i do a count of the number of healthy , initial and unhealthy in this file using below command :-
grep -c healthy file.txt
grep -c unhealthy file.txt
grep -c initial file.txt
Now i want a loop condition in shell script that does this for me :-
while [ $(grep -c "healthy" file.txt) -lt 6 -a $(grep -c "unhealthy" file.txt) != 0 -a $(grep -c "initial" file.txt) != 0 ]
do
bla bla bla
done
Basically all i am trying to do is that for this dynamic file whose entries will keep changing as part of some other script, i want a loop to happen as long as count of healthy in the file is less than equal to 6 and also count of unhealthy is anything above 0 and also count of initial is anything above 0, then do something else exit out of the loop. I am not getting the syntax right. Any help here would be greatly appreciated.
You're going about this the wrong way. This should be your starting point:
$ awk '{c[$1]++} END{for (i in c) print i, c[i]}' file
healthy 7
initial 2
unhealthy 1
wrt the conditions you want to act on you can just write them:
$ awk '
{ c[$1]++ }
END { exit ( (c["healthy"] <= 6) && (c["unhealthy"] > 0) && (c["initial"] > 0) ? 1 : 0 ) }
' file
$ echo $?
0
$ awk '
{ c[$1]++ }
END { exit ( (c["healthy"] <= 8) && (c["unhealthy"] > 0) && (c["initial"] > 0) ? 1 : 0 ) }
' file
$ echo $?
1
and use them as:
while awk '...' file; do
your stuff
done
Whatever else you want to do is likewise trivial, efficient, portable, and robust given the above starting point.
The short answer
After a discussion in the comments above, OP and I established that the only real problem in the proposed loop was that grep -c healthy
would also match unhealthy
, but otherwise the loop already works as intended.
\b
should be used to indicate word boundary, as in grep -c '\bhealthy'
, making the loop:
while [ $(grep -c '\bhealthy' file.txt) -lt 6 -a $(grep -c "unhealthy" file.txt) != 0 -a $(grep -c "initial" file.txt) != 0 ]
do
bla bla bla
done
EDIT: As @IanW pointed out in the comments, you can also use grep -c -w word
instead of adding \b
, which will be like adding \b
before and after each word.
Making it future proof
It is also worth repeating @CharlesDuffy's recommendation above to avoid -a
and -o
since they are flagged obsolescent, preferring [ ... ] && [ ... ]
instead. This is a good choice for long-term stable code.
So now the loop would look like this:
while [ $(grep -c '\bhealthy' file.txt) -lt 6 ] && [ $(grep -c "unhealthy" file.txt) != 0 ] && [ $(grep -c "initial" file.txt) != 0 ]
do
bla bla bla
done
Or making it bash specific
And finally I want to note that if this is going to be executed specifically in bash
and not sh
, [[ ... ]]
is faster because it is interpreted by bash itself rather than calling the program test
, which [
is an alias for. [[ ... ]]
is my personal preference, but unlike POSIX standard commands, it could break in the future and is not compatible with all shells. But it supports a syntax I find nicer and is often simpler to use, not requiring quoting variables all the time, in particular. See double vs single square brackets in bash for an interesting discussion on the topic.
So my own preferred format would be:
while [[ $(grep -c '\bhealthy' file.txt) -lt 6 && $(grep -c "unhealthy" file.txt) != 0 && $(grep -c "initial" file.txt) != 0 ]]
do
bla bla bla
done
How big is your file? If it's very large and you're scanning it with grep
three times that might make your script unnecessarily slow.
You can count the matches with one pass through the file using AWK:
read -r u_count h_count i_count <<< <(awk '{arr[$1]++} END {print arr["unhealthy"] arr["healthy"] arr["initial"]}'
while (( u_count < 6 && h_count != 0 && i_count != 0 ))
This will work as long as the data file looks like the example you posted or even if there are other whitespace delimited fields after those words. If those words aren't the first ones on each line, then the AWK script can be modified appropriately.
Unless these counts are changing inside the loop, you might just want to use an if
instead of a while
.