<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Super Funicular</title>
    <description>The latest articles on DEV Community by Super Funicular (@superfunicular).</description>
    <link>https://dev.to/superfunicular</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3905292%2F0eab2966-f7d2-41d0-983d-d240a6caa5d4.jpg</url>
      <title>DEV Community: Super Funicular</title>
      <link>https://dev.to/superfunicular</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/superfunicular"/>
    <language>en</language>
    <item>
      <title>((count++)) Stopped My Clip Counter Under set -e Before It Printed a Line: Bash Arithmetic Returns 1 on Zero</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Thu, 05 Nov 2026 19:54:14 +0000</pubDate>
      <link>https://dev.to/superfunicular/count-stopped-my-clip-counter-under-set-e-before-it-printed-a-line-bash-arithmetic-returns-4md2</link>
      <guid>https://dev.to/superfunicular/count-stopped-my-clip-counter-under-set-e-before-it-printed-a-line-bash-arithmetic-returns-4md2</guid>
      <description>&lt;p&gt;A small Bash script counts the camera clips in a folder before a nightly job moves them. It has &lt;code&gt;set -e&lt;/code&gt; at the top, a counter that starts at zero, and &lt;code&gt;((count++))&lt;/code&gt; inside the loop. Pointed at a folder of three clips, it printed nothing at all and exited with status 1. Not one &lt;code&gt;counted&lt;/code&gt; line, not the total, not an error message.&lt;/p&gt;

&lt;p&gt;Here is the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;((&lt;/span&gt;count++&lt;span class="o"&gt;))&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"counted &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"clips: &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is what it did, first as written and then with only the &lt;code&gt;set -e&lt;/code&gt; line taken out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; clips
&lt;span class="nb"&gt;touch &lt;/span&gt;clips/clip_01.mp4 clips/clip_02.mp4 clips/clip_03.mp4
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== with set -e"&lt;/span&gt;
bash count.sh
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit status: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== same script, set -e line removed"&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'^set -e$'&lt;/span&gt; count.sh &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; count_no_e.sh
bash count_no_e.sh
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit status: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== 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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;set -e&lt;/code&gt; it counts all three clips and exits 0. With it, the script stops before its first &lt;code&gt;echo&lt;/code&gt;, on the line that adds one to the counter.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;touch&lt;/code&gt; and &lt;code&gt;head&lt;/code&gt;, and every output is pasted from the scripts shown. Versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GNU bash, version 5.1.16(1)-release (aarch64-unknown-linux-gnu)
stat (GNU coreutils) 8.32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the manual says &lt;code&gt;(( ))&lt;/code&gt; returns
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;((count++))&lt;/code&gt; is not an assignment statement in the way &lt;code&gt;count=1&lt;/code&gt; 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 &lt;code&gt;(( expression ))&lt;/code&gt; in Conditional Constructs: "If the value of the expression is non-zero, the return status is 0; otherwise the return status is 1."&lt;/p&gt;

&lt;p&gt;The value of &lt;code&gt;count++&lt;/code&gt; is the value of &lt;code&gt;count&lt;/code&gt; 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 &lt;code&gt;count&lt;/code&gt; is now 1. &lt;code&gt;set -e&lt;/code&gt; sees a command that returned 1 and stops the script.&lt;/p&gt;

