<?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>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>
    <item>
      <title>wc -l Says 2, grep Says 3: A Missing Final Newline Drops the Last Clip From a while read Loop</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Sun, 01 Nov 2026 13:57:18 +0000</pubDate>
      <link>https://dev.to/superfunicular/wc-l-says-2-grep-says-3-a-missing-final-newline-drops-the-last-clip-from-a-while-read-loop-lcf</link>
      <guid>https://dev.to/superfunicular/wc-l-says-2-grep-says-3-a-missing-final-newline-drops-the-last-clip-from-a-while-read-loop-lcf</guid>
      <description>&lt;p&gt;&lt;code&gt;wc -l&lt;/code&gt; says a list of camera clips has two lines. &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt; and &lt;code&gt;sed&lt;/code&gt; say three. The file holds three clip names, and the &lt;code&gt;while read&lt;/code&gt; loop that uploads them handles two.&lt;/p&gt;

&lt;p&gt;All three answers come from one missing byte: the list does not end with a newline. A short Python script writes it for the shell loop to read:&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="n"&gt;names&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;clip_0700.mp4&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;clip_0715.mp4&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;clip_0730.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;with&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;upload.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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&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;names&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;bytes written:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&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;names&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;bytes written: 41
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;join()&lt;/code&gt; puts its separator between names, not after the last one. Three names, two newlines.&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 uploads anything. The clip names are made up, 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)
wc (GNU coreutils) 8.32
grep (GNU grep) 3.7
GNU Awk 5.1.0, API: 3.0 (GNU MPFR 4.1.0, GNU MP 6.2.1)
sed (GNU sed) 4.8
Python 3.10.12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Four tools, two answers
&lt;/h2&gt;

&lt;p&gt;First, ask a few common tools how many lines the file has:&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"wc -l:        &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &amp;lt; upload.txt&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;"grep -c '':   &lt;/span&gt;&lt;span class="si"&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="s1"&gt;''&lt;/span&gt; upload.txt&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;"awk END{NR}:  &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'END { print NR }'&lt;/span&gt; upload.txt&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;"sed -n '&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;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'$='&lt;/span&gt; upload.txt&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;"last byte:    &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 1 upload.txt | &lt;span class="nb"&gt;od&lt;/span&gt; &lt;span class="nt"&gt;-An&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;/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;wc -l:        2
grep -c '':   3
awk END{NR}:  3
sed -n '$=':  3
last byte:       4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;wc -l&lt;/code&gt; says 2. &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt; and &lt;code&gt;sed&lt;/code&gt; say 3. The last check shows why they disagree: the final byte of the file is the character &lt;code&gt;4&lt;/code&gt; from &lt;code&gt;clip_0730.mp4&lt;/code&gt;, not a newline.&lt;/p&gt;

&lt;p&gt;None of these tools is broken. They are counting different things, and only one of them says so in its name.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a line is, according to POSIX
&lt;/h2&gt;

&lt;p&gt;POSIX defines a line as "A sequence of zero or more non- characters plus a terminating  character." The newline is part of the line, not a separator between lines.&lt;/p&gt;

&lt;p&gt;It also has a name for what is left over at the end of this file. An incomplete line is "A sequence of one or more non- characters at the end of the file." So by the standard's own words, &lt;code&gt;upload.txt&lt;/code&gt; holds two lines and one incomplete line.&lt;/p&gt;

&lt;p&gt;The GNU manual for &lt;code&gt;wc&lt;/code&gt; is just as plain about what &lt;code&gt;-l&lt;/code&gt; does: "Print only the newline character counts. If a file ends in a non-newline character, its trailing partial line is not counted."&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;wc -l&lt;/code&gt; is right, and it was answering the question its manual says it answers. It counts newline characters. A script that treats that number as the count of clips in the list is the thing that is wrong.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt; and &lt;code&gt;sed&lt;/code&gt; are more forgiving and treat the trailing text as a last line. That is useful, but it means a quick sanity check with one of them will agree with the eye and disagree with &lt;code&gt;wc -l&lt;/code&gt;, and the check that happens to be in the script decides which answer you get.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop that skips the last clip
&lt;/h2&gt;

&lt;p&gt;The upload loop is the usual safe form, with &lt;code&gt;IFS=&lt;/code&gt; and &lt;code&gt;-r&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/bash&lt;/span&gt;
&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"upload &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; &amp;lt; upload.txt
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"loop body ran &lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt; times; leftover f='&lt;/span&gt;&lt;span class="nv"&gt;$f&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;upload clip_0700.mp4
upload clip_0715.mp4
loop body ran 2 times; leftover f='clip_0730.mp4'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The body ran twice. After the loop, &lt;code&gt;f&lt;/code&gt; still holds &lt;code&gt;clip_0730.mp4&lt;/code&gt;, so the name was read. The body just did not get it.&lt;/p&gt;

&lt;p&gt;The Bash manual explains this in the &lt;code&gt;read&lt;/code&gt; entry. &lt;code&gt;read&lt;/code&gt; does assign what it got, and then: "The exit status is zero, unless end-of-file is encountered, read times out (in which case the status is greater than 128), a variable assignment error (such as assigning to a readonly variable) occurs, or an invalid file descriptor is supplied as the argument to -u."&lt;/p&gt;

&lt;p&gt;Reading the last name hits end-of-file before a newline, so &lt;code&gt;read&lt;/code&gt; returns non-zero, and &lt;code&gt;while&lt;/code&gt; takes any non-zero status as the signal to stop. Here it is with the loop removed:&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="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'clip_0730.mp4'&lt;/span&gt; | &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"no newline:   status=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt; f='&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="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'clip_0730.mp4\n'&lt;/span&gt; | &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"with newline: status=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt; f='&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="o"&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 newline:   status=1 f='clip_0730.mp4'
with newline: status=0 f='clip_0730.mp4'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same name, same variable, different status. The loop looks at the status and nothing else.&lt;/p&gt;

&lt;p&gt;A non-zero status at end-of-file is how every &lt;code&gt;while read&lt;/code&gt; loop is meant to finish, so the short pass and a complete one end the same way. There is no error to catch, which is why this kind of bug tends to be found by someone counting files by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two fixes on the reading side
&lt;/h2&gt;

&lt;p&gt;If you cannot control the writer, make the reader tolerate the missing newline:&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="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&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="o"&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;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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"upload &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; &amp;lt; upload.txt
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"loop body ran &lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt; times"&lt;/span&gt;

&lt;span class="nb"&gt;mapfile&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; clips &amp;lt; upload.txt
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"mapfile got &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; entries: &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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;upload clip_0700.mp4
upload clip_0715.mp4
upload clip_0730.mp4
loop body ran 3 times
mapfile got 3 entries: clip_0700.mp4 clip_0715.mp4 clip_0730.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-n&lt;/code&gt; test after &lt;code&gt;||&lt;/code&gt; keeps the loop going for one more pass when &lt;code&gt;read&lt;/code&gt; hit end-of-file but still put text in &lt;code&gt;f&lt;/code&gt;. On the next pass &lt;code&gt;read&lt;/code&gt; gets nothing, &lt;code&gt;f&lt;/code&gt; is empty, and the loop stops. The order matters: the test has to come after &lt;code&gt;read&lt;/code&gt; in the same condition, because it is checking what that &lt;code&gt;read&lt;/code&gt; just assigned.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mapfile -t&lt;/code&gt; reads the whole file into an array and kept all three names here. The manual describes &lt;code&gt;-t&lt;/code&gt; as "Remove a trailing delim (default newline) from each line read." It is a good fit when the list is small enough to hold in memory and the body does not need to run while the file is still being read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same byte breaks concatenation
&lt;/h2&gt;

&lt;p&gt;Lists like this often get joined, for example a day's file appended to a weekly one. &lt;code&gt;cat&lt;/code&gt; copies bytes and adds nothing between files:&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="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'clip_0800.mp4\nclip_0815.mp4\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; tuesday.txt
&lt;span class="nb"&gt;cat &lt;/span&gt;upload.txt tuesday.txt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; week.txt
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f&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;$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; &amp;lt; week.txt
&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_0700.mp4]
[clip_0715.mp4]
[clip_0730.mp4clip_0800.mp4]
[clip_0815.mp4]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There were four clips across the two files. The loop sees three names, and one of them is two clips glued together. An upload step would try to open a file called &lt;code&gt;clip_0730.mp4clip_0800.mp4&lt;/code&gt;, fail on it, and if that failure is handled per file, carry on. This is worse than the skipped clip because two clips are lost from one missing byte, and the error that does appear names a file nobody made.&lt;/p&gt;

&lt;h2&gt;
  
  
  A check for the end of a file
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;tail -c 1&lt;/code&gt; prints the last byte. Command substitution strips trailing newlines from what it captures, so if that byte is a newline the result is empty, and if it is anything else the result is not:&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="k"&gt;for &lt;/span&gt;file &lt;span class="k"&gt;in &lt;/span&gt;upload.txt tuesday.txt&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-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;tail&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 1 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&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="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;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;: last line has no newline"&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;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;: ends cleanly"&lt;/span&gt;
  &lt;span class="k"&gt;fi
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;upload.txt: last line has no newline
tuesday.txt: ends cleanly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-s&lt;/code&gt; test skips empty files, which have no last byte to check. This is cheap enough to run before every loop over a list you did not write.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix on the writing side
&lt;/h2&gt;

&lt;p&gt;The real fix is to have the writer end every line with a newline, including the last one:&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="n"&gt;names&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;clip_0700.mp4&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;clip_0715.mp4&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;clip_0730.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;with&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;upload.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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&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;name&lt;/span&gt; &lt;span class="ow"&gt;in&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;f&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="n"&gt;name&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After running that, the same counting and loop scripts agree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wc -l:        3
grep -c '':   3
awk END{NR}:  3
sed -n '$=':  3
last byte:      \n
upload clip_0700.mp4
upload clip_0715.mp4
upload clip_0730.mp4
loop body ran 3 times; leftover f=''
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every tool says 3, the loop uploads all three clips, and &lt;code&gt;f&lt;/code&gt; is empty after the loop because the last &lt;code&gt;read&lt;/code&gt; found nothing left.&lt;/p&gt;

&lt;p&gt;If you keep the &lt;code&gt;join&lt;/code&gt;, write one more newline after it. The point is the same either way: in a list of lines, the newline belongs to each line, not between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to change in a script like this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Write every line with its own newline, the last one included. In Python, add the newline to each name as you write it, or write one more after a &lt;code&gt;join&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In Bash, read lists with the &lt;code&gt;read -r&lt;/code&gt; loop plus its &lt;code&gt;-n&lt;/code&gt; test, or with &lt;code&gt;mapfile -t&lt;/code&gt;, so a list from somewhere else does not lose its last entry.&lt;/li&gt;
&lt;li&gt;Do not use &lt;code&gt;wc -l&lt;/code&gt; as a count of records unless you know the file ends with a newline. It counts newline characters, as its manual says.&lt;/li&gt;
&lt;li&gt;Check the last byte before concatenating lists, or join them with a tool that adds the missing newline, rather than plain &lt;code&gt;cat&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Compare the number of items processed with the number expected and log both. A loop that ends cleanly one item short gives no other sign.&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 shells.&lt;/strong&gt; I ran Bash 5.1. &lt;code&gt;dash&lt;/code&gt;, &lt;code&gt;zsh&lt;/code&gt; and others have their own &lt;code&gt;read&lt;/code&gt;. The quote above is from the Bash manual, and I did not check another shell's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other versions of the tools.&lt;/strong&gt; The &lt;code&gt;wc&lt;/code&gt; quote is from the current GNU manual, and I ran coreutils 8.32. The output matched the quote, but I did not run another version. I did not read POSIX's page for &lt;code&gt;wc&lt;/code&gt; itself, so I make no claim about non-GNU &lt;code&gt;wc&lt;/code&gt; here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other &lt;code&gt;awk&lt;/code&gt; and &lt;code&gt;grep&lt;/code&gt; builds.&lt;/strong&gt; I ran GNU awk and GNU grep. Other builds may treat a trailing incomplete line differently, and I did not test any.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows line endings.&lt;/strong&gt; A list written with carriage returns has a separate problem, a stray &lt;code&gt;\r&lt;/code&gt; on every name. I did not mix that in here.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; Definitions are from &lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap03.html" rel="noopener noreferrer"&gt;POSIX.1-2024 Base Definitions, chapter 3: Definitions&lt;/a&gt; (3.172 Incomplete Line, 3.185 Line). The &lt;code&gt;wc&lt;/code&gt; quote is from the GNU coreutils manual page &lt;a href="https://www.gnu.org/software/coreutils/wc" rel="noopener noreferrer"&gt;wc invocation&lt;/a&gt;. The &lt;code&gt;read&lt;/code&gt; and &lt;code&gt;mapfile&lt;/code&gt; quotes are from the GNU Bash Reference Manual page &lt;a href="https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html" rel="noopener noreferrer"&gt;Bash Builtins&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 list like this to name, 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 blaming the upload loop, look at the last byte of the list it reads: &lt;code&gt;join()&lt;/code&gt; puts the newline between names, so the last name it writes is, to &lt;code&gt;wc -l&lt;/code&gt; and &lt;code&gt;while read&lt;/code&gt;, not yet a line.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>linux</category>
      <category>python</category>
      <category>shell</category>
    </item>
    <item>
      <title>Why Does My Phone Security Camera Only Show a Reflection at Night? Recording Through a Window, Double Glazing and Mosquito Mesh</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Sun, 01 Nov 2026 08:13:12 +0000</pubDate>
      <link>https://dev.to/superfunicular/why-does-my-phone-security-camera-only-show-a-reflection-at-night-recording-through-a-window-2jd9</link>
      <guid>https://dev.to/superfunicular/why-does-my-phone-security-camera-only-show-a-reflection-at-night-recording-through-a-window-2jd9</guid>
      <description>&lt;p&gt;You set an old phone on the windowsill, point it at the gate, and check the footage the next morning. The daytime part looks fine. The night part shows your own living room: the ceiling light, the TV, the shelf, and maybe the phone itself, floating over a faint, dark street.&lt;/p&gt;

&lt;p&gt;Nothing is wrong with the phone. This is plain physics, and it happens to every camera that records through glass from a lit room. The good news is that the fixes are free, and most of them take a minute.&lt;/p&gt;

&lt;p&gt;This guide covers three things people run into when they put a phone camera behind a window: night reflections, the "double image" from double glazing, and the mosquito mesh or window grille that sits in front of so many windows in homes and shops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does my phone security camera only show a reflection at night?
&lt;/h2&gt;

&lt;p&gt;Because at night, the inside of your room is much brighter than the outside, and window glass is a weak mirror.&lt;/p&gt;

