Bash retry function with exponential backoff
gist.github.com
gist.github.com
set -e
retry() {
until "$@"
do
sleep 1
done
}
f() {
false
echo oh no
}
retry f
# oh no
set -e is ignored when a function executes in the context of a predicate.Shell scripts are great for when you just need to run stuff. If something bad happens you notice it and run another command to ameliorate the error. This is how most interactive shell sessions pan out.
As soon as you need to handle errors programmatically I heartily suggest that you switch to a language with fewer quirks than sh.
I highly recommend https://mywiki.wooledge.org/BashPitfalls. It should be required reading before anyone writes a shell script.
Even mundane shit like ‘cd’ can fail where you least expect it, requiring || exit on practically every line of the script. Forget one place and boom. Defeating the premise of “it’s just a short “simple” script so let’s not write it in a sane language”.
If you want to write serious scripts and spend weeks scrutinizing every line, NASA style, it is perhaps better to explicitly handle every potential error. But good luck doing that in a large team of junior web developers just glueing stuff together in the build pipeline. The pitfalls with -e linked above are mostly related to functions, if your script reached the size of requiring functions you have kind of lost already.
This is true, but a much better solution is for everyone to use shellcheck. You can require that all shell scripts pass it without warnings and that every shellcheck disable comment has a good explanation attached; the first part can be automated with a pre-commit hook. In return, you get a script that is explicit and predictable rather than relying on a set of rules which you don't understand, and which may come back to bite you later when you change shells or even bash versions.
> spend weeks scrutinizing every line, NASA style
This is a colossal exaggeration. I've switched from set -e to putting || return or || exit everywhere and it barely takes more time after I got in the habit. On the rare occasions I get distracted and forget, shellcheck reminds me. I have my editor configured to run it on the fly and underline lines with warnings.
curl icanhazip.com > myip.txt
cat myip.txt
Taking you back to the every line must be scrutinized problem. Maybe you can do it. Maybe i can do it. But does the rest of your large organization of diverse experiences have the discipline to do it? In my experience, from multiple organizations of different sizes - no. Even with shellcheck and mandatory reviews it just doesn't work. It's tiring to for the 100th time remind someone that curl can fail and arguments must be quoted, you worry being labeled as the know-it-all-besserwisser that shoots down even simple scripts, that's assuming the reviewer spots the errors in the first place. What's worse is that the code doesn't immediately tell you if errors has been considered or not, making it very difficult to read existing scripts.Shellcheck btw has an optional check for the case of functions used in combination with errexit https://www.shellcheck.net/wiki/SC2310
-e and shellcheck should be mandatory for any script which can’t be written in Python. Once you’re over a screen or two of code, it’s almost always shorter to write in Python anyway and it’ll be easier to understand.
> Once you’re over a screen or two of code, it’s almost always shorter to write in Python anyway
That I agree with.
As soon as you need to handle errors programmatically I heartily
suggest that you switch to a language with fewer quirks than sh.
Basically that. `set -e` is evil.E.g. grep considers no matches worthy of a non-zero (a.k.a. error) exit code while df considers an invalid block size an error worthy of a exit code of zero. Ostensibly printf(1) exits with non-zero on an error, but a format specifier with insufficient arguments is not an error.
set -euo pipefail should be the default so that the script exits on at least some unexpected state/command results. If you want to do more then that with various fallbacks etc... then use another language.
https://stackoverflow.com/questions/4072984/how-do-i-get-the...
https://serverfault.com/questions/143445/what-does-set-e-do-...
pipefail is worse because tools like grep return non-zero for things that aren't inherently errors. Error handling in sh-like shells is archaic enough that if you're reaching for it you should strongly consider reaching for a different language.
I'm saying that a script which would have the potential to fail unintuitively from `set -eup pipefail` probably shouldn't be written in shell and most definitely shouldn't continue to execute once an unexpected state has occured.
You're right that there are a lot of cases in which non-zero exitcodes are the expected behaviour. But if you're accepting these commands with non-zero exitcodes then you're already doing error handling by verifying the output of the commands (at least hopefully), which was the original criteria from the OP for writing the script in another language.
#!/usr/bin/env bash
function retry {
local retries="$1"
shift
case "$retries" in
""|*[!0-9]*)
echo "retry: First argument must be a number, got '$retries'" 1>&2
return 1
;;
esac
local count=0
until "$@"; do
local exit="$?"
local wait="$((2 ** count))"
count="$((count + 1))"
if [ "$count" -lt "$retries" ]; then
echo "Retry $count/$retries exited $exit, retrying in $wait seconds..." 1>&2
sleep "$wait"
else
echo "Retry $count/$retries exited $exit, no more retries left." 1>&2
return "$exit"
fi
done
return 0
}
retry "$@"The technique is used extensively in the GNU coreutils test suite, as detailed in the "performance" section at https://www.pixelbeat.org/docs/coreutils-testing.html
Also fibonacci could be considered.
(Sorry if there’s minor errors here, on a phone and going off of memory)
https://github.com/majkinetor/posh/blob/master/MM_Sugar/Wait...
[mmh@x670]$ i=0
[mmh@x670]$ ((i++)); echo $i
1
[mmh@x670]$ ((i++)); echo $i
2
Beware that the above is very -bashy- and purists will rip your head off for using it. set -o errexit
Because *any bash arithmetic expression that evaluates to zero returns the exit code of 1*.So this works:
#!/usr/bin/env bash
set -o errexit
i=0
((++i))
echo $i # 1 will be printed to stdout
But this doesn't: #!/usr/bin/env bash
set -o errexit
i=0
((i++)) # terminates here with $? of 1
echo $i
The relevant doc is for `let`: let arg [arg ...]
Each arg is an arithmetic expression to be evaluated (see
ARITHMETIC EVALUATION above). If the last arg evaluates
to 0, let returns 1; 0 is returned otherwise.
https://www.gnu.org/software/bash/manual/bash.html#index-let i=$((i + 1))
The (( )) construct, without the $, is different, and it's also not POSIX. It has the gotchas around the exit code, which a plain assignment doesn't have.I’m at Lowes — does ((i+1)) work? Or is it just for incrementing / decrementing (which is still very useful!)
EDIT: thinking this over, does this work for variable substitution too? E.g. ((i)) being equivalent to $i. Not that you’d necessarily want to…
`(( i + 1 ))` will evaluate the result of adding one to `$i` and then throw away the result. It doesn't do anything useful (other than having a different exit code depending on whether the expression evaluated to 0 or not).
`$(( ... ))` evaluates the expression and then returns its value. ie `i=$(( i + 1 ))` will increment `$i`, just like `(( i++ ))` did.
[mmh@x670]$ ((i=i+2)); echo $i
6
[mmh@x670]$ ((i=i+2)); echo $i
8 $ i=0
$ echo $((++i))
1
$ echo $((i++))
1
$ echo $((++i))
3
$
((++i)) does add 1 before the command is run, and ((i++)) does add 1 after the command is run. let count=0
until "$@"; do
let wait=2**count
sleep $wait
let count=count+1
done$[i+4]
And, btw, res=$(command) instead of those pesky backticks res=`command` ,-)
And it's not only bash; it's feature of POSIX shell.
declare -i i
i+=1