&lt;p&gt;I checked that directly, without &lt;code&gt;set -e&lt;/code&gt;, printing the status after each form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="o"&gt;((&lt;/span&gt;count++&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"count++ from 0: status &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;, count now &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;((&lt;/span&gt;count++&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"count++ from 1: status &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;, count now &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="o"&gt;((&lt;/span&gt;++count&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"++count from 0: status &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;, count now &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="o"&gt;((&lt;/span&gt;total +&lt;span class="o"&gt;=&lt;/span&gt; size&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"total += 0 from 0: status &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;, total now &lt;/span&gt;&lt;span class="nv"&gt;$total&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;let&lt;/span&gt; &lt;span class="s2"&gt;"total += size"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"let total += 0 from 0: status &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things in that output matter for a clip script:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Post-increment from 0 returns 1.&lt;/strong&gt; The variable still goes up. The status reports the old value, not whether the increment worked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The second time, it returns 0.&lt;/strong&gt; Once &lt;code&gt;count&lt;/code&gt; is 1, &lt;code&gt;count++&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;+=&lt;/code&gt; with a zero result returns 1 too&lt;/strong&gt;, and so does &lt;code&gt;let&lt;/code&gt;. Pre-increment &lt;code&gt;++count&lt;/code&gt; from 0 has the value 1 and returns 0.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why it printed nothing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;set -e&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;An ERR trap makes the line visible. This is the same counter with one line added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'echo "ERR: status $? at line $LINENO: $BASH_COMMAND" &amp;gt;&amp;amp;2'&lt;/span&gt; ERR
&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;((&lt;/span&gt;count++&lt;span class="o"&gt;))&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"counted &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"clips: &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash trap.sh
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit status: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERR: status 1 at line 6: ((count++))
exit status: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trap fired on line 6, &lt;code&gt;((count++))&lt;/code&gt;, with status 1. If a script with &lt;code&gt;set -e&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The version that only fails on some nights
&lt;/h2&gt;

&lt;p&gt;The counter fails every time, which at least makes it easy to notice the first time you run it. Adding up file sizes with &lt;code&gt;+=&lt;/code&gt; is worse, because whether it fails depends on the files.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; %s &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;((&lt;/span&gt;total +&lt;span class="o"&gt;=&lt;/span&gt; size&lt;span class="o"&gt;))&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$size&lt;/span&gt;&lt;span class="s2"&gt; bytes"&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"total: &lt;/span&gt;&lt;span class="nv"&gt;$total&lt;/span&gt;&lt;span class="s2"&gt; bytes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; nightA nightB
: &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; nightA/clip_01.mp4
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 5000 /dev/zero &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; nightA/clip_02.mp4
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 5000 /dev/zero &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; nightB/clip_01.mp4
: &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; nightB/clip_02.mp4
&lt;span class="k"&gt;for &lt;/span&gt;night &lt;span class="k"&gt;in &lt;/span&gt;nightA nightB&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== &lt;/span&gt;&lt;span class="nv"&gt;$night&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  bash bytes.sh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$night&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit status: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== nightA
exit status: 1
== nightB
nightB/clip_01.mp4: 5000 bytes
nightB/clip_02.mp4: 0 bytes
total: 5000 bytes
exit status: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On &lt;code&gt;nightA&lt;/code&gt; the empty clip comes first. &lt;code&gt;total&lt;/code&gt; is 0, &lt;code&gt;size&lt;/code&gt; is 0, &lt;code&gt;total += size&lt;/code&gt; has the value 0, the command returns 1, and the script stops without printing a line. On &lt;code&gt;nightB&lt;/code&gt; the empty clip comes second. By then &lt;code&gt;total&lt;/code&gt; is already 5000, so &lt;code&gt;total += 0&lt;/code&gt; has the value 5000, the command returns 0, and the script finishes normally with the right total.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed, and what each fix covers
&lt;/h2&gt;

&lt;p&gt;I tried the fixes people usually reach for, all under &lt;code&gt;set -e&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nv"&gt;a&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt;++a&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"((++a)):            a=&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;b &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"b=&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;((b + 1)):       b=&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;c&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt;c++&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"((c++)) || true:    c=&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;t&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;t&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;t &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"t=&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;((t + 0)):       t=&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;v&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt;v +&lt;span class="o"&gt;=&lt;/span&gt; 0&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"((v += 0)) || true: v=&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt;n &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 0&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"if: n is positive"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;else &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"if ((n &amp;gt; 0)) with n=0: took the else branch"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"fixes.sh reached its last line"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash fixes.sh
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit status: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;((++a)):            a=1
b=$((b + 1)):       b=1
((c++)) || true:    c=1
t=$((t + 0)):       t=0
((v += 0)) || true: v=0
if ((n &amp;gt; 0)) with n=0: took the else branch
fixes.sh reached its last line
exit status: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line ran and the script reached its end. They do not all fix the same thing, though:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;((++a))&lt;/code&gt;&lt;/strong&gt; works for a counter that starts at 0, because the value after the increment is 1. It does nothing for &lt;code&gt;+=&lt;/code&gt; with a zero amount, and a counter that starts below zero can still land on 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;|| true&lt;/code&gt;&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;b=$((b + 1))&lt;/code&gt; and &lt;code&gt;t=$((t + 0))&lt;/code&gt;&lt;/strong&gt; are ordinary assignments. The arithmetic happens inside an expansion, and under &lt;code&gt;set -e&lt;/code&gt; the script carried on past both, including the one whose result was 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;if ((n &amp;gt; 0))&lt;/code&gt;&lt;/strong&gt; with &lt;code&gt;n&lt;/code&gt; at 0 took the &lt;code&gt;else&lt;/code&gt; branch and the script kept going. As the test of an &lt;code&gt;if&lt;/code&gt;, a status of 1 means false, which is what the arithmetic command is good at.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the byte total, the assignment form is the one I kept:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; %s &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;total &lt;span class="o"&gt;+&lt;/span&gt; size&lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$size&lt;/span&gt;&lt;span class="s2"&gt; bytes"&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"total: &lt;/span&gt;&lt;span class="nv"&gt;$total&lt;/span&gt;&lt;span class="s2"&gt; bytes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;night &lt;span class="k"&gt;in &lt;/span&gt;nightA nightB&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== &lt;/span&gt;&lt;span class="nv"&gt;$night&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  bash bytes_fixed.sh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$night&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit status: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== 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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both nights now finish with the same total and exit 0.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short checklist for clip scripts with &lt;code&gt;set -e&lt;/code&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Search the script for &lt;code&gt;((&lt;/code&gt; and &lt;code&gt;let&lt;/code&gt;. Any line where the expression can be 0 will stop a &lt;code&gt;set -e&lt;/code&gt; script, whether or not the arithmetic did what you meant.&lt;/li&gt;
&lt;li&gt;Counters starting at 0 with &lt;code&gt;count++&lt;/code&gt; fail on the first item every time. Totals with &lt;code&gt;+=&lt;/code&gt; fail when the first value is 0, which depends on the data.&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;x=$((x + n))&lt;/code&gt; for counting and totals, so the line's status says nothing about the number.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;(( ))&lt;/code&gt; for conditions, like &lt;code&gt;if ((count &amp;gt; 0))&lt;/code&gt;, where a status of 1 is supposed to mean false.&lt;/li&gt;
&lt;li&gt;Test with an empty file at the front of the folder, not only with clips that all have something in them.&lt;/li&gt;
&lt;li&gt;Add an ERR trap while debugging, so a silent exit says which line it stopped on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things I did not test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Other Bash versions.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other shells.&lt;/strong&gt; &lt;code&gt;(( ))&lt;/code&gt; and &lt;code&gt;let&lt;/code&gt; are not POSIX &lt;code&gt;sh&lt;/code&gt;. I did not test how &lt;code&gt;dash&lt;/code&gt;, &lt;code&gt;zsh&lt;/code&gt; or &lt;code&gt;ksh&lt;/code&gt; treat them, or whether their &lt;code&gt;set -e&lt;/code&gt; behaves the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The exceptions to &lt;code&gt;set -e&lt;/code&gt;.&lt;/strong&gt; The shell does not stop on every failing command; a status inside an &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;while&lt;/code&gt; or &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; list is handled differently. I tested a plain &lt;code&gt;(( ))&lt;/code&gt; line, the &lt;code&gt;|| true&lt;/code&gt; form and one &lt;code&gt;if&lt;/code&gt; above, and did not test &lt;code&gt;while&lt;/code&gt;, &lt;code&gt;until&lt;/code&gt; or &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; lists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real clips.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; The quote is from the GNU Bash Reference Manual page &lt;a href="https://www.gnu.org/software/bash/manual/html_node/Conditional-Constructs.html" rel="noopener noreferrer"&gt;Conditional Constructs&lt;/a&gt; (the &lt;code&gt;(( expression ))&lt;/code&gt; entry). Outputs are pasted unedited from the scripts above.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam" rel="noopener noreferrer"&gt;Background Camera RemoteStream on Google Play&lt;/a&gt;. Nothing above depends on it.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>linux</category>
      <category>shell</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Six Camera Clips, Three in the Archive, Every mv Exited 0: Testing Same-Name Moves From Two Folders</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Thu, 05 Nov 2026 13:55:07 +0000</pubDate>
      <link>https://dev.to/superfunicular/six-camera-clips-three-in-the-archive-every-mv-exited-0-testing-same-name-moves-from-two-folders-klg</link>
      <guid>https://dev.to/superfunicular/six-camera-clips-three-in-the-archive-every-mv-exited-0-testing-same-name-moves-from-two-folders-klg</guid>
      <description>&lt;p&gt;Two phones recording into two folders will sooner or later produce two clips with the same name. I set that up on purpose, with three clips in each folder named the same way, and moved everything into one archive folder four ordinary ways. Three of them ended with three clips in the archive instead of six, and every &lt;code&gt;mv&lt;/code&gt; in those three exited 0.&lt;/p&gt;

&lt;p&gt;The one form that refused to lose anything was the one that looks the least careful: a single &lt;code&gt;mv&lt;/code&gt; with both folders on one command line. Below are the test scripts, what each one printed, what the GNU coreutils manual says about &lt;code&gt;-n&lt;/code&gt;, &lt;code&gt;-i&lt;/code&gt; and &lt;code&gt;--backup&lt;/code&gt;, where this old &lt;code&gt;mv&lt;/code&gt; and the current manual disagree about exit status, and the version of the move I would run instead.&lt;/p&gt;

&lt;p&gt;Disclosure: I make an Android camera app. Nothing below uses its code or describes how it names or stores clips. The clips here are one-line text files made on a Linux machine, and every output is pasted from the scripts shown. Versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GNU bash, version 5.1.16(1)-release (aarch64-unknown-linux-gnu)
mv (GNU coreutils) 8.32
PRETTY_NAME="Ubuntu 22.04.5 LTS"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Each test starts from the same two folders. The first line of every clip says which camera recorded it, so the archive can be checked by reading it rather than by trusting a count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Two cameras, same clip names, one archive folder.&lt;/span&gt;
&lt;span class="c"&gt;# Each clip's first line says which camera recorded it.&lt;/span&gt;
setup&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; cam1 cam2 archive
  &lt;span class="nb"&gt;mkdir &lt;/span&gt;cam1 cam2 archive
  &lt;span class="k"&gt;for &lt;/span&gt;n &lt;span class="k"&gt;in &lt;/span&gt;0001 0002 0003&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'cam1 clip %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"cam1/clip_&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;.mp4"&lt;/span&gt;
    &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'cam2 clip %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"cam2/clip_&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;.mp4"&lt;/span&gt;
  &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
report&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"archive: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;archive | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; files,"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="s2"&gt;"left in cam1: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;cam1 | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;, cam2: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;cam2 | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1 archive/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One mv, both folders
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; ./setup.sh&lt;span class="p"&gt;;&lt;/span&gt; setup
&lt;span class="nb"&gt;mv &lt;/span&gt;cam1/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 cam2/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 archive/
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mv: will not overwrite just-created 'archive/clip_0001.mp4' with 'cam2/clip_0001.mp4'
mv: will not overwrite just-created 'archive/clip_0002.mp4' with 'cam2/clip_0002.mp4'
mv: will not overwrite just-created 'archive/clip_0003.mp4' with 'cam2/clip_0003.mp4'
exit 1
archive: 3 files, left in cam1: 0, cam2: 3
cam1 clip 0001
cam1 clip 0002
cam1 clip 0003
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GNU &lt;code&gt;mv&lt;/code&gt; noticed that the second &lt;code&gt;clip_0001.mp4&lt;/code&gt; would land on a file it had created moments earlier in the same command, refused, said so three times and exited 1. Nothing was lost: the cam2 clips stayed where they were. As far as I can tell from the outputs, this check only covers files the same &lt;code&gt;mv&lt;/code&gt; command just moved there. The next two tests are what happens without it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One mv per camera
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; ./setup.sh&lt;span class="p"&gt;;&lt;/span&gt; setup
&lt;span class="nb"&gt;mv &lt;/span&gt;cam1/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 archive/&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"cam1 exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mv &lt;/span&gt;cam2/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 archive/&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"cam2 exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cam1 exit 0
cam2 exit 0
archive: 3 files, left in cam1: 0, cam2: 0
cam2 clip 0001
cam2 clip 0002
cam2 clip 0003
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same clips, same destination, split into two commands. Both exited 0. The archive holds three files, both camera folders are empty, and every surviving clip is from cam2. The cam1 recordings were replaced, with no message.&lt;/p&gt;

&lt;h2&gt;
  
  
  A loop, one mv per clip
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; ./setup.sh&lt;span class="p"&gt;;&lt;/span&gt; setup
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;cam1/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 cam2/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; archive/
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"loop exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;loop exit 0
archive: 3 files, left in cam1: 0, cam2: 0
cam2 clip 0001
cam2 clip 0002
cam2 clip 0003
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the shape most scripts take, because a loop is where you add logging, skip partial files or build names. Same result as the previous test: three clips survive, all from cam2, and the loop exits 0. Nothing in the output says three recordings are gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the manual says
&lt;/h2&gt;

&lt;p&gt;The GNU coreutils manual's page for &lt;code&gt;mv&lt;/code&gt; describes the three options a script would reach for here.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;-n&lt;/code&gt;: "Do not overwrite an existing file; silently fail instead."&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;-i&lt;/code&gt;: "Prompt whether to overwrite each existing destination file, regardless of its permissions, and fail if the response is not affirmative."&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;--backup&lt;/code&gt;: "Make a backup of each file that would otherwise be overwritten or removed."&lt;/p&gt;

&lt;p&gt;And at the bottom: "An exit status of zero indicates success, and a nonzero value indicates failure."&lt;/p&gt;

&lt;p&gt;The page I read is the current online manual. The &lt;code&gt;mv&lt;/code&gt; I tested is coreutils 8.32, the one Ubuntu 22.04 ships, and the next two tests are where those two differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  mv -n
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; ./setup.sh&lt;span class="p"&gt;;&lt;/span&gt; setup
&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; cam1/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 archive/&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"cam1 exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; cam2/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 archive/&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"cam2 exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cam1 exit 0
cam2 exit 0
archive: 3 files, left in cam1: 0, cam2: 3
cam1 clip 0001
cam1 clip 0002
cam1 clip 0003
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-n&lt;/code&gt; protected the archive: the three cam1 clips are intact. It also left the three cam2 clips sitting in their folder and exited 0 both times. On this version it is silent, as described, but the exit status does not report a failure. A script that checks &lt;code&gt;$?&lt;/code&gt; after &lt;code&gt;mv -n&lt;/code&gt; and then empties or deletes the source folder would be deleting the cam2 clips, which exist nowhere else.&lt;/p&gt;

&lt;p&gt;The current manual points to &lt;code&gt;--update=none&lt;/code&gt; as the replacement for &lt;code&gt;-n&lt;/code&gt;, which "will skip existing files but not fail", and describes a &lt;code&gt;none-fail&lt;/code&gt; value where "any skipped files are diagnosed and induce a failure." This &lt;code&gt;mv&lt;/code&gt; does not have either:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; ./setup.sh&lt;span class="p"&gt;;&lt;/span&gt; setup
&lt;span class="nb"&gt;mv &lt;/span&gt;cam1/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 archive/
&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="nt"&gt;--update&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;none cam2/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 archive/&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"cam2 exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mv: option '--update' doesn't allow an argument
Try 'mv --help' for more information.
cam2 exit 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a script written against the current manual will stop with a usage error on an older system, and a script written for the older system gets a different exit status from &lt;code&gt;-n&lt;/code&gt; on a newer one. If you rely on the exit status of &lt;code&gt;-n&lt;/code&gt;, check which version is on the machine that runs the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  mv -i with nobody at the keyboard
&lt;/h2&gt;

&lt;p&gt;A nightly job has no terminal. Standard input here comes from &lt;code&gt;/dev/null&lt;/code&gt;, standing in for a job with no one to answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; ./setup.sh&lt;span class="p"&gt;;&lt;/span&gt; setup
&lt;span class="nb"&gt;mv &lt;/span&gt;cam1/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 archive/
&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; cam2/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 archive/ &amp;lt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;rc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"cam2 exit &lt;/span&gt;&lt;span class="nv"&gt;$rc&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mv: overwrite 'archive/clip_0001.mp4'? mv: overwrite 'archive/clip_0002.mp4'? mv: overwrite 'archive/clip_0003.mp4'? 
cam2 exit 0
archive: 3 files, left in cam1: 0, cam2: 3
cam1 clip 0001
cam1 clip 0002
cam1 clip 0003
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mv&lt;/code&gt; printed three prompts, read no answer, skipped all three and exited 0. The archive is safe and the cam2 clips are left behind, same as &lt;code&gt;-n&lt;/code&gt;. The manual says &lt;code&gt;-i&lt;/code&gt; should fail when the answer is not yes; on 8.32 the skip happened and the failure did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  mv --backup=numbered
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; ./setup.sh&lt;span class="p"&gt;;&lt;/span&gt; setup
&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="nt"&gt;--backup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;numbered cam1/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 archive/&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"cam1 exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="nt"&gt;--backup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;numbered cam2/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 archive/&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"cam2 exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
report
&lt;span class="nb"&gt;ls &lt;/span&gt;archive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cam1 exit 0
cam2 exit 0
archive: 6 files, left in cam1: 0, cam2: 0
cam2 clip 0001
cam1 clip 0001
cam2 clip 0002
cam1 clip 0002
cam2 clip 0003
cam1 clip 0003
clip_0001.mp4
clip_0001.mp4.~1~
clip_0002.mp4
clip_0002.mp4.~1~
clip_0003.mp4
clip_0003.mp4.~1~
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All six clips are in the archive. The cam1 clips were renamed to &lt;code&gt;clip_0001.mp4.~1~&lt;/code&gt; and so on, and the cam2 clips took the plain names. Nothing was lost, and both commands exited 0, which is correct here because nothing failed. The cost is that anything downstream that looks for &lt;code&gt;*.mp4&lt;/code&gt; will not see the backups, so half the footage is there but invisible to a job that globs on the extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would run instead
&lt;/h2&gt;

&lt;p&gt;The clips collide because the camera folder is part of the identity and the move throws it away. Keeping it in the name removes the collision instead of choosing a winner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; ./setup.sh&lt;span class="p"&gt;;&lt;/span&gt; setup
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;cam1/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 cam2/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;cam&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="p"&gt;%%/*&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;            &lt;span class="c"&gt;# cam1&lt;/span&gt;
  &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="p"&gt;##*/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;           &lt;span class="c"&gt;# clip_0001.mp4&lt;/span&gt;
  &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"archive/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;cam&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;report
&lt;span class="nb"&gt;ls &lt;/span&gt;archive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;archive: 6 files, left in cam1: 0, cam2: 0
cam1 clip 0001
cam1 clip 0002
cam1 clip 0003
cam2 clip 0001
cam2 clip 0002
cam2 clip 0003
cam1_clip_0001.mp4
cam1_clip_0002.mp4
cam1_clip_0003.mp4
cam2_clip_0001.mp4
cam2_clip_0002.mp4
cam2_clip_0003.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six files, every first line matches its name, both folders empty. I kept &lt;code&gt;-n&lt;/code&gt; in the loop as a second line of defence, in case a rerun meets a clip that is already in the archive.&lt;/p&gt;

&lt;p&gt;The other thing I would add to any move like this, whatever the naming, is a count that has to balance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; ./setup.sh&lt;span class="p"&gt;;&lt;/span&gt; setup
&lt;span class="nv"&gt;before&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;cam1/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 cam2/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;cam1/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 cam2/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; archive/
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nv"&gt;after&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;archive | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"before &lt;/span&gt;&lt;span class="nv"&gt;$before&lt;/span&gt;&lt;span class="s2"&gt;, after &lt;/span&gt;&lt;span class="nv"&gt;$after&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$before&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$after&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"MISMATCH: &lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;before &lt;span class="o"&gt;-&lt;/span&gt; after&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="s2"&gt; clips missing"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash 09_count.sh&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"script exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before 6, after 3
MISMATCH: 3 clips missing
script exit 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the plain loop from earlier, unchanged, with a check around it. It does not save the clips; it turns three missing recordings into a non-zero exit and a line on standard error, which is the part the earlier tests did not produce. A count only proves the numbers match, not that the right files arrived, so on footage I care about I would also compare names, or checksums, before removing anything from the source.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would check in an existing job
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find every move or copy into a shared folder&lt;/strong&gt; and ask whether two sources can produce the same file name. Two cameras, two days with a counter that resets, or a phone reset that restarts numbering will all do it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the job once on purpose against two folders whose clips share names.&lt;/strong&gt; Read the first line or a checksum of what survives, not the count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the script uses &lt;code&gt;mv -n&lt;/code&gt; or &lt;code&gt;mv -i&lt;/code&gt;, do not treat exit 0 as proof that everything moved.&lt;/strong&gt; On coreutils 8.32 both returned 0 with files left behind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the version on the machine that runs the job&lt;/strong&gt; before relying on &lt;code&gt;--update=none&lt;/code&gt; or the exit status of &lt;code&gt;-n&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make an imbalance fail loudly&lt;/strong&gt;, with a count or a manifest, before anything deletes or clears the source.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I did not test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Newer coreutils.&lt;/strong&gt; I only ran 8.32. I have not run the current release, so I have not confirmed that its &lt;code&gt;-n&lt;/code&gt; or &lt;code&gt;-i&lt;/code&gt; exits non-zero where 8.32 exited 0; the manual says they fail, and I am reporting the manual, not a test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moves across file systems.&lt;/strong&gt; Every test here moved within one disk, where &lt;code&gt;mv&lt;/code&gt; renames. Across disks it copies and then deletes, and I did not test whether anything above changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BSD and BusyBox &lt;code&gt;mv&lt;/code&gt;.&lt;/strong&gt; macOS and many small Linux images ship a different &lt;code&gt;mv&lt;/code&gt;, and the just-created check and the option names may differ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cp&lt;/code&gt;.&lt;/strong&gt; It shares most of these options, but none of the outputs above are from &lt;code&gt;cp&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; Quotes about &lt;code&gt;-n&lt;/code&gt;, &lt;code&gt;-i&lt;/code&gt;, &lt;code&gt;--backup&lt;/code&gt;, &lt;code&gt;--update&lt;/code&gt; and exit status are from the GNU coreutils manual, &lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/mv-invocation.html" rel="noopener noreferrer"&gt;mv: Move (rename) files&lt;/a&gt;. Outputs are pasted unedited from the scripts shown.&lt;/p&gt;

&lt;p&gt;If you want an old Android phone recording in the background so there is a folder of clips for a job like this to move, the app I make is &lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam" rel="noopener noreferrer"&gt;Background Camera RemoteStream on Google Play&lt;/a&gt;. Nothing above depends on it.&lt;/p&gt;

&lt;p&gt;Before a job moves clips from more than one folder into one place, give it two folders with matching file names and read what survives, because a per-folder or per-file &lt;code&gt;mv&lt;/code&gt; replaces the earlier clip and still exits 0.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>linux</category>
      <category>shell</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Why Does My Phone Camera's Live Stream Stop After a Few Hours on Mobile Data? The 1.5 GB/Day Limit and the 64 Kbps Speed After It</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Thu, 05 Nov 2026 08:13:39 +0000</pubDate>
      <link>https://dev.to/superfunicular/why-does-my-phone-cameras-live-stream-stop-after-a-few-hours-on-mobile-data-the-15-gbday-limit-341l</link>
      <guid>https://dev.to/superfunicular/why-does-my-phone-cameras-live-stream-stop-after-a-few-hours-on-mobile-data-the-15-gbday-limit-341l</guid>
      <description>&lt;p&gt;You set up an old phone as a camera for the shop or the house. It streams to YouTube Live over a prepaid SIM. It works in the evening. By the early morning the stream is frozen or gone, but the phone is still on, still charging, and the data icon still shows 4G.&lt;/p&gt;

&lt;p&gt;On many prepaid plans the reason is the &lt;strong&gt;daily data limit&lt;/strong&gt;. On a plan that says "1.5 GB/day", the data does not stop when the 1.5 GB is used. It gets &lt;strong&gt;slow&lt;/strong&gt;. This guide explains what that means for a phone camera, how many hours of streaming a daily pack really covers, and how to set up the camera so the important hours are not the ones that get cut.&lt;/p&gt;

&lt;p&gt;It is written for budget and older Android phones on prepaid data, the usual setup in India, Bangladesh and many other markets. The plan figures below come from Jio's own help pages, read on 5 November 2026. Your operator and plan may differ, so check your plan's own wording (the next section shows where to look).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does my phone camera stream stop after a few hours on mobile data?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; the plan's daily high-speed data ran out, and the speed dropped below what a live stream needs.&lt;/p&gt;

&lt;p&gt;Jio's ₹319 plan is a good example. Its help page lists the benefits as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Data - 1.5 GB/day (thereafter @ 64 Kbps)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The words to look for are &lt;strong&gt;"thereafter @ 64 Kbps"&lt;/strong&gt;. After the daily 1.5 GB, the connection keeps working, but at 64 kilobits per second. That is enough for a chat message. It is not enough for video.&lt;/p&gt;

&lt;p&gt;YouTube's own table of live encoder settings gives the &lt;strong&gt;minimum&lt;/strong&gt; bitrate for an H.264 stream at 360p or 480p, 30 fps, as &lt;strong&gt;0.4 Mbps&lt;/strong&gt; (400 Kbps). That is the lowest row in the whole table. 720p needs a minimum of 3 Mbps.&lt;/p&gt;

&lt;p&gt;So after the daily limit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;64 Kbps is &lt;strong&gt;6.25 times less&lt;/strong&gt; than YouTube's lowest H.264 minimum (400 ÷ 64).&lt;/li&gt;
&lt;li&gt;If you also send audio at the 128 Kbps YouTube recommends for stereo, the stream needs about 528 Kbps, which is &lt;strong&gt;8.25 times&lt;/strong&gt; more than 64 Kbps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The phone has not crashed, and the camera app may not have stopped. The pipe is just too narrow. What YouTube does when the incoming video stops or slows is covered in our guide to &lt;a href="https://dev.to/superfunicular/what-happens-to-a-phones-youtube-live-stream-when-mobile-data-drops-enableautostop-stream-3556"&gt;what happens to a phone's YouTube Live stream when mobile data drops&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to read your own plan
&lt;/h2&gt;

&lt;p&gt;Open your operator's app or website and find the plan details. Look for a line like one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"X GB/day, thereafter @ 64 Kbps" — a &lt;strong&gt;daily&lt;/strong&gt; limit with slow data after it.&lt;/li&gt;
&lt;li&gt;"X GB, valid for N days" — a &lt;strong&gt;total&lt;/strong&gt; pack. When it is used, data may stop completely until you recharge.&lt;/li&gt;
&lt;li&gt;"Unlimited" with a smaller line about fair usage — read the smaller line. It usually describes a speed after a limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the plan has a daily limit, also find when it &lt;strong&gt;resets&lt;/strong&gt;. Jio's Data Add-On help page says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you are on a Daily FUP plan, the daily data quota is renewed at midnight (between 00:00 hrs and 02:00 hrs).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one sentence explains a common pattern: the stream dies at some point in the night, and "comes back" after midnight. The camera did not fix itself. The quota came back.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many hours of live streaming does 1.5 GB per day give?
&lt;/h2&gt;

&lt;p&gt;Here is the arithmetic, so you can redo it for your own plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MB per hour = bitrate in Mbps × 3,600 seconds ÷ 8&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At YouTube's 0.4 Mbps minimum, that is &lt;strong&gt;180 MB per hour&lt;/strong&gt; of video. With 128 Kbps audio added, it is about &lt;strong&gt;238 MB per hour&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Daily high-speed data&lt;/th&gt;
&lt;th&gt;Hours at 0.4 Mbps (video only)&lt;/th&gt;
&lt;th&gt;Hours at ~0.53 Mbps (video + 128 Kbps audio)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;about 5.6 h&lt;/td&gt;
&lt;td&gt;about 4.2 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.5 GB&lt;/td&gt;
&lt;td&gt;about 8.3 h&lt;/td&gt;
&lt;td&gt;about 6.3 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 GB&lt;/td&gt;
&lt;td&gt;about 11.1 h&lt;/td&gt;
&lt;td&gt;about 8.4 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 GB&lt;/td&gt;
&lt;td&gt;about 16.7 h&lt;/td&gt;
&lt;td&gt;about 12.6 h&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are the &lt;strong&gt;best case&lt;/strong&gt;. They assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the stream runs at YouTube's lowest allowed rate, not the 4 Mbps it recommends for 480p;&lt;/li&gt;
&lt;li&gt;nothing else on the phone uses data;&lt;/li&gt;
&lt;li&gt;1 GB = 1,000 MB (some operators count 1,024, which changes the hours a little).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A real phone uses some data for other things: app updates, backups, the operator's own apps. So in practice the hours are lower. At 720p the picture is far worse: YouTube's 720p minimum is 3 Mbps, which is 1.35 GB per hour. A 1.5 GB daily pack is gone in a little over one hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest summary:&lt;/strong&gt; a 1.5 GB/day plan cannot carry a 24-hour live stream. At the lowest quality it covers a night, not a day and a night.&lt;/p&gt;

&lt;h2&gt;
  
  
  What still works after the daily limit?
&lt;/h2&gt;

&lt;p&gt;This is the good news. Two of the three things a phone camera does use &lt;strong&gt;no mobile data at all&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Recording to the phone.&lt;/strong&gt; Writing video to the phone's own storage or SD card does not touch the network. At 64 Kbps, or with mobile data switched off, the phone keeps recording. Background Camera RemoteStream records with the screen off and keeps the files on the phone. Nothing is uploaded to a cloud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Live view on the same local network.&lt;/strong&gt; Background Camera RemoteStream has a built-in web server (the browser live view is a Pro feature, per the Play listing). Any phone or laptop on the &lt;strong&gt;same Wi-Fi, or the same phone hotspot&lt;/strong&gt;, can open the live view in a browser. That traffic stays inside the local network, so it does not count against the SIM's daily data. If you are in the shop or the house, you can still look at the camera after the daily limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. YouTube Live over mobile data.&lt;/strong&gt; This is the only part that needs the SIM's high-speed data. It is the part that stops.&lt;/p&gt;

&lt;p&gt;So when people say "the camera stopped at night", it is worth asking which of the three stopped. Very often the recording is complete on the phone and only the live stream to YouTube failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to set up a phone camera on a daily data plan
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Decide which hours need a live stream
&lt;/h3&gt;

&lt;p&gt;Most people do not need to watch live all day. They need to watch the hours when nobody is there: the shop at night, the house while the family is at work or away. Stream those hours. Let the phone record locally the rest of the time.&lt;/p&gt;

&lt;p&gt;With 1.5 GB per day at the lowest setting, about 6 to 8 hours of streaming is a realistic target. Choose the hours on purpose, instead of letting the limit choose them for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Remember when the day resets
&lt;/h3&gt;

&lt;p&gt;If your quota resets around midnight, a stream that starts in the evening uses the &lt;strong&gt;old&lt;/strong&gt; day's data until midnight, then the &lt;strong&gt;new&lt;/strong&gt; day's data after it. Example on a 1.5 GB plan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you stream from 8 pm, and nothing else used data that day, the stream can run until midnight on the old quota.&lt;/li&gt;
&lt;li&gt;After the reset, the new 1.5 GB covers the rest of the night until about 6 am to 8 am, depending on audio.&lt;/li&gt;
&lt;li&gt;But the new day's data is then mostly used before the day starts. Anyone using that SIM for anything else will find it slow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is fine for a SIM that only serves the camera. It is not fine if the SIM is also someone's main phone number. Our &lt;a href="https://dev.to/superfunicular/should-you-leave-the-sim-card-in-an-old-phone-security-camera-a-dual-sim-guide-for-offline-wi-fi-587c"&gt;dual-SIM guide&lt;/a&gt; covers which SIM to leave in a camera phone, and why.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use the lowest stream quality you can accept
&lt;/h3&gt;

&lt;p&gt;For a security camera, 480p at the minimum bitrate is usually enough to see that someone came in, when, and roughly what they did. The full-quality recording stays on the phone. If you need a clear face later, you get it from the local file, not from the stream.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Stop other things from eating the camera's data
&lt;/h3&gt;

&lt;p&gt;On the camera phone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turn off automatic app updates over mobile data in the Play Store settings.&lt;/li&gt;
&lt;li&gt;Turn off photo and file backup over mobile data.&lt;/li&gt;
&lt;li&gt;Uninstall or disable apps you will never use on that phone.&lt;/li&gt;
&lt;li&gt;Check &lt;strong&gt;Settings &amp;gt; Network &amp;gt; Data usage&lt;/strong&gt; (the name varies by brand) after the first day to see which app used what.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data Saver is the other tool people reach for. It does not block a foreground camera service the way many guides say. We checked Android's source code for that in &lt;a href="https://dev.to/superfunicular/does-data-saver-cut-off-a-phone-security-camera-streaming-on-mobile-data-with-the-screen-off-what-452"&gt;Does Data Saver Cut Off a Phone Security Camera Streaming on Mobile Data With the Screen Off?&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Know what an add-on pack buys you
&lt;/h3&gt;

&lt;p&gt;If you need extra hours on a particular night, a data add-on can help. On 5 November 2026, Jio's Data Add-On page listed a ₹19 pack with 1 GB valid for 24 hours, and said: &lt;em&gt;"In case you have an active valid plan then the Data pack will be auto-activated on expiry of daily data allocation."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At the 0.4 Mbps minimum, 1 GB is about 5.6 hours of video, or about 4.2 hours with audio. Prices and packs change, so check the current list in your operator's app before you rely on this.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. If a phone hotspot is the uplink, the same rule applies
&lt;/h3&gt;

&lt;p&gt;Some setups use a second phone's hotspot as the internet for the camera phone. Then the &lt;strong&gt;hotspot phone's&lt;/strong&gt; daily limit is the one that matters, and its 64 Kbps "after" speed slows the stream in the same way. Live view between the two phones still works, because it stays on the hotspot's local network. Our &lt;a href="https://dev.to/superfunicular/how-to-use-an-old-phone-as-a-security-camera-without-a-wi-fi-router-the-hotspot-setup-nobody-1bd5"&gt;hotspot setup guide&lt;/a&gt; walks through that layout.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Test one night before you trust it
&lt;/h3&gt;

&lt;p&gt;Do this once, on a normal day:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Note the data used so far today in your operator's app.&lt;/li&gt;
&lt;li&gt;Start the stream at the hour you plan to use, at the quality you plan to use.&lt;/li&gt;
&lt;li&gt;After one hour, check the data used again. That number, in MB, is your real MB per hour.&lt;/li&gt;
&lt;li&gt;Divide your daily high-speed data by that number. That is your real hours per day.&lt;/li&gt;
&lt;li&gt;Next morning, open the YouTube Live archive and the local recordings. Check whether the stream stopped, when it stopped, and whether the local recording carried on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Also check one thing about your own setup: after the quota resets, does the stream start again by itself, or do you need to restart it? Test it, so you know before the night it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means if you are on an old 3G phone
&lt;/h2&gt;

&lt;p&gt;If the camera phone only supports 3G and your operator has switched 3G off, the phone may fall back to 2G and be slow &lt;strong&gt;before&lt;/strong&gt; any daily limit. That is a different problem, with a different fix. See &lt;a href="https://dev.to/superfunicular/will-an-old-3g-phone-work-as-a-security-camera-on-mobile-data-what-the-e-h-and-4g-icons-mean-for-13o6"&gt;Will an Old 3G Phone Work as a Security Camera on Mobile Data?&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;My stream stopped but the phone still shows 4G. Is the phone broken?&lt;/strong&gt;&lt;br&gt;
Probably not. On a daily plan with "thereafter @ 64 Kbps", the 4G connection stays up, but at a speed far below YouTube's 0.4 Mbps minimum. Check your data balance in the operator's app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did the camera stop recording too?&lt;/strong&gt;&lt;br&gt;
Not because of data. Recording to the phone's storage does not use mobile data. Check the local files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I still watch the camera at home after the limit?&lt;/strong&gt;&lt;br&gt;
Yes, on the same Wi-Fi or hotspot. Background Camera RemoteStream's local live view (a Pro feature) does not use the SIM's data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long will 1.5 GB a day stream for?&lt;/strong&gt;&lt;br&gt;
At best about 8 hours of 480p video at YouTube's minimum rate, or about 6 hours with audio. In practice a bit less.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a cloud account to use the camera?&lt;/strong&gt;&lt;br&gt;
No. Video stays on the phone and there is no cloud storage plan. Screen-off recording is in the core app; the browser live view and YouTube Live are Pro features per the Play listing, so check the listing for the current free vs Pro split. The only data cost is your own mobile plan, and only for the YouTube stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the app
&lt;/h2&gt;

&lt;p&gt;Background Camera RemoteStream turns an Android phone into a camera that records with the screen off, keeps video on the phone, offers live view on your local network through its built-in web server, and can stream to YouTube Live. Live view and YouTube Live are Pro features per the Play listing; check it for the current free vs Pro details.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.superfunicular.digicam&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Website: &lt;a href="https://superfunicular.com" rel="noopener noreferrer"&gt;https://superfunicular.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.jio.com/help/faq/mobile/prepaid-offerings/monthly-plan/what-are-the-benefits-of-the-259-plan/" rel="noopener noreferrer"&gt;Jio — What are the benefits of the ₹319 plan&lt;/a&gt; ("1.5 GB/day (thereafter @ 64 Kbps)")&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.jio.com/help/faq/mobile/prepaid-offerings/4g-data-voucher/i-have-used-my-daily-data-quota-the-data-speed-has-been-reduced-what-should-i-do-to-get-4g-speed-again/" rel="noopener noreferrer"&gt;Jio — I have used my daily data quota &amp;amp; the data speed has been reduced&lt;/a&gt; (midnight renewal, Data Add-On list)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://support.google.com/youtube/answer/2853702?hl=en" rel="noopener noreferrer"&gt;YouTube Help — Choose live encoder settings, bitrates, and resolutions&lt;/a&gt; (H.264 minimums, 128 Kbps stereo audio)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All hours and MB figures in this article are calculated from those numbers, not measured on a phone.&lt;/p&gt;

</description>
      <category>android</category>
      <category>tutorial</category>
      <category>networking</category>
      <category>security</category>
    </item>
    <item>
      <title>An Empty Folder Made My Upload Loop Report One Clip: What Bash Does With a Glob That Matches Nothing</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Wed, 04 Nov 2026 13:55:03 +0000</pubDate>
      <link>https://dev.to/superfunicular/an-empty-folder-made-my-upload-loop-report-one-clip-what-bash-does-with-a-glob-that-matches-nothing-2f6g</link>
      <guid>https://dev.to/superfunicular/an-empty-folder-made-my-upload-loop-report-one-clip-what-bash-does-with-a-glob-that-matches-nothing-2f6g</guid>
      <description>&lt;p&gt;I pointed a nightly upload loop at an empty folder of camera clips, the kind of folder you get on a night the phone recorded nothing, and it reported one clip uploaded. There was no clip. The loop had run once, with the pattern &lt;code&gt;clips/*.mp4&lt;/code&gt; itself as the file name.&lt;/p&gt;

&lt;p&gt;That is documented Bash behavior, and the usual fix, &lt;code&gt;shopt -s nullglob&lt;/code&gt;, brings its own surprise: with it on, &lt;code&gt;ls clips/*.mp4&lt;/code&gt; in an empty folder listed the files in the current directory instead. Below are the test scripts, what each one printed, what the Bash manual says about all three settings, what happens under &lt;code&gt;/bin/sh&lt;/code&gt;, and the version of the job I would run instead.&lt;/p&gt;

&lt;p&gt;Disclosure: I make an Android camera app. Nothing below uses its code or describes how it records, names or stores anything. The clips are random bytes or empty folders made on a Linux machine, and every output is pasted from the scripts shown. Versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GNU bash, version 5.1.16(1)-release (aarch64-unknown-linux-gnu)
dash 0.5.11+git20210903+057cd650a4ed-3build1 (/bin/sh)
ls (GNU coreutils) 8.32
tar (GNU tar) 1.34
find (GNU findutils) 4.8.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The loop on an empty night
&lt;/h2&gt;

&lt;p&gt;The job counts the clips in a folder and copies each one to an upload directory. The folder exists, but nothing was recorded into it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# A nightly job: count tonight's clips, then upload each one.&lt;/span&gt;
&lt;span class="c"&gt;# The phone recorded nothing, so the folder is empty.&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; clips
&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;n &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"uploading: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /tmp/upload/ 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^/  /'&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"clips uploaded tonight: &lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uploading: clips/*.mp4
  cp: cannot stat 'clips/*.mp4': No such file or directory
clips uploaded tonight: 1
exit status: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop body ran once. &lt;code&gt;$f&lt;/code&gt; held the literal text &lt;code&gt;clips/*.mp4&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt; failed on it, and the count went to one. The script still exited with status 0, because its last command was an &lt;code&gt;echo&lt;/code&gt; that worked. A log that keeps only the last line says one clip went up on a night when none existed.&lt;/p&gt;

&lt;p&gt;In this test the damage is a wrong number and a &lt;code&gt;cp&lt;/code&gt; error in the middle of the output. In a job that does more with &lt;code&gt;$f&lt;/code&gt;, such as writing it into a manifest, passing it to a remote command, or using it to build an output file name, the literal pattern goes wherever a real name would have gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the manual says
&lt;/h2&gt;

&lt;p&gt;The Bash Reference Manual, in the section on filename expansion, covers all three behaviors in one paragraph. The default: "If no matching filenames are found, and the shell option nullglob is disabled, the word is left unchanged."&lt;/p&gt;

&lt;p&gt;The first alternative: "If the nullglob option is set, and no matches are found, the word is removed."&lt;/p&gt;

&lt;p&gt;The second: "If the failglob shell option is set, and no matches are found, Bash prints an error message and does not execute the command."&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;shopt&lt;/code&gt; page adds that "Unless otherwise noted, the shopt options are disabled (off) by default." Neither &lt;code&gt;nullglob&lt;/code&gt; nor &lt;code&gt;failglob&lt;/code&gt; is noted as on, so a script that does not set one gets the unchanged word. That is the loop above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning on nullglob
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;nullglob&lt;/code&gt; fixes the loop: no match, no word, no iteration. The trouble is that it applies to every command in the script, and plenty of commands behave differently when handed no file arguments at all than when handed a pattern that matched nothing. Same empty folder, with two ordinary files in the directory the job runs from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Same empty folder, nullglob on. The loop is fixed; now watch what&lt;/span&gt;
&lt;span class="c"&gt;# the same setting does to commands that take the glob as arguments.&lt;/span&gt;
&lt;span class="nb"&gt;shopt&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; nullglob
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; work/clips &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;work
&lt;span class="nb"&gt;touch &lt;/span&gt;notes.txt&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;touch &lt;/span&gt;settings.json
&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;n &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"loop count: &lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== ls clips/*.mp4"&lt;/span&gt;
&lt;span class="nb"&gt;ls &lt;/span&gt;clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== newest clip: ls -t clips/*.mp4 | head -n 1"&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== rm -f clips/*.mp4"&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== rm clips/*.mp4"&lt;/span&gt;
&lt;span class="nb"&gt;rm &lt;/span&gt;clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== tar czf night.tgz clips/*.mp4"&lt;/span&gt;
&lt;span class="nb"&gt;tar &lt;/span&gt;czf night.tgz clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;loop count: 0
== ls clips/*.mp4
clips
notes.txt
settings.json
exit 0
== newest clip: ls -t clips/*.mp4 | head -n 1
settings.json
== rm -f clips/*.mp4
exit 0
== rm clips/*.mp4
rm: missing operand
Try 'rm --help' for more information.
exit 1
== tar czf night.tgz clips/*.mp4
tar: Cowardly refusing to create an empty archive
Try 'tar --help' or 'tar --usage' for more information.
exit 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line by line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The loop counted zero.&lt;/strong&gt; That is the fix working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ls clips/*.mp4&lt;/code&gt; listed the current directory.&lt;/strong&gt; With the word removed, the command became plain &lt;code&gt;ls&lt;/code&gt;, and &lt;code&gt;ls&lt;/code&gt; with no arguments lists where it is. It exited 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The newest-clip line picked &lt;code&gt;settings.json&lt;/code&gt;.&lt;/strong&gt; A job that uploads &lt;code&gt;$(ls -t clips/*.mp4 | head -n 1)&lt;/code&gt; as the latest recording would have picked a settings file, without an error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rm -f&lt;/code&gt; did nothing and exited 0.&lt;/strong&gt; That is harmless here, and it is also why nobody notices the setting is on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rm&lt;/code&gt; without &lt;code&gt;-f&lt;/code&gt; failed with &lt;code&gt;missing operand&lt;/code&gt;&lt;/strong&gt;, and &lt;code&gt;tar&lt;/code&gt; refused to make an empty archive and exited 2. Both are loud, which is better than &lt;code&gt;ls&lt;/code&gt;, but a job written for the default behavior now fails on a night that used to pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;code&gt;nullglob&lt;/code&gt; is not a setting to put at the top of an existing script and forget. Every command that receives a glob as arguments, rather than iterating over it, has to be checked for what it does with nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning on failglob
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;failglob&lt;/code&gt; takes the opposite approach: an unmatched pattern is an error and the command does not run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# failglob instead: an unmatched pattern is an error.&lt;/span&gt;
&lt;span class="nb"&gt;shopt&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; failglob
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; clips
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"before the loop"&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"uploading: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"after the loop, status &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;ls &lt;/span&gt;clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"after ls, status &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before the loop
./03_failglob.sh: line 6: no match: clips/*.mp4
after the loop, status 1
./03_failglob.sh: line 8: no match: clips/*.mp4
after ls, status 1
exit status: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both the loop and &lt;code&gt;ls&lt;/code&gt; were skipped, each with a &lt;code&gt;no match&lt;/code&gt; error naming the line, and &lt;code&gt;$?&lt;/code&gt; was 1 after each. The script itself kept going and exited 0. In this Bash, without &lt;code&gt;set -e&lt;/code&gt;, &lt;code&gt;failglob&lt;/code&gt; stopped the command and not the script.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;set -e&lt;/code&gt; added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# failglob together with set -e.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nb"&gt;shopt&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; failglob
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; clips
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"before the loop"&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"uploading: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"after the loop"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before the loop
./04_failglob_e.sh: line 7: no match: clips/*.mp4
exit status: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the script stopped at the loop and exited 1. That is a clear failure, which is what you want if an empty folder means something is wrong, and not what you want if an empty folder is a normal night. Whether a night with no clips is an error depends on the setup: a motion-triggered camera may record nothing on a quiet night, and one that records continuously should not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under /bin/sh
&lt;/h2&gt;

&lt;p&gt;A script run directly, by hand or from a crontab line, gets whatever interpreter its first line names, and plenty of scripts say &lt;code&gt;#!/bin/sh&lt;/code&gt;. On this machine that is dash, which has no &lt;code&gt;shopt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# The same loop under /bin/sh, which is dash on this machine.&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; clips
&lt;span class="nb"&gt;shopt&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; nullglob
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"would upload: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"uploading: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"guarded loop done"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./05_dash.sh: 4: shopt: not found
would upload: clips/*.mp4
guarded loop done
exit status: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;dash could not find &lt;code&gt;shopt&lt;/code&gt;, carried on, and the first loop ran once with the literal pattern, the same as Bash's default. A script that was tested under &lt;code&gt;bash script.sh&lt;/code&gt; and then installed with a &lt;code&gt;#!/bin/sh&lt;/code&gt; line loses its &lt;code&gt;nullglob&lt;/code&gt; line silently, apart from one error message.&lt;/p&gt;

&lt;p&gt;The second loop shows the portable guard: test &lt;code&gt;$f&lt;/code&gt; with &lt;code&gt;-e&lt;/code&gt; and &lt;code&gt;continue&lt;/code&gt; if it does not exist. If the word was left unchanged, there is no such file and the body is skipped. The &lt;code&gt;-e&lt;/code&gt; test is part of POSIX &lt;code&gt;test&lt;/code&gt;, so the guard does not depend on Bash. One limit I did not test: a real file whose name is literally &lt;code&gt;*.mp4&lt;/code&gt; would pass the guard, since it exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The version I would run
&lt;/h2&gt;

&lt;p&gt;For a Bash script, collect the matches into an array with &lt;code&gt;nullglob&lt;/code&gt; on, turn it straight back off, and decide what an empty array means before doing anything with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Collect matches into an array with nullglob on, then turn it back off&lt;/span&gt;
&lt;span class="c"&gt;# so no later command is changed by it. An empty night is reported.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;clips&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/upload
&lt;span class="nb"&gt;shopt&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; nullglob
&lt;span class="nv"&gt;clips&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;shopt&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; nullglob
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;clips&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"no clips in &lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt; tonight; nothing uploaded"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;3
&lt;span class="k"&gt;fi
for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;clips&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /tmp/upload/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"uploaded: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"clips uploaded tonight: &lt;/span&gt;&lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;clips&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a test of it on an empty folder, then on a folder with two clips, one with a space in its name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Run the job on an empty folder, then on a folder with two clips.&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; clips &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mkdir &lt;/span&gt;clips
./06_upload.sh clips&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 1024 /dev/urandom &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; clips/2026-11-03_0100.mp4
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 1024 /dev/urandom &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"clips/2026-11-03 0200.mp4"&lt;/span&gt;
./06_upload.sh clips&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;no clips in clips tonight; nothing uploaded
exit 3
uploaded: clips/2026-11-03 0200.mp4
uploaded: clips/2026-11-03_0100.mp4
clips uploaded tonight: 2
exit 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nullglob&lt;/code&gt; is on for one line.&lt;/strong&gt; It changes how the array is filled and nothing else, so &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;rm&lt;/code&gt; and &lt;code&gt;tar&lt;/code&gt; later in the script keep their usual behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The count is the length of the array&lt;/strong&gt;, so the number in the log and the files that were copied come from the same list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An empty folder is reported and exits with its own status (3 here)&lt;/strong&gt;, so a monitor can tell an empty night apart from a failed upload. If an empty folder is a normal night for your setup, change that branch to exit 0, but keep the message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every expansion is quoted&lt;/strong&gt;, and &lt;code&gt;cp --&lt;/code&gt; stops a name starting with a dash from being read as an option. The clip with a space in its name went through as one file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a &lt;code&gt;/bin/sh&lt;/code&gt; script, the alternative is to let &lt;code&gt;find&lt;/code&gt; do the matching. It treats a pattern with no matches as no output, not as a name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# For /bin/sh: let find do the matching. No match prints nothing.&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; clips
&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find clips &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.mp4'&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"clips found: &lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
find clips &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.mp4'&lt;/span&gt; &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; /tmp/upload/ &lt;span class="se"&gt;\;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"find exit: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clips found: 0
find exit: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero clips found, nothing copied, and &lt;code&gt;find&lt;/code&gt; exited 0. The count line runs &lt;code&gt;find&lt;/code&gt; a second time, so on a folder that is still being written to, the two runs could disagree; in a real job I would write the list to a file once and count and copy from that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would check in an existing job
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Search for unquoted patterns in &lt;code&gt;for&lt;/code&gt; loops and command arguments&lt;/strong&gt;, such as &lt;code&gt;for f in dir/*.mp4&lt;/code&gt; and &lt;code&gt;ls dir/*.log&lt;/code&gt;, and ask what each does on an empty folder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the job once against an empty folder on purpose.&lt;/strong&gt; That is the test that shows the literal pattern, and it takes a minute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the script sets &lt;code&gt;nullglob&lt;/code&gt;, look at every command that takes a glob as arguments&lt;/strong&gt;, especially &lt;code&gt;ls&lt;/code&gt;, which lists the current directory when given nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the first line.&lt;/strong&gt; A &lt;code&gt;#!/bin/sh&lt;/code&gt; script cannot use &lt;code&gt;shopt&lt;/code&gt;; use the &lt;code&gt;-e&lt;/code&gt; guard or &lt;code&gt;find&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide what an empty night means&lt;/strong&gt; and make the job say so with an exit status, instead of letting a count of one or zero stand in for that decision.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I did not test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Other shells.&lt;/strong&gt; This was Bash 5.1.16 and dash on Linux. zsh, ksh and BusyBox sh were not part of it, and I have not checked how each handles an unmatched pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newer Bash.&lt;/strong&gt; The online manual is for a later Bash than the one tested; it mentions settings such as &lt;code&gt;GLOBSORT&lt;/code&gt; that 5.1 does not have. I only quote the parts about unmatched patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dotglob&lt;/code&gt;, &lt;code&gt;nocaseglob&lt;/code&gt; and &lt;code&gt;globstar&lt;/code&gt;.&lt;/strong&gt; All change what a pattern matches; none was turned on here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Very large folders.&lt;/strong&gt; A pattern that matches many thousands of files can run into the system's argument length limit, which is a different failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; Quotes about unmatched patterns, &lt;code&gt;nullglob&lt;/code&gt;, &lt;code&gt;failglob&lt;/code&gt; and default settings are from the GNU Bash Reference Manual, &lt;a href="https://www.gnu.org/software/bash/manual/html_node/Filename-Expansion.html" rel="noopener noreferrer"&gt;Filename Expansion&lt;/a&gt; and &lt;a href="https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html" rel="noopener noreferrer"&gt;The Shopt Builtin&lt;/a&gt;. Outputs are pasted unedited from the scripts shown.&lt;/p&gt;

&lt;p&gt;If you want an old Android phone recording in the background so there is a folder of clips for a job like this to pick up, the app I make is &lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam" rel="noopener noreferrer"&gt;Background Camera RemoteStream on Google Play&lt;/a&gt;. Nothing above depends on it.&lt;/p&gt;

&lt;p&gt;By default, an empty night does not make the loop fail: it runs once on a file named &lt;code&gt;clips/*.mp4&lt;/code&gt; and counts it, so a night with nothing recorded reports the same total as a night with one clip.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>linux</category>
      <category>shell</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Does VOICE_RECOGNITION Make Breaking Glass Sound Duller? What a 4 kHz Ceiling Costs a Phone Camera's Sound Detection</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Wed, 04 Nov 2026 09:01:49 +0000</pubDate>
      <link>https://dev.to/superfunicular/does-voicerecognition-make-breaking-glass-sound-duller-what-a-4-khz-ceiling-costs-a-phone-28h1</link>
      <guid>https://dev.to/superfunicular/does-voicerecognition-make-breaking-glass-sound-duller-what-a-4-khz-ceiling-costs-a-phone-28h1</guid>
      <description>&lt;p&gt;Last week I wrote about &lt;a href="https://dev.to/superfunicular/why-does-my-phone-security-cameras-audio-hiss-in-a-quiet-room-android-audio-sources-automatic-1pmh"&gt;why a phone security camera's audio hisses in a quiet room&lt;/a&gt;. In short: automatic gain control (AGC) turns an empty room up. Of the audio sources a camera app would pick, &lt;code&gt;VOICE_RECOGNITION&lt;/code&gt; is the one with a written promise that AGC and noise reduction are off by default.&lt;/p&gt;

&lt;p&gt;Ahmed Znouda left a comment that deserves its own post. He asked two things:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Did you try VOICE_RECOGNITION for the camera case? The flat-response target only covers 100 Hz to 4 kHz, so I'd be curious whether a slamming door or breaking glass sounds duller than with UNPROCESSED."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He also suggested a check that costs almost nothing at runtime: create the effect on the recorder's session and read &lt;code&gt;getEnabled()&lt;/code&gt; before changing anything, then log the result per phone model.&lt;/p&gt;

&lt;p&gt;This post does both. First it tests his frequency question with a reproducible simulation. Then it turns his runtime check into a log line that also catches effects your code didn't create.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the compatibility rules promise, by frequency
&lt;/h2&gt;

&lt;p&gt;The two sources are specified differently. Both figures come from Android's Compatibility Definition (CDD):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Flatness the CDD asks for&lt;/th&gt;
&lt;th&gt;What it says above that band&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VOICE_RECOGNITION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;approximately flat, &lt;strong&gt;±3 dB from 100 Hz to 4000 Hz&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UNPROCESSED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;±10 dB from 100 Hz to 7000 Hz&lt;/strong&gt;, for every mic used&lt;/td&gt;
&lt;td&gt;within &lt;strong&gt;±30 dB from 7000 Hz to 22 kHz&lt;/strong&gt; of the mid band&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So Ahmed's reading is right, with one precise point. The CDD doesn't say a voice-recognition path &lt;em&gt;cuts&lt;/em&gt; above 4 kHz. It just doesn't promise anything there. A phone can pass compatibility with a voice path that rolls off steeply at 4 kHz, or with one that runs flat to 20 kHz. You can't know which from the spec.&lt;/p&gt;

&lt;p&gt;The useful question, then, is: &lt;strong&gt;if the phone does roll off at 4 kHz, what does that cost?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The simulation
&lt;/h2&gt;

&lt;p&gt;I built three synthetic events and put each into the same 30-second pink-noise room at the 10-second mark:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Door"&lt;/strong&gt;: brown noise, low-passed at 400 Hz, 0.4 s with a fade. Almost all of its energy is a low thump.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Glass (high)"&lt;/strong&gt;: white noise, high-passed twice at 3 kHz, 0.6 s with a fade. This is a deliberately unfavourable case: nearly all of its energy sits above the ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Glass (broad)"&lt;/strong&gt;: the same, but high-passed at 1 kHz, so part of its energy falls under 4 kHz.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not recordings of a real door or real glass. Real breaking glass has energy across a wide band, and how much sits above 4 kHz depends on the pane, the room and the microphone. The two glass versions bracket that range. They don't measure it.&lt;/p&gt;

&lt;p&gt;Then I ran each file through two stand-ins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 4 kHz ceiling&lt;/strong&gt;: four cascaded &lt;code&gt;lowpass=f=4000&lt;/code&gt; filters, which gives a steep roll-off. This is the &lt;em&gt;worst case&lt;/em&gt; the CDD allows for a voice path, not a description of any real phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AGC&lt;/strong&gt;: &lt;code&gt;dynaudnorm=f=500:g=31:m=30&lt;/code&gt;, the same stand-in as last week. It isn't any phone's AGC. It's a dynamic normaliser that raises quiet passages the way AGC does.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Room + one event at t=10 s (shown for the high glass; door and broad glass are built the same way)&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-f&lt;/span&gt; lavfi &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"anoisesrc=color=pink:amplitude=0.003:duration=30:sample_rate=48000:seed=7"&lt;/span&gt; room.wav
ffmpeg &lt;span class="nt"&gt;-f&lt;/span&gt; lavfi &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"anoisesrc=color=white:amplitude=0.5:duration=0.6:sample_rate=48000:seed=11"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="s2"&gt;"highpass=f=3000,highpass=f=3000,afade=t=out:st=0:d=0.6"&lt;/span&gt; glass.wav
ffmpeg &lt;span class="nt"&gt;-f&lt;/span&gt; lavfi &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"anoisesrc=color=brown:amplitude=0.9:duration=0.4:sample_rate=48000:seed=5"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="s2"&gt;"lowpass=f=400,afade=t=out:st=0:d=0.4"&lt;/span&gt; door.wav
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; room.wav &lt;span class="nt"&gt;-i&lt;/span&gt; glass.wav &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"[1]adelay=10000,apad=whole_len=1440000[e];[0][e]amix=inputs=2:normalize=0"&lt;/span&gt; &lt;span class="nt"&gt;-ac&lt;/span&gt; 1 mix_glass.wav

&lt;span class="c"&gt;# 4 kHz ceiling stand-in&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; mix_glass.wav &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="s2"&gt;"lowpass=f=4000,lowpass=f=4000,lowpass=f=4000,lowpass=f=4000"&lt;/span&gt; lp_glass.wav
&lt;span class="c"&gt;# AGC stand-in&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; mix_glass.wav &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="nv"&gt;dynaudnorm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;500:g&lt;span class="o"&gt;=&lt;/span&gt;31:m&lt;span class="o"&gt;=&lt;/span&gt;30 agc_mix_glass.wav

&lt;span class="c"&gt;# Levels: the event (10.0–10.4 s) and quiet room (12–21 s), RMS dBFS&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-ss&lt;/span&gt; 10 &lt;span class="nt"&gt;-t&lt;/span&gt; 0.4 &lt;span class="nt"&gt;-i&lt;/span&gt; lp_glass.wav &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="nv"&gt;astats&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;measure_perchannel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;none:measure_overall&lt;span class="o"&gt;=&lt;/span&gt;RMS_level &lt;span class="nt"&gt;-f&lt;/span&gt; null - 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"RMS level"&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-ss&lt;/span&gt; 12 &lt;span class="nt"&gt;-t&lt;/span&gt; 9   &lt;span class="nt"&gt;-i&lt;/span&gt; lp_glass.wav &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="nv"&gt;astats&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;measure_perchannel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;none:measure_overall&lt;span class="o"&gt;=&lt;/span&gt;RMS_level &lt;span class="nt"&gt;-f&lt;/span&gt; null - 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"RMS level"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Contrast&lt;/strong&gt; below is the event's RMS level minus the quiet room's RMS level. That gap is what a sound detector lives on. ffmpeg 4.4.2, run 2026-11-04.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Raw&lt;/th&gt;
&lt;th&gt;4 kHz ceiling only&lt;/th&gt;
&lt;th&gt;AGC stand-in only&lt;/th&gt;
&lt;th&gt;Both&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Door (low thump)&lt;/td&gt;
&lt;td&gt;43.4 dB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;45.5 dB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;28.5 dB&lt;/td&gt;
&lt;td&gt;30.5 dB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glass (broad, &amp;gt;1 kHz)&lt;/td&gt;
&lt;td&gt;50.4 dB&lt;/td&gt;
&lt;td&gt;40.1 dB&lt;/td&gt;
&lt;td&gt;31.7 dB&lt;/td&gt;
&lt;td&gt;29.7 dB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glass (high, &amp;gt;3 kHz)&lt;/td&gt;
&lt;td&gt;49.9 dB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30.6 dB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;31.4 dB&lt;/td&gt;
&lt;td&gt;26.0 dB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The event levels themselves show the same thing. Under the ceiling, the door's RMS didn't move (−21.3 → −21.3 dBFS). The high glass dropped 21.3 dB (−14.8 → −36.1). The broad glass dropped 12.4 dB (−14.2 → −26.6).&lt;/p&gt;

&lt;h2&gt;
  
  
  What that answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A slamming door: no, it doesn't get duller.&lt;/strong&gt; Its energy is under 4 kHz, so the ceiling takes nothing from it. Contrast actually rose 2.1 dB, because the ceiling also trimmed some of the room's hiss (the quiet floor fell from −64.7 to −66.7 dBFS). For low events, a voice-recognition path that keeps its no-AGC promise is a pure gain: you skip the ~15 dB the AGC stand-in costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaking glass: possibly, and by about as much as AGC costs.&lt;/strong&gt; In the unfavourable case, a hard 4 kHz ceiling cut the glass's contrast by 19.3 dB. The AGC stand-in cut it by 18.5 dB. So for a high-pitched event, a worst-case voice path doesn't remove the loss. It swaps one ~19 dB loss for another. With broader glass the ceiling cost 10.3 dB, about half as much.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both together is the worst row.&lt;/strong&gt; A phone that applies AGC &lt;em&gt;and&lt;/em&gt; rolls off high (which a camcorder-style path can do, since its tuning is vendor-specific) left the high glass at 26.0 dB.&lt;/p&gt;

&lt;p&gt;With the ceiling alone, a fixed −50 dB &lt;code&gt;silencedetect&lt;/code&gt; threshold (2 s minimum) still isolated every event: silence before 10 s, silence again right after the event. With the AGC stand-in, the room itself rose to about −42 to −45 dBFS, above that threshold. The detector then reported one long stretch of "activity" running from about 4–10 s to 27 s, with the event lost inside it. That's last week's finding again: a fixed threshold can't follow a room that AGC keeps moving. The ceiling shrinks the margin, and AGC moves the floor up into it. Put in a louder room, a fan, or a phone farther away, and the case that starts with the least margin is the one that drops out first.&lt;/p&gt;

&lt;p&gt;So there's no single best source for a camera. It depends on what you're listening for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Doors, footsteps, voices, knocks&lt;/strong&gt;: &lt;code&gt;VOICE_RECOGNITION&lt;/code&gt; is the safe choice. Everything that matters is under 4 kHz, and AGC is promised off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glass, alarms, smoke-detector chirps&lt;/strong&gt;: these sit high (a typical smoke alarm tone is around 3 kHz, with harmonics above it). &lt;code&gt;UNPROCESSED&lt;/code&gt;, where supported, is the source whose spec actually covers the upper band. Check &lt;code&gt;AudioManager.getProperty(AudioManager.PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED)&lt;/code&gt; first: it returns null when there's no support, and the source then behaves like &lt;code&gt;DEFAULT&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ahmed's runtime check, extended
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;audiofx&lt;/code&gt; docs say this directly: an AGC, noise suppressor or echo canceller "can be inserted by default in the capture path by the platform according to the &lt;code&gt;MediaRecorder.AudioSource&lt;/code&gt; used." The docs tell the app to create the effect and read its enabled state to learn the default for that session. That's Ahmed's check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Per-effect check (API 16+). null = this phone doesn't expose that effect through audiofx.&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;session&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;audioRecord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audioSessionId&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;agc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AutomaticGainControl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;ns&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NoiseSuppressor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;i&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"agcDefaultOn=${agc?.enabled} nsDefaultOn=${ns?.enabled}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;agc&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has one blind spot. It only answers for effect &lt;em&gt;types&lt;/em&gt; you think to ask about. Since API 29 there's a second view that lists whatever is running. &lt;code&gt;AudioRecord&lt;/code&gt; and &lt;code&gt;MediaRecorder&lt;/code&gt; both implement &lt;code&gt;AudioRecordingMonitor&lt;/code&gt;. Once capture has started, &lt;code&gt;getActiveRecordingConfiguration()&lt;/code&gt; returns an &lt;code&gt;AudioRecordingConfiguration&lt;/code&gt; with two lists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;getClientEffects()&lt;/code&gt;: the effects on &lt;em&gt;your&lt;/em&gt; capture client.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getEffects()&lt;/code&gt;: the effects on the capture &lt;em&gt;stream&lt;/em&gt;. Android's audio-sharing guide notes that these two can differ when another app is capturing at the same time with higher priority.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// API 29+. Call after start(); returns null before capture is active.&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;logCaptureEffects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AudioRecordingMonitor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;cfg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activeRecordingConfiguration&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AudioEffect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Descriptor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joinToString&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;AudioEffect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EFFECT_TYPE_AGC&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"AGC"&lt;/span&gt;
            &lt;span class="nc"&gt;AudioEffect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EFFECT_TYPE_NS&lt;/span&gt;  &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"NS"&lt;/span&gt;
            &lt;span class="nc"&gt;AudioEffect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EFFECT_TYPE_AEC&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"AEC"&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;ifEmpty&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;i&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"${Build.MANUFACTURER} ${Build.MODEL} source=${cfg.clientAudioSource} "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
               &lt;span class="s"&gt;"client=[${label(cfg.clientEffects)}] stream=[${label(cfg.effects)}]"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logged once per session, that gives you a per-model table of which source actually came with which processing. It's the measurement the spec can't give you. Two cautions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An empty list isn't proof of a flat path.&lt;/strong&gt; These lists report effects the framework knows about. A vendor can still tune gain or equalisation inside the HAL without listing it as an effect, so the descriptor list is a floor, not a ceiling. The ten-minute offline recording test from the &lt;a href="https://dev.to/superfunicular/why-does-my-phone-security-cameras-audio-hiss-in-a-quiet-room-android-audio-sources-automatic-1pmh"&gt;previous post&lt;/a&gt; is still the final check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;None of this tells you the frequency response.&lt;/strong&gt; For that, record one known high sound (a phone playing a 6 kHz tone works) with each source on the same device, and compare levels with &lt;code&gt;astats&lt;/code&gt; as above.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam" rel="noopener noreferrer"&gt;Background Camera RemoteStream&lt;/a&gt;, an Android app that turns a spare phone into a camera that keeps recording with the screen off. Recordings stay on the phone, there's a live view from any browser on the same Wi-Fi, and YouTube Live is there when you want an off-site copy. I'm not claiming a particular audio source for it here. The point of this post is that the right source depends on what you want to hear, and the behaviour depends on the phone. Both are things you measure, not things you assume.&lt;/p&gt;

&lt;p&gt;Thanks to Ahmed for a question that turned out to have a two-sided answer. More at &lt;a href="https://superfunicular.com" rel="noopener noreferrer"&gt;superfunicular.com&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AOSP — &lt;a href="https://source.android.com/docs/compatibility/12/android-12-cdd" rel="noopener noreferrer"&gt;Android 12 Compatibility Definition&lt;/a&gt; (§5.4 voice recognition ±3 dB 100–4000 Hz; §5.11 unprocessed ±10 dB 100–7000 Hz, ±30 dB 7000 Hz–22 kHz)&lt;/li&gt;
&lt;li&gt;Android Developers — &lt;a href="https://developer.android.com/reference/android/media/audiofx/NoiseSuppressor" rel="noopener noreferrer"&gt;NoiseSuppressor&lt;/a&gt;, &lt;a href="https://developer.android.com/reference/android/media/audiofx/AutomaticGainControl" rel="noopener noreferrer"&gt;AutomaticGainControl&lt;/a&gt;, &lt;a href="https://developer.android.com/reference/android/media/audiofx/AcousticEchoCanceler" rel="noopener noreferrer"&gt;AcousticEchoCanceler&lt;/a&gt; (default insertion by audio source; check enabled state after create)&lt;/li&gt;
&lt;li&gt;Android Developers — &lt;a href="https://developer.android.com/reference/android/media/AudioRecordingConfiguration" rel="noopener noreferrer"&gt;AudioRecordingConfiguration&lt;/a&gt; (&lt;code&gt;getEffects()&lt;/code&gt;, &lt;code&gt;getClientEffects()&lt;/code&gt;, API 29), &lt;a href="https://developer.android.com/reference/kotlin/android/media/AudioRecordingMonitor" rel="noopener noreferrer"&gt;AudioRecordingMonitor&lt;/a&gt;, &lt;a href="https://developer.android.com/media/platform/sharing-audio-input" rel="noopener noreferrer"&gt;Sharing audio input&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Simulation: ffmpeg 4.4.2 &lt;code&gt;anoisesrc&lt;/code&gt;, &lt;code&gt;lowpass&lt;/code&gt;, &lt;code&gt;highpass&lt;/code&gt;, &lt;code&gt;dynaudnorm&lt;/code&gt;, &lt;code&gt;astats&lt;/code&gt;, &lt;code&gt;silencedetect&lt;/code&gt;; commands above, run 2026-11-04&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>audio</category>
      <category>ffmpeg</category>
    </item>
    <item>
      <title>Why Does My Samsung Phone Camera Stop Recording at Night? Auto Restart, the 30% Battery Rule, and What to Turn Off</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Wed, 04 Nov 2026 08:12:55 +0000</pubDate>
      <link>https://dev.to/superfunicular/why-does-my-samsung-phone-camera-stop-recording-at-night-auto-restart-the-30-battery-rule-and-2nhh</link>
      <guid>https://dev.to/superfunicular/why-does-my-samsung-phone-camera-stop-recording-at-night-auto-restart-the-30-battery-rule-and-2nhh</guid>
      <description>&lt;p&gt;You set up an old Samsung Galaxy as a security camera. It is plugged in, the screen is off, it is recording. You check it in the morning and the recording stopped somewhere around 3 a.m. Nothing crashed. There was no power cut. The phone simply restarted itself, and after the restart nothing was recording.&lt;/p&gt;

&lt;p&gt;On many Galaxy phones this is not a fault. It is a setting, and Samsung documents it. This guide explains what the setting does, why a camera phone is exactly the kind of phone it picks, and how to check and turn it off on your own phone in about two minutes. It is written for a simple setup: one old Android phone, no cloud account, no subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does my Samsung phone restart by itself at night?
&lt;/h2&gt;

&lt;p&gt;Most likely because of &lt;strong&gt;Auto restart&lt;/strong&gt;, a Samsung maintenance feature that restarts the phone on a schedule or "when needed." Samsung's own support pages describe it in two places, depending on the phone model and One UI version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Settings → Battery and device care → ⋮ (More options) → Automation → Auto restart at set times.&lt;/strong&gt; Samsung's Philippines support page says: "Your phone will restart automatically within 1 hour of the time you set."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settings → Device care → Auto optimisation → Auto restart&lt;/strong&gt;, with two switches: &lt;strong&gt;Restart when needed&lt;/strong&gt; and &lt;strong&gt;Restart on schedule&lt;/strong&gt;. Samsung's Hong Kong support page (updated 8 September 2026) says Restart when needed will restart the device "when memory usage increases or if it has not been restarted for a period of time."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Samsung also says on both pages that menus "may vary depending on the device model and software version." So on your phone the names may be slightly different. The search box at the top of Settings is the quickest way to find it: type &lt;strong&gt;restart&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a camera phone is the perfect target for Auto restart
&lt;/h2&gt;

&lt;p&gt;Samsung lists the conditions under which the phone will restart itself. From the Philippines support page, the phone will only restart when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"The screen is off."&lt;/li&gt;
&lt;li&gt;"The phone isn't being used."&lt;/li&gt;
&lt;li&gt;"The battery is above 30%."&lt;/li&gt;
&lt;li&gt;"SIM card lock is off."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Hong Kong page says the same thing in shorter form: "By default, the screen must be off and the battery level must be at least 30%."&lt;/p&gt;

&lt;p&gt;Now look at a phone doing camera duty:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Samsung's condition&lt;/th&gt;
&lt;th&gt;A normal phone at 3 a.m.&lt;/th&gt;
&lt;th&gt;An old phone used as a camera&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Screen is off&lt;/td&gt;
&lt;td&gt;Usually&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Always&lt;/strong&gt; — screen-off recording is the point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Battery above 30%&lt;/td&gt;
&lt;td&gt;Sometimes&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Always&lt;/strong&gt; — it is on the charger 24/7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIM card lock off&lt;/td&gt;
&lt;td&gt;Usually&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Almost always&lt;/strong&gt; — many camera phones have no SIM at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phone isn't being used&lt;/td&gt;
&lt;td&gt;Usually&lt;/td&gt;
&lt;td&gt;Unclear — see below&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first three conditions are not "sometimes true" for a camera phone. They are true by design, every night. The battery rule is the interesting one: the 30% condition protects a phone that is running low, but a camera phone that never leaves the charger never falls below it. The setup that keeps your camera reliable (plugged in, screen off) is the same setup that makes it eligible for an automatic restart.&lt;/p&gt;

&lt;p&gt;The fourth condition, "the phone isn't being used," is the one nobody can answer for you. Samsung does not define it on these pages. A recording app running in the background with the screen off might count as "use," or it might not, and it may differ between phones and software versions. I am not going to guess. The honest answer is: &lt;strong&gt;test your own phone&lt;/strong&gt;, which is the last section of this guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually breaks after the restart
&lt;/h2&gt;

&lt;p&gt;A restart takes a minute or two. Losing a minute of footage is not the real problem. The real problem is what the phone is like &lt;strong&gt;after&lt;/strong&gt; it comes back.&lt;/p&gt;

&lt;p&gt;Samsung says it plainly on the Hong Kong page, under Restart when needed: "You must unlock your device to check messages and notifications normally after it restarts."&lt;/p&gt;

&lt;p&gt;That sentence is about messages, but the same rule applies to a camera. After any restart, Android starts in a locked state until someone enters the PIN, pattern or password. Android's developer documentation calls this Direct Boot mode and says: "By default, apps don't run during Direct Boot mode." An app only runs in that state if its developer built it to. So in an empty house or a closed shop, the phone may sit on the lock screen for days, waiting for a finger that is not coming.&lt;/p&gt;

&lt;p&gt;We went through that state in detail, with Google's exact wording, in &lt;a href="https://dev.to/superfunicular/the-restart-nobody-scheduled-direct-boot-and-what-an-unattended-android-camera-cannot-do-before-dek"&gt;The Restart Nobody Scheduled: Direct Boot, and What an Unattended Android Camera Cannot Do&lt;/a&gt;. This guide is the Samsung-specific half: the restart that Samsung schedules for you, and the switch that turns it off.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to turn off Auto restart on a Samsung camera phone (step by step)
&lt;/h2&gt;

&lt;p&gt;Do this on the camera phone itself, while you are standing next to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Newer menu (Battery and device care):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Settings&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Battery and device care&lt;/strong&gt; (on some phones just &lt;strong&gt;Device care&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Tap the &lt;strong&gt;three dots&lt;/strong&gt; at the top right.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Automation&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Auto restart at set times&lt;/strong&gt; and turn it &lt;strong&gt;off&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;While you are in Automation, look at the other items too. If there is an &lt;strong&gt;Auto optimise daily&lt;/strong&gt; or similar option, read what it says it does before leaving it on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Older or regional menu (Auto optimisation):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Settings → Device care&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Auto optimisation&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Auto restart&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Turn off &lt;strong&gt;Restart when needed&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Turn off &lt;strong&gt;Restart on schedule&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Cannot find it?&lt;/strong&gt; Use the search box at the top of Settings and type &lt;strong&gt;restart&lt;/strong&gt; or &lt;strong&gt;optimis&lt;/strong&gt;. The menu names change between One UI versions, but the search usually finds them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning it off does not stop every restart
&lt;/h2&gt;

&lt;p&gt;Samsung is clear that the switches do not cover everything. From the Hong Kong page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Even if both Restart when needed and Restart on schedule are turned off, a system restart may still occur depending on your device's condition."&lt;/li&gt;
&lt;li&gt;"Emergency security updates may restart the device automatically even when automatic software download settings are turned off, depending on your country or region."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;App updates from Google Play are a separate path again. An update can close a running app, and whether it comes back is up to the app. That is covered in &lt;a href="https://dev.to/superfunicular/the-update-you-did-not-schedule-what-google-play-does-to-a-running-camera-phone-and-which-switch-1f6l"&gt;The Update You Did Not Schedule: What Google Play Does to a Running Camera Phone&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So treat "Auto restart off" as removing the &lt;strong&gt;scheduled, every-night&lt;/strong&gt; restart, not as a guarantee that the phone never restarts. The goal is to remove the restart that happens on purpose, and then know what your phone does after the ones you cannot remove.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to keep a scheduled restart
&lt;/h2&gt;

&lt;p&gt;Samsung recommends regular restarts to stop a phone getting slow, and on a very old phone that advice may be sensible. If you want to keep it, make it work for you instead of against you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick a time when you are usually there.&lt;/strong&gt; For a shop camera, a time just after you open in the morning. For a home camera, a time when someone is normally awake. Then the person who is there can unlock the phone and check the camera is running again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remember the one-hour window.&lt;/strong&gt; Samsung says the restart happens "within 1 hour of the time you set," not exactly at that time. If you set 9:00, expect anything from 9:00 to 10:00.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use weekly, not daily.&lt;/strong&gt; If your menu lets you choose days, one restart a week is one gap a week instead of seven.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn it off before you travel.&lt;/strong&gt; A locked house for a week, with nobody to unlock the phone, is the worst case. (Our &lt;a href="https://dev.to/superfunicular/going-home-for-diwali-how-to-leave-an-old-phone-watching-a-locked-house-for-a-week-power-5egg"&gt;Diwali travel guide&lt;/a&gt; has the rest of that checklist.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 10-minute test: what does your camera do after a restart?
&lt;/h2&gt;

&lt;p&gt;This is the part that answers the question Samsung's pages leave open. Do it once, on the actual camera phone, with the actual app you use.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start recording&lt;/strong&gt; the normal way and turn the screen off.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Note the time.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restart the phone yourself&lt;/strong&gt; (hold the side key, or pull down the quick panel and tap the power icon, then Restart).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not unlock it.&lt;/strong&gt; Leave it on the lock screen for 5 minutes. This copies what happens at 3 a.m.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check from another device.&lt;/strong&gt; If your app has a live view over Wi-Fi, try to open it from a second phone or laptop on the same Wi-Fi.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Now unlock the phone&lt;/strong&gt; and look at the recordings. Is there a new file that started after the restart? Or does the last file end at the time you noted?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write down the result.&lt;/strong&gt; "Recording came back without unlocking", "came back only after unlocking", or "did not come back until I opened the app". All three are possible, and they depend on the app and the phone, not on you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the answer is anything except the first one, the setting in this guide matters a lot for your setup, and so does who can reach the phone to unlock it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist for a Samsung camera phone
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Auto restart at set times: &lt;strong&gt;off&lt;/strong&gt; (or set to a time when someone is there)&lt;/li&gt;
&lt;li&gt;[ ] Restart when needed / Restart on schedule: &lt;strong&gt;off&lt;/strong&gt;, if your phone has them&lt;/li&gt;
&lt;li&gt;[ ] Restart test done; you know what your camera app does after a restart&lt;/li&gt;
&lt;li&gt;[ ] Battery checked, so a power cut does not also become a restart (&lt;a href="https://dev.to/superfunicular/how-to-check-an-old-phones-battery-before-using-it-as-a-security-camera-your-backup-in-a-power-541h"&gt;how to check an old phone's battery&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Play Store auto-update decision made for the camera app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are setting up a phone camera for the first time, start with &lt;a href="https://dev.to/superfunicular/turn-an-old-android-phone-into-a-screen-off-security-camera-no-cloud-lan-only-5cll"&gt;Turn an old Android phone into a screen-off security camera (no cloud, LAN-only)&lt;/a&gt;, then come back to this checklist before you leave the phone alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Samsung Philippines support — &lt;a href="https://www.samsung.com/ph/support/mobile-devices/restart-your-galaxy-phone-regularly-to-prevent-it-from-slowing-down-or-freezing/" rel="noopener noreferrer"&gt;Restart your Galaxy phone regularly to prevent it from slowing down or freezing&lt;/a&gt; (Auto restart at set times, the one-hour window, the four conditions)&lt;/li&gt;
&lt;li&gt;Samsung Hong Kong support — &lt;a href="https://www.samsung.com/hk_en/support/mobile-devices/samsung-galaxy-device-may-appear-to-keep-restarting/" rel="noopener noreferrer"&gt;What to do if your Samsung Galaxy device appears to keep restarting&lt;/a&gt; (Auto optimisation, Restart when needed, Restart on schedule, restarts that still happen)&lt;/li&gt;
&lt;li&gt;Android Developers — &lt;a href="https://developer.android.com/privacy-and-security/direct-boot" rel="noopener noreferrer"&gt;Support Direct Boot mode&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Background Camera RemoteStream&lt;/strong&gt; turns an Android phone into a screen-off camera: it records with the screen off and keeps recordings on the phone, with no cloud account needed. Its Pro tier adds a live browser view over your own Wi-Fi and YouTube Live streaming; check the Play listing for what is free and what is Pro.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.superfunicular.digicam&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Website: &lt;a href="https://superfunicular.com" rel="noopener noreferrer"&gt;https://superfunicular.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>security</category>
      <category>privacy</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>UK Law Makes Phone Makers Publish When Security Updates End. One Statement I Read Ran Out in February 2025</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Tue, 03 Nov 2026 13:55:13 +0000</pubDate>
      <link>https://dev.to/superfunicular/uk-law-makes-phone-makers-publish-when-security-updates-end-one-statement-i-read-ran-out-in-42hd</link>
      <guid>https://dev.to/superfunicular/uk-law-makes-phone-makers-publish-when-security-updates-end-one-statement-i-read-ran-out-in-42hd</guid>
      <description>&lt;p&gt;Before a phone from the drawer gets a second job pointed at a door, somebody usually asks whether it is too old. That question tends to get answered by feel: the battery seems fine, the screen has a crack, it felt slow last year. There is a better input than feel for one part of it, and in the UK it has been a legal requirement since spring 2024. The maker has to write down how long the phone will get security updates, and publish it.&lt;/p&gt;

&lt;p&gt;I went and read the law, then read two of the documents it produced. One of them ended in February 2025. The other does not contain a calendar date at all. Both are worth seeing before you decide what an old phone should be trusted with.&lt;/p&gt;

&lt;p&gt;None of this is legal advice and I am not a lawyer. It is a close reading of a short statutory instrument and two manufacturer statements, aimed at people who are about to repurpose a handset.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the law asks a phone maker for
&lt;/h2&gt;

&lt;p&gt;The rules come from the Product Security and Telecommunications Infrastructure Act 2022 and a set of regulations made under it in 2023. The government's guidance page puts the start date plainly: "The UK’s consumer connectable product security regime came into effect on 29 April 2024."&lt;/p&gt;

&lt;p&gt;The same page lists the three security requirements in a line each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"banning universal default and easily guessable passwords"&lt;/li&gt;
&lt;li&gt;"publishing information on how to report security issues"&lt;/li&gt;
&lt;li&gt;"publishing information on minimum security update periods"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Phones are inside the regime. The exclusions on the guidance page are narrow, and the computing one reads "desktop computers, laptop computers and tablet computers which do not have the capability to connect to cellular networks". A handset with a SIM slot is none of those.&lt;/p&gt;

&lt;p&gt;The third requirement is the one that matters for a repurposed phone, and the regulations define its key term carefully. A "defined support period" means "the minimum length of time, expressed as a period of time with an end date, for which security updates will be provided".&lt;/p&gt;

&lt;p&gt;Three more sentences in the same schedule do most of the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"The defined support period must be published."&lt;/li&gt;
&lt;li&gt;It has to be written "in such a way that is understandable by a reader without prior technical knowledge."&lt;/li&gt;
&lt;li&gt;"The security requirements in this paragraph are not met if the defined support period is shortened after the publication of the information in sub-paragraph (2)."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the number can go up after publication but not down. The document that carries it is called a statement of compliance, and the regulations say it must include "the defined support period for the product that was correct when the manufacturer first supplied the product;".&lt;/p&gt;

&lt;p&gt;That is the promise in the abstract. Here is what it looks like on two real handsets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Statement one: a date that has already passed
&lt;/h2&gt;

&lt;p&gt;OPPO publishes its UK statements as PDFs. The one for model CPH2371, filed under the Find X5 Lite name, lists "Product Type: Mobile Phone" and then, on the line that matters, "Defined Support Period: 3 years (until 01-February-2025)", followed by an address where any update would be published.&lt;/p&gt;

&lt;p&gt;The statement is signed and dated "2024-04-12". I ran the arithmetic: from the signature to the end of the period is 295 days, a little under ten months. From the day the regime started to the end of the period is 278 days.&lt;/p&gt;

&lt;p&gt;None of that is a criticism. This is a model that went on sale years before the law existed, and the statement does what the law asks: it names a period and a calendar end date, and says where an extension would appear. What it shows is the situation a repurposing project is usually in. The handsets people hand down are, by construction, the old ones, and an old handset's published support period may be finished before anybody thinks to look. When I read it in November 2026, that end date was 640 days in the past.&lt;/p&gt;

&lt;h2&gt;
  
  
  Statement two: a period with no calendar date in it
&lt;/h2&gt;

&lt;p&gt;Google publishes its UK statements as help-centre pages, one per Pixel model. The statement for the Pixel 10a gives its UK defined support period as "At least seven years from when the device first became available on the Google Store in the US".&lt;/p&gt;

&lt;p&gt;The page is dated "November 7, 2025". The period is generous. What it does not contain is a date you can copy down. To get one you need a second fact, the day the model first went on sale in the United States, which Google points to on a separate page, and then you do the addition yourself.&lt;/p&gt;

&lt;p&gt;Whether a relative period like that satisfies "expressed as a period of time with an end date" is for the regulator to decide, not me. The practical point for someone holding the handset is simpler: two manufacturers answered the same legal question in two different shapes, and one of the shapes needs a lookup and a sum before it tells you anything.&lt;/p&gt;

&lt;p&gt;The same page also carries a scope note worth reading twice: the statement "is only applicable to products listed above where sold in the UK." Outside the UK, the number is still the best public signal of the maker's intent for that model. It is not a UK-law commitment to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the published date covers, and what it leaves out
&lt;/h2&gt;

&lt;p&gt;Reading the schedule closely, the security update requirement applies to the phone's hardware and to software pre-installed on it at the point of supply, plus software that has to be installed for the product to do what the manufacturer intends. Then there is a carve-out written specifically for phones. The broadest category of covered software is "software used for, or in connection with, any manufacturer’s intended purpose of the product unless the product is a smartphone or a tablet computer capable of connecting to cellular networks."&lt;/p&gt;

&lt;p&gt;My reading, which is a reading and nothing firmer: for most connected gadgets, companion software the maker supplies is pulled into scope. For a smartphone, that wider category is switched off, and the commitment stays with the phone itself and what ships on it.&lt;/p&gt;

&lt;p&gt;Either way, a camera app you download onto an old phone was not made by the phone's manufacturer and was not installed when the phone was supplied. The phone's support date says nothing about it. If you want to know how long an app will be maintained, that is a separate question for whoever publishes the app.&lt;/p&gt;

&lt;p&gt;Three more gaps, stated plainly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Handsets sold before the start date.&lt;/strong&gt; The regulations "come into force on 29th April 2024". A phone sold new before that was not required to arrive with a statement, although, as the OPPO example shows, makers have published statements for older models that were still on sale afterwards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phones bought outside the UK.&lt;/strong&gt; The Google note above limits the statement to UK sales.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Second-hand sales.&lt;/strong&gt; The guidance page frames the regime around people "who purchase new connectable products". I did not find text in the documents I read that settles whether a resold handset carries any obligation, so I am leaving that as an open question.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The password rule, briefly
&lt;/h2&gt;

&lt;p&gt;The first requirement is shorter and less useful to someone repurposing a phone, but it explains a habit worth keeping. The regulations say passwords must be "unique per product; or" "defined by the user of the product.", and passwords that are unique per product cannot be "based on incremental counters;" or "based on or derived from publicly available information;".&lt;/p&gt;

&lt;p&gt;That rule binds the hardware maker, and it applies to "hardware of the product when that product is not in the factory default state;". It does not reach a camera app you install afterwards. The habit it encodes travels well anyway: any starting password that came printed in a manual or a setup guide is, by definition, publicly available information. Before a repurposed phone streams anything, the login in front of that stream should be one you chose.&lt;/p&gt;

&lt;h2&gt;
  
  
  A check that takes about five minutes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find the model number on the handset itself.&lt;/strong&gt; It is usually under Settings, then About phone. Marketing names are often shared across several model numbers, and the statements are filed by model number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search for the maker's UK statement of compliance for that model.&lt;/strong&gt; The phrase &lt;em&gt;PSTI statement of compliance&lt;/em&gt; plus the model number is what both documents above are titled around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the defined support period.&lt;/strong&gt; If it is a calendar date, copy it. If it is a period counted from a launch, find the launch date the statement refers to and do the sum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the date on the phone.&lt;/strong&gt; A strip of tape on the back is enough. It is the one fact about this handset that will not change by feel, and the person who picks it up in two years will not remember where you found it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the date has passed, narrow the job.&lt;/strong&gt; A phone past its published support period still powers on and still records. What it no longer has is a public promise of security fixes. That argues for a smaller job: one task, on a network you control, signed in to as little as possible, with nothing on it you would mind losing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you cannot find a statement at all,&lt;/strong&gt; treat the support period as unknown rather than generous. Unknown is an honest entry. A guess written on the back of the phone is not.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I could not establish
&lt;/h2&gt;

&lt;p&gt;I could not settle the second-hand question from the documents I read, and I would rather leave a visible gap than fill it with a guess. I also have not checked whether OPPO published an extension for CPH2371 at the address its statement points to. The statement allows for one, and if it exists, the date in this piece is out of date for that model.&lt;/p&gt;

&lt;p&gt;What is settled is narrower and more useful. For phones sold new in the UK since April 2024, the end of security support is a published fact with a legal floor under it. For a handset about to start a second career, reading that fact takes less time than charging the battery.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam&amp;amp;referrer=utm_source%3Ddevto%26utm_medium%3Darticle%26utm_campaign%3D2026w45" rel="noopener noreferrer"&gt;Background Camera RemoteStream on Google Play&lt;/a&gt; records with the screen off and keeps the footage on the phone. Setup notes and other write-ups are at &lt;a href="https://superfunicular.com" rel="noopener noreferrer"&gt;superfunicular.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; &lt;a href="https://www.legislation.gov.uk/uksi/2023/1007/made/data.akn" rel="noopener noreferrer"&gt;The PSTI security requirements regulations on legislation.gov.uk&lt;/a&gt;, &lt;a href="https://www.gov.uk/guidance/regulations-consumer-connectable-product-security" rel="noopener noreferrer"&gt;the UK government guidance on consumer connectable product security&lt;/a&gt;, &lt;a href="https://www.oppo.com/content/dam/oppo/uk/static/certification/Find%20X5%20Lite%28CPH2371%29_SoC.pdf" rel="noopener noreferrer"&gt;OPPO's UK statement of compliance for model CPH2371&lt;/a&gt; and &lt;a href="https://support.google.com/product-documentation/answer/16879349" rel="noopener noreferrer"&gt;Google's UK statement of compliance for the Pixel 10a&lt;/a&gt;. Quotations are verbatim from those pages as fetched on 3 November 2026. The handset cannot tell you when its maker stops looking after it, but in the UK the maker now has to say so in writing.&lt;/p&gt;

</description>
      <category>android</category>
      <category>security</category>
      <category>privacy</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Going Home for Diwali? How to Leave an Old Phone Watching a Locked House for a Week: Power, Storage, and an Offline Security Camera Without Internet</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Tue, 03 Nov 2026 08:14:14 +0000</pubDate>
      <link>https://dev.to/superfunicular/going-home-for-diwali-how-to-leave-an-old-phone-watching-a-locked-house-for-a-week-power-5egg</link>
      <guid>https://dev.to/superfunicular/going-home-for-diwali-how-to-leave-an-old-phone-watching-a-locked-house-for-a-week-power-5egg</guid>
      <description>&lt;p&gt;Diwali falls on &lt;strong&gt;Sunday, 8 November&lt;/strong&gt; this year, and a lot of families will lock the house a few days before and travel home. The same thing happens for weddings, for Eid, and for any long visit to family. The house sits empty for a week, and somebody says: "Can we leave the old phone in the drawer as a camera?"&lt;/p&gt;

&lt;p&gt;You can. But a week is not one night, and the habits people have when they lock up a house can quietly stop the camera on the first day. The biggest one is the main switch.&lt;/p&gt;

&lt;p&gt;This guide is for a simple setup: one old Android phone, no subscription, no cloud account, and a house that will be empty for five to ten days. It is written in plain steps, with the arithmetic shown so you can check it for your own phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can an old phone work as an offline security camera while you travel for Diwali?
&lt;/h2&gt;

&lt;p&gt;Yes, if you plan for three things before you leave: &lt;strong&gt;power, storage, and how (or whether) you will watch it from far away.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Recording&lt;/strong&gt; works with no internet at all. An app like Background Camera RemoteStream records with the screen off and keeps the video on the phone itself. Nothing needs to be uploaded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watching live from another city&lt;/strong&gt; needs an internet connection &lt;em&gt;at the house&lt;/em&gt;, because the video has to leave the house somehow. If the Wi-Fi router is off, there is no remote view, whatever app you use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A whole week&lt;/strong&gt; is a long time for a phone's storage and battery. Most problems with holiday camera setups are not camera problems. They are a full storage chip or a phone that ran on battery until it died.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest of this guide takes those one by one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main switch problem: do not turn off the circuit the camera needs
&lt;/h2&gt;

&lt;p&gt;Many families switch off the main switch (MCB) before a long trip. It is a sensible habit for safety and for the electricity bill. But think about what it does to the camera:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The charger loses power. The phone starts running on battery.&lt;/li&gt;
&lt;li&gt;The Wi-Fi router turns off too. Any live view stops at that moment.&lt;/li&gt;
&lt;li&gt;The phone keeps recording until the battery is empty, and then it stops. For an old battery that can be a few hours, not a week.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the camera you set up so carefully may record the family loading the car, and then nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do instead:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Leave one circuit on&lt;/strong&gt;, the one with the socket for the phone charger (and the router, if you want live view). Many homes have separate MCBs for different rooms. Find out which one feeds the socket you will use, and switch off only the others.&lt;/li&gt;
&lt;li&gt;If your home has only one main switch, decide honestly: either leave it on, or accept that the camera is a "first few hours only" camera.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the phone's original charger or a good branded one&lt;/strong&gt;, and put the phone on a hard, open surface, not on a bed, sofa, or near curtains. During Diwali, keep it well away from where diyas and candles were placed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the battery before you trust it as a backup.&lt;/strong&gt; Power cuts happen in an empty house too. If the battery is weak, a short cut can be enough to stop recording. Our guide &lt;a href="https://dev.to/superfunicular/how-to-check-an-old-phones-battery-before-using-it-as-a-security-camera-your-backup-in-a-power-541h"&gt;How to Check an Old Phone's Battery Before Using It as a Security Camera&lt;/a&gt; shows how to test it, including a simple timing test you can do tonight.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Storage: a week of non-stop video does not fit on most old phones
&lt;/h2&gt;

&lt;p&gt;This is the part people do not calculate. Video size is easy to work out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GB per day = bitrate in Mbps × 86,400 seconds ÷ 8 ÷ 1,000&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Video bitrate&lt;/th&gt;
&lt;th&gt;Per day&lt;/th&gt;
&lt;th&gt;For 7 days&lt;/th&gt;
&lt;th&gt;For 10 days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 Mbps&lt;/td&gt;
&lt;td&gt;10.8 GB&lt;/td&gt;
&lt;td&gt;75.6 GB&lt;/td&gt;
&lt;td&gt;108 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 Mbps&lt;/td&gt;
&lt;td&gt;21.6 GB&lt;/td&gt;
&lt;td&gt;151.2 GB&lt;/td&gt;
&lt;td&gt;216 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 Mbps&lt;/td&gt;
&lt;td&gt;43.2 GB&lt;/td&gt;
&lt;td&gt;302.4 GB&lt;/td&gt;
&lt;td&gt;432 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(These are example bitrates for illustration, not the settings of any particular phone or app. Your phone's actual bitrate depends on resolution, the camera, and what is in the picture.)&lt;/p&gt;

&lt;p&gt;An old budget phone often has 32 GB or 64 GB in total, and much of that is already used by the system and apps. Even at a low 1 Mbps, a full week of non-stop recording needs about 76 GB. It will not fit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check free space today.&lt;/strong&gt; Go to &lt;strong&gt;Settings → Storage&lt;/strong&gt;. Write the number down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete what you don't need on that phone&lt;/strong&gt; (old WhatsApp videos, downloads) or move it to a computer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose the lowest resolution that still shows what you care about.&lt;/strong&gt; For a front door 3 metres away, you need to see a person and their clothes, not read a newspaper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide what happens when storage is full.&lt;/strong&gt; Either the phone stops recording, or old video is deleted to make room. Check how your setup behaves &lt;em&gt;before&lt;/em&gt; you leave, not after you come back. &lt;a href="https://dev.to/superfunicular/storage-not-heat-is-the-real-limit-on-247-android-recording-segment-rotation-retention-math-2j5"&gt;Storage, Not Heat, Is the Real Limit on 24/7 Android Recording&lt;/a&gt; explains the full storage arithmetic and why recording in many short files is safer than one long file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do a 24-hour test.&lt;/strong&gt; Record for one full day before the trip. Then look at how many GB were used. Multiply by the number of days you will be away. That number, from your own phone, is better than any table.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Watching from far away: what needs internet and what does not
&lt;/h2&gt;

&lt;p&gt;Be clear about what you want:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I only want proof of what happened."&lt;/strong&gt; Then you need no internet at all. Record locally, come home, check the footage. This is the simplest and most reliable setup, and it costs nothing every month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I want to look in while I am away."&lt;/strong&gt; Then the house needs an internet connection that stays on: either the broadband router (on a circuit you leave on), or a SIM with mobile data.&lt;/p&gt;

&lt;p&gt;With Background Camera RemoteStream, the live browser view works over your own Wi-Fi, so it is for when you are &lt;em&gt;at home&lt;/em&gt; or on the same network. To watch from another city, the app can stream to &lt;strong&gt;YouTube Live&lt;/strong&gt;, which needs internet at the house. (Both the browser view and YouTube Live are listed in the app's Pro tier; check the Play listing for the current free and Pro split.)&lt;/p&gt;

&lt;p&gt;Two numbers from YouTube's own help pages matter for a week away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data use.&lt;/strong&gt; YouTube's &lt;a href="https://support.google.com/youtube/answer/2853702?hl=en" rel="noopener noreferrer"&gt;live encoder settings page&lt;/a&gt; gives a &lt;em&gt;minimum&lt;/em&gt; of 0.4 Mbps for H.264 video at 360p or 480p. That is about 180 MB per hour (0.4 × 3,600 ÷ 8), or about 4.3 GB per day, before audio. A typical daily mobile data pack will not cover 24 hours of streaming. Stream for the hours that matter to you, such as night time, or rely on broadband. Our article on &lt;a href="https://dev.to/superfunicular/will-an-old-3g-phone-work-as-a-security-camera-on-mobile-data-what-the-e-h-and-4g-icons-mean-for-13o6"&gt;old 3G phones and mobile data&lt;/a&gt; has the full data-pack arithmetic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 12-hour archive limit.&lt;/strong&gt; YouTube's &lt;a href="https://support.google.com/youtube/answer/6247592?hl=en" rel="noopener noreferrer"&gt;archive help page&lt;/a&gt; says streams under 12 hours can be archived automatically, and a stream that goes past 12 hours "may not be captured at all." So one long stream for the whole week is not a backup. Keep the local recording on, and treat the live stream as a window, not a storage system.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Placement: where to put the phone in a locked house
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Point it at the way in.&lt;/strong&gt; The main door, the gate, or the back door that people forget. One phone watching the entry is more useful than one phone watching a whole room.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not put it where it can be seen from outside.&lt;/strong&gt; A phone visible through a window is itself something worth stealing. If someone does take it, the recording goes with it. Our guide &lt;a href="https://dev.to/superfunicular/what-if-someone-steals-the-phone-that-is-your-shop-camera-androids-new-theft-locks-mostly-skip-3bbi"&gt;What If Someone Steals the Phone That Is Your Shop Camera?&lt;/a&gt; covers what Android's theft protection does and does not do on budget phones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Think about light at night.&lt;/strong&gt; A locked house with every light off is very dark. If you leave one small light on (on the circuit you keep on), the camera will see much more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen off.&lt;/strong&gt; The screen is the part that makes heat and draws the most battery. Recording with the screen off is the whole point.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check the date and time before you leave
&lt;/h2&gt;

&lt;p&gt;If the phone has no SIM and the Wi-Fi is off, it cannot correct its own clock. After a power cut that empties the battery, an old phone may come back with the wrong date. Then the footage from "Tuesday night" is labelled with some other date. Check &lt;strong&gt;Settings → Date &amp;amp; time&lt;/strong&gt; against another phone before you leave, and read &lt;a href="https://dev.to/superfunicular/your-offline-security-camera-is-guessing-what-time-it-is-how-to-check-it-before-you-need-the-10ii"&gt;Your Offline Security Camera Is Guessing What Time It Is&lt;/a&gt; if the phone will be offline the whole time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tell one neighbour
&lt;/h2&gt;

&lt;p&gt;A camera records. It does not call anyone. Tell one trusted neighbour that you are away, ask them to look at the house once a day, and give them a number to call. If something happens, a person who can walk to the door in five minutes is worth more than any footage. &lt;a href="https://dev.to/superfunicular/two-keyholders-twenty-minutes-and-holiday-cover-the-half-of-a-security-setup-that-has-no-eab"&gt;Two Keyholders, Twenty Minutes, and Holiday Cover&lt;/a&gt; is about exactly this part of the plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The night-before checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] The circuit for the charger socket stays &lt;strong&gt;on&lt;/strong&gt; (and the router's, if you want live view)&lt;/li&gt;
&lt;li&gt;[ ] Phone on a hard surface, good charger, away from curtains and diyas&lt;/li&gt;
&lt;li&gt;[ ] Battery tested; you know roughly how long it lasts without power&lt;/li&gt;
&lt;li&gt;[ ] Free storage checked; 24-hour test done; you know how many days fit&lt;/li&gt;
&lt;li&gt;[ ] Lowest resolution that still shows the door clearly&lt;/li&gt;
&lt;li&gt;[ ] Phone not visible from outside&lt;/li&gt;
&lt;li&gt;[ ] Date and time correct&lt;/li&gt;
&lt;li&gt;[ ] Recording started, screen off, and checked once after 10 minutes&lt;/li&gt;
&lt;li&gt;[ ] One neighbour knows you are away&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this is your first time turning an old phone into a camera, start with &lt;a href="https://dev.to/superfunicular/turn-an-old-android-phone-into-a-screen-off-security-camera-no-cloud-lan-only-5cll"&gt;Turn an old Android phone into a screen-off security camera (no cloud, LAN-only)&lt;/a&gt;, do the setup this week, and use the days before you travel as your test.&lt;/p&gt;

&lt;p&gt;Happy Diwali, and safe travels.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Background Camera RemoteStream&lt;/strong&gt; turns an Android phone into a screen-off camera: it records with the screen off and keeps recordings on the phone, with no cloud account needed. Its Pro tier adds a live browser view over your own Wi-Fi and YouTube Live streaming; check the Play listing for what is free and what is Pro.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.superfunicular.digicam&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Website: &lt;a href="https://superfunicular.com" rel="noopener noreferrer"&gt;https://superfunicular.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>security</category>
      <category>privacy</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>tar czf on a Folder of Camera Clips Saved 1.3%, and 96% of That Was One CSV Log</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Tue, 03 Nov 2026 01:57:11 +0000</pubDate>
      <link>https://dev.to/superfunicular/tar-czf-on-a-folder-of-camera-clips-saved-13-and-96-of-that-was-one-csv-log-3j8i</link>
      <guid>https://dev.to/superfunicular/tar-czf-on-a-folder-of-camera-clips-saved-13-and-96-of-that-was-one-csv-log-3j8i</guid>
      <description>&lt;p&gt;A nightly job turns a folder of camera clips into one archive with &lt;code&gt;tar czf&lt;/code&gt;. The &lt;code&gt;z&lt;/code&gt; sends every byte through gzip. I measured what that bought on a test folder: the archive came out 1.31% smaller than plain &lt;code&gt;tar cf&lt;/code&gt;, and 96% of that saving came from the one CSV log sitting next to the video. The six clips together gave up 12,171 bytes out of 36.9 million.&lt;/p&gt;

&lt;p&gt;That result is not a surprise once you see where the bytes go, and the reason changes what the archive line should say. Below are the folder, five ways of archiving it, where the few kilobytes of clip savings actually come from, and a zip option that quietly cancels the fix.&lt;/p&gt;

&lt;p&gt;Disclosure: I make an Android camera app. Nothing below uses its code or describes how it records, encodes or stores anything. The clips are synthetic, made with ffmpeg on a Linux machine, and every output is pasted from the scripts shown. Versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Linux 6.8.0-138-generic
GNU bash, version 5.1.16(1)-release (aarch64-unknown-linux-gnu)
tar (GNU tar) 1.34
gzip 1.10
*** zstd command line interface 64-bits v1.4.8, by Yann Collet ***
xz (XZ Utils) 5.2.5
This is Zip 3.0 (July 5th 2008), by Info-ZIP.
ffmpeg version 4.4.2-0ubuntu0.22.04.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The test folder
&lt;/h2&gt;

&lt;p&gt;Real footage would be a better test, and I do not have a day of it that I can publish. The stand-in is six 10-second 720p H.264 clips of ffmpeg's built-in test pattern with moving grain on top, so the encoder cannot treat the frames as easy, plus a CSV log with one line every five seconds for a full day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Six 10-second 720p H.264 clips with moving grain, plus a CSV event log,&lt;/span&gt;
&lt;span class="c"&gt;# standing in for a day of camera footage. Nothing here comes from a phone.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; clips
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in &lt;/span&gt;1 2 3 4 5 6&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;ffmpeg &lt;span class="nt"&gt;-nostdin&lt;/span&gt; &lt;span class="nt"&gt;-loglevel&lt;/span&gt; error &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-f&lt;/span&gt; lavfi &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"testsrc2=size=1280x720:rate=30:duration=10"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"noise=alls=8:allf=t:all_seed=&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; veryfast &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="s2"&gt;"clips/clip_0&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;.mp4"&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'
import random
random.seed(1)
with open("clips/events.csv", "w") as f:
    f.write("time,clip,event,score\n")
    for s in range(0, 86400, 5):
        f.write("%02d:%02d:%02d,clip_0%d.mp4,%s,%.2f\n" % (s // 3600, s // 60 % 60, s % 60,
                1 + s % 6, random.choice(["motion", "none", "none", "sound"]), random.random()))
'&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; clips | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NR &amp;gt; 1 {print $5, $9}'&lt;/span&gt;
&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-cb&lt;/span&gt; clips/&lt;span class="k"&gt;*&lt;/span&gt;.mp4 | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5705602 clip_01.mp4
6682703 clip_02.mp4
6397985 clip_03.mp4
6045601 clip_04.mp4
5830938 clip_05.mp4
6227074 clip_06.mp4
548675 events.csv
36889903    total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is about 6 MB per 10-second clip. &lt;code&gt;ffprobe&lt;/code&gt; reports 4,560,848 bit/s for the first one. The CSV is 1.47% of the folder's bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five ways to archive the same folder
&lt;/h2&gt;

&lt;p&gt;Each archive is timed with &lt;code&gt;date&lt;/code&gt; before and after, which is crude but enough to see the gaps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# The same folder archived five ways. Size in bytes, then seconds of wall time.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
run&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  &lt;span class="c"&gt;# run NAME OUTPUT COMMAND...&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift &lt;/span&gt;2
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;t0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s.%N&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;t1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s.%N&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-22s %10d bytes  %6.2f s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; %s &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$t1&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="nv"&gt;$t0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | bc&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; clips.tar clips.tgz clips.tgz9 clips.tar.zst clips.txz
run &lt;span class="s2"&gt;"tar cf (no compression)"&lt;/span&gt; clips.tar   &lt;span class="nb"&gt;tar &lt;/span&gt;cf clips.tar clips
run &lt;span class="s2"&gt;"tar czf (gzip -6)"&lt;/span&gt;       clips.tgz   &lt;span class="nb"&gt;tar &lt;/span&gt;czf clips.tgz clips
run &lt;span class="s2"&gt;"gzip -9"&lt;/span&gt;                 clips.tgz9  sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'tar cf - clips | gzip -9 &amp;gt; clips.tgz9'&lt;/span&gt;
run &lt;span class="s2"&gt;"zstd -3"&lt;/span&gt;                 clips.tar.zst sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'tar cf - clips | zstd -q -3 -o clips.tar.zst'&lt;/span&gt;
run &lt;span class="s2"&gt;"tar cJf (xz -6)"&lt;/span&gt;         clips.txz   &lt;span class="nb"&gt;tar &lt;/span&gt;cJf clips.txz clips
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tar cf (no compression)   37447680 bytes    0.06 s
tar czf (gzip -6)        36958847 bytes    1.63 s
gzip -9                  36958643 bytes    1.55 s
zstd -3                  36968055 bytes    0.08 s
tar cJf (xz -6)          36924128 bytes   15.71 s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every compressor lands within 1.4% of plain &lt;code&gt;tar cf&lt;/code&gt;. The timings move from run to run: in two back-to-back runs &lt;code&gt;tar czf&lt;/code&gt; took 0.99 s and then 1.63 s. The pattern held in both. xz at its default level took between ten and sixteen times as long as gzip to save 34,719 more bytes, gzip -9 saved 204 bytes more than the default -6, and zstd at level 3 finished in about the time plain tar did.&lt;/p&gt;

&lt;p&gt;On a 37 MB folder none of those seconds matter. I measured only this folder, so I will not put a number on a full day of footage, but the per-clip saving is the part that does not grow into anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which bytes actually shrank
&lt;/h2&gt;

&lt;p&gt;Running gzip over each file on its own separates the clips from the log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Each file through gzip -6 on its own: original bytes, gzipped bytes, change.&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;clips/&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;a&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; %s &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="nt"&gt;-6&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-12s %9d -&amp;gt; %9d  %+7.2f%%\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"scale=4; (&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="s2"&gt;) * 100 / &lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | bc&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clip_01.mp4    5705602 -&amp;gt;   5703509    -0.04%
clip_02.mp4    6682703 -&amp;gt;   6680775    -0.03%
clip_03.mp4    6397985 -&amp;gt;   6396034    -0.03%
clip_04.mp4    6045601 -&amp;gt;   6043511    -0.03%
clip_05.mp4    5830938 -&amp;gt;   5828857    -0.04%
clip_06.mp4    6227074 -&amp;gt;   6225046    -0.03%
events.csv      548675 -&amp;gt;     78688   -85.66%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each clip shrank by about 2 KB, between 0.03% and 0.04%. The CSV shrank by 85.66%. Of the 488,833 bytes that &lt;code&gt;tar czf&lt;/code&gt; saved, 469,987 are the CSV.&lt;/p&gt;

&lt;p&gt;The gzip manual explains both numbers in one sentence. gzip uses "Lempel-Ziv coding (LZ77)", and "The amount of compression obtained depends on the size of the input and the distribution of common substrings." The CSV is made of common substrings: &lt;code&gt;clip_0&lt;/code&gt;, &lt;code&gt;.mp4,&lt;/code&gt;, &lt;code&gt;none&lt;/code&gt; and the same few event names, thousands of times over. The clips are the output of a video encoder, whose whole job is to remove repetition before the file is written, so gzip finds very little left to match.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2 KB is the index, not the picture
&lt;/h2&gt;

&lt;p&gt;An MP4 file is a sequence of boxes. Splitting the first clip at its top-level boxes and running each one through gzip on its own shows where the 2 KB came from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Split an MP4 into its top-level boxes and run each one through gzip -6 on its own.
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;I4s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                      &lt;span class="c1"&gt;# 64-bit size follows the type
&lt;/span&gt;        &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                    &lt;span class="c1"&gt;# box runs to the end of the file
&lt;/span&gt;        &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;
    &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gzip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%s %9d -&amp;gt; %9d  change %+6d bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ftyp        32 -&amp;gt;        48  change    +16 bytes
free         8 -&amp;gt;        28  change    +20 bytes
mdat   5701068 -&amp;gt;   5701904  change   +836 bytes
moov      4494 -&amp;gt;      1306  change  -3188 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;mdat&lt;/code&gt; box holds the encoded frames, and gzip made it 836 bytes larger. The &lt;code&gt;moov&lt;/code&gt; box holds the file's metadata, including the tables that say where each frame sits in &lt;code&gt;mdat&lt;/code&gt;. Those tables are rows of similar numbers, and they shrank from 4,494 bytes to 1,306. The small saving on the whole clip is the index shrinking by more than the picture grew.&lt;/p&gt;

&lt;p&gt;The growth on the picture is not a bug either. The manual states it outright: "Compression is always performed, even if the compressed file is slightly larger than the original. The worst case expansion is a few bytes for the gzip file header, plus 5 bytes every 32K block, or an expansion ratio of 0.015% for large files." 836 bytes on 5,701,068 is 0.0147%. The picture data sat right at gzip's documented worst case, which is what data gzip cannot compress looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  zip has an option for this, and -9 cancels it
&lt;/h2&gt;

&lt;p&gt;zip has a list of file types it stores without trying to compress, and video is not on it. From the Info-ZIP man page: "By default, zip does not compress files with extensions in the list .Z:.zip:.zoo:.arc:.lzh:.arj". The &lt;code&gt;-n&lt;/code&gt; option adds to that: "Do not attempt to compress files named with the given suffixes. Such files are simply stored (0% compression) in the output zip file, so that zip doesn't waste its time trying to compress them."&lt;/p&gt;

&lt;p&gt;The same man page has a sentence a few lines further down that is easy to skip: "The maximum compression option -9 also attempts compression on all files regardless of extension." I tried all three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# zip with and without -n .mp4, and with -9 added. unzip -v shows the method per member.&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; plain.zip stored.zip stored9.zip
zip &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; plain.zip clips
zip &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; .mp4 stored.zip clips
zip &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-9&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; .mp4 stored9.zip clips
&lt;span class="k"&gt;for &lt;/span&gt;z &lt;span class="k"&gt;in &lt;/span&gt;plain.zip stored.zip stored9.zip&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-12s %9d bytes  '&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$z&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; %s &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$z&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  unzip &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$z&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'$8 ~ /clip_01|events/ {printf "%s %s  ", $8, $2} END {print ""}'&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plain.zip     36957505 bytes  clips/events.csv Defl:N  clips/clip_01.mp4 Defl:N  
stored.zip    36969856 bytes  clips/events.csv Defl:N  clips/clip_01.mp4 Stored  
stored9.zip   36956640 bytes  clips/events.csv Defl:X  clips/clip_01.mp4 Defl:X  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;unzip -v&lt;/code&gt; prints the method for each member. In &lt;code&gt;plain.zip&lt;/code&gt; the clips were deflated (&lt;code&gt;Defl:N&lt;/code&gt;, normal). With &lt;code&gt;-n .mp4&lt;/code&gt; they were stored, and the archive came out 12,351 bytes larger, which is about the 12 KB the clips gave up under gzip. Adding &lt;code&gt;-9&lt;/code&gt; to the same command put &lt;code&gt;Defl:X&lt;/code&gt;, maximum, back on the clips. Someone who adds &lt;code&gt;-9&lt;/code&gt; because they want the smallest archive turns off the one option that told zip to leave the video alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would change in the archive line
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If the folder is almost all video, drop the &lt;code&gt;z&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;tar cf&lt;/code&gt; here was 9,102 bytes larger than the files it holds, which is tar's own headers and padding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If there are text logs worth shrinking, compress those and not the clips.&lt;/strong&gt; Either gzip the logs on their own, or pipe the whole tar through &lt;code&gt;zstd -3&lt;/code&gt;, which on this folder kept 479,625 of gzip's 488,833 saved bytes at close to plain tar's speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With zip, use &lt;code&gt;-n .mp4&lt;/code&gt; and leave out &lt;code&gt;-9&lt;/code&gt;.&lt;/strong&gt; Check with &lt;code&gt;unzip -v&lt;/code&gt; that the clips say &lt;code&gt;Stored&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a checksum if you drop compression.&lt;/strong&gt; One thing the &lt;code&gt;z&lt;/code&gt; does give you besides size: the manual says that for gzip and zip files, "gunzip checks a 32 bit CRC" when it decompresses, so a damaged archive is noticed on extraction. Drop the &lt;code&gt;z&lt;/code&gt; and that check goes with it, so keep a &lt;code&gt;sha256sum&lt;/code&gt; of the archive next to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things I did not test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real phone footage.&lt;/strong&gt; The clips here are a test pattern with added grain, encoded by ffmpeg's libx264. A phone's hardware encoder, a different bitrate, or HEVC instead of H.264 will give different sizes. I expect the picture data to behave the same way and have not measured it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A large folder.&lt;/strong&gt; Everything above is 37 MB. I did not test how the timings grow on a day of footage, or multi-threaded compressors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where &lt;code&gt;moov&lt;/code&gt; sits.&lt;/strong&gt; These clips have &lt;code&gt;moov&lt;/code&gt; at the end of the file. I did not test whether moving it to the front changes the 2 KB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other tool versions.&lt;/strong&gt; The quotes come from the gzip man page and the Info-ZIP zip 3.0 man page. Everything I ran matched them, on the versions listed at the top.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; gzip quotes are from the &lt;a href="https://man.he.net/man1/gzip" rel="noopener noreferrer"&gt;gzip(1) manual page as published by Hurricane Electric&lt;/a&gt;. zip quotes are from the &lt;a href="https://codegraph.jelmer.uk/zip/3.0-15/man/zip.1" rel="noopener noreferrer"&gt;Info-ZIP zip 3.0 manual page source&lt;/a&gt;. Outputs are pasted unedited from the scripts above.&lt;/p&gt;

&lt;p&gt;If you want an old Android phone recording in the background so there are clips for a job like this to archive, the app I make is &lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam" rel="noopener noreferrer"&gt;Background Camera RemoteStream on Google Play&lt;/a&gt;. Nothing above depends on it.&lt;/p&gt;

&lt;p&gt;On this folder &lt;code&gt;tar czf&lt;/code&gt; spent its time on the video and got its savings from the log, so compress the logs and store the clips.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>bash</category>
      <category>shell</category>
      <category>compression</category>
    </item>
    <item>
      <title>find -size -1M Matched Only the Empty File: Size Units in a Camera Clip Cleanup Script</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Mon, 02 Nov 2026 19:55:14 +0000</pubDate>
      <link>https://dev.to/superfunicular/find-size-1m-matched-only-the-empty-file-size-units-in-a-camera-clip-cleanup-script-3hl7</link>
      <guid>https://dev.to/superfunicular/find-size-1m-matched-only-the-empty-file-size-units-in-a-camera-clip-cleanup-script-3hl7</guid>
      <description>&lt;p&gt;A cleanup job for a folder of camera clips had one line meant to remove the broken stubs a recorder leaves behind when it stops mid-write: &lt;code&gt;find clips -size -1M -delete&lt;/code&gt;. Anything under a megabyte, gone. I tested that line on six files with exact byte counts, and it selected one of them: the empty one. A 500-byte stub survived it.&lt;/p&gt;

&lt;p&gt;The same test turned up two more ways a size in a shell script is not the number it looks like. &lt;code&gt;ls -lh&lt;/code&gt; and &lt;code&gt;ls -l --si&lt;/code&gt; printed the same letter for different units, and both rounded up. And a storage estimate that mixes the two kinds of gigabyte comes out about 7% short. Below are the files, what each tool printed, what the GNU manuals say about it, and the lines I would use instead.&lt;/p&gt;

&lt;p&gt;Disclosure: I make an Android camera app. Nothing below uses its code or describes how it records or stores anything. The files are random bytes made on a Linux machine, and every output is pasted from the scripts shown. Versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;find (GNU findutils) 4.8.0
ls (GNU coreutils) 8.32
Python 3.10.12
Linux 6.8.0-138-generic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Six files with known sizes
&lt;/h2&gt;

&lt;p&gt;The sizes are picked to sit on both sides of the two meanings of a megabyte: 1,000,000 bytes and 1,048,576 bytes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# Six stand-in clips with exact byte counts, from empty to just over one MiB.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; clips &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mkdir &lt;/span&gt;clips &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;clips
&lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 0 empty.mp4
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 500     /dev/urandom &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; stub_500.mp4
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 1000000 /dev/urandom &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; mb_1000000.mp4
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 1048576 /dev/urandom &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; mib_1048576.mp4
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 1048577 /dev/urandom &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; mib_1048577.mp4
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 1500000 /dev/urandom &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; mb_1500000.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three ways to print one size
&lt;/h2&gt;

&lt;p&gt;For each file: the byte count from &lt;code&gt;stat&lt;/code&gt;, then the size column from &lt;code&gt;ls -lh&lt;/code&gt; and from &lt;code&gt;ls -l --si&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# The same six files in three units: bytes, ls -h (1024) and ls --si (1000).&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;clips
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-16s %8s  -h %5s  --si %5s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; %s &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $5}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nt"&gt;--si&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $5}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;empty.mp4               0  -h     0  --si     0
mb_1000000.mp4    1000000  -h  977K  --si  1.0M
mb_1500000.mp4    1500000  -h  1.5M  --si  1.5M
mib_1048576.mp4   1048576  -h  1.0M  --si  1.1M
mib_1048577.mp4   1048577  -h  1.1M  --si  1.1M
stub_500.mp4          500  -h   500  --si   500
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read down the two right-hand columns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The letter does not say which unit.&lt;/strong&gt; &lt;code&gt;-h&lt;/code&gt; counts in 1024s and &lt;code&gt;--si&lt;/code&gt; in 1000s. The coreutils manual describes &lt;code&gt;-h&lt;/code&gt; as the format that "uses powers of 1024" and the SI format as one that "uses powers of 1000". Both printed a bare &lt;code&gt;M&lt;/code&gt; here, so a column of &lt;code&gt;1.1M&lt;/code&gt; values from someone else's terminal does not tell you which one you are looking at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both round up.&lt;/strong&gt; The file of exactly 1,048,576 bytes shows as &lt;code&gt;1.1M&lt;/code&gt; under &lt;code&gt;--si&lt;/code&gt;, and the file one byte larger shows as &lt;code&gt;1.1M&lt;/code&gt; under &lt;code&gt;-h&lt;/code&gt;. The manual says so in general terms: "Fractional block counts are rounded up to the nearest integer." A displayed size is a ceiling, not a floor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A decimal megabyte is 977K under &lt;code&gt;-h&lt;/code&gt;.&lt;/strong&gt; Anyone checking by eye that a file is at least a decimal megabyte with &lt;code&gt;ls -lh&lt;/code&gt; will think the 1,000,000-byte file is under.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is wrong. It is just three numbers for one file, and a script that copies one of them into a threshold inherits whatever the display did to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which files each find -size test picks
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# Which files each -size test selects.&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;clips
&lt;span class="k"&gt;for &lt;/span&gt;t &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nt"&gt;-1M&lt;/span&gt; 1M +1M &lt;span class="nt"&gt;-1024k&lt;/span&gt; &lt;span class="nt"&gt;-1048576c&lt;/span&gt; +999999c&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-11s %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-size&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-printf&lt;/span&gt; &lt;span class="s1"&gt;'%f\n'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-1M         empty.mp4 
1M          mb_1000000.mp4 mib_1048576.mp4 stub_500.mp4 
+1M         mb_1500000.mp4 mib_1048577.mp4 
-1024k      empty.mp4 mb_1000000.mp4 stub_500.mp4 
-1048576c   empty.mp4 mb_1000000.mp4 stub_500.mp4 
+999999c    mb_1000000.mp4 mb_1500000.mp4 mib_1048576.mp4 mib_1048577.mp4 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-size -1M&lt;/code&gt; matched only &lt;code&gt;empty.mp4&lt;/code&gt;. &lt;code&gt;-size 1M&lt;/code&gt;, which reads like exactly one megabyte, matched the 500-byte stub along with the two files that really are about a megabyte. And &lt;code&gt;-size +1M&lt;/code&gt; left out the file of exactly 1,048,576 bytes.&lt;/p&gt;

&lt;p&gt;The findutils manual explains all three with one rule. The test is "True if the file uses n units of space, rounding up", and later: "Bear in mind that the size is rounded up to the next unit." So with &lt;code&gt;M&lt;/code&gt; as the unit, a 500-byte file counts as one MiB, and so does a 1,000,000-byte file. Nothing non-empty is less than one unit, which is why the manual's own example says of &lt;code&gt;-size -1M&lt;/code&gt; against &lt;code&gt;-size -1048576c&lt;/code&gt;: "The former only matches empty files, the latter matches files from 0 to 1,048,575 bytes."&lt;/p&gt;

&lt;p&gt;One detail did not match my reading of the manual. It says a plus sign means the test succeeds "if the file uses at least n units of storage". The 1,048,576-byte file uses exactly one unit, and on findutils 4.8.0 &lt;code&gt;-size +1M&lt;/code&gt; did not select it. What I saw is more than one unit, after rounding up. The same paragraph says of the minus sign that "an exact size of n units does not match", and on this build the plus sign behaved the same way at the boundary. If your script depends on that edge, test it on your own build rather than trusting either reading.&lt;/p&gt;

&lt;p&gt;The two rows that worked are the ones with a small unit. &lt;code&gt;-1048576c&lt;/code&gt; counts bytes, so there is no rounding at all, and &lt;code&gt;-1024k&lt;/code&gt; rounds to the next KiB, which made no difference for these files. Both selected the empty file, the stub and the decimal megabyte, and left the 1,048,576-byte file alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gigabyte on the card and the G in df -h
&lt;/h2&gt;

&lt;p&gt;The same mismatch shows up in the arithmetic people do before leaving a phone recording: how many hours fit on the card. A card sold as 64 GB is counted in decimal gigabytes. &lt;code&gt;df -h&lt;/code&gt; reports in powers of 1024 and prints a &lt;code&gt;G&lt;/code&gt;. A stream bitrate is usually quoted in decimal megabits. Mix them and the estimate shifts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A card sold as 64 GB, in the units df -h prints, and hours of a stream at 8 Mbit/s.
&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;                 &lt;span class="c1"&gt;# bytes, decimal gigabytes
&lt;/span&gt;&lt;span class="n"&gt;gib&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="n"&gt;per_hour&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;   &lt;span class="c1"&gt;# bytes per hour at 8 Mbit/s
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;64 GB card      = %d bytes = %.2f GiB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gib&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8 Mbit/s for 1h = %d bytes = %.2f GB = %.2f GiB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;per_hour&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;per_hour&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours, same units  : %.2f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;per_hour&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours, GiB / GB/h  : %.2f  (%.1f%% short)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gib&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_hour&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gib&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_hour&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;per_hour&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;64 GB card      = 64000000000 bytes = 59.60 GiB
8 Mbit/s for 1h = 3600000000 bytes = 3.60 GB = 3.35 GiB
hours, same units  : 17.78
hours, GiB / GB/h  : 16.56  (6.9% short)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These numbers are arithmetic on a made-up stream, not a measurement of any recorder, and a real card also loses space to the file system, which I did not model. The point is the 6.9%: taking the figure &lt;code&gt;df -h&lt;/code&gt; shows and dividing by an hourly size worked out in decimal gigabytes understates the hours by that much. The opposite mix, a card's label divided by an hourly size in GiB, overstates them, by 7.4% on these numbers. Neither error is visible in the output, because both sides print a plain &lt;code&gt;G&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would write instead
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Give &lt;code&gt;find&lt;/code&gt; thresholds in bytes.&lt;/strong&gt; &lt;code&gt;find clips -type f -size -1048576c&lt;/code&gt; selects files under 1,048,576 bytes and nothing else. If you want the decimal megabyte, write &lt;code&gt;-size -1000000c&lt;/code&gt;. Either way, check with &lt;code&gt;-print&lt;/code&gt; before adding &lt;code&gt;-delete&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not use &lt;code&gt;-size -1M&lt;/code&gt; or &lt;code&gt;-size -1G&lt;/code&gt; to mean small files.&lt;/strong&gt; They only match empty files. If you do want empty files, &lt;code&gt;-empty&lt;/code&gt; says so directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read sizes with &lt;code&gt;stat -c %s&lt;/code&gt; in scripts,&lt;/strong&gt; not from the &lt;code&gt;ls -lh&lt;/code&gt; column. The display rounds up and drops the unit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick one unit for capacity math and say which.&lt;/strong&gt; Convert the card's label to bytes, convert the bitrate to bytes per hour, and divide. If you start from &lt;code&gt;df&lt;/code&gt;, use &lt;code&gt;df -B1&lt;/code&gt; or &lt;code&gt;df --output=avail -B1&lt;/code&gt; so the number is in bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things I did not test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BSD and macOS &lt;code&gt;find&lt;/code&gt; and &lt;code&gt;ls&lt;/code&gt;.&lt;/strong&gt; Everything here is GNU findutils and coreutils on Linux. I have not checked whether other implementations round the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sparse files and allocated size.&lt;/strong&gt; &lt;code&gt;-size&lt;/code&gt; and &lt;code&gt;ls -l&lt;/code&gt; both use the file's apparent length. Disk usage tools that count allocated blocks can disagree with both, which is a separate question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real recordings.&lt;/strong&gt; The six files are random bytes with chosen lengths. A stub left by a real recorder may be larger or smaller than 500 bytes; the rounding rule is the same either way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; &lt;code&gt;find -size&lt;/code&gt; quotes are from the &lt;a href="https://www.gnu.org/software/findutils/manual/html_node/find_html/Size.html" rel="noopener noreferrer"&gt;GNU findutils manual, section 2.6 Size&lt;/a&gt;. Block size quotes are from the &lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/Block-size.html" rel="noopener noreferrer"&gt;GNU coreutils manual, section on block size&lt;/a&gt;. Outputs are pasted unedited from the scripts above.&lt;/p&gt;

&lt;p&gt;If you want an old Android phone recording in the background so there is a folder of clips for a job like this to clean up, the app I make is &lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam" rel="noopener noreferrer"&gt;Background Camera RemoteStream on Google Play&lt;/a&gt;. Nothing above depends on it.&lt;/p&gt;

&lt;p&gt;In a cleanup script, a size written in megabytes is rounded before it is compared, so write the threshold in bytes and check what it selects before it deletes anything.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>bash</category>
      <category>shell</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>comm -23 Said Nine Camera Clips Were Missing From the Backup. Two Were.</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Mon, 02 Nov 2026 14:07:49 +0000</pubDate>
      <link>https://dev.to/superfunicular/comm-23-said-nine-camera-clips-were-missing-from-the-backup-two-were-5443</link>
      <guid>https://dev.to/superfunicular/comm-23-said-nine-camera-clips-were-missing-from-the-backup-two-were-5443</guid>
      <description>&lt;p&gt;I set up a test folder of camera clips and a backup folder with two of them missing, then ran the line a nightly backup check usually rests on, the one that prints what is in one list and not the other: &lt;code&gt;comm -23 phone.txt backup.txt&lt;/code&gt;. It reported nine missing clips.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;comm&lt;/code&gt; did not hide the problem. It printed a warning and exited with status 1. A job that only wants a number throws both away. Below are the test folders, what &lt;code&gt;comm&lt;/code&gt; printed, what the GNU manual says about it, a brute-force check of when &lt;code&gt;comm&lt;/code&gt; stays quiet, and the version of the job I would run instead.&lt;/p&gt;

&lt;p&gt;Disclosure: I make an Android camera app. Nothing below uses its code or describes how it records, names or stores anything. The clips are random bytes made on a Linux machine, and every output is pasted from the scripts shown. Versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;comm (GNU coreutils) 8.32
find (GNU findutils) 4.8.0
GNU bash, version 5.1.16(1)-release (aarch64-unknown-linux-gnu)
Python 3.10.12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Twelve clips, ten backed up
&lt;/h2&gt;

&lt;p&gt;One clip per hour from 06:00 to 17:00 in a phone folder, and a backup folder where the 09:00 and 12:00 clips did not arrive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# A "phone" folder with twelve hourly clips, and a "backup" folder&lt;/span&gt;
&lt;span class="c"&gt;# where the 09:00 and 12:00 clips never arrived.&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; phone backup
&lt;span class="k"&gt;for &lt;/span&gt;h &lt;span class="k"&gt;in &lt;/span&gt;06 07 08 09 10 11 12 13 14 15 16 17&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 2048 /dev/urandom &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"phone/2026-11-01_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;h&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;00.mp4"&lt;/span&gt;
&lt;span class="k"&gt;done
for &lt;/span&gt;h &lt;span class="k"&gt;in &lt;/span&gt;06 07 08 10 11 13 14 15 16 17&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"phone/2026-11-01_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;h&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;00.mp4"&lt;/span&gt; backup/
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;phone | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;ls &lt;/span&gt;backup | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12
10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the right answer is two files: &lt;code&gt;2026-11-01_0900.mp4&lt;/code&gt; and &lt;code&gt;2026-11-01_1200.mp4&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual comparison
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# The usual way: list both sides, ask comm for lines only in the first.&lt;/span&gt;
find phone &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.mp4'&lt;/span&gt; &lt;span class="nt"&gt;-printf&lt;/span&gt; &lt;span class="s1"&gt;'%f\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; phone.txt
&lt;span class="nb"&gt;ls &lt;/span&gt;backup &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backup.txt
&lt;span class="nb"&gt;comm&lt;/span&gt; &lt;span class="nt"&gt;-23&lt;/span&gt; phone.txt backup.txt
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit status: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;comm: file 1 is not in sorted order
2026-11-01_0800.mp4
2026-11-01_1000.mp4
2026-11-01_1200.mp4
2026-11-01_0700.mp4
2026-11-01_1500.mp4
2026-11-01_1100.mp4
2026-11-01_0900.mp4
2026-11-01_1400.mp4
2026-11-01_0600.mp4
comm: input is not in sorted order
exit status: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nine names, including seven that are sitting in the backup folder. The two clips that really are missing are in there, mixed in with the others, so a person reading the list would not know which to trust.&lt;/p&gt;

&lt;p&gt;The cause is the first input. &lt;code&gt;find&lt;/code&gt; printed the names in the order the directory handed them back, which on this machine was not by name. &lt;code&gt;ls&lt;/code&gt; sorts its output, so the backup list was in order and the phone list was not. &lt;code&gt;comm&lt;/code&gt; walks both files once, top to bottom, and decides what is unique by comparing the current line of each. That only works if both files are in the same order.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the manual says
&lt;/h2&gt;

&lt;p&gt;The coreutils manual puts the requirement first: "Before comm can be used, the input files must be sorted using the collating sequence specified by the LC_COLLATE locale."&lt;/p&gt;

&lt;p&gt;It also says what the warning and the status 1 mean: "If an input file is diagnosed as being unsorted, the comm command will exit with a nonzero status (and the output should not be used)."&lt;/p&gt;

&lt;p&gt;And it says when the check runs. With neither &lt;code&gt;--check-order&lt;/code&gt; nor &lt;code&gt;--nocheck-order&lt;/code&gt; on the command line, "wrongly sorted inputs are diagnosed only if an input file is found to contain unpairable lines." The &lt;code&gt;--help&lt;/code&gt; text on this machine words the stricter flag as "check that the input is correctly sorted, even if all input lines are pairable".&lt;/p&gt;

&lt;p&gt;One more line from the manual matters for a nightly job. In the section on &lt;code&gt;--total&lt;/code&gt;, it suggests portable scripts count with &lt;code&gt;wc&lt;/code&gt;, and gives &lt;code&gt;comm -23 file1 file2 | wc -l&lt;/code&gt; as the example. That example assumes sorted files. Point it at an unsorted one and &lt;code&gt;wc -l&lt;/code&gt; counts whatever &lt;code&gt;comm&lt;/code&gt; printed, the warning goes to the terminal or the log, and the count looks like any other count.&lt;/p&gt;

&lt;h2&gt;
  
  
  The job that only wanted a number
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# The same comparison inside a nightly job that only wants a number.&lt;/span&gt;
&lt;span class="nv"&gt;missing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;comm&lt;/span&gt; &lt;span class="nt"&gt;-23&lt;/span&gt; phone.txt backup.txt 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"clips missing from backup: &lt;/span&gt;&lt;span class="nv"&gt;$missing&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clips missing from backup: 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the shape a nightly check tends to take: &lt;code&gt;2&amp;gt;/dev/null&lt;/code&gt; to keep the cron mail quiet, and a count for the log. With the warning discarded, and the pipeline's status coming from &lt;code&gt;wc&lt;/code&gt; rather than &lt;code&gt;comm&lt;/code&gt;, nothing in the job's output says the nine is unusable. The number is also plausible: nine missing clips out of twelve is a bad night, not an unbelievable one.&lt;/p&gt;

&lt;h2&gt;
  
  
  When does comm stay quiet?
&lt;/h2&gt;

&lt;p&gt;The manual's wording left me with a question. If the check runs only when there are unpairable lines, can an unsorted input slip through with a wrong answer and exit status 0? I tried every ordering of up to four names on the phone side against every ordering of up to three names on the backup side, and compared &lt;code&gt;comm -23&lt;/code&gt; with the true set difference each time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Every ordering of up to four names on the phone side, every subset
# on the backup side (in any order). Count the runs where comm printed
# the wrong answer AND exited 0, and the unsorted runs that exited 0.
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="n"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abcd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wrong&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;silent_wrong&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unsorted_quiet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n1&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permutations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n2&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permutations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-23&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                                   &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                   &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LC_ALL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
                &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="n"&gt;unsorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;unsorted&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;unsorted_quiet&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;wrong&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;silent_wrong&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runs:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong answers:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wrong&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong answers with exit status 0:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;silent_wrong&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unsorted input, exit status 0:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unsorted_quiet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;runs: 2560
wrong answers: 1692
wrong answers with exit status 0: 0
unsorted input, exit status 0: 128
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In these 2560 runs, every wrong answer came with a nonzero exit status. That fits my reading of the manual: a wrong answer means some line was misjudged as unique, and a line judged unique is an unpairable line, which turns the check on. Unsorted input did pass quietly 128 times, but in each of those the answer happened to come out right anyway.&lt;/p&gt;

&lt;p&gt;That is a small test with single-letter names, on one version. It is not proof that the default check catches every case. It is enough to say the warning is worth reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The version I would run
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Sort both lists the same way comm will compare them, and stop on error.&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C
&lt;span class="nb"&gt;sort &lt;/span&gt;phone.txt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; phone.sorted
&lt;span class="nb"&gt;sort &lt;/span&gt;backup.txt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backup.sorted
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;missing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;comm&lt;/span&gt; &lt;span class="nt"&gt;--check-order&lt;/span&gt; &lt;span class="nt"&gt;-23&lt;/span&gt; phone.sorted backup.sorted&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"comm failed; not trusting the list"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$missing&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"count: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$missing&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; .&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== order-free cross-check"&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Fxv&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; backup.txt phone.txt | &lt;span class="nb"&gt;sort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-11-01_0900.mp4
2026-11-01_1200.mp4
count: 2
== order-free cross-check
2026-11-01_0900.mp4
2026-11-01_1200.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What changed, line by line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;export LC_ALL=C&lt;/code&gt;&lt;/strong&gt; makes &lt;code&gt;sort&lt;/code&gt; and &lt;code&gt;comm&lt;/code&gt; compare names by byte value, the same way. The manual ties the order &lt;code&gt;comm&lt;/code&gt; expects to &lt;code&gt;LC_COLLATE&lt;/code&gt;, so the safe move is to sort and compare under one locale in one script, rather than trust that two programs or two machines agree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both lists go through &lt;code&gt;sort&lt;/code&gt;&lt;/strong&gt;, including the one from &lt;code&gt;ls&lt;/code&gt; that already looked sorted. It costs nothing and removes the question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--check-order&lt;/code&gt;&lt;/strong&gt; makes the check unconditional, which is the stricter behavior the help text describes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The exit status is tested before the output is used.&lt;/strong&gt; If &lt;code&gt;comm&lt;/code&gt; fails, the job says so and stops, instead of reporting a count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The count comes from the stored result&lt;/strong&gt;, so the number and the list are the same thing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last block is a cross-check that does not depend on order at all. &lt;code&gt;grep -Fxv -f backup.txt phone.txt&lt;/code&gt; prints every line of the phone list that is not an exact, whole-line match for some line in the backup list. It treats every line of the backup list as a pattern, and I only ran it on a dozen names, so I have no numbers for a large archive. Here it gave the same two names from the unsorted files.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would check in an existing job
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find every &lt;code&gt;comm&lt;/code&gt; call, and look at where each input comes from.&lt;/strong&gt; &lt;code&gt;find&lt;/code&gt;, a database query, a file written by an app, or a list someone edited by hand are all inputs that may not be sorted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look for &lt;code&gt;2&amp;gt;/dev/null&lt;/code&gt; or a pipe into &lt;code&gt;wc -l&lt;/code&gt; right after it.&lt;/strong&gt; Those are the two places the warning and the status get lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it once with &lt;code&gt;--check-order&lt;/code&gt; on real data.&lt;/strong&gt; If it fails, the old output was not safe to use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the sort and the compare under the same locale.&lt;/strong&gt; Setting &lt;code&gt;LC_ALL=C&lt;/code&gt; at the top of the script is the simplest way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spot-check the count against one known-missing file.&lt;/strong&gt; Delete one clip from a test backup and confirm the job names it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I did not test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Other &lt;code&gt;comm&lt;/code&gt; implementations.&lt;/strong&gt; This was GNU coreutils 8.32 on Linux. BSD and macOS &lt;code&gt;comm&lt;/code&gt;, and BusyBox, may check order differently or not at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Larger names and real collation.&lt;/strong&gt; The brute-force test used four single letters under &lt;code&gt;LC_ALL=C&lt;/code&gt;. Names with spaces, mixed case or non-ASCII characters under another locale were not part of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File names with newlines.&lt;/strong&gt; A name with a newline in it breaks any one-name-per-line list. The manual's &lt;code&gt;-z&lt;/code&gt; option and &lt;code&gt;find -print0&lt;/code&gt; exist for that case; I did not test them here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Directory order on other file systems.&lt;/strong&gt; The phone list came out unsorted on this machine. On another file system &lt;code&gt;find&lt;/code&gt; may return names in a different order, sorted or not, which is the reason to sort rather than rely on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; Quotes about ordering, the order check and exit status are from the &lt;a href="https://www.gnu.org/software/coreutils/comm" rel="noopener noreferrer"&gt;GNU coreutils manual, comm invocation&lt;/a&gt;. The &lt;code&gt;--check-order&lt;/code&gt; wording is from &lt;code&gt;comm --help&lt;/code&gt; on the machine above. Outputs are pasted unedited from the scripts shown.&lt;/p&gt;

&lt;p&gt;If you want an old Android phone recording in the background so there is a folder of clips to back up and check, the app I make is &lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam" rel="noopener noreferrer"&gt;Background Camera RemoteStream on Google Play&lt;/a&gt;. Nothing above depends on it.&lt;/p&gt;

&lt;p&gt;Before a backup check trusts a list from comm, sort both inputs the same way and read its exit status, because an unsorted input makes it print a confident wrong answer.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>linux</category>
      <category>shell</category>
      <category>python</category>
    </item>
    <item>
      <title>Why Is My AlfredCamera Camera Disabled? The 2026 Two-Camera Free Limit, Legacy Pricing, and an Offline Option for Camera #3</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Mon, 02 Nov 2026 08:12:45 +0000</pubDate>
      <link>https://dev.to/superfunicular/why-is-my-alfredcamera-camera-disabled-the-2026-two-camera-free-limit-legacy-pricing-and-an-3aod</link>
      <guid>https://dev.to/superfunicular/why-is-my-alfredcamera-camera-disabled-the-2026-two-camera-free-limit-legacy-pricing-and-an-3aod</guid>
      <description>&lt;p&gt;You open AlfredCamera and one of your old phones shows as disabled. Nothing is broken, the phone is still on the shelf, it still has Wi-Fi. Or you try to add a third phone and the app asks you to upgrade.&lt;/p&gt;

&lt;p&gt;If that sounds familiar, the likely reason is not a bug. AlfredCamera announced a change to its plans for 2026, and part of that change is a limit on how many cameras can be online at once on each tier. This guide explains what the limit is, how the rollout handles phones that are over it, how to rotate cameras without paying, and what you can do with a third phone if you would rather not rotate at all.&lt;/p&gt;

&lt;p&gt;Everything below about AlfredCamera comes from its own Help Center announcement, linked at each point. Prices and dates can differ by country and can change, so the app itself is always the final word.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is my AlfredCamera camera disabled on the free plan?
&lt;/h2&gt;

&lt;p&gt;Because the free tier now allows &lt;strong&gt;up to 2 online cameras&lt;/strong&gt;. If your account has more cameras than that when the rollout reaches it, the extra ones are switched off for you.&lt;/p&gt;

&lt;p&gt;AlfredCamera's &lt;a href="https://support.alfred.camera/hc/en-us/articles/10414653338522--Announcement-Important-Update-to-AlfredCamera-s-2026-Plan-and-Pricing" rel="noopener noreferrer"&gt;2026 plan and pricing announcement&lt;/a&gt; sets these limits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AlfredCamera tier&lt;/th&gt;
&lt;th&gt;Online cameras&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Up to 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium Standard&lt;/td&gt;
&lt;td&gt;Up to 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium Plus&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The announcement says the limits arrive in a "gradual, phased rollout" later in 2026, and that you get "a personalized notification in the app well before any changes affect your specific account." So if you never saw that notification, your account may not have been reached yet, and a disabled camera may have another cause (battery saver, a killed app, a lost connection).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your cameras are not deleted.&lt;/strong&gt; The announcement's FAQ is clear on this. When the rollout finds more active cameras than your plan allows, it keeps "cameras with recent activity online," and cameras "that haven't been updated for a longer period will be temporarily disabled." In plain words: the phones you used least recently are the ones that get switched off.&lt;/p&gt;

&lt;p&gt;If you just want the short version: on free, two phones at a time stay online. You can choose which two. Or you can put the third phone on a different setup that does not count against any limit. Both options are below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in AlfredCamera's 2026 pricing?
&lt;/h2&gt;

&lt;p&gt;Two things, both from the same &lt;a href="https://support.alfred.camera/hc/en-us/articles/10414653338522--Announcement-Important-Update-to-AlfredCamera-s-2026-Plan-and-Pricing" rel="noopener noreferrer"&gt;announcement&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Price.&lt;/strong&gt; In the United States, the annual price for &lt;strong&gt;new&lt;/strong&gt; Premium Standard subscriptions went from &lt;strong&gt;$29.99 to $35.99 per year&lt;/strong&gt;, effective March 16, 2026. That is a $6 rise, about 20%. Other regions see local prices in the app from April 7, 2026, and the announcement notes that prices "may vary across different countries due to exchange rates and local taxes."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Camera limits.&lt;/strong&gt; The tier limits in the table above.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AlfredCamera explains the change as covering costs after "nearly half a decade" at the same price, plus investment in encrypted FHD streaming and AI detection. That is a reasonable thing for a cloud service to do. Running servers costs money, and a service that relays video through the internet has to pay for it somehow.&lt;/p&gt;

&lt;p&gt;A quick piece of arithmetic, if you are deciding whether to pay: at $35.99 a year for up to 4 online cameras, a full Premium Standard setup works out to about &lt;strong&gt;$9.00 per camera per year&lt;/strong&gt;. If you only run 2 cameras, free already covers you, and paying buys features rather than slots.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is AlfredCamera's "Legacy Protection"?
&lt;/h2&gt;

&lt;p&gt;This is the part people miss, and it matters if you already pay.&lt;/p&gt;

&lt;p&gt;According to the announcement, if you subscribed &lt;strong&gt;before&lt;/strong&gt; your region's effective date (US: March 16; other regions: April 7) and you keep the subscription active:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your renewal price is locked.&lt;/strong&gt; "Your renewal price is guaranteed at your current rate."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Camera limits come later for you.&lt;/strong&gt; Legacy users "will be prioritized with the longest available buffer periods" when the camera limits roll out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is one catch, and it is worth writing down:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If your subscription is cancelled or expires due to a payment failure, your Legacy status will be lost, and any resubscription will follow the 2026 pricing and plan rules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So if you are a legacy subscriber, an expired card can cost you the old price. Check that the card on your Google Play or App Store account is current before your renewal date. That one step protects the old rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to rotate cameras on the free plan
&lt;/h2&gt;

&lt;p&gt;If you have three or more phones and want to stay on free, AlfredCamera gives you a manual method. From the announcement's FAQ: go to the settings of any active camera and select &lt;strong&gt;"Disable"&lt;/strong&gt; to release its slot, then &lt;strong&gt;"Enable"&lt;/strong&gt; another camera within your plan's limit.&lt;/p&gt;

&lt;p&gt;A simple routine that works for a lot of homes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decide your two "always" cameras.&lt;/strong&gt; Usually the front door and the room where valuables or a pet live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat the third phone as a "sometimes" camera.&lt;/strong&gt; For example, the back gate only when you travel, or the shop counter only at night.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap before you leave, not after.&lt;/strong&gt; Disable one and enable the other while you are still at home, and check that the live view loads. Doing it from a café on mobile data is where things go wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the rotated phone charged.&lt;/strong&gt; A disabled camera is still a phone. If it sits off the charger for a week, it will be flat when you need it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don't confuse this with &lt;strong&gt;Remote Switch&lt;/strong&gt;, which is a separate AlfredCamera feature. Remote Switch lets the owner turn a camera off and on from the viewer device, from the camera thumbnail's settings (&lt;a href="https://support.alfred.camera/hc/en-us/articles/900002512183-Remote-Switch-Disable-Camera-Remotely" rel="noopener noreferrer"&gt;Remote Switch help page&lt;/a&gt;). The help article is about pausing a camera, not about the plan's slot count, so don't assume one replaces the other. Use the "Disable"/"Enable" steps from the announcement for slots.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can I do with camera #3 without paying?
&lt;/h2&gt;

&lt;p&gt;Rotation works, but it means one area is never watched while the other two are. If the third phone covers a spot you want recorded every night, there is another route: run that phone as a &lt;strong&gt;local, offline camera&lt;/strong&gt; that doesn't sign in to any service, so there is no online-camera count to run out of.&lt;/p&gt;

&lt;p&gt;This is a different kind of setup from AlfredCamera, and it is fair to be clear about the trade-off:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;AlfredCamera (cloud relay)&lt;/th&gt;
&lt;th&gt;Local, offline phone camera&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Account&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where video is kept&lt;/td&gt;
&lt;td&gt;Depends on plan and features&lt;/td&gt;
&lt;td&gt;On the phone's own storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watching from far away&lt;/td&gt;
&lt;td&gt;Built in (through Alfred's service)&lt;/td&gt;
&lt;td&gt;Not by default; you check the files, or watch on your own network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Counts against a camera limit&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No account, so no count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works with no internet&lt;/td&gt;
&lt;td&gt;Needs a connection to view remotely&lt;/td&gt;
&lt;td&gt;Records with no internet at all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mixed setup a lot of people end up with: keep your two most important views on AlfredCamera's free tier, where remote viewing is the point, and let the third phone record locally all night, every night. In the morning, or when something happens, you look at the files.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to set up the third phone as a local, offline camera
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam" rel="noopener noreferrer"&gt;Background Camera RemoteStream&lt;/a&gt;, so this is the one I'll describe. It needs no account, records with the screen off, and keeps recordings on the phone.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install it from Google Play&lt;/strong&gt; on the third phone. No sign-up step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give it the camera permission&lt;/strong&gt; (and microphone if you want sound).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn off battery optimisation for the app.&lt;/strong&gt; On many Android brands, especially budget ones, the system closes background apps to save power. Our guide &lt;a href="https://dev.to/superfunicular/why-your-old-phone-security-camera-dies-after-4-hours-and-how-to-fix-it-on-modern-android-3ppp"&gt;Why Your Old Phone Security Camera Dies After 4 Hours&lt;/a&gt; walks through the settings by brand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start recording, then turn the screen off.&lt;/strong&gt; The app runs as an Android foreground service, so recording continues with the display dark. A dark screen also stops the phone lighting up the room at night.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leave it on a charger&lt;/strong&gt; and do a one-hour test before trusting it overnight. Play the file back the next morning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check storage.&lt;/strong&gt; A long recording fills a cheap card faster than people expect. If your card is 32 GB or smaller, read &lt;a href="https://dev.to/superfunicular/why-does-a-phone-security-camera-recording-stop-at-almost-4-gb-the-fat32-sd-card-limit-exfat-and-2c3n"&gt;why a recording can stop at almost 4 GB&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Watching live is optional. The app's Pro tier adds a live view in any web browser on your own Wi-Fi, and YouTube Live streaming. Check the Play listing for the current free and Pro split before you rely on either. If you only need a record of what happened, the free recording is enough.&lt;/p&gt;

&lt;p&gt;For the full first-time setup, start with &lt;a href="https://dev.to/superfunicular/turn-an-old-android-phone-into-a-screen-off-security-camera-no-cloud-lan-only-5cll"&gt;Turn an old Android phone into a screen-off security camera (no cloud, LAN-only)&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which option should I choose?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You have 2 phones or fewer:&lt;/strong&gt; nothing changes for you on AlfredCamera free. Stay put.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You already pay and joined before the effective date:&lt;/strong&gt; keep the subscription active and the card current to hold Legacy pricing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have 3+ phones and need remote viewing on all of them, all the time:&lt;/strong&gt; Premium Standard (up to 4) or Premium Plus (unlimited) is built for exactly that. On Standard that works out to about $9 per camera per year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have 3+ phones and the extra ones mostly need to record:&lt;/strong&gt; keep two on AlfredCamera free and run the rest as local, offline cameras.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You don't want any account or cloud service at all:&lt;/strong&gt; run every phone locally. You give up easy viewing from far away, and in return nothing about your setup depends on a company's plan rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a broader comparison of free apps and what "free" means in each, see &lt;a href="https://dev.to/superfunicular/what-free-actually-gets-you-in-a-camera-app-2026-reading-the-fine-print-before-you-point-one-gm1"&gt;What "Free" Actually Gets You in a Camera App (2026)&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Look for AlfredCamera's in-app notice before assuming the limit hit you&lt;/li&gt;
&lt;li&gt;Pick your two "always" cameras&lt;/li&gt;
&lt;li&gt;Rotate with Disable/Enable before you leave home&lt;/li&gt;
&lt;li&gt;Legacy subscriber? Keep the card current; a lapse resets you to 2026 pricing&lt;/li&gt;
&lt;li&gt;Third phone recording locally: battery optimisation off, screen off, charger on, one-hour test&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Background Camera RemoteStream&lt;/strong&gt; turns an Android phone into a screen-off camera: it records with the screen off and keeps recordings on the phone, with no cloud account needed. Its Pro tier adds a live browser view over your own Wi-Fi and YouTube Live streaming; check the Play listing for what is free and what is Pro.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.superfunicular.digicam&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Website: &lt;a href="https://superfunicular.com" rel="noopener noreferrer"&gt;https://superfunicular.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;AlfredCamera details are taken from AlfredCamera's Help Center as of November 2026. Prices and limits may differ in your country; the app shows the current terms.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>security</category>
      <category>privacy</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
