219

What is the best way to emulate a do-while loop in Bash?

I could check for the condition before entering the while loop, and then continue re-checking the condition in the loop, but that's duplicated code. Is there a cleaner way?

Pseudo code of my script:

while [ current_time <= $cutoff ]; do
    check_if_file_present
    #do other stuff
done

This doesn't perform check_if_file_present if launched after the $cutoff time, and a do-while would.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Alex
  • 10,470
  • 8
  • 40
  • 62

5 Answers5

329

Two simple solutions:

  1. Execute your code once before the while loop

    actions() {
       check_if_file_present
       # Do other stuff
    }
    
    actions #1st execution
    while [ current_time <= $cutoff ]; do
       actions # Loop execution
    done
    
  2. Or:

    while : ; do
        actions
        [[ current_time <= $cutoff ]] || break
    done
    
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
clt60
  • 62,119
  • 17
  • 107
  • 194
  • 21
    `:` is a built in, equivalent to the built in `true`. They both "do nothing successfully". – loxaxs Dec 26 '17 at 03:40
  • 3
    @loxaxs that is true in e.g. zsh but not in Bash. `true` is an actual program whereas `:` is built-in. The former simply exits with `0` (and `false` with `1`) the latter does absolutely nothing. You can check with `which true`. – Fleshgrinder Dec 02 '19 at 07:01
  • 1
    @Fleshgrinder `:` is still usable in place of `true` in Bash. Try it with `while :; do echo 1; done`. – Alexej Magura Feb 12 '20 at 19:05
  • 2
    Never said anything different, only that `true` is not a built-in in Bash. It's a program usually found in `/bin`. – Fleshgrinder Feb 12 '20 at 22:07
  • 17
    `type true` in bash (all the way back to bash 3.2) returns `true is a shell builtin`. It's true that `/bin/true` is a program; what's not true about true is that `true` is not a builtin. (tl;dr: true is a bash builtin AND a program) – PJ Eby May 11 '20 at 23:44
  • 3
    @loxaxs, oh `comma`... Such a detail, yet in the sentence seems critical: "do nothing, successfully". – Artfaith Nov 09 '22 at 10:40
180

Place the body of your loop after the while and before the test. The actual body of the while loop should be a no-op.

while 
    check_if_file_present
    #do other stuff
    (( current_time <= cutoff ))
do
    :
done

Instead of the colon, you can use continue if you find that more readable. You can also insert a command that will only run between iterations (not before first or after last), such as echo "Retrying in five seconds"; sleep 5. Or print delimiters between values:

i=1; while printf '%d' "$((i++))"; (( i <= 4)); do printf ','; done; printf '\n'

I changed the test to use double parentheses since you appear to be comparing integers. Inside double square brackets, comparison operators such as <= are lexical and will give the wrong result when comparing 2 and 10, for example. Those operators don't work inside single square brackets.

Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • 2
    Is it equivalent to single-line `while { check_if_file_present; ((current_time<=cutoff)); }; do :; done`? I.e. are the commands inside the `while` condition effectively separated by semicolons not by e.g. `&&`, and grouped by `{}`? – Ruslan Oct 19 '16 at 19:49
  • 1
    @Ruslan: The curly braces are unnecessary. You shouldn't link anything to the test inside the double parentheses using `&&` or `||` since that effectively makes them part of the test that controls the `while`. Unless you're using this construct on the command line, I wouldn't do it as a one-liner (in a script, specifically) since the intent is unreadable. – Dennis Williamson Oct 19 '16 at 19:59
  • Yeah, I wasn't intending to use it as a one-liner: just to clarify how the commands in the test are connected. I was worrying that first command returning non-zero might render the whole condition false. – Ruslan Oct 19 '16 at 20:01
  • 5
    @ruslan: No, it's the last return value. `while false; false; false; true; do echo here; break; done` outputs "here" – Dennis Williamson Oct 19 '16 at 20:51
  • 1
    @thatotherguy: That _between_ capability is pretty cool! You could also use it to insert a delimiter in a string. Thanks! – Dennis Williamson Oct 12 '18 at 18:48
  • Putting commands to run in the condition block of a while loop (or for loop, for that matter) seems rather misleading. With `while true; do check_if_file_present; [[ current_time <= $cutoff ]] || break; done` the intent is much clearer. – Dario Seidl Sep 24 '21 at 16:55
