Michał Trybus
Michał Trybus

Reputation: 11784

Bash loop until a certain command stops failing

I would like to write a loop in bash which executes until a certain command stops failing (returning non-zero exit code), like so:

while ! my_command; do
    # do something
done

But inside this loop I need to check which exit code my_command returned, so I tried this:

while ! my_command; do
    if [ $? -eq 5 ]; then
        echo "Error was 5"
    else
        echo "Error was not 5"
    fi
    # potentially, other code follows...
done

But then the special variable ? becomes 0 inside the loop body. The obvious solution is:

while true; do
    my_command
    EC=$?
    if [ $EC -eq 0 ]; then
        break
    fi
    some_code_dependent_on_exit_code $EC
done

How can I check the exit code of my_command (called in loop header) inside loop body without rewriting this example using a while true loop with a break condition as shown above?

Upvotes: 24

Views: 32089

Answers (5)

F. Hauri  - Give Up GitHub
F. Hauri - Give Up GitHub

Reputation: 70722

Looping over result of a command while keeping result into a variable

Read quietely Lists, if.*then.*else and while.*do.*done paragraphs in man bash:

man bash | sed -ne '/if.*then.*else/,/^$/p'
 if list; then list; [ elif list; then list; ] ... [ else list; ] fi
        The if list is executed.  If its exit status is zero,  the  then
        list  is  executed.   Otherwise,  each  elif list is executed in
        turn, and if its exit status is  zero,  the  corresponding  then
        list is executed and the command completes.  Otherwise, the else
        list is executed, if present.  The exit status is the exit  sta‐
        tus of the last command executed, or zero if no condition tested
        true.
man bash | sed -ne '/while.*do.*done/,/^$/p'
 while list-1; do list-2; done
 until list-1; do list-2; done
        The while command continuously executes the list list-2 as  long
        as the last command in the list list-1 returns an exit status of
        zero.  The until command is identical to the while command,  ex‐
        cept that the test is negated: list-2 is executed as long as the
        last command in list-1 returns a non-zero exit status.  The exit
        status of the while and until commands is the exit status of the
        last command executed in list-2, or zero if none was executed.
man bash | sed -ne '/^[[:space:]]*Lists/,/^$/p'
man bash | sed -ne \
      '/^[[:space:]]*Lists/,/^ \{2,6\}[^ ]/{ /^ \{2,6\}[^ ]/{/List/!d}; p}'
 Lists
     A list is a sequence of one or more pipelines separated by one of the
     operators  ;, &, &&, or ||, and optionally terminated by one of ;, &,
     or <newline>.

     Of these list operators, && and || have equal precedence, followed by
     ; and &, which have equal precedence.

     A  sequence of one or more newlines may appear in a list instead of a
     semicolon to delimit commands.

     If a command is terminated by the control operator &, the shell  exe‐
     cutes  the  command  in the background in a subshell.  The shell does
     not wait for the command to finish,  and  the  return  status  is  0.
     These  are  referred to as asynchronous commands.  Commands separated
     by a ; are executed sequentially; the shell waits for each command to
     terminate  in turn.  The return status is the exit status of the last
     command executed.

     AND and OR lists are sequences of one or more pipelines separated  by
     the  && and || control operators, respectively.  AND and OR lists are
     executed with left associativity.  An AND list has the form

            command1 && command2

     command2 is executed if, and only if, command1 returns an exit status
     of zero (success).

     An OR list has the form

            command1 || command2

     command2  is  executed  if,  and only if, command1 returns a non-zero
     exit status.  The return status of AND and OR lists is the exit  sta‐
     tus of the last command executed in the list.

Preamble: My test command:

Here is the function I've used for testing all this:

my_command() {
    return $(( RANDOM % ${1:-3} ))
}

Run repetitively, they will result randomly return 0, 1 time over 3... By default! If an integer is submitted as argument, they will by used instead.

In practice

So you could build a list of command for preparing result code or condition expected by if command, (when do operator encountered):

while my_command 10; ret=$? ; [ $ret -ne 0 ];do
    echo "My command failed with '$ret'."
done;echo "My command finally succeded with: '${ret}'."

This could be simplified:

while my_command ; ((ret=$?)); do
    echo "My command failed with '$ret'."
done

With some cosmetic

