Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Bash & the POSIX Shell  Module 5 – Loops

while, until, break, continue and select

Repeat Bash commands while a test succeeds or until it does, leave nested loops with break 2, retry with growing pauses and build menus with select.

  • Beginner
  • 10 minutes
  • Examples run with Bash 5.3.20, dash 0.5.13.5, shellcheck 0.11.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Write condition-driven loops with while and until, and predict the status they end with
  • Use break N and continue N to leave or skip the passes of nested loops
  • Build a retry loop with back-off and a final status, a polling loop with a deadline, and a menu with select

Before you start

What you’ll learn · Before you start

What you will learn

  • Write condition-driven loops with while and until, and predict the status they end with
  • Use break N and continue N to leave or skip the passes of nested loops
  • Build a retry loop with back-off and a final status, a polling loop with a deadline, and a menu with select

Before you start

On this page

A retry loop written as for attempt in 1 2 3 4 5; do upload && break; sleep $(( 2 ** attempt )); done pauses for 62 seconds when every upload fails, and then the script carries on with status 0 as if one had worked. The same five attempts need only 30 seconds of pauses, and they can end with the real failure, once the loop knows when to stop.

You will write loops that run while a command succeeds or until it does, leave or skip the passes of nested loops with break and continue, build that retry loop and a polling loop with a deadline, and offer a numbered menu with select.

while runs while a command succeeds, until while it fails

A retry policy that starts at 2 seconds and doubles pauses for 2, 4, 8 and 16 seconds if it stops before 32. A while loop can print that schedule. It runs its test, which is any command, and while the test ends with exit status: The number from 0 to 255 every command ends with, kept by the shell in $?. 0 means success; Bash uses 126 for found but not executable and 127 for not found. More in “Your first script: shebang, chmod and running it” 0 it runs the body and tests again. until is the same loop with the test turned round: it runs the body while the test fails and stops at the first success. The session prints the schedule with while, with until and once more in dash:

The back-off schedule with while, until and dash, an unused file name and two status rules Python · schedule_session.py
from session import run

run("""
delay=2; while (( delay <= 16 )); do echo "pause ${delay}s"; delay=$(( delay * 2 )); done; echo "after the loop: delay=$delay"
delay=2; until (( delay > 16 )); do echo "pause ${delay}s"; delay=$(( delay * 2 )); done
dash -c 'delay=2; while [ "$delay" -le 16 ]; do echo "pause ${delay}s"; delay=$((delay * 2)); done'
touch report.1 report.2 report.3
n=1; while [[ -e report.$n ]]; do n=$(( n + 1 )); done; echo "next free name: report.$n"
false; while false; do echo "never printed"; done; echo "status after no pass: $?"
check() { return 3; }; tries=0; until check; do echo "check failed with $?"; tries=$(( tries + 1 )); (( tries == 2 )) && break; done
""")

Output

$ delay=2; while (( delay <= 16 )); do echo "pause ${delay}s"; delay=$(( delay * 2 )); done; echo "after the loop: delay=$delay"
pause 2s
pause 4s
pause 8s
pause 16s
after the loop: delay=32
$ delay=2; until (( delay > 16 )); do echo "pause ${delay}s"; delay=$(( delay * 2 )); done
pause 2s
pause 4s
pause 8s
pause 16s
$ dash -c 'delay=2; while [ "$delay" -le 16 ]; do echo "pause ${delay}s"; delay=$((delay * 2)); done'
pause 2s
pause 4s
pause 8s
pause 16s
$ touch report.1 report.2 report.3
$ n=1; while [[ -e report.$n ]]; do n=$(( n + 1 )); done; echo "next free name: report.$n"
next free name: report.4
$ false; while false; do echo "never printed"; done; echo "status after no pass: $?"
status after no pass: 0
$ check() { return 3; }; tries=0; until check; do echo "check failed with $?"; tries=$(( tries + 1 )); (( tries == 2 )) && break; done
check failed with 3
check failed with 3

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 schedule_session.py

The table follows the first loop one test at a time. The body doubles delay, so the test eventually fails and the loop ends:

