a73x

fe38451f

test: bans.sh refuses a backtick inside an unquoted heredoc

a73x   2026-09-04 10:16

Commit message
test: bans.sh refuses a backtick inside an unquoted heredoc

In an unquoted heredoc the shell runs a backquoted word as a command
substitution wherever it stands, comments included. Every ptyclient script
in this suite is an unquoted heredoc, because they interpolate a
fingerprint or a port, and this repo quotes a key or a verb in backticks
everywhere else, so writing one into such a comment is the natural thing to
do. There is an expect program on macOS: the shell starts it, it reads the
rest of the heredoc as its own script, and the group hangs until its budget
runs out with nothing on stdout. That shape cost four gate runs on this
branch, and one of the four was the comment written to explain it.

Two assertions, so the check is seen to fire. A planted fixture holds three
backticked comments under three openers and exactly two must be reported:
the plain one, the dash form, and a QUOTED delimiter that performs no
substitution and is spared. Then the tree itself, over the same four
directories shellGate compiles, which must report nothing. Graded by
mutation: putting one backtick back in the flip leg's comment fails the
check and names both the line and the opener it belongs to.

The fixture's opener is built from a variable rather than written out, so
no line of this file holds the two angle brackets side by side. Spelling
them would make the sweep read this script's own fixture as a heredoc and
report it, and the ways out of that are an exception for this file or a
rule that stops covering it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SakwJEwD9dXBoRP5kWbemW

