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: comm -23 phone.txt backup.txt. It reported nine missing clips.
comm 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 comm printed, what the GNU manual says about it, a brute-force check of when comm stays quiet, and the version of the job I would run instead.
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:
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
Twelve clips, ten backed up
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.
#!/bin/bash
# A "phone" folder with twelve hourly clips, and a "backup" folder
# where the 09:00 and 12:00 clips never arrived.
mkdir -p phone backup
for h in 06 07 08 09 10 11 12 13 14 15 16 17; do
head -c 2048 /dev/urandom > "phone/2026-11-01_${h}00.mp4"
done
for h in 06 07 08 10 11 13 14 15 16 17; do
cp "phone/2026-11-01_${h}00.mp4" backup/
done
ls phone | wc -l; ls backup | wc -l
12
10
So the right answer is two files: 2026-11-01_0900.mp4 and 2026-11-01_1200.mp4.
The usual comparison
#!/bin/bash
# The usual way: list both sides, ask comm for lines only in the first.
find phone -name '*.mp4' -printf '%f\n' > phone.txt
ls backup > backup.txt
comm -23 phone.txt backup.txt
echo "exit status: $?"
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
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.
The cause is the first input. find printed the names in the order the directory handed them back, which on this machine was not by name. ls sorts its output, so the backup list was in order and the phone list was not. comm 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.
What the manual says
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."
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)."
And it says when the check runs. With neither --check-order nor --nocheck-order on the command line, "wrongly sorted inputs are diagnosed only if an input file is found to contain unpairable lines." The --help text on this machine words the stricter flag as "check that the input is correctly sorted, even if all input lines are pairable".
One more line from the manual matters for a nightly job. In the section on --total, it suggests portable scripts count with wc, and gives comm -23 file1 file2 | wc -l as the example. That example assumes sorted files. Point it at an unsorted one and wc -l counts whatever comm printed, the warning goes to the terminal or the log, and the count looks like any other count.
The job that only wanted a number
#!/bin/bash
# The same comparison inside a nightly job that only wants a number.
missing=$(comm -23 phone.txt backup.txt 2>/dev/null | wc -l)
echo "clips missing from backup: $missing"
clips missing from backup: 9
This is the shape a nightly check tends to take: 2>/dev/null to keep the cron mail quiet, and a count for the log. With the warning discarded, and the pipeline's status coming from wc rather than comm, 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.
When does comm stay quiet?
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 comm -23 with the true set difference each time.
# 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.
import itertools, subprocess
names = "abcd"
runs = wrong = silent_wrong = unsorted_quiet = 0
for n1 in range(1, 5):
for p in itertools.permutations(names, n1):
for n2 in range(1, 4):
for b in itertools.permutations(names, n2):
open("p.txt", "w").write("\n".join(p) + "\n")
open("b.txt", "w").write("\n".join(b) + "\n")
r = subprocess.run(["comm", "-23", "p.txt", "b.txt"],
capture_output=True, text=True,
env={"LC_ALL": "C"})
want = sorted(x for x in p if x not in b)
runs += 1
unsorted = list(p) != sorted(p) or list(b) != sorted(b)
if unsorted and r.returncode == 0:
unsorted_quiet += 1
if sorted(r.stdout.split()) != want:
wrong += 1
if r.returncode == 0:
silent_wrong += 1
print("runs:", runs)
print("wrong answers:", wrong)
print("wrong answers with exit status 0:", silent_wrong)
print("unsorted input, exit status 0:", unsorted_quiet)
runs: 2560
wrong answers: 1692
wrong answers with exit status 0: 0
unsorted input, exit status 0: 128
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.
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.
The version I would run
#!/bin/bash
# Sort both lists the same way comm will compare them, and stop on error.
export LC_ALL=C
sort phone.txt > phone.sorted
sort backup.txt > backup.sorted
if ! missing=$(comm --check-order -23 phone.sorted backup.sorted); then
echo "comm failed; not trusting the list" >&2
exit 1
fi
printf '%s\n' "$missing"
echo "count: $(printf '%s\n' "$missing" | grep -c .)"
echo "== order-free cross-check"
grep -Fxv -f backup.txt phone.txt | sort
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
What changed, line by line:
-
export LC_ALL=Cmakessortandcommcompare names by byte value, the same way. The manual ties the ordercommexpects toLC_COLLATE, 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. -
Both lists go through
sort, including the one fromlsthat already looked sorted. It costs nothing and removes the question. -
--check-ordermakes the check unconditional, which is the stricter behavior the help text describes. -
The exit status is tested before the output is used. If
commfails, the job says so and stops, instead of reporting a count. - The count comes from the stored result, so the number and the list are the same thing.
The last block is a cross-check that does not depend on order at all. grep -Fxv -f backup.txt phone.txt 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.
What I would check in an existing job
-
Find every
commcall, and look at where each input comes from.find, a database query, a file written by an app, or a list someone edited by hand are all inputs that may not be sorted. -
Look for
2>/dev/nullor a pipe intowc -lright after it. Those are the two places the warning and the status get lost. -
Run it once with
--check-orderon real data. If it fails, the old output was not safe to use. -
Keep the sort and the compare under the same locale. Setting
LC_ALL=Cat the top of the script is the simplest way. - Spot-check the count against one known-missing file. Delete one clip from a test backup and confirm the job names it.
What I did not test
-
Other
commimplementations. This was GNU coreutils 8.32 on Linux. BSD and macOScomm, and BusyBox, may check order differently or not at all. -
Larger names and real collation. The brute-force test used four single letters under
LC_ALL=C. Names with spaces, mixed case or non-ASCII characters under another locale were not part of it. -
File names with newlines. A name with a newline in it breaks any one-name-per-line list. The manual's
-zoption andfind -print0exist for that case; I did not test them here. -
Directory order on other file systems. The phone list came out unsorted on this machine. On another file system
findmay return names in a different order, sorted or not, which is the reason to sort rather than rely on it.
Sources. Quotes about ordering, the order check and exit status are from the GNU coreutils manual, comm invocation. The --check-order wording is from comm --help on the machine above. Outputs are pasted unedited from the scripts shown.
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 Background Camera RemoteStream on Google Play. Nothing above depends on it.
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.
Top comments (0)