Test delay (( delay <= 16 )) Then
1 2 status 0 prints pause 2s
2 4 status 0 prints pause 4s
3 8 status 0 prints pause 8s
4 16 status 0 prints pause 16s
5 32 status 1 the loop ends; delay stays 32

until (( delay > 16 )) printed the same four lines, because “until the delay passes 16” is “while it does not”. The dash line is the POSIX form of the same counter: (( )) is a Bash command that POSIX does not have, as the shell arithmetic lesson showed, so a script for sh tests with [ "$delay" -le 16 ] and computes with $((delay * 2)). A while loop with a counter is also how sh counts when the C-style for of the for loops lesson is not there.

The test does not have to be arithmetic. while [[ -e report.$n ]] went on adding 1 while a file of that name existed, and stopped at report.4, the first name nobody had taken.

A loop that never changes its own test

Wrong: n=1; while [[ -e report.$n ]]; do echo "taken: report.$n"; done, which never changes n and repeats the same line until someone stops it. Right: change what the test reads in every pass, here with n=$(( n + 1 )), and keep a limit when the condition may never change, as the polling loop below does.

Two rules about statuses come from the manual and POSIX. A loop ends with the status of the last command its body ran, or 0 when the body never ran: while false ran no pass, so the session printed 0 although false had failed. The test’s own status is not the loop’s status. Inside the body of until, though, $? still holds the status of the failed test, which is how until check could print “check failed with 3”, and how a loop can keep it for later.

Key idea

while repeats while its test command succeeds and until while it fails; either loop ends with the status of the last body command it ran, or 0 when it ran none.

Checkpoint

Check: while and until loops

  1. Question 1 of 2 until grep -q READY status.txt; do sleep 1; done. When does the body run?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Each time grep finds no READY line, so the loop waits until one appears

    until cmd reads as while not cmd: the session's until (( delay > 16 )) printed the same four pauses as while (( delay <= 16 )).

  2. Question 2 of 2 What does false; until true; do echo never; done; echo $? print on its last line?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 0

    The test true succeeds at once, so the body never runs, and a loop that ran no pass ends with status 0, as the session's while false printed 0. The status of false before the loop plays no part.

break leaves a loop, continue skips a pass, and a number reaches outer loops

: is a builtin that always succeeds, so while : never stops by itself; break ends it. The session’s first line counted attempts 1, 2 and 3 and left after the third, the shape to use when the test belongs at the end of the body, because the shell has no do-while loop. continue does the opposite of finishing: it skips the rest of the body and starts the next pass, after testing again in while and until, or with the next word in for.

while : with break, break 2 and continue in Bash and dash, and search.sh Python · nested_session.py
from session import files, run, write

files("search.sh")
write("mon/a.log", "INFO backup started\nINFO backup done\n")
write("mon/b.log", "INFO 12 files copied\n")
write("tue/a.log", "ARCHIVED to cold storage\n")
write("tue/b.log", "ERROR this old line is never read\n")
write("wed/a.log", "INFO backup started\nERROR disk full\n")
write("wed/b.log", "ERROR not reached either\n")
write("thu/a.log", "INFO backup done\n")
run("""
attempt=0; while :; do attempt=$(( attempt + 1 )); echo "attempt $attempt"; (( attempt == 3 )) && break; done; echo "left the loop after $attempt"
for i in 1 2; do for c in a b c; do [ "$c" = b ] && continue; [ "$i" = 2 ] && break 2; echo "$i$c"; done; done; echo "after both loops"
dash -c 'for i in 1 2; do for c in a b c; do [ "$c" = b ] && continue; [ "$i" = 2 ] && break 2; echo "$i$c"; done; done; echo "after both loops"'
bash search.sh
""")

Output