test/bans.sh
Old New
@@ -14,6 +14,10 @@
14 # 14 #
15 # The scratch file is removed by an EXIT trap, so a failing assertion never 15 # The scratch file is removed by an EXIT trap, so a failing assertion never
16 # leaves a planted needle behind to refuse every later build in the tree. 16 # leaves a planted needle behind to refuse every later build in the tree.
17 #
18 # The last check in this file is not build.zig's at all: it reads the SHELL
19 # scripts for one shape no compiler sees, for the same reason as the rest.
20 # A rule nothing asserts is a rule that has already stopped biting.
17 set -u 21 set -u
18 22
19 ZIG="${1:-deps/zig/zig}" 23 ZIG="${1:-deps/zig/zig}"
@@ -111,5 +115,119 @@ must_skip src/server 'test "bans probe" {
111 _ = probe; 115 _ = probe;
112 }' 116 }'
113 117
118 # ---- the shell rule: no backtick inside an UNQUOTED heredoc -------------
119 #
120 # In an unquoted heredoc the shell runs a backquoted word as a command
121 # substitution wherever it stands, comments included. Every ptyclient script
122 # in this suite is an unquoted heredoc — they interpolate a fingerprint or a
123 # port — and this repo quotes a key or a verb in backticks everywhere else,
124 # so writing one into such a comment is the natural thing to do and it is a
125 # trap. There IS an expect program on macOS: the shell starts it, it reads
126 # the rest of the heredoc as its own script, and the group hangs until its
127 # budget runs out with nothing on stdout. That cost four gate runs on the
128 # macOS port before anyone spelled the rule (2026-09-04), and one of the
129 # four was the comment written to explain it.
130 #
131 # The same four directories shellGate compiles, so a script added tomorrow
132 # is covered without anybody remembering to add it. A heredoc whose
133 # delimiter is QUOTED is left alone, which is what makes this a rule about
134 # the opener rather than a ban on a byte.
135 heredoc_backticks() {
136 awk '
137 # The delimiter this line opens a heredoc with, or "" for a line that
138 # opens none. A three-angle form is a here-string and has no body. A
139 # QUOTED delimiter performs no substitution in its body, so there is
140 # nothing there to catch; the quotes are spelled \047 and \042 so this
141 # whole program fits inside one shell-quoted string.
142 function delim_of(line, p, rest, c) {
143 if (index(line, "<<<") > 0) return ""
144 p = index(line, "<<")
145 if (p == 0) return ""
146 rest = substr(line, p + 2)
147 sub(/^-/, "", rest)
148 sub(/^[ \t]+/, "", rest)
149 c = substr(rest, 1, 1)
150 if (c == "\047" || c == "\042") return ""
151 if (match(rest, /^[A-Za-z_][A-Za-z0-9_]*/)) return substr(rest, RSTART, RLENGTH)
152 return ""
153 }
154 FNR == 1 { inbody = 0 }
155 # Matched ANYWHERE on the line and not just at its end: the site this
156 # rule was written for spells its opener with a trailing space after
157 # the delimiter, and a first sweep that anchored on end-of-line walked
158 # straight past it.
159 !inbody {
160 d = delim_of($0)
161 if (d != "") { inbody = 1; delim = d; opened = FNR }
162 next
163 }
164 {
165 line = $0
166 gsub(/^[ \t]+|[ \t]+$/, "", line)
167 if (line == delim) { inbody = 0; next }
168 if (index($0, "\140") > 0)
169 printf "%s:%d (heredoc opened at %d): %s\n", FILENAME, FNR, opened, $0
170 }
171 ' "$@"
172 }
173
174 # The positive: the check has to SEE the shape, in both spellings of the
175 # opener, or it is a line in a file that never fires. Planted rather than
176 # quoted out of the tree, because the tree is the negative below and a
177 # check that only ever reads a clean tree proves nothing.
178 SHPROBE="$ROOT/test/zz_bans_heredoc_$$.sh"
179 # printf and not a heredoc, because this script would otherwise have to
180 # contain the very shape it refuses — and the trailing space after the
181 # first opener is half of what is being pinned.
182 #
183 # The opener is BUILT rather than written, so that no line of this file
184 # holds the two angle brackets next to each other. Spelling them here would
185 # make the negative sweep below read this script's own fixture as a heredoc
186 # and report its three backticks, and the only ways out of that are an
187 # exception for this file or a rule that stops covering it. Neither is
188 # worth a pair of characters.
189 _a='<'
190 _hd="$_a$_a"
191 printf '%s\n' \
192 "cat ${_hd}EOF " \
193 '# prose quoting a `verb` the way this repo quotes one' \
194 'EOF' \
195 "cat ${_hd}-INNER" \
196 '# and a `second` under the dash form of the opener' \
197 'INNER' \
198 "cat ${_hd}'QUOTED'" \
199 '# a `third` here is inert, because the delimiter is quoted' \
200 'QUOTED' > "$SHPROBE"
201 _hits=$(heredoc_backticks "$SHPROBE" | wc -l | tr -d ' ')
202 rm -f "$SHPROBE"
203 if [ "$_hits" -eq 2 ]; then
204 echo "bans ok: the heredoc rule catches both openers and spares a quoted one"
205 else
206 echo "bans FAIL: the heredoc rule found $_hits of the 2 planted backticks."
207 echo " An unquoted opener must be caught, including the spelling"
208 echo " with a trailing space after the delimiter, and a quoted"
209 echo " one must be spared."
210 FAILED=1
211 fi
212
213 # The negative: the tree as it stands.
214 _sh=""
215 for _d in test tools deps deps/quic; do
216 for _f in "$ROOT/$_d"/*.sh; do
217 [ -f "$_f" ] && _sh="$_sh $_f"
218 done
219 done
220 # shellcheck disable=SC2086 # the list is words, and split is the point
221 _left=$(heredoc_backticks $_sh)
222 if [ -z "$_left" ]; then
223 echo "bans ok: no backtick inside an unquoted heredoc"
224 else
225 echo "bans FAIL: a backtick inside an unquoted heredoc:"
226 printf '%s\n' "$_left" | sed 's/^/ /'
227 echo " The shell runs it as a command substitution. Say the word"
228 echo " in plain prose instead; there is no way to quote it here."
229 FAILED=1
230 fi
231
114 [ "$FAILED" -eq 0 ] || { echo "bans: FAILED"; exit 1; } 232 [ "$FAILED" -eq 0 ] || { echo "bans: FAILED"; exit 1; }
115 echo "bans: every folder rule bit, and the test skip held" 233 echo "bans: every folder rule bit, the test skip held, and no heredoc runs its own comments"