2

I want to execute a command in a loop until it exits with the exit code 0, so I tried:

$ while/until $?; do $command; done

But until/while seems to only accept booleans like true/false and not 0/1.

Did I miss something like [[ ]] or something else?

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
aab
  • 31

2 Answers2

15

The while and until keywords takes a command as their argument. If the command returns a zero exit-status, signalling "success", while would run the commands in the body of the loop, while until would not run the commands in the body of the loop.

Your code seems to use while and until with $?, the exit-status of the most recent command, as a command. This will not work, unless you have commands with integers as names on the system. What would work is to use the test command to compare it against 0, for example, with

until test "$?" -eq 0; do some-command; done

or, using the short form of test,

until [ "$?" -eq 0 ]; do some-command; done

The issue now is obviously that $? may well be zero when entering the loop, preventing the loop from running even once. It would make more sense to let the command itself give its exit-status directly to either while or until (unless you want to initialize $? to a non-zero value by calling e.g. false just before the loop, which would look strange).

Using standard POSIX (though not Bourne, not that it matters much these days) shell syntax:

while ! some-command; do continue; done

or (both Bourne and POSIX),

until some-command; do continue; done

Both of these runs some-command until it returns a zero exit-status ("success"). In each iteration, it executes the continue keyword inside the loop body. Executing continue skips ahead to the next iteration of the loop immediately, but you could replace it with e.g. sleep 5 to have a short delay in each iteration, or with : which does nothing.

Another variant would be to have an infinite loop that you exit when your command succeeds:

while true; do some-command && break; done

or, slightly more verbose, and with added air for readability,

while true; do
    if some-command; then
        break
    fi
done

The only time you actually need to use $? is when you are required to keep track of this value for a while, as in

somefunction () {
    grep -q 'pattern' file
    ret=$?
if [ "$ret" -ne 0 ]; then
    printf 'grep failed with exit code %s\n' "$ret"
fi >&2

return "$ret"

}

In the code above, the exit-status in $? is reset by the test [ "$ret" -ne 0 ] and we would not be able to print its value nor return it from the function without storing it in a separate variable.


Another thing to point out is that you seem to be using a string, $command (unquoted), as your command. It would be much better to use an array if you want to run a command stored in a variable:

thecommand=( awk -F ',' 'BEGIN { "uptime" | getline; split($4,a,": "); if (a[2] > 5) exit 1 }' )

while ! "${thecommand[@]}"; do sleep 5; done

(This example code sleeps in intervals of five seconds until the 1-minute average system load has gone below 5.)

Note how by using an array and quoting the expansion as "${thecommand[@]}", I can run my command without worrying about how the shell would split the string into words and apply filename globbing to each of those words.

See also How can we run a command stored in a variable? for more info on this aspect of your issue.

Kusalananda
  • 333,661
  • Instead of test or [ I'd recommend using arithmetic expansion: while (($?)) – Konrad Rudolph Mar 14 '21 at 18:50
  • @KonradRudolph Can you motivate why you would want to do that? Take into account that the rest of my answer is free of bash-ism and that I don't actually recommend using test or [ but to let the while or until keywords act on the exist-status of the command directly (by executing the command) instead of on $?. – Kusalananda Mar 14 '21 at 19:02
  • The question is tagged Bash. I’m fine with avoiding most bashisms to improve portability, but using [/test is simply a lot more error-prone than the Bash alternatives. That said, the real solution (which your answer contains) of course uses none of these alternatives. – Konrad Rudolph Mar 14 '21 at 19:29
  • @KonradRudolph I think we have a matter of opinion and/or personal preference here. I tend to keep to POSIX syntax if I can, as making a script, or portion of a script, non-portable on purpose seems unnecessary. Also, developers that have issues with quoting for test, will also have issues with the special syntactic sugar that bash provides. – Kusalananda Mar 14 '21 at 19:34
1

You could do something like:

false; while [[ $? -ne 0 ]]; do
  command
done

Here, false would initially set $? to a non-zero value, allowing the loop to run at least once. The loop then runs the given command until it exits with a zero exit-status.

Kusalananda
  • 333,661
jesse_b
  • 37,005