$ attempt=0; while :; do attempt=$(( attempt + 1 )); echo "attempt $attempt"; (( attempt == 3 )) && break; done; echo "left the loop after $attempt"
attempt 1
attempt 2
attempt 3
left the loop after 3
$ for i in 1 2; do for c in a b c; do [ "$c" = b ] && continue; [ "$i" = 2 ] && break 2; echo "$i$c"; done; done; echo "after both loops"
1a
1c
after both loops
$ dash -c 'for i in 1 2; do for c in a b c; do [ "$c" = b ] && continue; [ "$i" = 2 ] && break 2; echo "$i$c"; done; done; echo "after both loops"'
1a
1c
after both loops
$ bash search.sh
checked mon/a.log
checked mon/b.log
skipped tue: archived
first error in wed/a.log
search finished

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 nested_session.py

In nested loops a number says which loop is meant, counted from the inside. In the second line, continue skipped the echo when c was b, and break 2 ended both loops as soon as i was 2, so only 1a and 1c were printed, in Bash and in dash alike. POSIX adds that a number larger than the loops around the command leaves the outermost one. The four forms, as they act from inside an inner loop:

Command What it does from the inner loop
break ends the inner loop; the outer pass goes on after the inner done
break 2 ends both loops; the script goes on after the outer done
continue starts the inner loop’s next pass
continue 2 abandons the rest of this outer pass; the outer loop takes its next value

search.sh uses both numbers on four folders of logs: a folder whose log starts with ARCHIVED is skipped as a whole, and the first ERROR line anywhere ends the search.

