A small Bash script counts the camera clips in a folder before a nightly job moves them. It has set -e at the top, a counter that starts at zero, and ((count++)) inside the loop. Pointed at a folder of three clips, it printed nothing at all and exited with status 1. Not one counted line, not the total, not an error message.
Here is the script:
#!/usr/bin/env bash
set -e
count=0
for f in clips/*.mp4; do
((count++))
echo "counted $f"
done
echo "clips: $count"
And here is what it did, first as written and then with only the set -e line taken out:
mkdir -p clips
touch clips/clip_01.mp4 clips/clip_02.mp4 clips/clip_03.mp4
echo "== with set -e"
bash count.sh
echo "exit status: $?"
echo "== same script, set -e line removed"
grep -v '^set -e$' count.sh > count_no_e.sh
bash count_no_e.sh
echo "exit status: $?"
== with set -e
exit status: 1
== same script, set -e line removed
counted clips/clip_01.mp4
counted clips/clip_02.mp4
counted clips/clip_03.mp4
clips: 3
exit status: 0
Without set -e it counts all three clips and exits 0. With it, the script stops before its first echo, on the line that adds one to the counter.
Disclosure: I make an Android camera app. Nothing below uses its code or describes how it records or stores anything. The clips here are empty files made with touch and head, and every output is pasted from the scripts shown. Versions:
GNU bash, version 5.1.16(1)-release (aarch64-unknown-linux-gnu)
stat (GNU coreutils) 8.32
What the manual says (( )) returns
((count++)) is not an assignment statement in the way count=1 is. It is the arithmetic command, and the Bash manual gives it an exit status based on the value of the expression. From the entry for (( expression )) in Conditional Constructs: "If the value of the expression is non-zero, the return status is 0; otherwise the return status is 1."
The value of count++ is the value of count before it goes up. On the first trip through the loop that value is 0, so the command returns 1, even though it did its job and count is now 1. set -e sees a command that returned 1 and stops the script.
I checked that directly, without set -e, printing the status after each form:
#!/usr/bin/env bash
count=0
((count++)); echo "count++ from 0: status $?, count now $count"
((count++)); echo "count++ from 1: status $?, count now $count"
count=0
((++count)); echo "++count from 0: status $?, count now $count"
total=0; size=0
((total += size)); echo "total += 0 from 0: status $?, total now $total"
let "total += size"; echo "let total += 0 from 0: status $?"
count++ from 0: status 1, count now 1
count++ from 1: status 0, count now 2
++count from 0: status 0, count now 1
total += 0 from 0: status 1, total now 0
let total += 0 from 0: status 1
Three things in that output matter for a clip script:
- Post-increment from 0 returns 1. The variable still goes up. The status reports the old value, not whether the increment worked.
-
The second time, it returns 0. Once
countis 1,count++has the value 1, which is non-zero. So the failure is only ever on the first increment, which is why the script died before printing anything. -
+=with a zero result returns 1 too, and so doeslet. Pre-increment++countfrom 0 has the value 1 and returns 0.
Why it printed nothing
set -e stops the script quietly. There is no message saying which line failed. The status is 1, the same status a script returns for many other kinds of failure, so a cron log or a wrapper that only records the exit status shows a job that failed with no clue as to why.
An ERR trap makes the line visible. This is the same counter with one line added:
#!/usr/bin/env bash
set -e
trap 'echo "ERR: status $? at line $LINENO: $BASH_COMMAND" >&2' ERR
count=0
for f in clips/*.mp4; do
((count++))
echo "counted $f"
done
echo "clips: $count"
bash trap.sh
echo "exit status: $?"
ERR: status 1 at line 6: ((count++))
exit status: 1
The trap fired on line 6, ((count++)), with status 1. If a script with set -e exits 1 and prints nothing useful, a trap like this one is a quick way to find out which command it stopped on before you start reading the logic.
The version that only fails on some nights
The counter fails every time, which at least makes it easy to notice the first time you run it. Adding up file sizes with += is worse, because whether it fails depends on the files.
This script totals the bytes in a folder of clips. Recorders that stop mid-write can leave a clip of 0 bytes behind, so the test sets up two nights with the same two clips in a different order: one where the empty clip sorts first, and one where it sorts second.
#!/usr/bin/env bash
set -e
total=0
for f in "$1"/*.mp4; do
size=$(stat -c %s "$f")
((total += size))
echo "$f: $size bytes"
done
echo "total: $total bytes"
mkdir -p nightA nightB
: > nightA/clip_01.mp4
head -c 5000 /dev/zero > nightA/clip_02.mp4
head -c 5000 /dev/zero > nightB/clip_01.mp4
: > nightB/clip_02.mp4
for night in nightA nightB; do
echo "== $night"
bash bytes.sh "$night"
echo "exit status: $?"
done
== nightA
exit status: 1
== nightB
nightB/clip_01.mp4: 5000 bytes
nightB/clip_02.mp4: 0 bytes
total: 5000 bytes
exit status: 0
On nightA the empty clip comes first. total is 0, size is 0, total += size has the value 0, the command returns 1, and the script stops without printing a line. On nightB the empty clip comes second. By then total is already 5000, so total += 0 has the value 5000, the command returns 0, and the script finishes normally with the right total.
Same script, same two files, and whether it exits 1 depends on which file the glob happens to list first. A test folder without an empty file at the front will pass every time.
What I changed, and what each fix covers
I tried the fixes people usually reach for, all under set -e:
#!/usr/bin/env bash
set -e
a=0; ((++a)); echo "((++a)): a=$a"
b=0; b=$((b + 1)); echo "b=\$((b + 1)): b=$b"
c=0; ((c++)) || true; echo "((c++)) || true: c=$c"
t=0; t=$((t + 0)); echo "t=\$((t + 0)): t=$t"
v=0; ((v += 0)) || true; echo "((v += 0)) || true: v=$v"
n=0; if ((n > 0)); then echo "if: n is positive"; else echo "if ((n > 0)) with n=0: took the else branch"; fi
echo "fixes.sh reached its last line"
bash fixes.sh
echo "exit status: $?"
((++a)): a=1
b=$((b + 1)): b=1
((c++)) || true: c=1
t=$((t + 0)): t=0
((v += 0)) || true: v=0
if ((n > 0)) with n=0: took the else branch
fixes.sh reached its last line
exit status: 0
Every line ran and the script reached its end. They do not all fix the same thing, though:
-
((++a))works for a counter that starts at 0, because the value after the increment is 1. It does nothing for+=with a zero amount, and a counter that starts below zero can still land on 0. -
|| truekeeps the script running, but it throws the status away completely. If the arithmetic itself is wrong, the line now fails silently in the other direction. -
b=$((b + 1))andt=$((t + 0))are ordinary assignments. The arithmetic happens inside an expansion, and underset -ethe script carried on past both, including the one whose result was 0. -
if ((n > 0))withnat 0 took theelsebranch and the script kept going. As the test of anif, a status of 1 means false, which is what the arithmetic command is good at.
For the byte total, the assignment form is the one I kept:
#!/usr/bin/env bash
set -e
total=0
for f in "$1"/*.mp4; do
size=$(stat -c %s "$f")
total=$((total + size))
echo "$f: $size bytes"
done
echo "total: $total bytes"
for night in nightA nightB; do
echo "== $night"
bash bytes_fixed.sh "$night"
echo "exit status: $?"
done
== nightA
nightA/clip_01.mp4: 0 bytes
nightA/clip_02.mp4: 5000 bytes
total: 5000 bytes
exit status: 0
== nightB
nightB/clip_01.mp4: 5000 bytes
nightB/clip_02.mp4: 0 bytes
total: 5000 bytes
exit status: 0
Both nights now finish with the same total and exit 0.
A short checklist for clip scripts with set -e
- Search the script for
((andlet. Any line where the expression can be 0 will stop aset -escript, whether or not the arithmetic did what you meant. - Counters starting at 0 with
count++fail on the first item every time. Totals with+=fail when the first value is 0, which depends on the data. - Prefer
x=$((x + n))for counting and totals, so the line's status says nothing about the number. - Keep
(( ))for conditions, likeif ((count > 0)), where a status of 1 is supposed to mean false. - Test with an empty file at the front of the folder, not only with clips that all have something in them.
- Add an ERR trap while debugging, so a silent exit says which line it stopped on.
Things I did not test
- Other Bash versions. I ran 5.1. The quote is from the current online manual, and what I ran matched it, but I did not try a newer or older Bash.
-
Other shells.
(( ))andletare not POSIXsh. I did not test howdash,zshorkshtreat them, or whether theirset -ebehaves the same way. -
The exceptions to
set -e. The shell does not stop on every failing command; a status inside anif,whileor&&list is handled differently. I tested a plain(( ))line, the|| trueform and oneifabove, and did not testwhile,untilor&&lists. - Real clips. The files here are empty or filled with zero bytes. The arithmetic does not care what is inside a clip, but I did not run this against a folder from a real recorder.
Sources. The quote is from the GNU Bash Reference Manual page Conditional Constructs (the (( expression )) entry). Outputs are pasted unedited from the scripts above.
If you want an old Android phone recording in the background so there is a folder of clips for a script like this to count, the app I make is Background Camera RemoteStream on Google Play. Nothing above depends on it.
In a script that stops on any non-zero status, an arithmetic line reports the number it produced, so the safe place for counting is an assignment, where the status belongs to the line and not to the total.
Top comments (0)