&lt;p&gt;Ordinary uncoated glass reflects about 4% of the light that hits it straight on, at each surface. That number comes from the Fresnel equations: with glass at a refractive index of about 1.5, the reflectance is ((1.5 − 1) / (1.5 + 1))² = 0.04. A single pane has two surfaces, front and back, so it reflects roughly 8% in total (&lt;a href="https://en.wikipedia.org/wiki/Fresnel_equations" rel="noopener noreferrer"&gt;Wikipedia: Fresnel equations&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In daylight, 8% does not matter. The street is thousands of times brighter than your room, so the weak mirror image of the room is lost under it.&lt;/p&gt;

&lt;p&gt;At night the balance flips. Here are some typical light levels from the &lt;a href="https://en.wikipedia.org/wiki/Lux" rel="noopener noreferrer"&gt;Wikipedia table on lux&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scene&lt;/th&gt;
&lt;th&gt;Illuminance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full moon on a clear night&lt;/td&gt;
&lt;td&gt;0.05–0.3 lux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dark limit of civil twilight&lt;/td&gt;
&lt;td&gt;3.4 lux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public areas with dark surroundings&lt;/td&gt;
&lt;td&gt;20–50 lux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Family living room lights&lt;/td&gt;
&lt;td&gt;50 lux&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now put those together in a simple model. I am treating the room and the street as if their surfaces reflect light equally well, which real scenes don't, so read these as rough ratios, not measurements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outside scene&lt;/th&gt;
&lt;th&gt;Glass&lt;/th&gt;
&lt;th&gt;Room reflection vs. outside scene&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Moonlit yard (0.3 lux)&lt;/td&gt;
&lt;td&gt;Single pane&lt;/td&gt;
&lt;td&gt;reflection ≈ 14× brighter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Moonlit yard (0.3 lux)&lt;/td&gt;
&lt;td&gt;Double glazing&lt;/td&gt;
&lt;td&gt;reflection ≈ 30× brighter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dusk (3.4 lux)&lt;/td&gt;
&lt;td&gt;Single pane&lt;/td&gt;
&lt;td&gt;about equal (1.25×)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lit street (20 lux)&lt;/td&gt;
&lt;td&gt;Single pane&lt;/td&gt;
&lt;td&gt;scene ≈ 5× brighter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(For double glazing I used four surfaces: 1 − 0.96⁴ ≈ 15% reflected. The script that made these numbers is one line of arithmetic per row; the room is 50 lux in every case.)&lt;/p&gt;

&lt;p&gt;That table explains almost every complaint. If the outside is lit by a streetlight, you usually see the street with a faint ghost of the room on top. If the outside is lit only by the moon, the room wins by a wide margin and the camera records a picture of your ceiling light.&lt;/p&gt;

&lt;p&gt;The camera's auto-exposure makes it worse, not better. It sets brightness for the whole frame, and the brightest thing in the frame is now the reflection, so it exposes for that. (If faces or objects come out black for a different reason, a bright background, see the companion piece: &lt;a href="https://dev.to/superfunicular/why-do-faces-come-out-black-on-my-phone-security-camera-backlight-auto-exposure-and-where-to-11kn"&gt;Why Do Faces Come Out Black on My Phone Security Camera?&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  The one fix that matters most: lens flat against the glass
&lt;/h2&gt;

&lt;p&gt;A reflection reaches the lens only if light from the room hits the glass in front of the lens and bounces back into it. If the lens is touching the glass, or nearly touching it, the patch of glass in front of the lens is covered by the phone itself. Room light cannot reach that patch, so there is nothing to bounce.&lt;/p&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Put the camera lens right against the glass.&lt;/strong&gt; A few millimetres is fine; a few centimetres is not. Every centimetre of gap lets room light in from the sides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block the sides with something dark.&lt;/strong&gt; A ring cut from black foam, a folded black cloth, or a strip of dark tape around the lens. Photographers use rubber lens hoods pressed against windows for exactly this reason. Your version can cost nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the phone square to the glass.&lt;/strong&gt; A small tilt is fine. A steep angle is not: glass reflects more as the angle gets shallower, which is why a window looks like a mirror when you look along it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you only do one thing from this article, do this one. It removes the problem rather than reducing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn off the light behind the camera
&lt;/h2&gt;

&lt;p&gt;If the lens can't sit flat on the glass (a deep window ledge, a curtain rail in the way), the next best fix is to take away the thing being reflected.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Switch off or dim the light directly behind the phone.&lt;/strong&gt; Ceiling lights and tube lights are the worst offenders, because they are the brightest objects in the room.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move lamps to the side.&lt;/strong&gt; A light that is out of the camera's reflected view does not appear in the footage, even if it is on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for small, bright sources.&lt;/strong&gt; A TV, a router's LEDs, a charger light, or a lit phone screen all show up as bright spots in a dark night frame.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is worth a moment. A phone recording with its screen on is itself a light source, sitting a few centimetres from the glass, aimed straight at it. Recording with the screen off removes that source entirely. That is one of the reasons Background Camera RemoteStream records with the screen off: no glowing screen on the windowsill, no screen reflection in the footage, and less heat and battery drain on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Double glazing: why you see a ghost of every streetlight
&lt;/h2&gt;

&lt;p&gt;Double-glazed windows have two panes and four surfaces. Putting the lens on the inner pane stops room reflections, but there is a second effect you can't fix the same way: bright outdoor lights bounce between the two panes and show up as a faint second copy, slightly offset from the real one.&lt;/p&gt;

&lt;p&gt;You'll notice it as a "ghost" streetlight or a double headlight. For a security camera, it is mostly cosmetic. It does not hide a person walking past. What helps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Aim so the brightest streetlight is near the edge of the frame or out of it.&lt;/strong&gt; Ghosts are offset in a consistent direction, so a light at the edge often has its ghost pushed out of the frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean both inner surfaces you can reach.&lt;/strong&gt; Smudges and dust spread a bright light into a haze that covers far more of the picture than a ghost does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for condensation.&lt;/strong&gt; A damp morning can fog the inner pane on the room side. Fog turns every outdoor light into a big glowing blob.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mosquito mesh and window grilles
&lt;/h2&gt;

&lt;p&gt;In many homes and shops, the window has a mosquito mesh, a metal grille, or both. These cause different problems depending on how far they are from the lens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mesh pressed against the lens.&lt;/strong&gt; Any object much closer than the lens's minimum focus distance is rendered as a heavy blur. So a mesh touching the lens does not appear as a sharp pattern. Instead it blocks some of the light and lowers contrast, like looking through a dirty glass. At night it also scatters streetlight into a soft glow. A phone camera's opening is only a few millimetres across (for example, a lens with a 4.7 mm focal length at f/1.8 has an entrance opening of about 4.7 / 1.8 ≈ 2.6 mm), so a mesh strand crossing it blocks a meaningful share of the light.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mesh or grille further away.&lt;/strong&gt; If the grille is, say, 20–40 cm in front of the phone, it may be far enough away that the camera can focus on it. In low light, autofocus has less to work with and may settle on the grille bars instead of the street, leaving the gate soft all night.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find a gap.&lt;/strong&gt; If the grille has openings bigger than the phone's lens, put the lens through a gap or right in the middle of one, so the bars are out of the view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you can't avoid the mesh, make it touch.&lt;/strong&gt; A mesh pressed flat against the lens is better than a mesh 5 cm away. Up close, it becomes a light blur; a short distance away, it becomes a visible pattern over everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open the mesh panel if it slides.&lt;/strong&gt; Many mesh frames slide or hinge. A small opening in one corner, with the phone inside it, solves the problem entirely, and mosquitoes will not care about a gap the size of a phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test focus at night, not in the daytime.&lt;/strong&gt; Autofocus that looks fine at noon can behave differently at 11 pm.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For developers: on Android's Camera2 API, &lt;code&gt;LENS_INFO_MINIMUM_FOCUS_DISTANCE&lt;/code&gt; tells you the closest distance a lens can focus, in diopters, and is 0 for a fixed-focus lens. Setting &lt;code&gt;CONTROL_AF_MODE_OFF&lt;/code&gt; and choosing a &lt;code&gt;LENS_FOCUS_DISTANCE&lt;/code&gt; lets an app hold focus at a set distance so it can't drift onto a grille in low light. (There's more on how focus, exposure and white balance behave over long recordings in &lt;a href="https://dev.to/superfunicular/camera2-api-handling-orientation-focus-and-exposure-in-background-how-to-keep-your-android-1612"&gt;Camera2 API: Handling Orientation, Focus, and Exposure in Background&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Offline security camera through a window: the night test
&lt;/h2&gt;

&lt;p&gt;Here is a five-minute test that tells you whether your setup works, without guessing. It needs no internet, no account, and no cloud.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set up the phone in the exact spot&lt;/strong&gt;, lens against the glass, at the time you'll actually use it: after dark, with the room lights as they normally are.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Start recording with the screen off.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at the live picture from another device on the same Wi-Fi.&lt;/strong&gt; Background Camera RemoteStream has a built-in web server, so you can open the live view in a browser on a second phone or laptop on your home network (the Play listing puts this browser view in the Pro tier). Without it, record for five minutes and play the clip back instead. The camera's video stays on the camera phone; nothing is uploaded anywhere. This also lets you check the frame without walking up to the window and lighting it with your own phone screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walk past the window outside&lt;/strong&gt; while someone watches the live view, or review the recording afterwards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change one thing at a time&lt;/strong&gt;: lights off behind the phone, a dark cloth around the lens, lens moved to a grille gap. Re-check after each change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want an off-site copy as well, the app can also stream to YouTube Live (also listed under Pro), but that needs a working internet connection. Local recording and the LAN live view work without one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the glass cannot fix
&lt;/h2&gt;

&lt;p&gt;Being honest about the limits saves wasted evenings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Glass blocks some light.&lt;/strong&gt; Each surface reflects about 4%, so a single pane passes about 92% and double glazing about 85%. On a moonlit night, that loss sits on top of a scene that is already very dark. For what phones can and can't see at night in general, read &lt;a href="https://dev.to/superfunicular/can-an-old-android-phone-see-in-the-dark-an-honest-guide-to-overnight-recording-2026-490n"&gt;Can an Old Android Phone See in the Dark?&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tinted or coated glass is different.&lt;/strong&gt; Some windows have coatings or films that reflect more than plain glass. The lens-on-glass fix still works best, but expect a darker picture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most phones have no infrared light of their own.&lt;/strong&gt; A dedicated CCTV camera with infrared LEDs behind glass has the opposite problem: its own IR bounces straight back off the window. A phone without an IR illuminator doesn't have that issue, but it also can't light up a dark yard. A small outdoor light on a sensor helps more than any setting.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Lens touching the glass (or within a few millimetres)&lt;/li&gt;
&lt;li&gt;Dark foam, cloth or tape around the lens&lt;/li&gt;
&lt;li&gt;Light directly behind the phone off or moved&lt;/li&gt;
&lt;li&gt;Screen off while recording&lt;/li&gt;
&lt;li&gt;Lens in a grille gap, or mesh pressed flat against it&lt;/li&gt;
&lt;li&gt;Glass clean on both sides you can reach&lt;/li&gt;
&lt;li&gt;Night test done, checked live or from a playback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're setting up an old phone as a 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 here for the window.&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>camera</category>
    </item>
    <item>
      <title>clip_08.mp4 Stopped My Bash Loop and the Script Still Exited 0: A Leading Zero Means Octal in Bash Arithmetic</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Sun, 01 Nov 2026 07:55:12 +0000</pubDate>
      <link>https://dev.to/superfunicular/clip08mp4-stopped-my-bash-loop-and-the-script-still-exited-0-a-leading-zero-means-octal-in-bash-35i5</link>
      <guid>https://dev.to/superfunicular/clip08mp4-stopped-my-bash-loop-and-the-script-still-exited-0-a-leading-zero-means-octal-in-bash-35i5</guid>
      <description>&lt;p&gt;A small Bash loop takes zero-padded camera clip names, pulls out the number, and works out the number of the clip that comes next. It handles &lt;code&gt;clip_07.mp4&lt;/code&gt;. At &lt;code&gt;clip_08.mp4&lt;/code&gt; it prints one error line, skips &lt;code&gt;clip_08&lt;/code&gt;, &lt;code&gt;clip_09&lt;/code&gt; and &lt;code&gt;clip_10&lt;/code&gt; without another word, carries on with the rest of the script, and exits 0.&lt;/p&gt;

&lt;p&gt;Here is the loop:&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;# for each clip, work out the number of the clip that follows it&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;clip_07.mp4 clip_08.mp4 clip_09.mp4 clip_10.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;&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="p"&gt;#clip_&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&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;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="p"&gt;%.mp4&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;"start &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;next&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;"  next is &lt;/span&gt;&lt;span class="nv"&gt;$next&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;"after the loop"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with &lt;code&gt;bash next_clip.sh&lt;/code&gt;, then print &lt;code&gt;$?&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;start clip_07.mp4
  next is 8
start clip_08.mp4
next_clip.sh: line 6: 08: value too great for base (error token is "08")
after the loop
exit status: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things went wrong, and the second is the one that costs you clips. The first is that Bash read &lt;code&gt;08&lt;/code&gt; as an octal number and rejected it. The second is that the error did not stop the script. It abandoned the loop, and the script went on to its last line as if the loop had finished.&lt;/p&gt;

&lt;p&gt;Disclosure: I make an Android camera app. Nothing below uses its code or describes how it names, numbers or stores clips. The clip names are made up, 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
date (GNU coreutils) 8.32
expr (GNU coreutils) 8.32
GNU Awk 5.1.0, API: 3.0 (GNU MPFR 4.1.0, GNU MP 6.2.1)
Python 3.10.12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A leading zero means octal
&lt;/h2&gt;

&lt;p&gt;The Bash Reference Manual's section on shell arithmetic is short about this: "Constants with a leading 0 are interpreted as octal numbers." That sentence has been in the manual since at least the 2.05a edition, which is the copy I read it in.&lt;/p&gt;

&lt;p&gt;Octal digits run from 0 to 7. So &lt;code&gt;07&lt;/code&gt; is a fine octal number, &lt;code&gt;08&lt;/code&gt; and &lt;code&gt;09&lt;/code&gt; are not numbers at all, and &lt;code&gt;010&lt;/code&gt; is a number, but it is eight, not ten:&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="k"&gt;for &lt;/span&gt;n &lt;span class="k"&gt;in &lt;/span&gt;07 08 09 010 012&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;"n=&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;((n + 1)) -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;echo&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="o"&gt;)&lt;/span&gt; 2&amp;gt;&amp;amp;1 &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;n=07  $((n + 1)) -&amp;gt; 8
n=08  $((n + 1)) -&amp;gt; constants.sh: line 3: 08: value too great for base (error token is "08")
n=09  $((n + 1)) -&amp;gt; constants.sh: line 3: 09: value too great for base (error token is "09")
n=010  $((n + 1)) -&amp;gt; 9
n=012  $((n + 1)) -&amp;gt; 11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The errors are the lucky case. &lt;code&gt;08&lt;/code&gt; and &lt;code&gt;09&lt;/code&gt; fail loudly. &lt;code&gt;010&lt;/code&gt; and &lt;code&gt;012&lt;/code&gt; do not fail. They come back as 9 and 11, the right answers for octal eight and ten, plus one. A loop that walks &lt;code&gt;clip_010.mp4&lt;/code&gt;, &lt;code&gt;clip_011.mp4&lt;/code&gt; and so on with three-digit padding will compute wrong numbers with no error at all, until it reaches a name with an 8 or a 9 in it.&lt;/p&gt;

&lt;p&gt;This is why the bug looks random. With two-digit padding, clips 01 to 07 work, clips 08 and 09 break, and from clip 10 onward everything works again, because &lt;code&gt;10&lt;/code&gt; has no leading zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The error abandons the loop, not the script
&lt;/h2&gt;

&lt;p&gt;The part I did not expect was what happened after the error. &lt;code&gt;clip_08.mp4&lt;/code&gt; printed &lt;code&gt;start&lt;/code&gt;, then the error, and then nothing else from the loop. &lt;code&gt;clip_09&lt;/code&gt; and &lt;code&gt;clip_10&lt;/code&gt; did not print &lt;code&gt;start&lt;/code&gt; at all. The line after the loop ran, and the exit status was 0.&lt;/p&gt;

&lt;p&gt;That 0 is the status of the last command, the final &lt;code&gt;echo&lt;/code&gt;. The failure left one line on stderr and nothing in the exit status. In a cron job whose stderr goes nowhere, that is a silent skip.&lt;/p&gt;

&lt;p&gt;Bash has a mode that treats this differently. In the list of things that change in POSIX mode, the same manual says: "Non-interactive shells exit if a syntax error in an arithmetic expansion results in an invalid expression." I ran the same script three ways:&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="k"&gt;for &lt;/span&gt;sh &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"bash"&lt;/span&gt; &lt;span class="s2"&gt;"bash --posix"&lt;/span&gt; &lt;span class="s2"&gt;"dash"&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"== &lt;/span&gt;&lt;span class="nv"&gt;$sh&lt;/span&gt;&lt;span class="s2"&gt; next_clip.sh"&lt;/span&gt;
  &lt;span class="nv"&gt;$sh&lt;/span&gt; next_clip.sh 2&amp;gt;&amp;amp;1
  &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;== bash next_clip.sh
start clip_07.mp4
  next is 8
start clip_08.mp4
next_clip.sh: line 6: 08: value too great for base (error token is "08")
after the loop
exit status: 0
== bash --posix next_clip.sh
start clip_07.mp4
  next is 8
start clip_08.mp4
next_clip.sh: line 6: 08: value too great for base (error token is "08")
exit status: 1
== dash next_clip.sh
start clip_07.mp4
  next is 8
start clip_08.mp4
next_clip.sh: 6: Illegal number: 08
exit status: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default Bash abandoned the loop and kept going. &lt;code&gt;bash --posix&lt;/code&gt; stopped the script with status 1. &lt;code&gt;dash&lt;/code&gt;, which is &lt;code&gt;/bin/sh&lt;/code&gt; on this system, stopped with status 2. So the same file behaves differently depending on which shell runs it, and the one that hid the failure here was plain &lt;code&gt;bash&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I did not test &lt;code&gt;set -e&lt;/code&gt; here. I am not making a claim about whether it would have stopped this script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not every comparison does arithmetic
&lt;/h2&gt;

&lt;p&gt;The next surprise is that the three common ways to compare numbers in Bash do not agree about &lt;code&gt;08&lt;/code&gt; or about &lt;code&gt;010&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/bash&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;n &lt;span class="k"&gt;in &lt;/span&gt;08 010&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;'n=%-4s'&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="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'[ -lt 9 ]: '&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;[&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="nt"&gt;-lt&lt;/span&gt; 9 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'yes  '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'no  '&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'[[ -lt 9 ]]: '&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 9 &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'yes  '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'no  '&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &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="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'(( n &amp;lt; 9 )): '&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; n &amp;lt; 9 &lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'yes'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'no'&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &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="nb"&gt;echo
&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;n=08  [ -lt 9 ]: yes  [[ -lt 9 ]]: compare.sh: line 5: [[: 08: value too great for base (error token is "08") no  (( n &amp;lt; 9 )): compare.sh: line 6: ((: 08: value too great for base (error token is "08") no
n=010 [ -lt 9 ]: no  [[ -lt 9 ]]: yes  (( n &amp;lt; 9 )): yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The single-bracket &lt;code&gt;test&lt;/code&gt; read &lt;code&gt;08&lt;/code&gt; as eight and &lt;code&gt;010&lt;/code&gt; as ten. It compared them as decimal integers here.&lt;/li&gt;
&lt;li&gt;The double-bracket test and &lt;code&gt;(( ))&lt;/code&gt; both read them as arithmetic. &lt;code&gt;08&lt;/code&gt; was an error, and &lt;code&gt;010&lt;/code&gt; was eight, so it came out less than 9.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So for &lt;code&gt;010&lt;/code&gt; the single-bracket test said no and the double-bracket test said yes, on the same shell and the same value. Moving a test from single to double brackets changes the answer for zero-padded input.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hour field does this every morning
&lt;/h2&gt;

&lt;p&gt;The most common place to meet this is &lt;code&gt;date +%H&lt;/code&gt;, which pads the hour to two digits. An hourly job that does arithmetic on the hour works all day except at 8 and 9 in the morning:&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;# what an hourly job sees at half past each hour&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;h &lt;span class="k"&gt;in &lt;/span&gt;07 08 09 10&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;hh&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; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"2026-10-29 &lt;/span&gt;&lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="s2"&gt;:30"&lt;/span&gt; +%H&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;bare&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; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"2026-10-29 &lt;/span&gt;&lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="s2"&gt;:30"&lt;/span&gt; +%-H&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"%H=&lt;/span&gt;&lt;span class="nv"&gt;$hh&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;((hh + 1)) -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;hh &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="o"&gt;)&lt;/span&gt; 2&amp;gt;&amp;amp;1 &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;"       %-H=&lt;/span&gt;&lt;span class="nv"&gt;$bare&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;((bare + 1)) -&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;bare &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="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;%H=07  $((hh + 1)) -&amp;gt; 8
       %-H=7  $((bare + 1)) -&amp;gt; 8
%H=08  $((hh + 1)) -&amp;gt; hours.sh: line 6: 08: value too great for base (error token is "08")
       %-H=8  $((bare + 1)) -&amp;gt; 9
%H=09  $((hh + 1)) -&amp;gt; hours.sh: line 6: 09: value too great for base (error token is "09")
       %-H=9  $((bare + 1)) -&amp;gt; 10
%H=10  $((hh + 1)) -&amp;gt; 11
       %-H=10  $((bare + 1)) -&amp;gt; 11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The times are pinned with &lt;code&gt;-d&lt;/code&gt; and &lt;code&gt;-u&lt;/code&gt; so the output does not depend on when you run it. &lt;code&gt;%H&lt;/code&gt; gives &lt;code&gt;08&lt;/code&gt; and &lt;code&gt;09&lt;/code&gt;, and both fail. GNU &lt;code&gt;date&lt;/code&gt; also accepts &lt;code&gt;%-H&lt;/code&gt;, where the hyphen asks for no padding, and that version works for every hour. The same hyphen worked on other fields I tried, &lt;code&gt;%-M&lt;/code&gt; for minutes and &lt;code&gt;%-d&lt;/code&gt; for the day of the month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other tools read the same string as decimal
&lt;/h2&gt;

&lt;p&gt;Bash's own &lt;code&gt;printf&lt;/code&gt; builtin agrees with Bash arithmetic. Most other tools do not:&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="k"&gt;for &lt;/span&gt;n &lt;span class="k"&gt;in &lt;/span&gt;08 010&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;"n=&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;"  printf %d:  &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%d'&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;)&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  expr:       &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;expr&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; + 1&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;"  awk:        &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&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="s1"&gt;'BEGIN { print n + 1 }'&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;"  python int: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"print(int('&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;') + 1)"&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;n=08
  printf %d:  other_tools.sh: line 4: printf: 08: invalid octal number 0
  expr:       9
  awk:        9
  python int: 9
n=010
  printf %d:  8
  expr:       11
  awk:        11
  python int: 11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;printf '%d' 08&lt;/code&gt; printed an error and then a &lt;code&gt;0&lt;/code&gt;, so a script using its output would carry on with zero. &lt;code&gt;printf '%d' 010&lt;/code&gt; printed 8. &lt;code&gt;expr&lt;/code&gt;, GNU &lt;code&gt;awk&lt;/code&gt; and Python's &lt;code&gt;int()&lt;/code&gt; all read both values as decimal.&lt;/p&gt;

&lt;p&gt;That makes handing the number to another tool a real fix in some scripts, but it also means you cannot assume two parts of one pipeline read a number the same way. If one step uses &lt;code&gt;$(( ))&lt;/code&gt; and another uses &lt;code&gt;awk&lt;/code&gt;, they can disagree on the same clip number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forcing base ten
&lt;/h2&gt;

&lt;p&gt;Bash lets you name the base of a number with &lt;code&gt;base#value&lt;/code&gt;, and the manual describes that form in the sentences right after the octal rule. &lt;code&gt;10#08&lt;/code&gt; is eight read in base ten, and with it the loop works for every 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="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;clip_07.mp4 clip_08.mp4 clip_09.mp4 clip_10.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;&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="p"&gt;#clip_&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&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;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="p"&gt;%.mp4&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;next&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="c"&gt;#$n + 1))&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s -&amp;gt; clip_%02d.mp4\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="nv"&gt;$next&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_07.mp4 -&amp;gt; clip_08.mp4
clip_08.mp4 -&amp;gt; clip_09.mp4
clip_09.mp4 -&amp;gt; clip_10.mp4
clip_10.mp4 -&amp;gt; clip_11.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;printf '%02d'&lt;/code&gt; puts the padding back for the next name. Note that this is printing a number that is already decimal, so the octal problem does not apply to it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;10#&lt;/code&gt; has edges of its own. On this version it rejects an empty value and a value with a sign in front, and it fails on anything that is not digits:&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="k"&gt;for &lt;/span&gt;n &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"08"&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="s2"&gt;"-08"&lt;/span&gt; &lt;span class="s2"&gt;"x8"&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"n='&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;'  &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;((10#&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;n + 1)) -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="c"&gt;#$n + 1))) 2&amp;gt;&amp;amp;1 )"&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;n='08'  $((10#$n + 1)) -&amp;gt; 9
n=''  $((10#$n + 1)) -&amp;gt; edges.sh: line 3: 10#: invalid integer constant (error token is "10#")
n='-08'  $((10#$n + 1)) -&amp;gt; edges.sh: line 3: 10#: invalid integer constant (error token is "10#")
n='x8'  $((10#$n + 1)) -&amp;gt; edges.sh: line 3: 10#x8: value too great for base (error token is "10#x8")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty value is what you get when the name does not match the pattern you stripped, so in a real loop it is the likely case, not an exotic one. A minus sign could come from a name like &lt;code&gt;clip_-08.mp4&lt;/code&gt; or from a subtraction earlier in the script. Newer Bash releases may handle the signed case differently. I did not test any other version, and I make no claim about them.&lt;/p&gt;

&lt;p&gt;The safest shape I found is to check that the value is digits before any arithmetic, and to make the check fail in a way the caller can see:&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;
next_number&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^[0-9]+&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt; &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;"not a clip number: '&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;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="c"&gt;#$n + 1))&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;n &lt;span class="k"&gt;in &lt;/span&gt;&lt;span class="m"&gt;08&lt;/span&gt; &lt;span class="m"&gt;09&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; x8&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;next_number &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="si"&gt;)&lt;/span&gt; &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;"'&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;' -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&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;"'&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;' -&amp;gt; skipped, 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;'08' -&amp;gt; 9
'09' -&amp;gt; 10
not a clip number: ''
'' -&amp;gt; skipped, status 1
not a clip number: 'x8'
'x8' -&amp;gt; skipped, status 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bad input now produces a message and a non-zero status for that one call, and the loop moves on to the next name on purpose rather than by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to change in a script like this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Assume a padded value is a string, not a number, until it is converted. Clip numbers, hours from &lt;code&gt;date +%H&lt;/code&gt;, minutes, days of the month and parts of a file name are all likely to be padded.&lt;/li&gt;
&lt;li&gt;Convert with &lt;code&gt;$((10#$n))&lt;/code&gt;, and check that &lt;code&gt;$n&lt;/code&gt; is only digits before you do.&lt;/li&gt;
&lt;li&gt;When you only need the number for display or comparison, ask for it unpadded, as in &lt;code&gt;date +%-H&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Do not switch a numeric test from single to double brackets on padded input without checking, because the two can give different answers.&lt;/li&gt;
&lt;li&gt;Count the items your loop processed and compare that with the number you expected, and log both. A loop that is abandoned partway leaves no other sign in the exit status.&lt;/li&gt;
&lt;li&gt;If a script must stop on an arithmetic error, test it under the shell that actually runs it. Default Bash, &lt;code&gt;bash --posix&lt;/code&gt; and &lt;code&gt;dash&lt;/code&gt; did three different things here.&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 Bash 5.1. The octal sentence is old and I expect it to hold, but I did not run another release, and the behavior of &lt;code&gt;10#&lt;/code&gt; with a sign or an empty value is the part most likely to differ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;set -e&lt;/code&gt; and &lt;code&gt;errexit&lt;/code&gt; traps.&lt;/strong&gt; I did not check whether either would have stopped the default-mode script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;zsh, ksh and BusyBox.&lt;/strong&gt; I ran Bash and dash only. Other shells have their own rules for leading zeros, and I make no claim about them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-GNU &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;expr&lt;/code&gt; and &lt;code&gt;awk&lt;/code&gt;.&lt;/strong&gt; The &lt;code&gt;%-H&lt;/code&gt; hyphen is a GNU &lt;code&gt;date&lt;/code&gt; feature here, and I did not test any other &lt;code&gt;date&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; Both quotes are from the GNU Bash Reference Manual, read in the archived &lt;a href="https://ftp.gnu.org/old-gnu/Manuals/bash-2.05a/html_chapter/bashref_6.html" rel="noopener noreferrer"&gt;2.05a edition, chapter 6: Bash Features&lt;/a&gt; (sections 6.5 Shell Arithmetic and 6.11 Bash POSIX Mode). The current manual's section is &lt;a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Arithmetic.html" rel="noopener noreferrer"&gt;Shell Arithmetic&lt;/a&gt;; that page did not load for me while checking, so the quotes are from the archived copy. 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 numbered clips for a loop like this to walk, 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;Until &lt;code&gt;10#&lt;/code&gt; turns it into a decimal, a padded clip number is octal to Bash, and the clip after &lt;code&gt;clip_07&lt;/code&gt; is where the loop quietly stops.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>linux</category>
      <category>shell</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Why Do Faces Come Out Black on My Phone Security Camera? Backlight, Auto-Exposure, and Where to Point the Phone</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Sat, 31 Oct 2026 17:26:46 +0000</pubDate>
      <link>https://dev.to/superfunicular/why-do-faces-come-out-black-on-my-phone-security-camera-backlight-auto-exposure-and-where-to-11kn</link>
      <guid>https://dev.to/superfunicular/why-do-faces-come-out-black-on-my-phone-security-camera-backlight-auto-exposure-and-where-to-11kn</guid>
      <description>&lt;p&gt;You set up an old Android phone as a camera at the shop door or the front entrance. The picture looks fine on the shelf. Then you check the recording: the street outside is bright and clear, and the person who walked in is a dark shape with no face.&lt;/p&gt;

&lt;p&gt;This is not a broken camera, and it is not low light. It is &lt;strong&gt;backlight&lt;/strong&gt;, and it is one of the most common reasons a phone security camera records footage you cannot use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do faces come out black on a phone security camera?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; the phone sets one brightness for the whole picture. If a bright doorway, window, glass shop front or sky fills a large part of the frame, the camera darkens everything to keep that bright area from turning white. The person standing in front of it is much darker than the background, so they drop to near-black. The fix is mostly placement: put the bright light &lt;em&gt;behind or beside the camera&lt;/em&gt;, not behind the person, and keep bright areas small in the frame.&lt;/p&gt;

&lt;p&gt;The rest of this guide explains why, shows the numbers, gives a 10-minute test, and (for developers) lists the Camera2 controls that affect it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the phone is actually doing
&lt;/h2&gt;

&lt;p&gt;Every Android camera runs an &lt;strong&gt;auto-exposure&lt;/strong&gt; routine (AE). Its job is to pick a shutter time and sensor gain so the picture is "correctly" bright. In Android's Camera2 API, when AE is on, Google's reference says it overrides the exposure time, sensitivity and frame duration the app asks for. The phone, not the app, decides.&lt;/p&gt;

&lt;p&gt;To decide, AE measures the brightness of the scene. How each phone weighs the frame is the manufacturer's own algorithm, and it is not published. But the basic behaviour is the same everywhere: a large bright area pulls the whole exposure down.&lt;/p&gt;

&lt;p&gt;Brightness in photography is counted in &lt;strong&gt;EV&lt;/strong&gt; (exposure value). Android's own documentation puts it simply: one EV step doubles the image brightness, and minus one EV halves it. So:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;th&gt;Brightness factor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 EV&lt;/td&gt;
&lt;td&gt;2×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 EV&lt;/td&gt;
&lt;td&gt;4×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 EV&lt;/td&gt;
&lt;td&gt;8×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 EV&lt;/td&gt;
&lt;td&gt;16×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 EV&lt;/td&gt;
&lt;td&gt;32×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A sunny street seen through a shop door is easily several EV brighter than the shaded room inside. That is the gap the camera has to deal with.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple model of the problem
&lt;/h2&gt;

&lt;p&gt;Real phones do not just average the frame, so treat this as an illustration, not a measurement of any phone. Imagine the phone averages brightness and sets the average to its target.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;70% of the frame is the room (brightness 1).&lt;/li&gt;
&lt;li&gt;30% of the frame is the doorway, 5 EV brighter (brightness 32).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The average is 0.7 × 1 + 0.3 × 32 = &lt;strong&gt;10.3&lt;/strong&gt;. To bring that average to the target, the camera darkens everything by about 10×. The room, and the person in it, end up at roughly &lt;strong&gt;one tenth&lt;/strong&gt; of normal brightness, about &lt;strong&gt;−3.4 EV&lt;/strong&gt;. That is a silhouette.&lt;/p&gt;

&lt;p&gt;The same model with other numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bright area of frame&lt;/th&gt;
&lt;th&gt;Bright area is…&lt;/th&gt;
&lt;th&gt;Room/person ends up at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;3 EV brighter&lt;/td&gt;
&lt;td&gt;−0.8 EV (slightly dark)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;5 EV brighter&lt;/td&gt;
&lt;td&gt;−2.0 EV (dark)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;3 EV brighter&lt;/td&gt;
&lt;td&gt;−1.6 EV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;5 EV brighter&lt;/td&gt;
&lt;td&gt;−3.4 EV (silhouette)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;5 EV brighter&lt;/td&gt;
&lt;td&gt;−4.0 EV (silhouette)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things stand out. &lt;strong&gt;Size matters&lt;/strong&gt;: a small bright patch does little harm. &lt;strong&gt;Brightness gap matters&lt;/strong&gt;: the same doorway at dusk is fine, and at noon it ruins the shot. That is why a camera can look perfect when you set it up in the evening and fail the next afternoon.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(These figures were computed with a short script for this article. They come from the simplified averaging model above, not from any real phone.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just make it brighter" is not enough
&lt;/h2&gt;

&lt;p&gt;Many camera apps and phones offer exposure compensation (the +/− brightness slider). In the model above, +2 EV brings the room from −3.4 EV to about −1.4 EV. Better, but still dark.&lt;/p&gt;

&lt;p&gt;And the doorway, which was already above target, goes from about +1.6 EV to about &lt;strong&gt;+3.6 EV&lt;/strong&gt;: now the street is plain white. You traded a black face for a white background. With a strong backlight, the phone's sensor cannot hold both ends at once. Changing the brightness only chooses which end you lose.&lt;/p&gt;

&lt;p&gt;So the real fixes are about light and position.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: Put the light behind the camera, not behind the person
&lt;/h2&gt;

&lt;p&gt;This is the single most effective change.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shop with a glass front:&lt;/strong&gt; do not mount the phone at the back of the shop looking out at the street. Mount it near the front, high up, looking &lt;em&gt;in&lt;/em&gt; toward the counter and the door from the inside edge. Customers walking in are then lit by the daylight coming through the glass, instead of standing in front of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Home entrance:&lt;/strong&gt; point the camera at the door from the side, not from the far end of a hallway with the door's window in the middle of the frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Window:&lt;/strong&gt; a window in the frame is a bright panel all day. Turn the phone so the window is at the edge of the picture, or out of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fix 2: Make the bright area smaller
&lt;/h2&gt;

&lt;p&gt;If you cannot move the camera, change what it sees.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tilt the phone down a little so less sky, street or doorway is in frame.&lt;/li&gt;
&lt;li&gt;Frame tighter on the area that matters: the counter, the cash drawer, the inside of the door.&lt;/li&gt;
&lt;li&gt;A curtain, a blind half-closed, or a tinted film on a glass door reduces the brightness gap. You are lowering the 5 EV, not fighting it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fix 3: Add light on the person's side
&lt;/h2&gt;

&lt;p&gt;A cheap LED bulb or tube light inside the entrance, on the camera's side, lifts the face toward the background's brightness. Narrowing the gap by even 1–2 EV can turn a silhouette into a recognisable face. In shops this often costs less than any camera upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 4: Test at the worst hour
&lt;/h2&gt;

&lt;p&gt;A backlit camera fails at a particular time of day, so test then.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find the hour when the bright area is brightest: midday for a door facing the sun, late afternoon for a west-facing window, evening if headlights shine in.&lt;/li&gt;
&lt;li&gt;Start the recording and stand where a visitor would stand, facing the camera, for 30 seconds. Then walk in and out once.&lt;/li&gt;
&lt;li&gt;Stop and play the clip. Pause on your own face. Can you recognise yourself? Can you read printed text held at chest height?&lt;/li&gt;
&lt;li&gt;Change one thing (angle, tilt, light) and repeat. Keep the setup that passes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ten minutes at the worst hour tells you more than a full day of guessing. If the camera supports live view in a browser on your Wi-Fi, you can watch the result from another phone while you adjust, without touching the camera phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does this work as an offline security camera without internet?
&lt;/h2&gt;

&lt;p&gt;Yes. Backlight is about light and position, and none of these fixes need internet or a cloud service. A phone recording to its own storage on a local network, or fully offline, behaves exactly the same. The test above works with no data plan at all. If you are setting up a shop camera with no internet plan, see &lt;a href="https://dev.to/superfunicular/how-to-watch-your-shop-with-an-old-android-phone-no-internet-plan-no-subscription-no-credit-card-1bdl"&gt;how to watch your shop with an old Android phone, no internet plan, no subscription&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  For developers: the Camera2 controls involved
&lt;/h2&gt;

&lt;p&gt;If you build camera apps, here is what Android's Camera2 reference says about the controls that touch this problem. All of this is from Google's public API documentation for &lt;code&gt;CaptureRequest&lt;/code&gt; and &lt;code&gt;CameraCharacteristics&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;CONTROL_AE_REGIONS&lt;/code&gt;, metering areas.&lt;/strong&gt; An app can tell AE which part of the frame to measure, using weighted rectangles. A region on the doorway's inside edge, or the counter, would tell AE to expose for the person rather than the street. Two catches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is only available if &lt;code&gt;CONTROL_MAX_REGIONS_AE&lt;/code&gt; is greater than 0. The docs only guarantee at least 1 region on FULL-capability devices. On many older budget phones (often LEGACY or LIMITED), you may get &lt;strong&gt;zero&lt;/strong&gt; metering regions. Check before relying on it.&lt;/li&gt;
&lt;li&gt;The coordinates are in the sensor's active array, not your preview. The docs warn that if the preview aspect ratio differs from the crop region (for example 16:9 preview from a 4:3 sensor), you must account for the extra crop. Get this wrong and your region meters the wrong part of the scene.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;CONTROL_AE_EXPOSURE_COMPENSATION&lt;/code&gt;, the brightness offset.&lt;/strong&gt; Counted in steps of &lt;code&gt;CONTROL_AE_COMPENSATION_STEP&lt;/code&gt; (often 1/3 or 1/2 EV). If &lt;code&gt;CONTROL_AE_COMPENSATION_RANGE&lt;/code&gt; is &lt;code&gt;[0, 0]&lt;/code&gt;, compensation is not supported. For LIMITED and FULL devices that support it, the range must reach at least −2 EV and +2 EV; the docs say LEGACY devices may support a smaller range. After a change, AE can take several frames to settle. As shown above, compensation alone cannot rescue a strong backlight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;CONTROL_AE_LOCK&lt;/code&gt;, freezing exposure.&lt;/strong&gt; Tempting: set it once when the scene looks right and lock it. For a 24/7 camera that is a trap. A lock keeps the exposure from one moment, and daylight changes for hours afterwards. A setting locked at noon will be far too dark at dusk. Support is reported by &lt;code&gt;CONTROL_AE_LOCK_AVAILABLE&lt;/code&gt;, which is guaranteed true only on devices with MANUAL_SENSOR or BURST_CAPTURE capability.&lt;/p&gt;

&lt;p&gt;The practical lesson for app developers matches the one for users: metering tricks help at the margin, but on old phones you cannot assume they exist, and no software setting removes a 5 EV gap. Placement guidance is worth more than an exposure slider.&lt;/p&gt;

&lt;p&gt;Related: low light is a different problem from backlight. For dark scenes, see &lt;a href="https://dev.to/superfunicular/correcting-our-own-low-light-guide-brightening-live-video-on-android-no-longer-has-to-leave-the-3pcp"&gt;our correction on brightening live video in low light&lt;/a&gt; and &lt;a href="https://dev.to/superfunicular/why-does-my-phone-security-camera-video-look-smeary-at-night-noise-reduction-edge-sharpening-and-51gn"&gt;why night footage looks smeary&lt;/a&gt;. If your frame looks narrower in the recording than in the camera app, that is &lt;a href="https://dev.to/superfunicular/why-does-my-phone-security-camera-show-less-of-the-room-video-stabilization-crops-the-edges-and-a-3ha2"&gt;video stabilization cropping the edges&lt;/a&gt;, which also changes how much bright area ends up in the shot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Background Camera RemoteStream fits
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam" rel="noopener noreferrer"&gt;Background Camera RemoteStream&lt;/a&gt; turns a spare Android phone into a camera that keeps recording with the screen off, stores the video on the phone itself (no cloud account needed), and can show the camera live in a browser on your own Wi-Fi. If you want people outside your Wi-Fi to watch live, it can stream to YouTube Live. Check the Play listing for which features are in the free version and which are in Pro.&lt;/p&gt;

&lt;p&gt;Live view is useful for exactly this job: you can stand at the door and check on another phone whether your face is visible, then move the camera until it is. More setup guides: &lt;a href="https://superfunicular.com" rel="noopener noreferrer"&gt;superfunicular.com&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this guide does not tell you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How your phone's auto-exposure weighs the frame.&lt;/strong&gt; That algorithm is the manufacturer's and is not published. The table above is a simplified averaging model, not a measurement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether your phone supports metering regions or exposure compensation.&lt;/strong&gt; That depends on the camera hardware level. The only reliable answer is to query the device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How a specific app uses these controls.&lt;/strong&gt; This guide describes the Android API, not any app's internals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Sources: Android Developers API reference, &lt;code&gt;CaptureRequest&lt;/code&gt; (CONTROL_AE_MODE, CONTROL_AE_REGIONS, CONTROL_AE_EXPOSURE_COMPENSATION, CONTROL_AE_LOCK) and &lt;code&gt;CameraCharacteristics&lt;/code&gt; (CONTROL_AE_COMPENSATION_RANGE, CONTROL_AE_COMPENSATION_STEP, CONTROL_MAX_REGIONS_AE, CONTROL_AE_LOCK_AVAILABLE), read on 31 October 2026. EV tables computed with a short script for this article.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>camera</category>
      <category>security</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Python's round() and JavaScript's Math.round() Disagree on Half the Days in a Footage Summary</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Thu, 29 Oct 2026 16:56:50 +0000</pubDate>
      <link>https://dev.to/superfunicular/pythons-round-and-javascripts-mathround-disagree-on-half-the-days-in-a-footage-summary-epf</link>
      <guid>https://dev.to/superfunicular/pythons-round-and-javascripts-mathround-disagree-on-half-the-days-in-a-footage-summary-epf</guid>
      <description>&lt;p&gt;Here is a four-day summary of camera footage, in minutes, rounded to whole hours. Python and Node are given the same numbers and the same job:&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;# footage.py: hours of footage per day, rounded to whole hours for a summary
&lt;/span&gt;&lt;span class="n"&gt;minutes&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;Mon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;210&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Thu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;total&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;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hours&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;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&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;sum of rounded:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;| true total:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&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 javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// footage.js: the same summary in a Node dashboard&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Mon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Tue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Wed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;210&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Thu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;total&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sum of rounded:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;| true total:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&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 shell"&gt;&lt;code&gt;python3 footage.py
node footage.js
&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;Mon 2.5 -&amp;gt; 2
Tue 1.5 -&amp;gt; 2
Wed 3.5 -&amp;gt; 4
Thu 0.5 -&amp;gt; 0
sum of rounded: 8 | true total: 8.0
Mon 2.5 -&amp;gt; 3
Tue 1.5 -&amp;gt; 2
Wed 3.5 -&amp;gt; 4
Thu 0.5 -&amp;gt; 1
sum of rounded: 10 | true total: 8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of the four days disagree, and the totals differ by two hours. Neither program has a bug. They round halves by different rules, and every day in this example lands exactly on a half.&lt;/p&gt;

&lt;p&gt;Python's total happens to match the true total of eight hours. The per-day values do not look like school rounding at all: Monday's 2.5 hours became 2 and Thursday's half hour became 0. If a reader checks one row by hand, Python looks wrong. If the reader adds up the rows, Node looks wrong.&lt;/p&gt;

&lt;p&gt;Disclosure: I make an Android camera app. Nothing below uses its code or describes how it counts or stores anything. The minutes are typed in by hand, the scripts are a desktop Python and Node, 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;Python 3.10.12
v22.23.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What round() is documented to do
&lt;/h2&gt;

&lt;p&gt;Python's built-in docs state the rule plainly: "if two multiples are equally close, rounding is done toward the even choice". Their own examples are that &lt;code&gt;round(0.5)&lt;/code&gt; and &lt;code&gt;round(-0.5)&lt;/code&gt; are both &lt;code&gt;0&lt;/code&gt;, and &lt;code&gt;round(1.5)&lt;/code&gt; is &lt;code&gt;2&lt;/code&gt;. This is often called banker's rounding, or half to even.&lt;/p&gt;

&lt;p&gt;Running a few halves through both languages shows each rule:&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;# cases.py: what round() and format() do with halves
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&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="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2.5&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;round(%s) = %s&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;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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;round(2.675, 2) =&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.675&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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;stored value of 2.675:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.675&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;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{2.5:.0f}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; =&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{2.675:.2f}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; =&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mf"&gt;2.675&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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;round(0.5) = 0
round(1.5) = 2
round(2.5) = 2
round(3.5) = 4
round(-0.5) = 0
round(-2.5) = -2
round(2.675, 2) = 2.67
stored value of 2.675: 2.67499999999999982236431605997495353221893310546875
f'{2.5:.0f}' = 2
f'{2.675:.2f}' = 2.67
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// cases.js: the JavaScript side of the same halves&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Math.round(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(2.675).toFixed(2) =&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.675&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(2.5).toFixed(0) =&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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;/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;Math.round(0.5) = 1
Math.round(1.5) = 2
Math.round(2.5) = 3
Math.round(3.5) = 4
Math.round(-0.5) = 0
Math.round(-2.5) = -2
(2.675).toFixed(2) = 2.67
(2.5).toFixed(0) = 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the two lists side by side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python's &lt;code&gt;round()&lt;/code&gt;&lt;/strong&gt; sends each exact half to the even neighbour: 0.5 to 0, 2.5 to 2, 3.5 to 4, and -2.5 to -2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript's &lt;code&gt;Math.round&lt;/code&gt;&lt;/strong&gt; sent every positive half up, and sent -2.5 to -2, not -3. So in this run it does not round away from zero either. Halves went toward the larger number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python's string formatting&lt;/strong&gt; with &lt;code&gt;.0f&lt;/code&gt; printed 2 for 2.5, the same as &lt;code&gt;round()&lt;/code&gt;. Switching from &lt;code&gt;round()&lt;/code&gt; to an f-string does not change the rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript's &lt;code&gt;toFixed(0)&lt;/code&gt;&lt;/strong&gt; printed 3 for 2.5. So even inside one language, two built-in ways to round a half gave different answers here.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The case that is not about halves at all
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;round(2.675, 2)&lt;/code&gt; printed &lt;code&gt;2.67&lt;/code&gt;, and so did JavaScript's &lt;code&gt;toFixed(2)&lt;/code&gt;. That looks like half to even again, since 7 is odd and 8 is even. It is not. The docs give this exact example in a note: "This is not a bug: it’s a result of the fact that most decimal fractions can’t be represented exactly as a float."&lt;/p&gt;

&lt;p&gt;The line in &lt;code&gt;cases.py&lt;/code&gt; that prints the stored value shows why. Converting the float to &lt;code&gt;Decimal&lt;/code&gt; writes out the binary value it really holds, and that value is a little below 2.675. There is no tie to break. The nearest two-place answer to the stored number is 2.67, and both languages return it.&lt;/p&gt;

&lt;p&gt;So there are two separate effects, and they are easy to confuse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A tie rule&lt;/strong&gt; decides what happens when a value is exactly halfway. Python and JavaScript use different rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Float storage&lt;/strong&gt; means many values you type as halves, at two or more decimal places, are not halfway in memory. No tie rule applies to them, because there is no tie.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Halves like 0.5, 1.5 and 2.5 are exact in binary, which is why the first list behaves according to the tie rule. Values like 2.675 are not, which is why they surprise people who already know about the tie rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting school rounding on purpose
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;decimal&lt;/code&gt; module names its modes in words. &lt;code&gt;ROUND_HALF_EVEN&lt;/code&gt; is documented as "Round to nearest with ties going to nearest even integer." and &lt;code&gt;ROUND_HALF_UP&lt;/code&gt; as "Round to nearest with ties going away from zero."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ROUND_HALF_UP&lt;/code&gt; is the rule most people learned in school:&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;# half_up.py: school rounding with decimal, from the string and from the float
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROUND_HALF_UP&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;half_up&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;places&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;quantize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;places&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rounding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ROUND_HALF_UP&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;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2.5&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="n"&gt;x&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;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;half_up&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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;2.675 via str   -&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;half_up&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.675&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.01&lt;/span&gt;&lt;span class="sh"&gt;"&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;2.675 via float -&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.675&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;quantize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rounding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ROUND_HALF_UP&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;0.5 -&amp;gt; 1
1.5 -&amp;gt; 2
2.5 -&amp;gt; 3
-2.5 -&amp;gt; -3
2.675 via str   -&amp;gt; 2.68
2.675 via float -&amp;gt; 2.67
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Building the &lt;code&gt;Decimal&lt;/code&gt; from &lt;code&gt;str(x)&lt;/code&gt; gives 2.68, because the string is the short decimal &lt;code&gt;2.675&lt;/code&gt;. Building it from the float directly gives 2.67, because the decimal docs say construction from a float "performs an exact conversion", and the exact value is the one just below 2.675. If you want school rounding, the conversion step matters as much as the rounding mode.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;str()&lt;/code&gt; step only helps when the float already holds the decimal you meant. When the value comes out of arithmetic, &lt;code&gt;str()&lt;/code&gt; faithfully prints the arithmetic's result:&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;# str_caveat.py: str() only helps when the float already holds the decimal you meant
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROUND_HALF_UP&lt;/span&gt;

&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.05&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;x =&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&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;half up to 0.1 via str -&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;quantize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rounding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ROUND_HALF_UP&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;x = 2.3499999999999996
half up to 0.1 via str -&amp;gt; 2.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On paper, 1.1 + 1.2 + 0.05 is 2.35, which rounds half up to 2.4. The float sum is 2.3499999999999996, &lt;code&gt;str()&lt;/code&gt; keeps those digits, and the answer is 2.3. To avoid that, the inputs have to be decimals from the start, for example by reading them from text as &lt;code&gt;Decimal('1.1')&lt;/code&gt; rather than converting floats after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line half up that is not one
&lt;/h2&gt;

&lt;p&gt;A common shortcut is &lt;code&gt;math.floor(x + 0.5)&lt;/code&gt;. It looks like school rounding:&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;# floor_trick.py: the one-line "round half up" that is not one
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&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="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.49999999999999994&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="nf"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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; floor(x + 0.5) =&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&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;2.5 -&amp;gt; floor(x + 0.5) = 3
-2.5 -&amp;gt; floor(x + 0.5) = -2
0.49999999999999994 -&amp;gt; floor(x + 0.5) = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does round 2.5 to 3. It rounds -2.5 to -2, which is the JavaScript result, not school rounding. And the third line is the one that breaks it: the largest float below one half goes to 1. Adding 0.5 to it produces a sum that cannot be stored exactly, the sum is rounded up to 1.0 by the float addition itself, and the floor of 1.0 is 1. A value that is less than one half rounds up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the default is half to even
&lt;/h2&gt;

&lt;p&gt;Half to even is not an oddity in Python. It exists to stop rounding from pushing totals in one direction. This script rounds every half from 0.5 up to 99.5 both ways and compares the sums:&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;# bias.py: round every half from 0.5 to 99.5, then compare the sums
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROUND_HALF_UP&lt;/span&gt;

&lt;span class="n"&gt;halves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&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;100&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;true_sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;halves&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;even_sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;halves&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;up_sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;quantize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rounding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ROUND_HALF_UP&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;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;halves&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;true sum:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;true_sum&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;half-even sum:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;even_sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;| off by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;even_sum&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;true_sum&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;half-up sum:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;up_sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;| off by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;up_sum&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;true_sum&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;true sum: 5000.0
half-even sum: 5000 | off by 0.0
half-up sum: 5050 | off by 50.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Half up adds half a unit for every tie, so with a hundred ties the total is off by fifty. Half to even sends half of them up and half down, and the errors cancel. That is the same effect as the footage example at the top, where Python's per-day numbers looked wrong but the total came out right.&lt;/p&gt;

&lt;p&gt;That is a good property for sums, and a confusing one for a single displayed number. Which one you want depends on who reads the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Often the better fix: do not round floats at all
&lt;/h2&gt;

&lt;p&gt;The footage data started as whole minutes. Keeping it in integers until the last step gives an exact answer with a rule you choose:&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;# integer.py: keep minutes as integers and round once, exactly
&lt;/span&gt;&lt;span class="n"&gt;minutes&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;Mon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;210&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Thu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&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;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;min -&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&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="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&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;total:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;min -&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&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="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&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;Mon 150 min -&amp;gt; 3 h
Tue 90 min -&amp;gt; 2 h
Wed 210 min -&amp;gt; 4 h
Thu 30 min -&amp;gt; 1 h
total: 480 min -&amp;gt; 8 h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;(m + 30) // 60&lt;/code&gt; is half up for minutes that are zero or positive, with no float involved. It is not a general rounding function, and it would need a different formula for negative values, but footage durations are not negative. The total is rounded once, from the exact sum, instead of being built from rounded parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Decide which tie rule each number needs, and write it down in the code, not in a comment about what &lt;code&gt;round()&lt;/code&gt; is assumed to do.&lt;/li&gt;
&lt;li&gt;When two programs show the same number, check that they round it the same way. Python's &lt;code&gt;round()&lt;/code&gt; and JavaScript's &lt;code&gt;Math.round&lt;/code&gt; do not.&lt;/li&gt;
&lt;li&gt;For display rules like school rounding, use &lt;code&gt;decimal&lt;/code&gt; with &lt;code&gt;ROUND_HALF_UP&lt;/code&gt;, and build the &lt;code&gt;Decimal&lt;/code&gt; from text, not from a float.&lt;/li&gt;
&lt;li&gt;Do not use &lt;code&gt;math.floor(x + 0.5)&lt;/code&gt; as a rounding function.&lt;/li&gt;
&lt;li&gt;If the data starts as whole units, such as seconds, minutes or bytes, do the arithmetic in integers and round once at the end.&lt;/li&gt;
&lt;li&gt;For totals, round the sum, not the parts. If the parts must also be shown, expect them to not add up exactly, and say so in the report.&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;Newer Python.&lt;/strong&gt; I ran 3.10. The quotes come from the current online documentation. The behaviour matched what I saw, but I did not run a newer interpreter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browsers.&lt;/strong&gt; The JavaScript ran in Node. I did not run these lines in a browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other tools.&lt;/strong&gt; Spreadsheets, databases and other languages each have their own rounding functions and rules. I did not test any of them, and I make no claim about what they do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NumPy and pandas.&lt;/strong&gt; Their rounding functions are separate from the built-in &lt;code&gt;round()&lt;/code&gt;. Nothing here covers them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; Quotes are from the Python documentation pages &lt;a href="https://docs.python.org/3/builtins/functions.html" rel="noopener noreferrer"&gt;Built-in functions&lt;/a&gt; (the &lt;code&gt;round&lt;/code&gt; entry) and &lt;a href="https://docs.python.org/3/library/decimal.html" rel="noopener noreferrer"&gt;decimal — Decimal fixed-point and floating-point arithmetic&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 footage for a summary like this, 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;When durations come in half-hour steps, as all four days here do, every odd half hour is a tie, and the tie rule, not the footage, decides the total a reader sees.&lt;/p&gt;

</description>
      <category>python</category>
      <category>javascript</category>
      <category>node</category>
      <category>beginners</category>
    </item>
    <item>
      <title>A Crontab Line That Works in Your Terminal Fails Under cron: What % Means in a Crontab</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Thu, 29 Oct 2026 12:55:57 +0000</pubDate>
      <link>https://dev.to/superfunicular/a-crontab-line-that-works-in-your-terminal-fails-under-cron-what-means-in-a-crontab-ehh</link>
      <guid>https://dev.to/superfunicular/a-crontab-line-that-works-in-your-terminal-fails-under-cron-what-means-in-a-crontab-ehh</guid>
      <description>&lt;p&gt;I wanted a nightly crontab line that archives a folder of camera clips into a file named for the date: &lt;code&gt;tar czf backup/clips-$(date +%F).tgz clips&lt;/code&gt;. Typed into a terminal, it made &lt;code&gt;clips-2026-10-29.tgz&lt;/code&gt;. Pasted into a crontab, the same text would not make a file, and nothing about the line looks wrong.&lt;/p&gt;

&lt;p&gt;The reason is one character. In a crontab, &lt;code&gt;%&lt;/code&gt; is not plain text.&lt;/p&gt;

&lt;p&gt;Disclosure: I make an Android camera app. Nothing below uses its code or describes how it stores or backs up files. I could not run the cron daemon in this environment (it needs root, and I do not have it here), so I did the next best thing: I took the rules the Debian &lt;a href="https://manpages.debian.org/unstable/cron/crontab.5.en.html" rel="noopener noreferrer"&gt;crontab(5) manual page&lt;/a&gt; states, applied them with a short script, and ran the result through &lt;code&gt;/bin/sh&lt;/code&gt; the way cron would. Read every cron result below as the manual's description of cron, not as a test of the daemon itself.&lt;/p&gt;

&lt;p&gt;What the tests showed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An unescaped &lt;code&gt;%&lt;/code&gt; ends the command.&lt;/strong&gt; Everything after it becomes standard input. A date format string cuts the line in half.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The shell then sees half a command&lt;/strong&gt; and fails with a syntax error, so no archive is written.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;\%&lt;/code&gt; fixes it&lt;/strong&gt;, but inside double quotes the backslash may survive into the output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two day fields are ORed, not ANDed.&lt;/strong&gt; A first-Sunday-of-the-month line runs on eleven days in November 2026, not one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A day field that starts with &lt;code&gt;*&lt;/code&gt; changes that rule&lt;/strong&gt; on Debian's cron, which is not what POSIX says.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PATH=$HOME/bin:$PATH&lt;/code&gt; in a crontab is taken literally&lt;/strong&gt;, so every command on the line after it goes missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setup
&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;# Scratch folder with two clips and a backup target.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eu&lt;/span&gt;
&lt;span class="nv"&gt;D&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/cron_demo
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/clips"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/backup"&lt;/span&gt;
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 4096 /dev/urandom &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/clips/clip_0001.mp4"&lt;/span&gt;
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 4096 /dev/urandom &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;/clips/clip_0002.mp4"&lt;/span&gt;
&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-sr&lt;/span&gt;
&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /bin/sh
bash &lt;span class="nt"&gt;--version&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;
python3 &lt;span class="nt"&gt;--version&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;Linux 6.8.0-138-generic
/usr/bin/dash
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;p&gt;&lt;code&gt;/bin/sh&lt;/code&gt; here is dash. The manual says cron runs commands with &lt;code&gt;/usr/bin/sh&lt;/code&gt; unless the crontab sets &lt;code&gt;SHELL&lt;/code&gt;, and on Debian and Ubuntu that is normally dash too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The emulator
&lt;/h2&gt;

&lt;p&gt;This is the whole of the cron behaviour I am relying on. It follows the manual's sentence: "Percent-signs (%) in the command, unless escaped with backslash (), will be changed into newline characters, and all data after the first % will be sent to the command as standard input."&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;#!/usr/bin/python3
# Applies the crontab(5) rule for the command field, then runs it:
#   text up to the first unescaped % goes to the shell,
#   every later unescaped % becomes a newline,
#   everything after the first % is sent to the command as standard input.
# A backslash before % is kept here; the shell removes it (see below).
&lt;/span&gt;&lt;span class="kn"&gt;import&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&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="p"&gt;,&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="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;i&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;field&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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;c&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;2&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="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&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;cur&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;i&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;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;stdin&lt;/span&gt; &lt;span class="o"&gt;=&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;cur&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&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;cur&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stdin&lt;/span&gt;

&lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&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="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stdin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&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;shell gets : %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;cmd&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;stdin gets : %r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;stdin&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;/bin/sh&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;cmd&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;stdin&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;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;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/cron_demo&lt;/span&gt;&lt;span class="sh"&gt;"&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;exit status: %d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&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;returncode&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;line&lt;/span&gt; &lt;span class="ow"&gt;in&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="o"&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;stderr&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;splitlines&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;output     : %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test one: the date in the file name
&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="nb"&gt;cd&lt;/span&gt; /tmp/cron_demo
&lt;span class="nv"&gt;LINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'tar czf backup/clips-$(date +%F).tgz clips'&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"--- typed in a terminal"&lt;/span&gt;
bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINE&lt;/span&gt;&lt;span class="s2"&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;"exit status: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&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;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; backup/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"--- the same text as a crontab command field"&lt;/span&gt;
python3 /tmp/cron_demo_code/cronsplit.py &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINE&lt;/span&gt;&lt;span class="s2"&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;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- typed in a terminal
exit status: 0
clips-2026-10-29.tgz
--- the same text as a crontab command field
shell gets : tar czf backup/clips-$(date +
stdin gets : 'F).tgz clips'
exit status: 2
output     : /bin/sh: 1: Syntax error: end of file unexpected (expecting ")")
0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the terminal the line worked. As a crontab command, the shell received &lt;code&gt;tar czf backup/clips-$(date +&lt;/code&gt; and nothing else. The rest, &lt;code&gt;F).tgz clips&lt;/code&gt;, went to standard input, which &lt;code&gt;tar&lt;/code&gt; ignores here. Dash reported a syntax error and exited 2, and the backup folder stayed empty.&lt;/p&gt;

&lt;p&gt;On a real system the error text would not reach your terminal either. The manual says "Command output is mailed to the crontab owner". If the machine has no working mail setup, I do not know where that message ends up, and I did not test it. The practical result is the same: a backup job that has not produced a single file, and nothing on screen saying so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test two: escaping it
&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="nb"&gt;cd&lt;/span&gt; /tmp/cron_demo
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; backup/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nv"&gt;LINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'tar czf backup/clips-$(date +\%F).tgz clips'&lt;/span&gt;
python3 /tmp/cron_demo_code/cronsplit.py &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;ls &lt;/span&gt;backup | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'/^output/s/[0-9]/N/g'&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"--- the same escape inside double quotes"&lt;/span&gt;
python3 /tmp/cron_demo_code/cronsplit.py &lt;span class="s1"&gt;'date "+\%F"'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'/^output/s/[0-9]/N/g'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shell gets : tar czf backup/clips-$(date +\%F).tgz clips
stdin gets : ''
exit status: 0
clips-2026-10-29.tgz
--- the same escape inside double quotes
shell gets : date "+\%F"
stdin gets : ''
exit status: 0
output     : \NNNN-NN-NN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;\%&lt;/code&gt; outside quotes the archive was written. The shell's own rule removes a backslash in front of an ordinary character, so &lt;code&gt;+\%F&lt;/code&gt; reaches &lt;code&gt;date&lt;/code&gt; as &lt;code&gt;+%F&lt;/code&gt; whether or not cron strips the backslash first.&lt;/p&gt;

&lt;p&gt;Inside double quotes it is different. A backslash before &lt;code&gt;%&lt;/code&gt; is not special there, so the shell keeps it, and &lt;code&gt;date&lt;/code&gt; printed a literal backslash in front of the date. The manual says to escape &lt;code&gt;%&lt;/code&gt; with a backslash but does not say whether cron removes that backslash before the shell sees it. My emulator keeps it, which is the worse case. I did not test which one Debian's cron actually does, so I keep the escaped &lt;code&gt;%&lt;/code&gt; outside quotes, where both cases give the same answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;%&lt;/code&gt; works this way
&lt;/h2&gt;

&lt;p&gt;It is a feature, not an accident. The manual's own example file uses it to write a mail body on one line: &lt;code&gt;mail -s "It's 10pm" joe%Joe,%%Where are your kids?%&lt;/code&gt;. The same mechanism, smaller:&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;# % used on purpose, as in the manual's mail example: lines on standard input.&lt;/span&gt;
python3 /tmp/cron_demo_code/cronsplit.py &lt;span class="s1"&gt;'cat -n%first line%second 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 plaintext"&gt;&lt;code&gt;shell gets : cat -n
stdin gets : 'first line\nsecond line\n'
exit status: 0
output     :      1 first line
output     :      2 second line
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is useful for a one-line mail or a short input, and it is the reason a date format in a crontab needs care.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test three: the two day fields
&lt;/h2&gt;

&lt;p&gt;A crontab has two day fields, day of month and day of week. A common first try at 03:00 on the first Sunday of the month is &lt;code&gt;0 3 1-7 * sun&lt;/code&gt;. The manual: "If both fields are restricted (i.e., aren't *), the command will be run when either field matches the current time."&lt;/p&gt;

&lt;p&gt;There is a second rule hiding in the same page. Debian's cron "only checks if the first character is a &lt;em&gt;", so a field like `&lt;/em&gt;/2&lt;code&gt; counts as unrestricted, and then both fields have to match. POSIX would treat &lt;/code&gt;*/2` as restricted.&lt;/p&gt;

&lt;p&gt;This script lists the November 2026 days each line would match, under Debian's rule and under the POSIX one:&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;#!/usr/bin/python3
# Which days in November 2026 match a crontab's two day fields?
# Rule from crontab(5): if both day fields are restricted, run when EITHER
# matches. Debian's cron decides "restricted" by the field's first character
# only: a field starting with * counts as unrestricted, so the two are ANDed.
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="n"&gt;DOW&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;sun&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mon&lt;/span&gt;&lt;span class="sh"&gt;"&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;tue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thu&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fri&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;expand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&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;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;field&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;step&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;part&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&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;part&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;part&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&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;names&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&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="nf"&gt;get&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="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;names&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;int&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;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&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;names&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;out&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;names&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dom&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;31&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;W&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dow&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="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DOW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&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;d&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="n"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monthrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&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="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="n"&gt;wd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&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="nf"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;()&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="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;   &lt;span class="c1"&gt;# Sunday = 0
&lt;/span&gt;        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&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;wd&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;W&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;debian&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;star_dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;star_dow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dom&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="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dow&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="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# POSIX: unrestricted only if the field IS *
&lt;/span&gt;            &lt;span class="n"&gt;star_dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;star_dow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dom&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dow&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;star_dom&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;star_dow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;or&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dow&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1-7&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;sun&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;1-7&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;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;*/2&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;sun&lt;/span&gt;&lt;span class="sh"&gt;"&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;rule&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;debian&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;posix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&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;%-4s %-3s %-6s -&amp;gt; %2d runs: %s&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;dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&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;h&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &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="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1-7  sun debian -&amp;gt; 11 runs: 1 2 3 4 5 6 7 8 15 22 29
1-7  sun posix  -&amp;gt; 11 runs: 1 2 3 4 5 6 7 8 15 22 29
1-7  *   debian -&amp;gt;  7 runs: 1 2 3 4 5 6 7
1-7  *   posix  -&amp;gt;  7 runs: 1 2 3 4 5 6 7
*/2  sun debian -&amp;gt;  3 runs: 1 15 29
*/2  sun posix  -&amp;gt; 17 runs: 1 3 5 7 8 9 11 13 15 17 19 21 22 23 25 27 29
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first-Sunday line matches eleven days: every day from the 1st to the 7th, plus every Sunday in the month. Only one of those is the day I meant. The &lt;code&gt;*/2 sun&lt;/code&gt; line shows the first-character rule: three runs under Debian's reading, seventeen under the POSIX one. The manual describes that same line the same way: it "runs every Sunday that's an uneven date while the POSIX standard would have it run every Sunday and on every uneven date." The same line can mean a different schedule on a different cron.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix the manual suggests
&lt;/h2&gt;

&lt;p&gt;Use one day field and put the other test in the command. The manual's own example does this for the second Saturday, and its test uses &lt;code&gt;date +\%u&lt;/code&gt;, escaped, for the reason in test one.&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 fix the manual suggests: one day field, plus a test inside the command.&lt;/span&gt;
&lt;span class="c"&gt;# Here the test runs for each of November 1-7, 2026 instead of waiting for cron.&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /tmp/cron_demo
&lt;span class="k"&gt;for &lt;/span&gt;d &lt;span class="k"&gt;in &lt;/span&gt;01 02 03 04 05 06 07&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;LINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"test &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;(date -d 2026-11-&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt; +&lt;/span&gt;&lt;span class="se"&gt;\%&lt;/span&gt;&lt;span class="s2"&gt;u) -eq 7 &amp;amp;&amp;amp; echo 2026-11-&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt; would run"&lt;/span&gt;
  python3 /tmp/cron_demo_code/cronsplit.py &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LINE&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;-E&lt;/span&gt; &lt;span class="s1"&gt;'^output'&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;output     : 2026-11-01 would run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of the seven days the line would wake up in that week, the test let exactly one through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test four: a PATH line
&lt;/h2&gt;

&lt;p&gt;Crontabs accept &lt;code&gt;NAME=value&lt;/code&gt; lines, and it is natural to write &lt;code&gt;PATH=$HOME/bin:$PATH&lt;/code&gt;. The manual says the value "is not parsed for environmental substitutions or replacement of variables or tilde(~) expansion", and gives that exact line as one that "will not work as you might expect".&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;# crontab(5): values are not expanded, so PATH=$HOME/bin:$PATH is taken literally.&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /tmp/cron_demo
&lt;span class="nb"&gt;env&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'PATH=$HOME/bin:$PATH'&lt;/span&gt; /bin/sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'echo "PATH is: $PATH"; tar --version'&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="nb"&gt;env&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'PATH=/usr/local/bin:/usr/bin:/bin'&lt;/span&gt; /bin/sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'tar --version'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PATH is: $HOME/bin:$PATH
/bin/sh: 1: tar: not found
exit status: 127
tar (GNU tar) 1.34
exit status: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the literal value, &lt;code&gt;PATH&lt;/code&gt; held the text &lt;code&gt;$HOME/bin:$PATH&lt;/code&gt;, no real directory, and &lt;code&gt;tar&lt;/code&gt; was not found (exit 127). Writing the directories out in full fixed it. The manual also offers an &lt;code&gt;eval&lt;/code&gt; workaround (&lt;code&gt;P=PATH=/a/b/c:$PATH&lt;/code&gt;, then &lt;code&gt;eval $P &amp;amp;&amp;amp; some commands&lt;/code&gt; in the command field), which I did not test.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do instead
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Escape every &lt;code&gt;%&lt;/code&gt; in a crontab command as &lt;code&gt;\%&lt;/code&gt;, and keep it outside double quotes.&lt;/li&gt;
&lt;li&gt;Better, move anything with a date format into a script and have the crontab call the script. The script is ordinary shell, and &lt;code&gt;%&lt;/code&gt; means nothing special there.&lt;/li&gt;
&lt;li&gt;For a rule like the first Sunday of the month, restrict one day field and test the other inside the command.&lt;/li&gt;
&lt;li&gt;Start a day field with &lt;code&gt;*&lt;/code&gt; only when you mean any day for that field.&lt;/li&gt;
&lt;li&gt;Write &lt;code&gt;PATH=&lt;/code&gt; in full, with real directories.&lt;/li&gt;
&lt;li&gt;After installing a new line, check the next morning that the file it should write exists. A job that fails at the shell stage leaves no file and no error on your screen.&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;The cron daemon itself.&lt;/strong&gt; Every cron result here is the manual's rule applied by a script. A different cron build could behave differently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cronie, BusyBox crond, systemd timers.&lt;/strong&gt; Their manuals may differ, and BusyBox crond was not checked at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether cron strips the backslash in &lt;code&gt;\%&lt;/code&gt;.&lt;/strong&gt; I worked around it rather than answering it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mail delivery of the error.&lt;/strong&gt; Not tested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; Quotes are from the Debian &lt;a href="https://manpages.debian.org/unstable/cron/crontab.5.en.html" rel="noopener noreferrer"&gt;crontab(5) manual page&lt;/a&gt; (cron 3.0pl1-212). Results come from the scripts above, each run once, with output pasted unedited.&lt;/p&gt;

&lt;p&gt;If you want an old phone recording clips in the background so there is a folder worth backing up every night, 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;The line was fine as shell. It just was not shell yet.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>bash</category>
      <category>cron</category>
      <category>devops</category>
    </item>
    <item>
      <title>Sending Phone Security Camera Footage on WhatsApp: Why the Clip Looks Blurry, the 100 MB / 720p Limit, and When to Send It as a Document</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Thu, 29 Oct 2026 07:52:55 +0000</pubDate>
      <link>https://dev.to/superfunicular/sending-phone-security-camera-footage-on-whatsapp-why-the-clip-looks-blurry-the-100-mb-720p-1akh</link>
      <guid>https://dev.to/superfunicular/sending-phone-security-camera-footage-on-whatsapp-why-the-clip-looks-blurry-the-100-mb-720p-1akh</guid>
      <description>&lt;p&gt;Something happened in front of your shop camera. Maybe a customer walked out without paying, maybe a scooter was knocked over, maybe a delivery went missing. The recording is safe on the old phone. Now your brother, your landlord or the police station wants to see it, and the quickest way to send anything is WhatsApp.&lt;/p&gt;

&lt;p&gt;You send the clip. It arrives looking soft and smeared, and the face or the number plate you needed is a blur.&lt;/p&gt;

&lt;p&gt;Nothing is wrong with your camera phone. This guide explains what WhatsApp does to video, using only what WhatsApp's own Help Center says, and gives you a simple way to send the clip so the other person gets the file you actually recorded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does my security camera clip look blurry on WhatsApp?
&lt;/h2&gt;

&lt;p&gt;Because a normal video message is not the original file. WhatsApp's Android help page for sending media says it plainly: "The quality of media uploaded to WhatsApp might be lower than the original version because media files are usually compressed when shared."&lt;/p&gt;

&lt;p&gt;The same page gives the default limits for a video message:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With a faster internet connection: &lt;strong&gt;100 MB and 720p resolution&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;With a slower internet connection: &lt;strong&gt;64 MB and 480p resolution&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So on a weak mobile data signal, a clip recorded at 1080p can be sent at 480p. That matters more than it sounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 480p does to a face or a number plate
&lt;/h2&gt;

&lt;p&gt;Resolution is just a count of pixels. Here is the arithmetic for the three sizes involved (width × height, standard 16:9 frames):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resolution&lt;/th&gt;
&lt;th&gt;Pixels per frame&lt;/th&gt;
&lt;th&gt;Share of a 1080p frame&lt;/th&gt;
&lt;th&gt;A 100-pixel-wide object becomes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1080p (1920 × 1080)&lt;/td&gt;
&lt;td&gt;2,073,600&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;720p (1280 × 720)&lt;/td&gt;
&lt;td&gt;921,600&lt;/td&gt;
&lt;td&gt;44.4%&lt;/td&gt;
&lt;td&gt;66.7 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;480p (854 × 480)&lt;/td&gt;
&lt;td&gt;409,920&lt;/td&gt;
&lt;td&gt;19.8%&lt;/td&gt;
&lt;td&gt;44.5 px&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 480p frame has about one fifth of the pixels of a 1080p frame. A number plate that was 100 pixels wide in your recording is about 44 pixels wide after scaling to 480p, before any compression is applied on top. Small details like a plate or a face across the shop are exactly what gets lost first.&lt;/p&gt;

&lt;p&gt;This is the same reason we told you in our &lt;a href="https://dev.to/superfunicular/h264-or-h265-for-an-offline-phone-security-camera-half-the-storage-or-footage-you-can-actually-1947"&gt;H.264 vs H.265 guide&lt;/a&gt; to keep footage in a format the other person can actually open. Sharing is part of the recording plan, not an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  How big is your clip? A quick estimate
&lt;/h2&gt;

&lt;p&gt;WhatsApp's limit is in megabytes, so it helps to know how big your clip is. The rule is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;size in MB ≈ bitrate in Mbit/s × seconds ÷ 8&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bitrate depends on your phone and settings, so the numbers below are examples, not a measurement of any particular phone:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If the recording bitrate is&lt;/th&gt;
&lt;th&gt;1 minute&lt;/th&gt;
&lt;th&gt;5 minutes&lt;/th&gt;
&lt;th&gt;10 minutes&lt;/th&gt;
&lt;th&gt;Fits in 64 MB&lt;/th&gt;
&lt;th&gt;Fits in 100 MB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4 Mbit/s&lt;/td&gt;
&lt;td&gt;30 MB&lt;/td&gt;
&lt;td&gt;150 MB&lt;/td&gt;
&lt;td&gt;300 MB&lt;/td&gt;
&lt;td&gt;128 s&lt;/td&gt;
&lt;td&gt;200 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 Mbit/s&lt;/td&gt;
&lt;td&gt;60 MB&lt;/td&gt;
&lt;td&gt;300 MB&lt;/td&gt;
&lt;td&gt;600 MB&lt;/td&gt;
&lt;td&gt;64 s&lt;/td&gt;
&lt;td&gt;100 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12 Mbit/s&lt;/td&gt;
&lt;td&gt;90 MB&lt;/td&gt;
&lt;td&gt;450 MB&lt;/td&gt;
&lt;td&gt;900 MB&lt;/td&gt;
&lt;td&gt;43 s&lt;/td&gt;
&lt;td&gt;67 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To find your own number, open the clip in your phone's Files app, check its size and its length, and divide. At 8 Mbit/s, roughly one minute of video is already at the slow-connection limit.&lt;/p&gt;

&lt;p&gt;The lesson: &lt;strong&gt;a full hour-long recording is never going to go as a normal video message.&lt;/strong&gt; You need to either cut it down or send it a different way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: Trim first, then send in HD
&lt;/h2&gt;

&lt;p&gt;Most of the time, the person only needs the 20 or 30 seconds where the thing happened.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find the moment. Write down the time shown in the recording, or the time on the clock in the shop.&lt;/li&gt;
&lt;li&gt;Open the chat, tap the attach button and pick the clip from your gallery.&lt;/li&gt;
&lt;li&gt;WhatsApp's editor lets you &lt;strong&gt;trim a video by dragging the slider at the top&lt;/strong&gt; (from WhatsApp's "How to edit photos and videos" help page). Keep a few seconds before and after the event.&lt;/li&gt;
&lt;li&gt;Before sending, tap the quality button and choose &lt;strong&gt;HD quality&lt;/strong&gt;. WhatsApp's help page says HD photos and videos "are clearer than Standard quality photos and videos, but use more data and take longer to send."&lt;/li&gt;
&lt;li&gt;If your signal is weak, wait until you are on Wi-Fi or a stronger signal. The help page links the 480p default to a slower connection.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;WhatsApp's help pages do not promise that HD sends the full original resolution, so treat HD as "better", not "the original".&lt;/p&gt;

&lt;p&gt;Do not add stickers, text, filters or drawings to a clip you may need as proof. The editor can do all of these, and every one changes the picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: Send it as a document (the original file)
&lt;/h2&gt;

&lt;p&gt;WhatsApp has a second path that is meant for files, not video messages. On Android: tap the attach button, choose &lt;strong&gt;Document&lt;/strong&gt;, and pick the video file. The Help Center says this route accepts "a document of up to &lt;strong&gt;2 GB&lt;/strong&gt;".&lt;/p&gt;

&lt;p&gt;The compression note on the help page talks about &lt;strong&gt;media files&lt;/strong&gt;. Documents have their own separate 2 GB limit. In practice, this is the route people use when the file itself has to arrive. You do not have to trust that, you can check it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before sending, note the exact file size in your Files app.&lt;/li&gt;
&lt;li&gt;Ask the other person to save the document and check its size on their side.&lt;/li&gt;
&lt;li&gt;If the two sizes match, they have your original file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Things to know about the document route:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It costs real data.&lt;/strong&gt; A 300 MB file uses about 300 MB of your data to send, and about the same for the person downloading it. On a daily data pack, check your balance first. Wi-Fi is better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not play as a video bubble.&lt;/strong&gt; The other person sees a file and taps it to open it with their video player.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2 GB is the ceiling.&lt;/strong&gt; At 8 Mbit/s, 2 GB is roughly half an hour of video. If your phone records long single files (and on an older SD card a single file can run close to 4 GB, see our &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;FAT32 guide&lt;/a&gt;), you will need to cut out the part that matters before sending.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Option 3: Do not use WhatsApp for the main copy
&lt;/h2&gt;

&lt;p&gt;If the clip may be needed later by the police, an insurer or a court, the safest copy is the one that never went through a messaging app at all. Copy the original file off the phone with an SD card or a USB cable, which our guide on &lt;a href="https://dev.to/superfunicular/how-to-get-recordings-off-an-old-phone-security-camera-without-a-cloud-account-and-the-folder-17d8"&gt;getting recordings off an old-phone camera without a cloud account&lt;/a&gt; walks through step by step.&lt;/p&gt;

&lt;p&gt;Then use WhatsApp for a short HD preview, so people can see what happened today, while the original stays safe.&lt;/p&gt;

&lt;p&gt;We are not lawyers, and rules for evidence are different in every country. If an official asks for footage, ask them how they want to receive it. Many will want the original file on a pen drive or a disc, not a forwarded chat message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips for the person receiving the clip
&lt;/h2&gt;

&lt;p&gt;The other side of the chat matters too, especially in a family or shop group:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-download can eat their data.&lt;/strong&gt; WhatsApp's "How to configure auto-download" page says you can choose which media downloads automatically on mobile data, on Wi-Fi and when roaming, under &lt;strong&gt;Settings &amp;gt; Storage and data &amp;gt; Media auto-download&lt;/strong&gt;. It also warns that roaming data "is often expensive."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downloaded media goes to the gallery.&lt;/strong&gt; The help page says downloaded media is saved to the phone's gallery by default, and each chat has a &lt;strong&gt;Media visibility&lt;/strong&gt; option to change that. On a shared family phone, a shop clip in the gallery may be seen by everyone who uses that phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forwarding does not re-upload.&lt;/strong&gt; WhatsApp says that when you forward media or documents, "you don't have to upload them again." So forwarding a clip around a group does not cost the sender extra data each time. It still spreads the clip further than you may want, so think before forwarding footage of real people.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 5-minute test to do before you ever need it
&lt;/h2&gt;

&lt;p&gt;Do this on a quiet day, not on the day something goes wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Record a 1-minute test clip on the camera phone. Hold up a page with a phone number written on it in normal-sized handwriting, at the distance of your shop door.&lt;/li&gt;
&lt;li&gt;Send it to a family member three ways: a normal video message (Standard), a video message in HD, and as a Document.&lt;/li&gt;
&lt;li&gt;Ask them to pause each one on the page and read the number.&lt;/li&gt;
&lt;li&gt;Ask them to check the size of the Document copy. It should match your original.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now you know which route gives a readable number, and how much data each costs on your connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Background Camera RemoteStream fits
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://play.google.com/store/apps/details?id=com.superfunicular.digicam" rel="noopener noreferrer"&gt;Background Camera RemoteStream&lt;/a&gt; turns a spare Android phone into a camera that keeps recording with the screen off, stores the video on the phone itself (no cloud account needed), and can show the camera live in a browser on your own Wi-Fi. If you want people outside your Wi-Fi to watch live, it can stream to YouTube Live. Check the Play listing for which features are in the free version and which are in Pro.&lt;/p&gt;

&lt;p&gt;That means the full-quality recording is already sitting on your own phone when you need it. What happens to it after that, including how you share it, is your choice. Pick the route that keeps the details you need.&lt;/p&gt;

&lt;p&gt;More guides for shop and home setups on old phones: &lt;a href="https://superfunicular.com" rel="noopener noreferrer"&gt;superfunicular.com&lt;/a&gt;. If you are worried about the camera phone itself being taken, read &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 happens if someone steals the phone that is your shop camera&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this guide does not tell you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exactly what WhatsApp does to an over-size clip on your phone.&lt;/strong&gt; The help page gives default limits (100 MB / 720p, 64 MB / 480p) and a separate help topic is titled "I get a message that my video is too long and it won't send." It does not publish the full rules, and they may change. Run the test above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether HD sends the original resolution.&lt;/strong&gt; WhatsApp says HD is clearer than Standard. It does not say HD equals the original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything about your phone's bitrate.&lt;/strong&gt; The size table uses example bitrates. Measure your own clip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legal advice.&lt;/strong&gt; Ask the person or office requesting the footage how they want it delivered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Sources: WhatsApp Help Center, "How to send media, contacts, or location" (Android), "How to edit photos and videos" (Android) and "How to configure auto-download" (Android), all read on 29 October 2026. Pixel and file-size figures are arithmetic, computed with a short script for this article.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>privacy</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Is Camera 1 Still Recording? A 15-Minute Check That Doesn't Lie When the Clip Folder Is Full</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Wed, 28 Oct 2026 08:01:15 +0000</pubDate>
      <link>https://dev.to/superfunicular/is-camera-1-still-recording-a-15-minute-check-that-doesnt-lie-when-the-clip-folder-is-full-5c94</link>
      <guid>https://dev.to/superfunicular/is-camera-1-still-recording-a-15-minute-check-that-doesnt-lie-when-the-clip-folder-is-full-5c94</guid>
      <description>&lt;p&gt;A camera phone that stops recording does not tell you. The folder just stops growing. So the job is a small check that runs every 15 minutes and asks one question: has camera 1 written a clip lately? If not, send an alert.&lt;/p&gt;

&lt;p&gt;This is a companion to &lt;a href="https://dev.to/superfunicular/set-o-pipefail-failed-my-clip-script-on-some-nights-and-not-others-exit-status-141-head-and-grep-99"&gt;set -o pipefail Failed My Clip Script on Some Nights and Not Others&lt;/a&gt;, which showed why a pipeline ending in &lt;code&gt;head&lt;/code&gt; or &lt;code&gt;grep -q&lt;/code&gt; exits 141 under &lt;code&gt;pipefail&lt;/code&gt; once there is more output than a pipe holds. A reader, c-wind, added the case that post skipped: what to do when the early exit is not something &lt;code&gt;find&lt;/code&gt; can express, and how a real error can hide behind the 141. They also asked a direct question. This post builds the 15-minute check and answers it.&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 files. The folder is fake, the log is fake, and every result comes from the scripts shown, run on this machine:&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)
find (GNU findutils) 4.8.0
CPUs: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Same generator as the first post: empty files, one minute apart, all from camera 1, all dated October 2025, so every clip is stale.&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;# mk.sh N : make N empty clip files, one minute apart, oldest first.&lt;/span&gt;
&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&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="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 &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="si"&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;f&lt;/span&gt;&lt;span class="o"&gt;=&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;'clips/cam1_%05d.mp4'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  : &lt;span class="o"&gt;&amp;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;
  &lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"@&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;1761000000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; i &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="k"&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;$f&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;h2&gt;
  
  
  The answer to the question first
&lt;/h2&gt;

&lt;p&gt;The question was: on the 3000-clip &lt;code&gt;grep -q&lt;/code&gt; runs, was &lt;code&gt;PIPESTATUS&lt;/code&gt; &lt;code&gt;141 0&lt;/code&gt;, the same as the &lt;code&gt;sort | head&lt;/code&gt; trace? Here it is, 100 runs per folder size:&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="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; pipefail
&lt;span class="k"&gt;for &lt;/span&gt;n &lt;span class="k"&gt;in &lt;/span&gt;10 3000&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  ./mk.sh &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="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nv"&gt;seen&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;k &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 100&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;find clips &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.mp4'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'cam1_'&lt;/span&gt;
    &lt;span class="nv"&gt;s&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;PIPESTATUS&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;
    seen[&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &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="k"&gt;done
  for &lt;/span&gt;s &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="p"&gt;!seen[@]&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"clips=&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;  PIPESTATUS=&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/100"&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;unset &lt;/span&gt;seen
&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;clips=10  PIPESTATUS=0 0  100/100
clips=3000  PIPESTATUS=141 0  100/100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes. &lt;code&gt;grep -q&lt;/code&gt; matched and exited 0; &lt;code&gt;find&lt;/code&gt;, still listing, took &lt;code&gt;SIGPIPE&lt;/code&gt;. On 10 clips &lt;code&gt;find&lt;/code&gt; finished before &lt;code&gt;grep&lt;/code&gt; left, so both are 0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version one: the newest clip, by &lt;code&gt;ls -t&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The obvious first draft asks for the newest clip and checks its age. Before any age check, the name lookup itself:&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="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="c"&gt;# Name of the newest camera 1 clip, by modification time.&lt;/span&gt;
&lt;span class="nv"&gt;newest&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; &lt;span class="nt"&gt;-t&lt;/span&gt; clips/cam1_&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="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"newest: &lt;/span&gt;&lt;span class="nv"&gt;$newest&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;newest_ls clips=50 failed=0/100
newest_ls clips=3000 failed=95/100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 3000 clips, a single run printed nothing and exited 141. It is the same failure as the first post with &lt;code&gt;ls&lt;/code&gt; as the writer: 95 of 100 runs died before the check ever looked at a time. If the scheduler treats a non-zero exit as "alert", that is 95 false alarms; if it ignores non-zero exits, that is 95 checks that never happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version two: let &lt;code&gt;find&lt;/code&gt; ask the whole question
&lt;/h2&gt;

&lt;p&gt;The question is not "what is the newest clip". It is "is there at least one camera 1 clip modified in the last 15 minutes". &lt;code&gt;find&lt;/code&gt; can say that directly with &lt;code&gt;-mmin -15&lt;/code&gt;, and stop at the first hit with &lt;code&gt;-quit&lt;/code&gt;. No pipe, nothing left writing into a closed one:&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="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nb"&gt;dir&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;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="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# Any camera 1 clip modified in the last 15 minutes? find stops at the first one.&lt;/span&gt;
&lt;span class="nv"&gt;recent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &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="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'cam1_*.mp4'&lt;/span&gt; &lt;span class="nt"&gt;-mmin&lt;/span&gt; &lt;span class="nt"&gt;-15&lt;/span&gt; &lt;span class="nt"&gt;-print&lt;/span&gt; &lt;span class="nt"&gt;-quit&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&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;$recent&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;"OK: &lt;/span&gt;&lt;span class="nv"&gt;$recent&lt;/span&gt;&lt;span class="s2"&gt;"&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;"ALERT: no cam1 clip in 15 min"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three cases: the stale 3000-clip folder, the same folder after touching one clip so it is fresh, and a folder that does not exist.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- stale folder (all clips from 2025) ---
ALERT: no cam1 clip in 15 min
exit 0
--- after touching newest now ---
OK: clips/cam1_03000.mp4
exit 0
--- missing dir ---
find: ‘no_such_dir’: No such file or directory
exit 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right answer on the stale folder, right answer on the fresh one, and a missing folder is an error (exit 1), not a quiet "OK" and not a quiet "ALERT". That last one matters: "the folder is gone" and "the camera stopped" need different fixes, and the exit status is the only thing that tells them apart.&lt;/p&gt;

&lt;p&gt;For a clip folder, this is the whole check. Most of the time, the fix is to stop asking a pipeline a question the first command can answer itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  When &lt;code&gt;find&lt;/code&gt; can't express it
&lt;/h2&gt;

&lt;p&gt;c-wind's point was that &lt;code&gt;-quit&lt;/code&gt; only works when the test is a &lt;code&gt;find&lt;/code&gt; predicate. Sometimes it isn't. Say the recorder writes a text log, and the real signal is a line like &lt;code&gt;cam1 segment closed&lt;/code&gt; in the last 5000 lines. That is a content test, so it needs &lt;code&gt;grep&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="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 5000 rec.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;' cam1 segment closed'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fake log is 20,000 lines, alternating camera 1 and camera 2, about 1.3 MB. The 5000 lines &lt;code&gt;tail&lt;/code&gt; sends are more than a pipe holds, so &lt;code&gt;grep -q&lt;/code&gt; leaves early and &lt;code&gt;tail&lt;/code&gt; takes &lt;code&gt;SIGPIPE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Their approach: keep &lt;code&gt;pipefail&lt;/code&gt;, and branch on each side's status yourself.&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="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail
&lt;span class="c"&gt;# Did camera 1 close a segment in the last 5000 log lines? Branch on each side's status.&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 5000 rec.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;' cam1 segment closed'&lt;/span&gt;
&lt;span class="nv"&gt;st&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;PIPESTATUS&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="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&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;st&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="s2"&gt;"0 0"&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="s2"&gt;"141 0"&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;"MATCH   (tail=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; grep=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[1]&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="s2"&gt;"0 1"&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;"NOMATCH (tail=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; grep=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[1]&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;*&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;"ERROR   (tail=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; grep=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[1]&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="nb"&gt;exit &lt;/span&gt;2 &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;set -e&lt;/code&gt; is off here on purpose: the script reads the statuses instead of dying on them. &lt;code&gt;PIPESTATUS&lt;/code&gt; has to be copied on the very next line, because the next command overwrites it.&lt;/p&gt;

&lt;p&gt;100 runs on the log, then a log with every &lt;code&gt;cam1&lt;/code&gt; changed to &lt;code&gt;cam3&lt;/code&gt;, then no log at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MATCH   (tail=141 grep=0)  100/100
--- log with no cam1 ---
NOMATCH (tail=0 grep=1)
--- log missing ---
tail: cannot open 'rec.log' for reading: No such file or directory
ERROR   (tail=1 grep=1)
exit 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three come out right. A match is &lt;code&gt;141 0&lt;/code&gt; every time on this log, so a &lt;code&gt;case&lt;/code&gt; that only accepted &lt;code&gt;0 0&lt;/code&gt; would have called every one of those 100 runs an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  The error that 141 hides
&lt;/h2&gt;

&lt;p&gt;The same thread had the catch: "a real find error (EACCES, directory gone) after grep has already left still gets hidden, because pipefail reports the 141 and drops the 1." Here is that case. The 3000-clip folder gets a subfolder that &lt;code&gt;find&lt;/code&gt; cannot read:&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;clips/locked &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;touch &lt;/span&gt;clips/locked/cam2_x.mp4 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;000 clips/locked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the same question two ways. &lt;code&gt;grep -q&lt;/code&gt; leaves at the first match; &lt;code&gt;grep -c&lt;/code&gt; counts, so it reads to the end:&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="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail
find clips &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.mp4'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'cam1_'&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PIPESTATUS=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PIPESTATUS&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;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="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail
&lt;span class="c"&gt;# grep -c reads to the end, so find finishes and its own status survives.&lt;/span&gt;
&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;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.mp4'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'cam1_'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;st&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&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="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;  pipeline status=&lt;/span&gt;&lt;span class="nv"&gt;$st&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the subfolder locked, then unlocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PIPESTATUS=141 0
find: ‘clips/locked’: Permission denied
count=3000  pipeline status=1
--- unlocked ---
PIPESTATUS=141 0
count=3000  pipeline status=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;grep -q&lt;/code&gt;, the locked and unlocked runs are identical: &lt;code&gt;141 0&lt;/code&gt;, nothing on stderr. In this run &lt;code&gt;find&lt;/code&gt; was killed before it reported the folder at all, so there was no error message to lose; the status could not have shown it either. With &lt;code&gt;grep -c&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt; gets to the locked folder, prints &lt;code&gt;Permission denied&lt;/code&gt;, exits 1, and &lt;code&gt;pipefail&lt;/code&gt; carries that 1 out.&lt;/p&gt;

&lt;p&gt;So the &lt;code&gt;PIPESTATUS&lt;/code&gt; branch is correct about &lt;em&gt;what grep saw&lt;/em&gt;, and blind to anything the writer would have hit after the reader left. Whether that matters depends on the check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Is there at least one?"&lt;/strong&gt; A match is a match. A permission error further down does not change the answer, so &lt;code&gt;grep -q&lt;/code&gt; plus the branch is fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Did we look everywhere?"&lt;/strong&gt; Then a reader that leaves early can never give you that, because the writer never finishes. Use a reader that drains (&lt;code&gt;grep -c&lt;/code&gt;, &lt;code&gt;grep ... &amp;gt; /dev/null&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt; with an &lt;code&gt;END&lt;/code&gt;), and accept that it reads the whole stream.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a camera heartbeat, "at least one fresh clip" is the question, so early exit is fine. For a nightly job that counts clips before deleting old ones, it is "did we look everywhere", and a hidden &lt;code&gt;Permission denied&lt;/code&gt; means it counted less than the folder holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check I'd schedule
&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="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nb"&gt;dir&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;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="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;recent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &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="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'cam1_*.mp4'&lt;/span&gt; &lt;span class="nt"&gt;-mmin&lt;/span&gt; &lt;span class="nt"&gt;-15&lt;/span&gt; &lt;span class="nt"&gt;-print&lt;/span&gt; &lt;span class="nt"&gt;-quit&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&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;$recent&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;exit &lt;/span&gt;0
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"camera 1: no new clip in 15 minutes"&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&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit 0 means fresh clip. Exit 3 means the camera stopped. Exit 1 (from &lt;code&gt;find&lt;/code&gt; under &lt;code&gt;set -e&lt;/code&gt;) means the folder itself is broken. Three different problems, three different numbers, and no pipe for a 141 to come out of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ask &lt;code&gt;find&lt;/code&gt; the whole question when you can: &lt;code&gt;-mmin&lt;/code&gt;, &lt;code&gt;-newer&lt;/code&gt;, &lt;code&gt;-name&lt;/code&gt;, then &lt;code&gt;-print -quit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When you need a content test, copy &lt;code&gt;PIPESTATUS&lt;/code&gt; on the next line and treat &lt;code&gt;141 0&lt;/code&gt; as a match, not an error.&lt;/li&gt;
&lt;li&gt;Know which question you're asking. "At least one" tolerates a reader that leaves early. "Did we look everywhere" does not.&lt;/li&gt;
&lt;li&gt;Give "camera stopped" and "folder broken" different exit codes, so the alert says which one it is.&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;Clock jumps.&lt;/strong&gt; &lt;code&gt;-mmin&lt;/code&gt; uses the system clock. A phone or server whose clock jumps will make clips look older or newer than they are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clips still being written.&lt;/strong&gt; A clip that is open and growing has a fresh mtime; one written to a temp name and renamed at the end may not match &lt;code&gt;cam1_*.mp4&lt;/code&gt; until it closes. Check how your recorder writes before picking the window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network shares.&lt;/strong&gt; Over SMB or NFS, mtime and caching may differ. I only tested a local folder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other shells and &lt;code&gt;find&lt;/code&gt; builds.&lt;/strong&gt; bash 5.1 and GNU findutils 4.8 only. &lt;code&gt;-printf&lt;/code&gt;, &lt;code&gt;-mmin&lt;/code&gt; and &lt;code&gt;-quit&lt;/code&gt; are GNU; BSD &lt;code&gt;find&lt;/code&gt; differs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order.&lt;/strong&gt; &lt;code&gt;find&lt;/code&gt; lists in directory order, not sorted. Here the locked subfolder came 655th of 3002 entries, about 13 KB of names in, and &lt;code&gt;grep -q&lt;/code&gt; had already left. In another folder it could come first, and the error would show.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; The quote is c-wind's comment on the &lt;a href="https://dev.to/superfunicular/set-o-pipefail-failed-my-clip-script-on-some-nights-and-not-others-exit-status-141-head-and-grep-99"&gt;first post&lt;/a&gt;. &lt;code&gt;PIPESTATUS&lt;/code&gt; and &lt;code&gt;pipefail&lt;/code&gt; are documented in the &lt;a href="https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html" rel="noopener noreferrer"&gt;Bash manual&lt;/a&gt;; &lt;code&gt;-mmin&lt;/code&gt; and &lt;code&gt;-quit&lt;/code&gt; in the &lt;a href="https://www.gnu.org/software/findutils/manual/html_mono/find.html" rel="noopener noreferrer"&gt;GNU find manual&lt;/a&gt;. Related: &lt;a href="https://dev.to/superfunicular/find-mtime-7-keeps-eight-days-testing-a-cleanup-job-for-a-folder-of-camera-clips-before-it-288h"&gt;find -mtime +7 keeps eight days&lt;/a&gt; and &lt;a href="https://dev.to/superfunicular/rm-mp4-said-argument-list-too-long-and-deleted-nothing-what-execve-counts-that-the-shell-does-not-513k"&gt;rm *.mp4 said Argument list too long&lt;/a&gt;. Outputs are pasted unedited.&lt;/p&gt;

&lt;p&gt;If you want an old phone recording clips in the background so there's a folder like this to watch, 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;A heartbeat check should fail for one reason at a time. Let &lt;code&gt;find&lt;/code&gt; answer the question, and keep the pipe out of it.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>linux</category>
      <category>shell</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Why Does My Phone Security Camera's Audio Hiss in a Quiet Room? Android Audio Sources, Automatic Gain Control, and an Offline Test</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:14:04 +0000</pubDate>
      <link>https://dev.to/superfunicular/why-does-my-phone-security-cameras-audio-hiss-in-a-quiet-room-android-audio-sources-automatic-1pmh</link>
      <guid>https://dev.to/superfunicular/why-does-my-phone-security-cameras-audio-hiss-in-a-quiet-room-android-audio-sources-automatic-1pmh</guid>
      <description>&lt;p&gt;You set an old phone up as an offline security camera, play back a night recording, and the picture is fine. The sound is not. In the quiet stretches there's a hiss that seems to swell, as if the room is breathing. When a door finally slams, it doesn't sound as loud as it should.&lt;/p&gt;

&lt;p&gt;Usually nothing is broken. You're hearing the audio processing Android chose for the recording, and that choice starts from one value an app passes before recording begins: the &lt;strong&gt;audio source&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post covers what Android's documentation says about audio sources, what automatic gain control (AGC) does to a quiet room, a small reproducible simulation with real numbers, and a test you can run on your own recordings without an internet connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does my phone security camera's audio hiss in a quiet room?
&lt;/h2&gt;

&lt;p&gt;Most likely, automatic gain control is raising the level when nothing is happening. AGC is built for speech. It turns quiet input up and loud input down so a voice lands at a steady level. Point it at an empty room at night and the only thing it can turn up is the room's own noise floor: the fridge, the fan, the microphone's own self-noise. So the hiss gets louder during the quiet minutes. When a real sound arrives, AGC pulls the gain back down, and that sound ends up less dominant than it was in the room.&lt;/p&gt;

&lt;p&gt;Whether AGC is applied, and how strongly, depends on the &lt;strong&gt;audio source&lt;/strong&gt; the app records from and on the phone maker's own audio tuning. The next sections cover both.&lt;/p&gt;

&lt;h2&gt;
  
  
  An audio source is a use case, not a microphone
&lt;/h2&gt;

&lt;p&gt;Android apps don't pick a physical microphone. They pick an &lt;code&gt;AudioSource&lt;/code&gt;, which describes what the recording is &lt;em&gt;for&lt;/em&gt;. Android's own audio implementation guide says this directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"In Android app development, a use case is referred to as an &lt;code&gt;AudioSource&lt;/code&gt; instance; and app developers request to use the &lt;code&gt;AudioSource&lt;/code&gt; abstraction instead of the actual audio hardware device. The Android Audio Policy Manager maps an &lt;code&gt;AudioSource&lt;/code&gt; instance to a given capture path configuration (including device, gain, and pre processing) according to product-specific rules."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The important phrase is &lt;strong&gt;product-specific rules&lt;/strong&gt;. The same source can sound different on a Samsung, a Xiaomi and a Pixel, because each maker decides which microphone, which gain and which preprocessing go with it. The default effects for each source live in a vendor file, &lt;code&gt;/vendor/etc/audio_effects.xml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The guide's own example config turns on AGC for the camcorder source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;preprocess&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;stream&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"voice_communication"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;apply&lt;/span&gt; &lt;span class="na"&gt;effect=&lt;/span&gt;&lt;span class="s"&gt;"aec"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;apply&lt;/span&gt; &lt;span class="na"&gt;effect=&lt;/span&gt;&lt;span class="s"&gt;"ns"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/stream&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;stream&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"camcorder"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;apply&lt;/span&gt; &lt;span class="na"&gt;effect=&lt;/span&gt;&lt;span class="s"&gt;"agc"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/stream&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/preprocess&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also lists example tunings: a noise suppressor "tuned for wind noise suppressor for &lt;code&gt;CAMCORDER&lt;/code&gt;", a stationary-noise suppressor for &lt;code&gt;VOICE_COMMUNICATION&lt;/code&gt;, and AGC "tuned for far-talk for &lt;code&gt;CAMCORDER&lt;/code&gt;".&lt;/p&gt;

&lt;p&gt;So it would be reasonable, and fully documented, for a phone to apply far-talk AGC and wind-noise suppression to anything recorded with the camcorder source. That's a sensible choice for someone filming a birthday party. For a camera watching an empty shop at 3 a.m., it's the choice that produces the breathing hiss.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the documentation actually guarantees per source
&lt;/h2&gt;

&lt;p&gt;Here are the sources a security-camera app would realistically choose between, and what is written down about each:&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;What the docs say&lt;/th&gt;
&lt;th&gt;What a camera owner should expect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MIC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;General microphone input&lt;/td&gt;
&lt;td&gt;Whatever the maker tuned as the default path; not specified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CAMCORDER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"Microphone audio source tuned for video recording, with the same orientation as the camera if available"&lt;/td&gt;
&lt;td&gt;AGC and wind suppression are the documented &lt;em&gt;example&lt;/em&gt; tuning; actual effects are product-specific&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VOICE_RECOGNITION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tuned for voice recognition; the CDD requires noise reduction and automatic gain control to be &lt;strong&gt;disabled by default&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;The only source with a written promise of no default AGC or noise reduction&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;"Microphone audio source tuned for unprocessed (raw) sound if available, behaves like DEFAULT otherwise" (added in Android 7.0)&lt;/td&gt;
&lt;td&gt;Raw, if the phone supports it, and you have to check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VOICE_COMMUNICATION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tuned for calls; Android 10+ implementations should provide echo cancellation&lt;/td&gt;
&lt;td&gt;Built for a two-way call, not for a room&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two details matter here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;VOICE_RECOGNITION&lt;/code&gt; is the one written guarantee.&lt;/strong&gt; Android's compatibility rules (the CDD) say devices must, by default, disable any noise reduction and any automatic gain control on this source. The AOSP guide adds a warning: turning noise suppression on by default for this source "causes the device to fail the compatibility requirement." The same section also sets tuning targets for it, including a flat frequency response (±3 dB) from 100 Hz to 4 kHz and "Effects/preprocessing disabled by default".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;UNPROCESSED&lt;/code&gt; is optional.&lt;/strong&gt; A phone that doesn't support it quietly behaves like &lt;code&gt;DEFAULT&lt;/code&gt;. The CDD has devices report support through &lt;code&gt;AudioManager.getProperty(PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED)&lt;/code&gt;, which returns null when there's no support. On older and budget phones, check before relying on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simulation: what AGC does to an empty room
&lt;/h2&gt;

&lt;p&gt;I wanted real numbers, so I built a 60-second test file with ffmpeg: a pink-noise room floor and a single 1.5-second loud event at the 20-second mark. Then I ran it through ffmpeg's &lt;code&gt;dynaudnorm&lt;/code&gt; filter as a &lt;strong&gt;stand-in&lt;/strong&gt; for AGC.&lt;/p&gt;

&lt;p&gt;To be clear about the limits: this isn't any phone's AGC, and it doesn't show what your phone does. &lt;code&gt;dynaudnorm&lt;/code&gt; is a dynamic normaliser that, like AGC, raises quiet passages and holds back loud ones. It shows the shape of the effect. The test at the end of this post measures your own phone.&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;# Room: pink noise floor + one loud 1.5 s event at t=20 s&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=60:sample_rate=48000:seed=7"&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;"anoisesrc=color=white:amplitude=0.35:duration=1.5:sample_rate=48000:seed=3"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[1]adelay=20000|20000,apad=whole_len=2880000[e1];[0][e1]amix=inputs=2:normalize=0[m]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[m]"&lt;/span&gt; &lt;span class="nt"&gt;-ac&lt;/span&gt; 1 room.wav

&lt;span class="c"&gt;# AGC stand-in&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; room.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.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Levels measured with &lt;code&gt;astats&lt;/code&gt; (dBFS, RMS):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stretch&lt;/th&gt;
&lt;th&gt;Raw room&lt;/th&gt;
&lt;th&gt;After AGC stand-in&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Quiet, 0–15 s&lt;/td&gt;
&lt;td&gt;−64.8&lt;/td&gt;
&lt;td&gt;−42.9&lt;/td&gt;
&lt;td&gt;+21.9 dB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quiet, 21.5–36.5 s (just after the event)&lt;/td&gt;
&lt;td&gt;−64.7&lt;/td&gt;
&lt;td&gt;−39.0&lt;/td&gt;
&lt;td&gt;+25.7 dB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quiet, 45–60 s&lt;/td&gt;
&lt;td&gt;−64.6&lt;/td&gt;
&lt;td&gt;−38.7&lt;/td&gt;
&lt;td&gt;+25.9 dB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The event, 20–21.5 s&lt;/td&gt;
&lt;td&gt;−13.9&lt;/td&gt;
&lt;td&gt;−5.3&lt;/td&gt;
&lt;td&gt;+8.6 dB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The quiet room went up by roughly 22 to 26 dB. The event went up by less than 9 dB. In the raw file the event stood about &lt;strong&gt;51 dB&lt;/strong&gt; above the room. After processing it stood about &lt;strong&gt;33 to 37 dB&lt;/strong&gt; above it. The event didn't get quieter. Everything else got louder.&lt;/p&gt;

&lt;p&gt;That gap is what a sound detector depends on. I ran ffmpeg's &lt;code&gt;silencedetect&lt;/code&gt; at −50 dB with a 2-second minimum on both files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raw room:&lt;/strong&gt; silent 0–20 s, silent 21.5–60 s. The event is isolated exactly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AGC stand-in:&lt;/strong&gt; silent only for the first 2.2 s and the last 2 s. Everything from 2.2 s to 58 s counts as one long stretch of "activity", and the event can't be told apart from the room.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've tried to find the noises in a long overnight recording, this is part of why a fixed threshold can let you down. I wrote up the threshold side of that problem in &lt;a href="https://dev.to/superfunicular/ffmpegs-silencedetect-found-nothing-at-40-db-finding-the-noises-in-a-long-recording-5ccb"&gt;ffmpeg's silencedetect Found Nothing at -40 dB: Finding the Noises in a Long Recording&lt;/a&gt;. With gain control in the chain, the room's level moves too, so no single threshold fits the whole night.&lt;/p&gt;

&lt;h2&gt;
  
  
  For developers: check before you choose
&lt;/h2&gt;

&lt;p&gt;If you build a recorder, the platform lets you inspect, and partly control, what's applied. These are the documented calls:&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;// 1. Does this phone support a truly raw source?&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;am&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSystemService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AudioManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&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;rawSupported&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;am&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;AudioManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Which preprocessing effects does this phone expose at all?&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;agcAvailable&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;isAvailable&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;nsAvailable&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;isAvailable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// 3. For an AudioRecord session, explicitly turn an exposed effect off&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;audioRecord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audioSessionId&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="n"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&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="c1"&gt;// when the session ends&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few cautions, all from how the docs are worded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;create()&lt;/code&gt; can return null.&lt;/strong&gt; An effect that isn't exposed through &lt;code&gt;audiofx&lt;/code&gt; can't be controlled through it. The CDD's control requirement is written for speech-tuned noise suppression ("MUST allow this audio effect to be controllable with the &lt;code&gt;android.media.audiofx.NoiseSuppressor&lt;/code&gt; API"). It doesn't promise that every vendor processing step is reachable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your choice of source can differ from what the capture path actually does.&lt;/strong&gt; The Audio Policy Manager maps your source to a path "according to product-specific rules." Measure on real hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's no universally right source.&lt;/strong&gt; &lt;code&gt;VOICE_RECOGNITION&lt;/code&gt; gives you the written no-AGC promise, but it's tuned around voice frequencies. &lt;code&gt;UNPROCESSED&lt;/code&gt; gives you raw sound where it's supported. &lt;code&gt;CAMCORDER&lt;/code&gt; matches the camera's orientation and may suit a moving, handheld recording better. For a fixed camera in a quiet room, the documented no-AGC option is the one to test first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more thing that silently breaks audio on unattended recorders: the microphone needs its own foreground service type. Declaring &lt;code&gt;camera&lt;/code&gt; doesn't cover it. The details, including Android 17's change to how the failure shows up, are in &lt;a href="https://dev.to/superfunicular/androids-camera-foreground-service-type-does-not-include-the-microphone-and-from-android-17-the-37i4"&gt;Android's camera Foreground Service Type Does Not Include the Microphone&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The offline test: measure your own phone in ten minutes
&lt;/h2&gt;

&lt;p&gt;You don't need the internet, an account or a cloud service for this. You need the camera phone, a laptop on the same Wi-Fi (or a USB cable), and ffmpeg.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Record ten quiet minutes.&lt;/strong&gt; Put the phone where it will actually live. Start recording, leave the room, and make one known sound at about the five-minute mark, like a single clap or a door closing. Then stay out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy the file off the phone&lt;/strong&gt; over your local network or by cable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure the quiet before and after the sound:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Quiet stretch before the clap (minute 1 to 4)&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-ss&lt;/span&gt; 60  &lt;span class="nt"&gt;-t&lt;/span&gt; 180 &lt;span class="nt"&gt;-i&lt;/span&gt; night.mp4 &lt;span class="nt"&gt;-vn&lt;/span&gt; &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;span class="c"&gt;# Quiet stretch well after the clap (minute 6 to 9)&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-ss&lt;/span&gt; 360 &lt;span class="nt"&gt;-t&lt;/span&gt; 180 &lt;span class="nt"&gt;-i&lt;/span&gt; night.mp4 &lt;span class="nt"&gt;-vn&lt;/span&gt; &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;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the result.&lt;/strong&gt; If both quiet stretches are within a dB or two of each other and clearly below the clap, your audio path is behaving like a plain recorder. If the quiet level drifts, or sits only a little below the clap, gain control is very likely working on the room.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Listen to the minute right after the clap.&lt;/strong&gt; A dip followed by hiss that slowly swells back is the "breathing" sound AGC is known for.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Run it once in the day and once at night. A fan or fridge that switches on changes the floor, and that's useful to know too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup tips that help no matter which source is used
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep the microphone holes clear.&lt;/strong&gt; Most phones have more than one. Check the bottom edge and near the top camera. Don't stand the phone on its bottom edge in a groove, and don't tape over a hole to stop wind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't put the phone in a closed box.&lt;/strong&gt; A box muffles the sound and traps heat. Heat is the bigger long-term risk for a 24/7 phone, as covered in &lt;a href="https://dev.to/superfunicular/the-hardest-part-of-running-a-phone-as-a-247-camera-isnt-the-camera-its-heat-and-the-battery-4n8d"&gt;The Hardest Part of Running a Phone as a 24/7 Camera Isn't the Camera — It's Heat and the Battery&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move the phone away from steady noise.&lt;/strong&gt; A fridge compressor, a ceiling fan or a router fan gives AGC something to amplify all night.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point the bottom microphone toward the room&lt;/strong&gt;, not toward the wall it's mounted on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the law where you are.&lt;/strong&gt; Many places treat recording sound more strictly than recording video. Some people turn audio off entirely for that reason.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where Background Camera RemoteStream 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, with no cloud account. You can watch live from any browser on the same Wi-Fi through the phone's built-in web server, and you can stream to YouTube Live when you do want an off-site copy. It's a normal install from Google Play, with no subscription and no card.&lt;/p&gt;

&lt;p&gt;I'm not going to claim a particular audio source for it here. Every point in this post depends on the phone the app runs on, so the honest answer is to run the ten-minute test above on your own device. That works the same way for any recording app.&lt;/p&gt;

&lt;p&gt;More on the project 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/core/audio/implement-pre-processing" rel="noopener noreferrer"&gt;Configure preprocessing effects&lt;/a&gt; (AudioSource as use case, Audio Policy Manager "product-specific rules", &lt;code&gt;audio_effects.xml&lt;/code&gt;, CAMCORDER far-talk AGC / wind-noise examples, VOICE_RECOGNITION tuning and the noise-suppression warning)&lt;/li&gt;
&lt;li&gt;Android Developers — &lt;a href="https://developer.android.com/reference/android/media/MediaRecorder.AudioSource" rel="noopener noreferrer"&gt;MediaRecorder.AudioSource&lt;/a&gt; (CAMCORDER, UNPROCESSED, VOICE_RECOGNITION definitions)&lt;/li&gt;
&lt;li&gt;AOSP — &lt;a href="https://source.android.com/docs/compatibility/16/android-16-cdd" rel="noopener noreferrer"&gt;Android 16 Compatibility Definition&lt;/a&gt; (§5.4 VOICE_RECOGNITION defaults; §5.11 unprocessed capture and &lt;code&gt;PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Android Developers — &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/NoiseSuppressor" rel="noopener noreferrer"&gt;NoiseSuppressor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Simulation: ffmpeg 4.4.2 &lt;code&gt;anoisesrc&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-10-28&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>privacy</category>
      <category>security</category>
    </item>
    <item>
      <title>Python Wrote NaN Into My JSON and Only Python Could Read It Back</title>
      <dc:creator>Super Funicular</dc:creator>
      <pubDate>Wed, 28 Oct 2026 04:56:12 +0000</pubDate>
      <link>https://dev.to/superfunicular/python-wrote-nan-into-my-json-and-only-python-could-read-it-back-1n3</link>
      <guid>https://dev.to/superfunicular/python-wrote-nan-into-my-json-and-only-python-could-read-it-back-1n3</guid>
      <description>&lt;p&gt;I had a small Python script writing one JSON line per camera clip, with a temperature reading next to each file name. Sometimes there was no reading, so the script stored &lt;code&gt;float('nan')&lt;/code&gt; as a placeholder. Python read the file back without a complaint. A Node dashboard reading the same file threw on two of the three lines:&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;# write_log.py: one JSON line per clip, with a temperature reading that may be missing
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;clips&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;clip&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;clip_0001.mp4&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;temp_c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;31.5&lt;/span&gt;&lt;span class="p"&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;clip&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;clip_0002.mp4&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;temp_c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;  &lt;span class="c1"&gt;# sensor gave no reading
&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clip&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;clip_0003.mp4&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;temp_c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;  &lt;span class="c1"&gt;# "no limit" placeholder
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;with&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;clips.jsonl&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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;clips&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&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="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&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;/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;python3 write_log.py
&lt;span class="nb"&gt;cat &lt;/span&gt;clips.jsonl
&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": "clip_0001.mp4", "temp_c": 31.5}
{"clip": "clip_0002.mp4", "temp_c": NaN}
{"clip": "clip_0003.mp4", "temp_c": Infinity}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# read_py.py: the Python side reads its own file back
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;with&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;clips.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&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;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&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="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&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 javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// read_node.js: a dashboard reads the same file&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clips.jsonl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&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="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 read_py.py
node read_node.js
&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': 'clip_0001.mp4', 'temp_c': 31.5}
{'clip': 'clip_0002.mp4', 'temp_c': nan}
{'clip': 'clip_0003.mp4', 'temp_c': inf}
{ clip: 'clip_0001.mp4', temp_c: 31.5 }
SyntaxError: Unexpected token 'N', ...""temp_c": NaN}" is not valid JSON
SyntaxError: Unexpected token 'I', ...""temp_c": Infinity}" is not valid JSON
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file is not valid JSON. Python wrote &lt;code&gt;NaN&lt;/code&gt; and &lt;code&gt;Infinity&lt;/code&gt; as bare words, and the JSON grammar has no such values. Python's own reader accepts them, so a test that writes with Python and reads with Python passes, and the problem shows up only when another program opens the file.&lt;/p&gt;

&lt;p&gt;Disclosure: I make an Android camera app. Nothing below uses its code or describes how it stores anything. The scripts here are a desktop Python and Node writing and reading a hand-made file, 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;Python 3.10.12
v22.23.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the Python docs say about this
&lt;/h2&gt;

&lt;p&gt;This is documented, not a bug. The &lt;code&gt;json&lt;/code&gt; module's page describes the &lt;code&gt;allow_nan&lt;/code&gt; parameter of &lt;code&gt;dump&lt;/code&gt; and &lt;code&gt;dumps&lt;/code&gt; like this: "If True (the default), their JavaScript equivalents (NaN, Infinity, -Infinity) are used."&lt;/p&gt;

&lt;p&gt;The same page is direct about the result. Under its section on infinite and NaN values, the example comment reads "Neither of these calls raises an exception, but the results are not valid JSON". The page also says the module "does not comply with the RFC in a strict fashion, implementing some extensions that are valid JavaScript but not valid JSON."&lt;/p&gt;

&lt;p&gt;So the default favours a round trip inside Python over a file that any JSON parser can read. If Python is the sole reader, you may not notice. If the reader is a browser, a dashboard, &lt;code&gt;JSON.parse&lt;/code&gt; in a serverless function, or a strict parser in another language, the line is rejected.&lt;/p&gt;

&lt;p&gt;The docs call these "JavaScript equivalents", but JavaScript's own JSON writer does not produce them. Asked to write the same values, it writes &lt;code&gt;null&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// js_writes.js: what the JavaScript side writes for the same values&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clip_0002.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;temp_c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;NaN&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clip_0003.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;temp_c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;Infinity&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;{"clip":"clip_0002.mp4","temp_c":null}
{"clip":"clip_0003.mp4","temp_c":null}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the two sides do not agree. Python writes a token that JavaScript's parser rejects, and JavaScript, given the same value, writes something different that Python reads as &lt;code&gt;None&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the writer fail instead
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;allow_nan=False&lt;/code&gt; turns the silent write into an error at the moment the bad value is produced:&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;# strict_write.py: same records, allow_nan=False
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clip&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;clip_0001.mp4&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;temp_c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;31.5&lt;/span&gt;&lt;span class="p"&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;clip&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;clip_0002.mp4&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;temp_c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}]:&lt;/span&gt;
    &lt;span class="k"&gt;try&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="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_nan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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;ValueError:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&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;{"clip": "clip_0001.mp4", "temp_c": 31.5}
ValueError: Out of range float values are not JSON compliant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first record is written and the second raises &lt;code&gt;ValueError&lt;/code&gt;. That is usually where you want the failure: in the script that produced the value, with the record still in memory, not later in a dashboard that cannot say where the value came from.&lt;/p&gt;

&lt;p&gt;The check is not limited to top-level fields. A value nested inside a list inside an object is caught too:&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;# nested.py: allow_nan=False also covers values nested deep in the object
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;summary&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;day&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;2026-10-26&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;stats&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;clips&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;31.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]}}&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_nan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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;ValueError:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&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;ValueError: Out of range float values are not JSON compliant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what the message leaves out. It does not name the key or the position of the bad value. On a large object you will need to find it yourself, so it helps to clean values before they reach &lt;code&gt;dumps&lt;/code&gt; rather than relying on the error alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide what a missing reading should be
&lt;/h2&gt;

&lt;p&gt;Raising an error tells you there is a problem. It does not decide what the file should contain. For a reading that is missing, &lt;code&gt;null&lt;/code&gt; is the value JSON has for the absence of a value, and it is also what JavaScript's writer produced above. One way to get there is to replace any float that is not finite before writing:&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;# clean_write.py: write a missing or out-of-range reading as null
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isfinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clip&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;clip_0001.mp4&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;temp_c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;31.5&lt;/span&gt;&lt;span class="p"&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;clip&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;clip_0002.mp4&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;temp_c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;clip&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;clip_0003.mp4&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;temp_c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}]:&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&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;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_nan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&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;{"clip": "clip_0001.mp4", "temp_c": 31.5}
{"clip": "clip_0002.mp4", "temp_c": null}
{"clip": "clip_0003.mp4", "temp_c": null}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line is now valid JSON, and &lt;code&gt;allow_nan=False&lt;/code&gt; stays on as a guard in case a value slips past the cleaning step.&lt;/p&gt;

&lt;p&gt;Two things to decide deliberately rather than by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;null&lt;/code&gt; loses the difference between NaN and infinity.&lt;/strong&gt; In the original records, one value meant the sensor gave nothing and the other was a placeholder for no limit. After cleaning they are both &lt;code&gt;null&lt;/code&gt;. If a reader needs to tell them apart, write a separate field, such as a status string, instead of overloading one number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This version only cleans top-level values.&lt;/strong&gt; The &lt;code&gt;clean&lt;/code&gt; function above does not walk into nested lists or objects. That is why &lt;code&gt;allow_nan=False&lt;/code&gt; stays on: if a nested value is not finite, the write fails loudly instead of producing a bad line.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Make the Python reader as strict as the dashboard
&lt;/h2&gt;

&lt;p&gt;The other half of the problem is that Python's reader accepted the bad file, which is why the bug got past a Python-only test. The docs describe the &lt;code&gt;parse_constant&lt;/code&gt; hook on &lt;code&gt;loads&lt;/code&gt; as a function called with "'-Infinity', 'Infinity', or 'NaN'" that "can be used to raise an exception if invalid JSON numbers are encountered." Using it in a test makes Python reject what the Node reader rejected:&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;# strict_read.py: make the Python reader refuse what the dashboard would refuse
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reject&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="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;non-standard JSON constant: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&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;clips.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&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;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&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="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_constant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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;ValueError:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&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;{'clip': 'clip_0001.mp4', 'temp_c': 31.5}
ValueError: non-standard JSON constant: NaN
ValueError: non-standard JSON constant: Infinity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output matches the Node run line for line: the first record loads, the other two fail. A test that reads files this way will catch a writer that has gone back to the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pass &lt;code&gt;allow_nan=False&lt;/code&gt; to every &lt;code&gt;json.dump&lt;/code&gt; and &lt;code&gt;json.dumps&lt;/code&gt; call whose output another program will read.&lt;/li&gt;
&lt;li&gt;Decide what a missing or out-of-range number should be in the file, and write that on purpose, usually &lt;code&gt;null&lt;/code&gt; plus a status field if the reason matters.&lt;/li&gt;
&lt;li&gt;Clean nested values too, or keep &lt;code&gt;allow_nan=False&lt;/code&gt; on so that nested ones fail loudly.&lt;/li&gt;
&lt;li&gt;In tests, read with &lt;code&gt;parse_constant&lt;/code&gt; set to a function that raises, so Python is at least as strict as the other readers.&lt;/li&gt;
&lt;li&gt;If a file has already been written with the default, it is not valid JSON. Fix it at the writer; changing every reader to accept &lt;code&gt;NaN&lt;/code&gt; spreads the problem.&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;Newer Python.&lt;/strong&gt; I ran 3.10. The quotes come from the current online documentation, which covers newer releases. The behaviour matched what I saw, and the default has been the same for a long time, but I did not run a newer interpreter, and the exact error message text may differ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browsers.&lt;/strong&gt; The JavaScript runs were in Node. Browser engines implement the same &lt;code&gt;JSON.parse&lt;/code&gt; and &lt;code&gt;JSON.stringify&lt;/code&gt; rules, but I did not run these lines in a browser, and their error messages are worded differently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other parsers and tools.&lt;/strong&gt; Command-line tools and other languages' JSON libraries vary in whether they accept these tokens. I did not test any of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NumPy and pandas.&lt;/strong&gt; Their own export functions have separate options for missing values. Nothing here covers them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; Quotes are from the Python documentation page &lt;a href="https://docs.python.org/3/library/json.html" rel="noopener noreferrer"&gt;json — JSON encoder and decoder&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 script like this to log, 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;Python's &lt;code&gt;json&lt;/code&gt; module will write a file that only Python can read unless you tell it not to, so tell it not to.&lt;/p&gt;

</description>
      <category>python</category>
      <category>javascript</category>
      <category>json</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
