While loop condition in shell script

2019-08-13 13:10发布

问题:

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.

回答1:

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.



回答2:

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


回答3:

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.