Reputation: 11784
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
Reputation: 70722
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.
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.
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
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! ;-)
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
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
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
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
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