midstr='';plural='';cnt=0; while my_command; ((ret=$?,ret)); do
    midstr="finally (after $((++cnt)) trie$plural) " plural=s;
    echo "My command failed with '$ret'.";
done;echo "My command ${midstr}succeded with: '${ret}'." 

or even, using until:

until my_command ; ((ret=$?,ret==0)); do
    echo "My command failed with '$ret'."
done

More complicated list encapsulation

while LIST1;do LIST5; done

Because LIST1 is a group if LIST; then LIST; else LIST; fi...

while if LIST2; then LIST3;else LIST4;fi; do LIST5; done

midstr='';plural='';cnt=0;while
      if my_command ; ((ret=$?));then
        (( ret > 1));
      else
        true;
      fi;
  do
    midstr="finally (after $((++cnt)) trie$plural) " plural=s;
    echo "My command failed with '$ret'.";
 done;echo "My command ${midstr}succeded with: '${ret}'."

Of course the result of this double condtion could be written ((ret==1)), but this has no matter. This is just a demo of imbrication. I've used some this for storing all cpu min and max frequencies in one operation without having to know the number of cores before: How to find the highest clocked CPU core under linux in bash?

i=0
while for l in min max; do
          read -r "f${l^}[i]" \
           </sys/devices/system/cpu/cpu$i/cpufreq/cpuinfo_${l}_freq
      done 2>/dev/null; do
      ((i++))
done

When read will fail, this will be because number of cores are reached.

( Hmmm wrong sample: not same kind of list for.*do.*done was not mentioned before... Sorry, my bad, I've missed this one! Please, Read The Fine Manual! ;-)

But if you don't need ResultCode.

Then you could simply:

while my_command ; [ $? -ne 0 ];do
    echo Loop on my_command
done

or

while my_command ; (($?)) ;do
    echo Loop on my_command
done

And maybe, why not?

while ! my_command ;do
    echo Loop on my_command
done

But from there you could better use until as chepner suggest

Upvotes: 24

helper
helper

Reputation: 170

I found this a while back:

https://github.com/minfrin/retry retry for bash scripts - usable in pipes

Example:

~$ retry --until=success --delay "1,2,4,8,16,32,64" -- false
retry: false returned 1, backing off for 1 second and trying again...
retry: false returned 1, backing off for 2 seconds and trying again...
retry: false returned 1, backing off for 4 seconds and trying again...
retry: false returned 1, backing off for 8 seconds and trying again...

Upvotes: 0

GET
GET

Reputation: 59

So in my case I also need to ignore some exit codes and want to provide some useful output to the user so I wrote this up:

retrycmd(){
  MSG=$1
  IGNORE=$2
  shift 2
  local SLEEP_T=5
  local L_CNT=5
  local C_CNT=0
  while ((C_CNT++ < ${L_CNT})) && ! $@;do
    RET=${PIPESTATUS[0]}
    #echo "RET: ${RET}"
    for I in ${IGNORE//,/ };do # bashism: replace(/) all(/) match(,) with(/) value(<space>)
      if ((${RET} == ${I}));then
        #echo "${RET} = ${I}"
        break 2
      fi
    done
    echo "${MSG} failure ${C_CNT}"
    sleep ${SLEEP_T}
  done
  if ((${C_CNT} > ${L_CNT}));then
    echo "${MSG} failed"
    poweroff
  fi
}

#retrycmd "Doing task" "IGNORE,CSV" <CMD>
retrycmd "Ping google" "0" ping www.google.com

Upvotes: 0

pjh
pjh

Reputation: 8054

You can get the status of a negated command from the PIPESTATUS built-in variable:

while ! my_command ; do
    some_code_dependent_on_exit_code "${PIPESTATUS[0]}"
done

chepner's solution is better in this case, but PIPESTATUS is sometimes useful for similar problems.

Upvotes: 4

chepner
chepner

Reputation: 530862

In addition to the well-known while loop, POSIX provides an until loop that eliminates the need to negate the exit status of my_command.

# To demonstrate
my_command () { read number; return $number; }

until my_command; do
    if [ $? -eq 5 ]; then
        echo "Error was 5"
    else
        echo "Error was not 5"
    fi
    # potentially, other code follows...
done

Upvotes: 46

Related Questions