Bash until Loop: Run Commands Until a Condition Becomes True

Learn Bash until loops: syntax, exit statuses, numeric tests, retries, file polling, timeouts, break, continue, and comparisons with while loops.

An until loop repeatedly runs a group of commands while its condition is false. As soon as the condition becomes true, Bash stops repeating the loop. This makes until useful for counters, retries, polling for files, and waiting for services.

The key rule is the opposite of a while loop:

  • while continues when its condition is true.
  • until continues when its condition is false and stops when it is true.

For a complete beginner-oriented comparison, see the Bash until loop guide.

How an until Loop Works

Bash evaluates the loop condition before every iteration, including before the first iteration. If the condition returns a nonzero exit status, Bash runs the loop body. If it returns zero, Bash skips the body or stops repeating.

Because this is a pre-test loop, the body can run zero times. If the condition is already true before the loop starts, Bash immediately exits the loop.

until condition; do
  commands
 done

The condition is a command or test expression. The commands between do and done are the loop body.

ComponentRoleNotes
untilStarts the loop structureRepeats while the condition is false.
condition commandDetermines whether to continueZero means true; nonzero means false.
doMarks the beginning of the bodyMay follow the condition on the same line after a semicolon.
loop bodyCommands repeated by the loopUsually changes the state tested by the condition.
doneEnds the loopBash returns to the condition after the body finishes.

Multiline and Same-Line Forms

The conventional multiline form places do on its own line:

until condition
 do
  commands
 done

In normal Bash style, do is commonly written without the leading indentation shown above:

until condition
 do
  commands
done

It can also appear on the same line as the condition. The semicolon terminates the condition command before do:

until condition; do
  commands
done

Exit Statuses and Conditions

A shell command returns a numeric exit status when it finishes. By convention:

  • Exit status 0 means success, which shell conditions interpret as true.
  • A nonzero exit status means failure, which shell conditions interpret as false.

An until loop runs its body when the condition command returns nonzero. It stops when that command returns zero.

until grep -q "ready" status.txt; do
  printf '%s\n' 'The ready marker has not been found.'
  sleep 1
done

Here, grep returns nonzero when it does not find ready, so the loop continues. When grep finds the text, it returns zero and the loop stops.

The [ ... ] Test Command