20

This implementation:

  • Has no code duplication
  • Doesn't require extra functions()
  • Doesn't depend on the return value of code in the "while" section of the loop:
do=true
while $do || conditions; do
  do=false
  # your code ...
done

It works with a read loop, too, skipping the first read:

do=true
while $do || read foo; do
  do=false

  # your code ...
  echo $foo
done
gunr2171
  • 16,104
  • 25
  • 61
  • 88
KJ7LNW
  • 1,437
  • 5
  • 11
  • I like this answer but also the second example of the accepted answer. This method however does not mess up any of Bash keyword semantics, switching the blocks meaning seems sloppy to me. I tested a bit and obviously `while`/`until` are hardcoded to ignore `set +e` (even if you put it in the list) while `do` obeys both `-e/+e` as expected. – mpe Jan 21 '22 at 22:45
  • Edit: Oh, and also if you happen to be monitoring return codes, you cannot pass that back with `while` or `until` (again obviously) but you need `do` for that as well. (Why is there this 5min edit window. Can't I deserve just 15 minutes of thinking about this.) – mpe Jan 21 '22 at 23:02
  • @mpe, Can you expand on what you mean by "if you happen to be monitoring return codes, you cannot pass that back with while or until (again obviously) but you need do for that as well." ? – KJ7LNW Jan 22 '22 at 00:43
  • Well, with this solution a script can use `while ; do ; done || $?; ` as is normal, the other examples show a flow where you can never get the end-result status as it is in a `while` condition which treats this non-zero as an implicit `break` and then discards it. – mpe Jan 22 '22 at 01:57
9

We can emulate a do-while loop in Bash with while [[condition]]; do true; done like this:

while [[ current_time <= $cutoff ]]
    check_if_file_present
    #do other stuff
do true; done

For an example. Here is my implementation on getting ssh connection in bash script:

#!/bin/bash
while [[ $STATUS != 0 ]]
    ssh-add -l &>/dev/null; STATUS="$?"
    if [[ $STATUS == 127 ]]; then echo "ssh not instaled" && exit 0;
    elif [[ $STATUS == 2 ]]; then echo "running ssh-agent.." && eval `ssh-agent` > /dev/null;
    elif [[ $STATUS == 1 ]]; then echo "get session identity.." && expect $HOME/agent &> /dev/null;
    else ssh-add -l && git submodule update --init --recursive --remote --merge && return 0; fi
do true; done

It will give the output in sequence as below:

Step #0 - "gcloud": intalling expect..
Step #0 - "gcloud": running ssh-agent..
Step #0 - "gcloud": get session identity..
Step #0 - "gcloud": 4096 SHA256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX /builder/home/.ssh/id_rsa (RSA)
Step #0 - "gcloud": Submodule '.google/cloud/compute/home/chetabahana/.docker/compose' (git@github.com:chetabahana/compose) registered for path '.google/cloud/compute/home/chetabahana/.docker/compose'
Step #0 - "gcloud": Cloning into '/workspace/.io/.google/cloud/compute/home/chetabahana/.docker/compose'...
Step #0 - "gcloud": Warning: Permanently added the RSA host key for IP address 'XXX.XX.XXX.XXX' to the list of known hosts.
Step #0 - "gcloud": Submodule path '.google/cloud/compute/home/chetabahana/.docker/compose': checked out '24a28a7a306a671bbc430aa27b83c09cc5f1c62d'
Finished Step #0 - "gcloud"
eQ19
  • 9,880
  • 3
  • 65
  • 77
  • 1
    Where is `$STATUS` initialized? – RonJohn Mar 05 '21 at 04:20
  • 1
    It's initialized through the bash script it self. You may get more detail on *[How to automatically push after committing in git?](https://stackoverflow.com/a/57366062/4058484)*. – eQ19 Mar 07 '21 at 02:35
0

There are lots of good answers here, but they all seem to be more complex than they need to be. I believe the simplest way to do this would be to seed the variable being tested so that thee first check passes.

cutoff="maxtime"
while [ current_time <= $cutoff ]; do
        check_if_file_present
        #do other stuff
done

In this instance, "maxtime" would be the maximum possible time that it could be. Of course, the exact format/value would depend on what the "current_time" command/script/function generates.

Peter
  • 121
  • 1
  • 4