search.sh: the first ERROR, folder by folder Shell · search.sh
#!/usr/bin/env bash
# search.sh: finds the first log with an ERROR line, folder by folder.
# A folder whose log starts with ARCHIVED is skipped as a whole.
for day in mon tue wed thu; do
  for log in "$day"/*.log; do
    if grep -q '^ARCHIVED' "$log"; then
      echo "skipped $day: archived"
      continue 2
    fi
    if grep -q ERROR "$log"; then
      echo "first error in $log"
      break 2
    fi
    echo "checked $log"
  done
done
echo "search finished"

Step through the run to see which files the two numbers kept the script from opening:

Interactive · step by step search.sh with continue 2 and break 2 Step through the search. Each row is a day and each box a log file; its sub-label says what the script did with it.
search.sh with continue 2 and break 2, step 6 of 6. Showing mon/a.log (checked), mon/b.log (checked), tue/a.log (ARCHIVED), tue/b.log (never read), wed/a.log (first error), wed/b.log (never read), thu/a.log (never read), search finished (printed).next daycontinue 2break 2mon/a.logcheckedmon/b.logcheckedtue/a.logARCHIVEDtue/b.lognever readwed/a.logfirst errorwed/b.lognever readthu/a.lognever readsearch finishedprinted

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The first file. The outer loop sets day to mon and the inner loop takes mon/a.log. It has no ARCHIVED line and no ERROR line, so the script prints "checked mon/a.log".
  2. The inner loop ends. mon/b.log is clean too. The inner loop has no files left, so the outer loop takes its next day, tue.
  3. continue 2 skips a day. tue/a.log starts with ARCHIVED. continue 2 starts the next pass of the second loop out, the day loop, so tue/b.log is never opened, although it holds an ERROR line.
  4. break 2 ends both loops. wed/a.log holds "ERROR disk full". The script prints "first error in wed/a.log", and break 2 leaves the inner loop and the day loop together.
  5. What the loops never reached. wed/b.log and thu/a.log are never read. Execution goes on after the outer done, and the last line prints "search finished".
  6. The rule. Without a number, break and continue act on the innermost loop. With 2 they act on the loop around it, so one test deep inside can skip or end the whole search.

tue/b.log holds an ERROR line, yet the search reported wed/a.log: continue 2 had already moved on to the next day, as intended for an archived folder. A plain continue there would have opened tue/b.log and stopped the search on an old error.

One loop too few

Wrong: a plain break after the first error inside the inner loop, which ends only the file loop and lets the day loop open the next folder. Right: break 2, which leaves both loops at once.

Checkpoint

Check: break and continue, with a number

  1. Question 1 of 2 What does for d in a b; do for n in 1 2 3; do [ "$n" = 2 ] && break 2; echo "$d$n"; done; done print?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: a1 only

    The number counts loops from the inside: 1 is the n loop, 2 the d loop around it. The session's 1a 1c example stopped the same way when i reached 2.

  2. Question 2 of 2 In search.sh, continue 2 is changed to a plain continue. What does the run print after "skipped tue: archived"?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: "first error in tue/b.log", then "search finished"

    continue 2 abandoned the whole archived day; continue abandons only the current file, so the old error in tue/b.log would have been reported.

A retry loop needs a limit, a growing pause and a check after the last try

The opening’s loop shows what goes wrong. The session replaces sleep with a function that prints the pause instead of waiting, and lets upload fail every time:

The naive retry loop, then retry.sh on a flaky command and on false Python · retry_session.py
from session import files, run

files("retry.sh", "flaky.sh")
run("""
# upload stands in for a command that always fails; this sleep prints the pause instead of waiting
upload() { return 1; }; sleep() { echo "(sleep $1)"; }
for attempt in 1 2 3 4 5; do upload && break; sleep $(( 2 ** attempt )); done; echo "carried on, status $?"
unset -f sleep
RETRY_SLEEP=: bash retry.sh bash flaky.sh
!RETRY_SLEEP=: bash retry.sh false
""")

Output

# upload stands in for a command that always fails; this sleep prints the pause instead of waiting
$ upload() { return 1; }; sleep() { echo "(sleep $1)"; }
$ for attempt in 1 2 3 4 5; do upload && break; sleep $(( 2 ** attempt )); done; echo "carried on, status $?"
(sleep 2)
(sleep 4)
(sleep 8)
(sleep 16)
(sleep 32)
carried on, status 0
$ unset -f sleep
$ RETRY_SLEEP=: bash retry.sh bash flaky.sh
flaky: call 1
retry: attempt 1 failed with status 3, next in 2s
flaky: call 2
retry: attempt 2 failed with status 3, next in 4s
flaky: call 3
retry: succeeded on attempt 3
$ RETRY_SLEEP=: bash retry.sh false
retry: attempt 1 failed with status 1, next in 2s
retry: attempt 2 failed with status 1, next in 4s
retry: attempt 3 failed with status 1, next in 8s
retry: attempt 4 failed with status 1, next in 16s
retry: giving up after 5 attempts, last status 1
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 retry_session.py

The naive loop printed five pauses, 2 + 4 + 8 + 16 + 32 = 62 seconds, the last one after the final failure, when no attempt was left to wait for. Then it reported status 0, because a for loop ends with the status of its last command, and that was sleep. A script that goes on to delete the local copy would delete it after five failed uploads.

retry.sh fixes all three problems: the wasted last pause, the lost status and the script that carries on. It runs the command as the test of until, so the body runs only after a failure. The body saves the status, gives up with that status after attempt 5, and otherwise pauses 2 ** attempt seconds, an exponential back-off: A retry policy whose pause grows by a fixed factor after every failed attempt, such as 2, 4, 8 and 16 seconds, so that a struggling service gets more time to recover with each try. that gives a struggling service more time with every try:

retry.sh: up to 5 attempts, pauses of 2, 4, 8 and 16 seconds, the last status Shell · retry.sh
#!/usr/bin/env bash
# retry.sh COMMAND [ARGUMENT...]: runs COMMAND up to 5 times, pausing 2, 4, 8 and 16 seconds
# between attempts, and exits with the status of the last attempt.
# RETRY_SLEEP=: skips the pauses (the session sets it, so the page shows them without waiting).
max=5
attempt=1
until "$@"; do
  status=$?
  if (( attempt == max )); then
    echo "retry: giving up after $attempt attempts, last status $status" >&2
    exit "$status"
  fi
  pause=$(( 2 ** attempt ))
  echo "retry: attempt $attempt failed with status $status, next in ${pause}s" >&2
  "${RETRY_SLEEP:-sleep}" "$pause"
  attempt=$(( attempt + 1 ))
done
echo "retry: succeeded on attempt $attempt"

The session set RETRY_SLEEP=:, so the pauses were printed but not waited. flaky.sh failed twice with status 3 and succeeded on its third call, after 2 + 4 = 6 seconds of pauses. With false, every attempt failed: the pauses came to 2 + 4 + 8 + 16 = 30 seconds, and the script exited with 1, the status of the last attempt. Step through the calls:

Interactive · message by message retry.sh runs flaky.sh until it succeeds Step through the three attempts, then the case where every attempt fails.
retry.sh runs flaky.sh until it succeeds, step 5 of 5. Messages so far: retry.sh to flaky.sh: call 1; flaky.sh to retry.sh: status 3; flaky.sh to retry.sh: status 3; flaky.sh to retry.sh: status 0.retry.shexit status 1flaky.shsucceededcall 1status 3status 3status 0

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Attempt 1. until runs its test, the command retry.sh was given. flaky.sh prints "flaky: call 1".
  2. A failure runs the body. flaky.sh fails with status 3. Because the test failed, until runs the body, which saves status=3 and pauses 2 ** 1 = 2 seconds.
  3. Attempt 2. The test runs again. flaky.sh fails a second time with status 3, and the pause doubles to 2 ** 2 = 4 seconds.
  4. Attempt 3 succeeds. flaky.sh prints "flaky: call 3" and returns 0. until stops without running the body, and retry.sh prints "succeeded on attempt 3" after 6 seconds of pauses.
  5. When every attempt fails. With false as the command, attempts 1 to 4 pause 2, 4, 8 and 16 seconds, 30 in all. After attempt 5 the body exits at once with status 1 instead of pausing 32 seconds more.

polling: Checking a state again and again at a fixed interval, such as testing once a second whether a file exists, until it holds or a deadline passes. is the same loop pointed at a state instead of a command’s success. wait-for-file.sh tests once a second whether a file exists and gives up at a deadline taken from SECONDS, which Bash keeps as the number of seconds since the shell started, read from the system clock at one-second resolution. So a limit of 2 is good to about a second. It sleeps whole seconds because POSIX’s sleep promises nothing finer.

wait-for-file.sh: an until loop with a deadline Shell · wait-for-file.sh
#!/usr/bin/env bash
# wait-for-file.sh FILE LIMIT: checks once a second until FILE exists,
# and gives up when LIMIT seconds have passed.
file=$1
limit=$2
deadline=$(( SECONDS + limit ))
until [[ -e $file ]]; do
  if (( SECONDS >= deadline )); then
    echo "wait-for-file: gave up, $file did not appear" >&2
    exit 1
  fi
  sleep 1
done
echo "wait-for-file: $file is there"

In the session it gave up on a file nobody created, with status 1, and found ready.flag once a background job created it two seconds later:

wait-for-file.sh giving up, and finding a file another process creates Python · wait_session.py
from session import files, run

files("wait-for-file.sh")
run("""
!bash wait-for-file.sh missing.flag 2
(sleep 2; touch ready.flag) & bash wait-for-file.sh ready.flag 10
""")

Output

$ bash wait-for-file.sh missing.flag 2
wait-for-file: gave up, missing.flag did not appear
[exit status 1]
$ (sleep 2; touch ready.flag) & bash wait-for-file.sh ready.flag 10
wait-for-file: ready.flag is there

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 wait_session.py

If you keep the for form

Record the outcome before the loop ends: ok=0; for attempt in 1 2 3; do if upload; then ok=1; break; fi; done, then (( ok )) || exit 1. The test after the loop is what keeps the script from going on as if it had worked.

Checkpoint

Check: Retry and polling loops

  1. Question 1 of 2 retry.sh runs a command that always fails with status 7. How does it end?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: After 5 attempts, with status 7

    With false the session showed "giving up after 5 attempts, last status 1" and exit status 1; a command that fails with 7 gives 7 the same way.

  2. Question 2 of 2 retry.sh is changed to max=4 and every attempt fails. How many seconds of pauses does it spend in all?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 14 seconds

    Attempts 1 to 3 are followed by pauses of 2, 4 and 8 seconds; the fourth attempt is the last, so it exits without pausing: 2 + 4 + 8 = 14. With max=5 the session's pauses came to 30.

select prints a numbered menu and puts the answer in REPLY

select turns a list of words into a menu. It prints each word with a number on standard error, prompts with PS3, and reads one line from standard input. When the line is the number of a word, the loop variable takes that word, any other text empties it, and the line itself always lands in REPLY. The body runs after every line that is not empty, and the loop prompts again until a break ends it. Here is a menu of three service actions:

menu.sh: a select menu of service actions Shell · menu.sh
#!/usr/bin/env bash
# menu.sh: a numbered menu of service actions; the choice comes from standard input.
PS3='action number: '
select action in start stop status; do
  if [[ -n $action ]]; then
    echo "chose $action (REPLY=$REPLY)"
    break
  fi
  echo "no action has the number $REPLY"
done

The session feeds the answers through a pipe, so the typed numbers do not appear after the prompt:

select with a valid number, a wrong one, an empty line, no input, and dash Python · menu_session.py
from session import files, run

files("menu.sh")
run("""
printf '2\\n' | bash menu.sh
printf '9\\n\\n3\\n' | bash menu.sh
printf '1\\n' | bash menu.sh 2> /dev/null
!bash menu.sh < /dev/null
!dash menu.sh
""")

Output

$ printf '2\n' | bash menu.sh
1) start
2) stop
3) status
action number: chose stop (REPLY=2)
$ printf '9\n\n3\n' | bash menu.sh
1) start
2) stop
3) status
action number: no action has the number 9
action number: 1) start
2) stop
3) status
action number: chose status (REPLY=3)
$ printf '1\n' | bash menu.sh 2> /dev/null
chose start (REPLY=1)
$ bash menu.sh < /dev/null
1) start
2) stop
3) status
action number:
[exit status 1]
$ dash menu.sh
menu.sh: 4: Syntax error: "do" unexpected
[exit status 2]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 menu_session.py

Each input line had its own effect, as the table shows:

Input action REPLY What select did
2 stop 2 ran the body, which printed the choice and broke out
9 empty 9 ran the body, then printed only the prompt again
an empty line unchanged empty printed the menu and the prompt again, without the body
end of input unchanged empty ended the loop with status 1

The menu came back only after the empty line: since bash-2.05b, select reprints it only when REPLY is empty, as its NEWS file records. 2> /dev/null hid the menu and the prompt and left “chose start”, which shows they go to standard error, so a script’s output stays clean when its menu is used. dash stopped at do with a syntax error and status 2: POSIX lists select among the words a shell may reserve with unspecified results, and dash has no such loop.

Checkpoint

Check: Menus with select

  1. One question A user types 7 at menu.sh, whose menu has three actions. What do action and REPLY hold in the body?

    Choose one answer.

    Show the answer: One question

    Answer: action is empty and REPLY is 7

    The session's 9 printed "no action has the number 9" from the body, then only the prompt again.

Practise: loops that stop on time

These questions move the lesson’s loops onto new cases: other file names, a step that jumps over its target, nested loops with continue 2, a retry whose pauses grow as 3 ** attempt, and menu.sh at end of input. Work out what each prints, or how long it waits, before you check your answer:

Practice that adapts to you

Practise this lesson

16 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 16 until cmd; do body; done runs body as long as …

    Choose one answer.

    Show the answer: Question 1 of 16

    Answer: cmd fails

    The Bash manual: until runs its body as long as the test commands have a status which is not zero.

  2. Question 2 of 16 report.1 to report.5 and report.7 exist. What does n hold after n=1; while [[ -e report.$n ]]; do n=$(( n + 1 )); done?

    Type a number.

    Show the answer: Question 2 of 16

    Answer: 6

    The loop stops at the first name that does not exist, report.6; report.7 is never tested. In the session the same loop stopped at report.4 after report.1 to report.3.

  3. Question 3 of 16 What does i=0; while (( i < 3 )); do i=$(( i + 1 )); done; echo $? print?

    Type a number.

    Show the answer: Question 3 of 16

    Answer: 0

    A loop ends with the status of the last command its body ran, here the assignment, which succeeded. The failed test that ended the loop is not the loop's status.

  4. Question 4 of 16 n=1; until (( n == 10 )); do n=$(( n + 2 )); done. What happens?

    Choose one answer.

    Show the answer: Question 4 of 16

    Answer: It never ends: n takes 1, 3, 5, 7, 9, 11 … and never equals 10

    Test the range, until (( n >= 10 )), so the loop stops even when the step jumps over the value, and keep a limit when you cannot be sure the test will ever hold.

  5. Question 5 of 16 Without a number, which loop does break end?

    Choose one answer.

    Show the answer: Question 5 of 16

    Answer: The innermost loop around it

    The manual: break exits from a for, while, until or select loop, and break n from the nth enclosing loop.

  6. Question 6 of 16 What does for i in 1 2 3; do for j in 1 2 3; do (( j == 2 )) && continue 2; echo "$i$j"; done; done print?

    Choose one answer.

    Show the answer: Question 6 of 16

    Answer: 11, 21 and 31

    continue 2 abandons the rest of the inner loop and starts the outer loop's next pass, as continue 2 skipped the archived day in search.sh.

  7. Question 7 of 16 for d in mon tue; do for f in a b; do echo "$d/$f"; break; done; echo "end of $d"; done. What changes when break becomes continue 2?

    Choose one answer.

    Show the answer: Question 7 of 16

    Answer: The two "end of" lines disappear; mon/a and tue/a are still printed

    With break the run prints mon/a, end of mon, tue/a and end of tue; with continue 2 it prints mon/a and tue/a, in Bash and in dash. From the inner loop, break goes on after the inner done, and continue 2 starts the outer loop's next pass.

  8. Question 8 of 16 A script has two nested loops, and break 3 runs inside the inner one. What happens?

    Choose one answer.

    Show the answer: Question 8 of 16

    Answer: Both loops end, and the script goes on after the outer done

    Bash and dash both behave this way; POSIX writes it into the break utility's description.

  9. Question 9 of 16 What makes while : stop?

    Choose one answer.

    Show the answer: Question 9 of 16

    Answer: A break (or an exit) in its body

    while : with a break is the shell's loop whose exit test sits at the end of the body, since there is no do-while.

  10. Question 10 of 16 upload always fails. What does for attempt in 1 2 3; do upload && break; sleep 1; done; echo $? print?

    Type a number.

    Show the answer: Question 10 of 16

    Answer: 0

    The last command of the last pass is sleep 1, which succeeded, so the loop ends with 0 although every upload failed: the naive loop of the session reported "carried on, status 0" the same way.

  11. Question 11 of 16 A retry pauses 3 ** attempt seconds after each failed attempt but the last, and gives up after attempt 4. How many seconds of pauses when all four fail?

    Type a number.

    Show the answer: Question 11 of 16

    Answer: 39 seconds

    Pauses follow attempts 1, 2 and 3: 3 + 9 + 27 = 39 seconds. A pause after attempt 4 would add 81 seconds of waiting for nothing.

  12. Question 12 of 16 A polling loop sets deadline=$(( SECONDS + 1 )) and gives up when SECONDS reaches it. Why can it give up after a few hundredths of a second?

    Choose one answer.

    Show the answer: Question 12 of 16

    Answer: SECONDS counts whole seconds by the system clock, so it can turn from 0 to 1 just after the shell starts

    Use a limit of a few seconds or more, or a finer clock, when the precision matters.

  13. Question 13 of 16 Which argument does POSIX promise that sleep accepts?

    Choose one answer.

    Show the answer: Question 13 of 16

    Answer: 2, a whole number of seconds

    That is why wait-for-file.sh polls once a second with sleep 1.

  14. Question 14 of 16 Where does select print its numbered menu?

    Choose one answer.

    Show the answer: Question 14 of 16

    Answer: On standard error

    Keeping the menu on standard error leaves standard output for the script's results.

  15. Question 15 of 16 At a select menu the user presses Enter on an empty line. What happens?

    Choose one answer.

    Show the answer: Question 15 of 16

    Answer: The menu and the prompt are printed again, and the body does not run

    In the session the empty line brought back "1) start" to "3) status" and the prompt.

  16. Question 16 of 16 bash menu.sh < /dev/null reads end of input at once. What status does the script end with?

    Type a number.

    Show the answer: Question 16 of 16

    Answer: 1

    At end of input select completes and returns 1, and the loop is the script's last command, so the session printed [exit status 1].

Interview questions

Warm-up (fresher to 2–5 years): how do while and until differ, and what is while : for?

Both run a test command before every pass. while runs the body as long as the test succeeds, with status 0, and until runs it as long as the test fails, so until cmd reads as “while not cmd”. The colon is a builtin that always succeeds, so while : loops until a break inside the body ends it: that is how the shell writes a loop whose exit test sits at the end, since it has no do-while. Either loop ends with the status of the last body command it ran, or 0 when the body never ran, so a loop’s status does not say whether its condition ever held.

Two harder questions follow: one about deciding which failures a retry wrapper should retry and how long it may take in all, and one about a search whose break stopped working after a refactor. Decide what you would check first, then open each first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A release script downloads an artefact with curl --fail inside a loop of 6 attempts with doubling pauses. It retries a 404 for a wrong version number as patiently as a 503, and one run hung for 40 minutes on a stalled transfer. How would you decide what to retry, and bound the total time?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    Nested loops over hosts and ports stopped with break 2 at the first port that answered. A refactor moved the check and its break 2 into a function, try_port. Now every host and port is tried, and Bash warns that break is "only meaningful" in a loop. Why, and how do you fix it?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How do while and until differ?

    Both run a test command before every pass. while repeats the body as long as the test succeeds, until as long as it fails, so until cmd reads as while not cmd.

  2. Which status does a while or until loop end with?

    The status of the last body command it ran, or 0 when the body never ran.

  3. What do break, continue and break 2 do?

    break ends the innermost loop and continue starts its next pass at once. With a number, break 2 or continue 2 acts on the loop around it.

  4. How does the shell write a loop whose exit test sits at the end?

    while : with a break inside the body. The colon always succeeds, so only the break ends the loop; the shell has no do-while.

  5. What does a sound retry loop need?

    A limit on attempts, a pause that grows after each failure, no pause after the last attempt, and the last attempt's status at the end so the caller can see the failure.

  6. How does a polling loop avoid waiting for ever?

    It checks the state at a fixed interval and also stops at a deadline, for example one counted with Bash's SECONDS variable.

  7. What does select do with the answer it reads?

    It prints a numbered menu on standard error, prompted by PS3, and reads a line into REPLY. A listed number sets the loop variable to that word, any other text empties it, and an empty line shows the menu again.

  8. When does a select loop end, and which shells have it?

    It goes on until a break, or until input ends, when it finishes with status 1. Bash has it; POSIX sh and dash do not.

Key takeaways

  • while runs its body while a test command succeeds and until while it fails; the test can be any command, and the loop ends with the status of the last body command, or 0 when no pass ran.
  • break ends the innermost loop and continue starts its next pass; break 2 and continue 2 act on the loop around it, and while : with a break is the shell’s loop whose test sits at the end.
  • A retry loop needs a limit, a pause that grows, no pause after the last attempt and the last attempt’s status at the end, which until "$@" with an exit inside the body gives; a polling loop adds a deadline such as one from SECONDS.
  • select prints a numbered menu on standard error and reads a line from standard input into REPLY: a listed number sets its variable to that word, any other text empties it, an empty line brings the menu back, and the loop goes on until break, in Bash but not in POSIX sh or dash.

Terms in this lesson

exponential back-off
A retry policy whose pause grows by a fixed factor after every failed attempt, such as 2, 4, 8 and 16 seconds, so that a struggling service gets more time to recover with each try.
polling
Checking a state again and again at a fixed interval, such as testing once a second whether a file exists, until it holds or a deadline passes.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.