((count++)) Stopped My Clip Counter Under set -e Before It Printed a Line: Bash Arithmetic Returns 1 on Zero

2026-11-06来源 dev.to LinuxEnglish

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 count is 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 does let. Pre-increment ++count from 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.
  • || true keeps 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)) and t=$((t + 0)) are ordinary assignments. The arithmetic happens inside an expansion, and under set -e the script carried on past both, including the one whose result was 0.
  • if ((n > 0)) with n at 0 took the else branch and the script kept going. As the test of an if, 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 (( and let. Any line where the expression can be 0 will stop a set -e script, 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, like if ((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. (( )) and let are not POSIX sh. I did not test how dash, zsh or ksh treat them, or whether their set -e behaves the same way.
  • The exceptions to set -e. The shell does not stop on every failing command; a status inside an if, while or && list is handled differently. I tested a plain (( )) line, the || true form and one if above, and did not test while, until or && 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.

原文:https://www.a8q9.cn/en/article/count-stopped-my-clip-counter-under-set-e-before-it-printed-a-line-bash-arithmet-88009d.html

相关文章