Bash while condition

bashshelltest

I have two conditions wait some time and check if within directory is more than one file. When one of these conditions is true loop has to be done so it is normal OR logical statement.

The expected behavior means that if time turn by or in directory is more than one file loop will be over. Currently is still waiting for more than one file. Can someone explain me why my while loop doesn't work as expected?

i=1; while [ $i -le 10 ] || [ $( ls /opt/hosts/ | wc -l ) -lt 2 ]; do sleep 3 $(( i++ )); done

Unfortunately i need one line command. I am wondering why my command is endless loop why OR does not work like should work: (i changed first condition to 3)

+ echo host_name 
+ i=1 
+ '[' 1 -le 2 ']' 
+ sleep 1 1 
+ '[' 2 -le 2 ']' 
+ sleep 1 2 
+ '[' 3 -le 2 ']' 
++ ls /opt/hosts/ 
++ wc -l 
+ '[' 1 -lt 2 ']' 
+ sleep 1 3 
+ '[' 4 -le 2 ']' 
++ ls /opt/hosts/ 
++ wc -l 
+ '[' 1 -lt 2 ']' 
+ sleep 1 4 
+ '[' 5 -le 2 ']' 
++ ls /opt/hosts/ 
++ wc -l 
+ '[' 1 -lt 2 ']' 
+ sleep 1 5 ```

Best Answer

  • You're giving sleep two arguments, 3 and $(( i++ )). With GNU tools, this means that the loop would sleep an increasing number of seconds in each iteration. On a non-GNU system, you would get an error from sleep.

  • Your loop will always run at least 10 times due to the way you have written your test. Your test says "Iterate while $i is less or equal to 10". Once $i reaches 10, the other part of the test will come into effect. You probably want a logical AND test here rather than an OR.

  • You should not use ls | wc -l to count files in a directory. This will give the wrong result in some (albeit pathological) cases.

Instead:

i=1
while [ "$i" -ne 10 ]; do
    set -- /opt/hosts/*
    if [ "$#" -ge 2 ]; then
        break
    fi

    sleep 3         # or, to sleep longer and longer: sleep "$(( 3 + i ))"
    i=$(( i + 1 ))
done

This properly increments i in each iteration, and also uses a safer way to count the number of names in the /opt/hosts directory (by expanding the * glob in the directory and counting the number of names that it expands to). The loop exits whenever the number of names is two or more, or whenever $i reaches the value 10.

After the loop, if $i is 10, then the files failed to materialize.

If you need to preserve your positional parameters (these would be overwritten by the set command), then expand the glob into an array with names=( /opt/hosts/* ) and then use "${#names[@]}" to get the length of that array instead of using "$#".

You could also write it as

i=1
while [ "$i" -ne 10 ] && ( set -- /opt/hosts/*; [ "$#" -lt 2 ] )
do
    sleep 3
    i=$(( i + 1 ))
done

This would not clobber your existing positional parameters as set is running in a subshell. It would also be closer to the type of thing you attempted yourself.


As a "one-liner":

i=1; while [ "$i" -ne 10 ]; do set -- /opt/hosts/*; [ "$#" -ge 2 ] && break; sleep 3; i=$(( i + 1 )); done

Or, with an "arithmetic for loop" in bash:

for (( i=1; i<=10; i++ )); do set -- /opt/hosts/*; [ "$#" -ge 2 ] && break; sleep 3; done

Or, with that last piece of code above the divider:

i=1; while [ "$i" -ne 10 ] && ( set -- /opt/hosts/*; [ "$#" -lt 2 ] ); do sleep 3; i=$(( i + 1 )); done
Related Question