The bracket form is a command named [. It evaluates file, string, and numeric tests. The closing bracket is a required final argument, so spaces are mandatory:

until [ "$status" -eq 0 ]; do
  commands
done

Do not write [$status -eq 0]. The missing spaces change the arguments and usually produce an error or an unexpected result. Quote variable expansions in bracket tests so an empty or whitespace-containing value does not break the expression.

TestMeaningSuitable use in an until loop
-eq and -neNumbers are equal or not equalStop when a counter reaches a specific value.
-lt and -geLess than or greater than or equalContinue below a limit or stop at a lower bound.
-eA path existsWait for any kind of file-system entry.
-fA regular file existsWait for a generated file or completion marker.
-dA directory existsWait for a directory to be created.
command exit statusA command succeeded or failedRetry an operation until it succeeds.

Arithmetic Conditions

Bash arithmetic evaluation uses (( ... )). It is convenient for numeric conditions and counters:

count=0
until (( count >= 5 )); do
  printf 'count=%s\n' "$count"
  ((count++))
done

Inside (( ... )), a nonzero arithmetic result is treated as true and zero as false. Use a Bash shebang when relying on Bash-specific arithmetic syntax.

Counter Example

This script initializes an integer variable to zero, prints it, and increments it until it reaches five:

#!/usr/bin/env bash

num=0
until [ "$num" -eq 5 ]; do
  printf 'The number is %s.\n' "$num"
  ((num++))
done

Expected output:

The number is 0.
The number is 1.
The number is 2.
The number is 3.
The number is 4.

The body prints values from 0 through 4. After printing 4, ((num++)) changes the value to 5. Bash then tests [ "$num" -eq 5 ]; the test is true, so the body does not process or print the terminating value.

Use numeric operators such as -eq, -lt, and -ge for numeric intent. String comparisons are not a substitute for numeric comparisons.

Updating Loop State

Every useful loop needs progress. The loop body must change the variable or external state checked by the condition:

((count++))
((count += 1))
count=$((count + 1))

If the condition checks a value that never changes, the condition may remain false forever. This creates an infinite loop. A misplaced continue can cause the same problem if it skips the update.

count=0
until (( count == 3 )); do
  printf 'count=%s\n' "$count"
  ((count += 1))
done

Using Commands as the Condition

The condition does not have to be a comparison. Any command can be used. Its exit status determines whether the loop continues.

Retry a Command Until It Succeeds

This example retries an HTTP health check. The --fail option makes unsuccessful HTTP responses produce a failure status, while --silent and --output /dev/null suppress normal output:

#!/usr/bin/env bash

until curl --fail --silent --output /dev/null https://example.com/health; do
  printf '%s\n' 'Service is not ready; retrying...'
  sleep 5
done
printf '%s\n' 'Service is ready.'

The delay prevents a rapid retry loop that wastes CPU or floods the service with requests. For remote systems, exponential backoff can be preferable to a fixed delay.

Wait Until a File Appears

A file test can poll for a generated file or completion marker:

#!/usr/bin/env bash

attempt=1
max_attempts=10
until [ -f /tmp/job-complete ]; do
  if [ "$attempt" -gt "$max_attempts" ]; then
    printf '%s\n' 'Timed out waiting for completion marker.' >&2
    exit 1
  fi

  printf 'Waiting for completion marker (attempt %s of %s)...\n' \
    "$attempt" "$max_attempts"
  ((attempt++))
  sleep 2
done
printf '%s\n' 'Completion marker found.'

The retry limit gives the script a bounded termination path. Without it, a missing file could cause the script to wait indefinitely.

Wait for a Network Endpoint with a Limit

A production-oriented retry loop should report failure when its maximum attempts are exhausted:

#!/usr/bin/env bash

attempt=1
max_attempts=6
until curl --fail --silent --output /dev/null http://localhost:8080/health; do
  if [ "$attempt" -ge "$max_attempts" ]; then
    printf 'Endpoint did not become reachable after %s attempts.\n' \
      "$max_attempts" >&2
    exit 1
  fi

  printf 'Endpoint is unavailable; retrying attempt %s.\n' "$((attempt + 1))"
  ((attempt++))
  sleep 3
done
printf '%s\n' 'Endpoint is reachable.'

break and continue

break exits the nearest loop immediately. Use it when an additional event means the loop should end before its normal condition becomes true.

continue skips the rest of the current iteration and starts the next condition evaluation. Update required state before continue, or the loop may never make progress.

count=0
until (( count >= 10 )); do
  ((count++))

  if (( count % 2 == 0 )); then
    continue
  fi

  printf 'Odd value: %s\n' "$count"

  if (( count >= 7 )); then
    break
  fi
done

The increment happens before continue, so even iterations are skipped safely. The break ends the loop once the required odd value has been printed.

Safe Termination and Interrupts

Foreground scripts can usually be interrupted with Ctrl+C. The terminal sends an interrupt signal, commonly handled by Bash by stopping the foreground command or script. This is useful interactively, but unattended scripts cannot depend on a person pressing Ctrl+C.

For external-state loops, use at least one safety mechanism:

  • A maximum attempt count.
  • A deadline or elapsed-time limit.
  • A delay between attempts.
  • Exponential backoff for expensive or remote operations.
  • Explicit failure handling with a nonzero exit status.

until Versus while

These loops can express inverse forms of the same idea:

# Continues while count is below 5
while [ "$count" -lt 5 ]; do
  # commands
done

# Continues until count reaches 5
until [ "$count" -eq 5 ]; do
  # commands
done
Loop typeBody runs when condition isStops when condition isTypical use
untilFalse or nonzeroTrue or zeroRetrying until success or waiting for a desired state.
whileTrue or zeroFalse or nonzeroContinuing while a state remains valid.

An until loop is not equivalent to a do-while loop. A do-while loop runs its body once before checking its condition; Bash until checks first and may run zero times.

Use a for loop when iterating over a known list or fixed range. It communicates that the set of items or iterations is already known, whereas until is clearer when stopping depends on a condition becoming true.

Shell Portability

until is available in Bash and POSIX-style shells. The bracket test form, such as [ "$value" -eq 5 ], is portable across those environments when used with valid POSIX operators.

[[ ... ]] provides Bash and some other shell-specific conveniences, but it is not portable POSIX syntax. Likewise, some arithmetic forms and features are Bash-specific. If a script uses Bash-specific syntax, declare that requirement explicitly:

#!/usr/bin/env bash

For maximum shell portability, use a POSIX shell shebang and portable [ ... ] tests. Do not mix Bash-only features into a script intended to run under /bin/sh.

Troubleshooting until Loops

The Loop Never Stops

  • Check whether the variable in the condition is updated.
  • Make sure an update is not skipped by continue.
  • Verify the comparison operator and the value being tested.
  • For external commands, confirm that success is possible.
  • Add a retry limit and a delay.
printf 'current value: %s\n' "$count"

During debugging, run the condition by itself and inspect its exit status:

[ "$count" -eq 5 ]
echo "$?"

The Conditional Expression Reports an Error

Check for spaces around the bracket command and quote variable expansions:

[ "$value" -eq 5 ]

Use numeric operators for numbers. A string operator used with numeric intent can produce incorrect behavior.

The Loop Runs When It Should Stop

Remember that nonzero means false to the shell. An until loop continues after a nonzero condition status and stops after zero. A condition copied from a while loop may need to be inverted or rewritten.

The Body Never Runs

The condition was already true before the first test. Initialize the state so the condition starts false when at least one iteration is required. If the body must always run once, choose a structure designed for post-test behavior rather than relying on until.

Retries Use Too Much CPU or Network Traffic

Add sleep between attempts, and use a maximum count or deadline. For unreliable remote services, progressively increase the delay with exponential backoff.

Exam-Relevant Summary

  • until runs the body while its condition is false.
  • The loop stops when the condition command returns exit status zero.
  • Exit status zero means true or success; nonzero means false or failure.
  • The condition is tested before the first iteration, so the body can run zero times.
  • Use spaces around [ and ], and quote variable expansions.
  • Use -eq, -lt, and related operators for numeric comparisons.
  • Update loop state on every path, including paths involving continue.
  • Use sleep, retry limits, deadlines, and failure exits for external-state loops.
  • break exits the nearest loop; continue begins the next iteration.
  • until is pre-test, so it is not a Bash do-while loop.