test/selftest/070-and-comm-s-two-inputs-must.sh requires every sort feeding a comm to pin LC_ALL=C. Its pattern only matches the pipeline form, so the process-substitution form passes unchecked.
The gap
test/selftest/070:24 _cm_sorts="$(grep -E '\|[[:space:]]*sort' "$_f" || true)"
That matches ... | sort. It cannot match <(sort "$1").
The guard's own comment already notices the distinction — "The first reads a FILE and is not a pipeline at all" — but the regex only covers the pipeline half.
It was live, and the guard was green on it
Found by @OffgridwithJD reviewing #1110. test/run_all_versions.sh used comm in three helpers, every one through process substitution:
matches for '| sort' : 0
occurrences of '<(sort ...)': 3
selftest 070 verdict : run_all_versions.sh passes the guard
So a file using comm three times read as compliant because it never piped into sort. One of the three, pgc_own_mechanism_suites, had been there since #928.
The reordering is real, not theoretical
Measured on real suite names, two collations:
LC_ALL=C ... native_repack pg_dump_roundtrip pgc_setup ... projection_update projections
LC_ALL=en_US ... native_repack pgc_setup pg_dump_roundtrip ... projections projection_update
Two pairs swap, and comm says so when fed the mismatch:
comm: file 1 is not in sorted order
comm: input is not in sorted order
It is not live on CI: the container is C.UTF-8, which orders these as C does, and the workflows set no locale. It bites a developer on en_US.UTF-8, which is the default on plenty of boxes — and it bites them as a wrong set, not an error, because comm's complaint goes to stderr while it still prints output.
A second trap worth encoding
LC_ALL=C comm <(sort a) <(sort b) pins only comm's own comparison. The substitutions run in subshells of the parent and inherit the parent's locale, so that form still sorts on the caller's locale. Both halves need pinning:
LC_ALL=C comm -23 <(LC_ALL=C sort "$1") <(LC_ALL=C sort "$2")
A widened guard should assert both, or it will bless the half-pinned form.
Current state
#1110 pins all three helpers in run_all_versions.sh and adds arms over that file specifically, so nothing is unpinned in the tree today:
process-substitution sorts feeding comm, unpinned, tree-wide: 0
This issue is about the guard, not the tree. The next file to reach for comm with process substitution gets the same free pass, and the arms in #1110 only cover run_all_versions.sh.
What to do
Widen 070's pattern to reach <(sort as well as | sort, and assert the substitution is pinned rather than only the comm. It needs its own removal proof: plant a file using each form, unpinned, and require the guard to name each one. The pipeline half already has coverage, so the new arms are the substitution form and the half-pinned form.
🤖 Generated with Claude Code
https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
test/selftest/070-and-comm-s-two-inputs-must.shrequires everysortfeeding acommto pinLC_ALL=C. Its pattern only matches the pipeline form, so the process-substitution form passes unchecked.The gap
test/selftest/070:24 _cm_sorts="$(grep -E '\|[[:space:]]*sort' "$_f" || true)"That matches
... | sort. It cannot match<(sort "$1").The guard's own comment already notices the distinction — "The first reads a FILE and is not a pipeline at all" — but the regex only covers the pipeline half.
It was live, and the guard was green on it
Found by @OffgridwithJD reviewing #1110.
test/run_all_versions.shusedcommin three helpers, every one through process substitution:So a file using
commthree times read as compliant because it never piped intosort. One of the three,pgc_own_mechanism_suites, had been there since #928.The reordering is real, not theoretical
Measured on real suite names, two collations:
Two pairs swap, and
commsays so when fed the mismatch:It is not live on CI: the container is
C.UTF-8, which orders these asCdoes, and the workflows set no locale. It bites a developer onen_US.UTF-8, which is the default on plenty of boxes — and it bites them as a wrong set, not an error, becausecomm's complaint goes to stderr while it still prints output.A second trap worth encoding
LC_ALL=C comm <(sort a) <(sort b)pins onlycomm's own comparison. The substitutions run in subshells of the parent and inherit the parent's locale, so that form still sorts on the caller's locale. Both halves need pinning:A widened guard should assert both, or it will bless the half-pinned form.
Current state
#1110 pins all three helpers in
run_all_versions.shand adds arms over that file specifically, so nothing is unpinned in the tree today:This issue is about the guard, not the tree. The next file to reach for
commwith process substitution gets the same free pass, and the arms in #1110 only coverrun_all_versions.sh.What to do
Widen 070's pattern to reach
<(sortas well as| sort, and assert the substitution is pinned rather than only thecomm. It needs its own removal proof: plant a file using each form, unpinned, and require the guard to name each one. The pipeline half already has coverage, so the new arms are the substitution form and the half-pinned form.🤖 Generated with Claude Code
https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK