Skip to content

Commit 851a529

Browse files
committed
fix(hooks): read $(...), backticks and ${...} inside "..." as their own quoting context in every Bash guard
guard-main-checkout-bash.sh's subst_end() now also answers where a ${...} met inside a double-quoted word ends, and decides itself whether the character opens such a context. guard-shared-stash.sh, guard-process-kill.sh and guard-tree-enum.sh carry a byte-identical copy and delegate to it from split_segments()'s in-quote branch. guard-tree-enum.sh also gains the in-quote backslash branch its two siblings already had, so the three simple segmenters are identical. Claude-Session: https://claude.ai/code/session_01M82feuetLzrCiBz41BJb7L Co-authored-by: Claude <noreply@anthropic.com>
1 parent 62bc129 commit 851a529

4 files changed

Lines changed: 252 additions & 34 deletions

File tree

‎.claude/hooks/guard-main-checkout-bash.sh‎

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
# node -e, awk > (the target is not a shell token, so there is nothing to read);
4545
# - any target containing an expansion or glob ($VAR, `…`, $(…), *, ?, ~) — unresolvable
4646
# without running it;
47+
# - everything after a `'` inside a QUOTED "${…}" unless the quoted run holds none of
48+
# `"` `\` `$` `}` or a backtick: bash and POSIX shells disagree on where such an expansion
49+
# ends (subst_end());
4750
# - relative targets when the payload carries no cwd.
4851
# Each of those is a hole by choice, not by accident: OS_ALLOW_MAIN_EDITS=1 already exists
4952
# for anyone who means to write there, and the target of this guard is the reflexive
@@ -52,8 +55,8 @@
5255
# Writing ABOUT the ban must never trip the ban (#4890's lesson). Four layers:
5356
# 1. quote-aware segmentation + tokenisation — a `>` or a `sed -i` inside '…' or "…" is
5457
# literal text, so `grep -n "sed -i" .claude/` and `echo "never sed -i in main"` pass;
55-
# a `$(…)` or backtick inside "…" is a quoting context of its own, so a `"` in its body
56-
# nests instead of closing the outer string (subst_end(), shared by every pass);
58+
# a `$(…)`, a backtick or a `${…}` inside "…" is a quoting context of its own, so a `"` in
59+
# its body nests instead of closing the outer string (subst_end(), shared by every pass);
5760
# 2. heredoc bodies are stripped before analysis — the LINES of a `cat > /tmp/notes <<EOF`
5861
# body are documentation, not commands, and segmentation alone (which splits on
5962
# newlines) would happily read them as such;
@@ -121,35 +124,66 @@ fi
121124

122125
[ -n "$cmd" ] || exit 0
123126

124-
# --- a command substitution inside "…" is its own quoting context ---------------------
125-
# Inside "…", `$(` or a backtick opens a NEW quoting context: a `"` in the body starts a string
126-
# of its own and does not close the outer one. Each scanner below used to read that `"` as the
127-
# closer, so the rest of the body became bare shell, and a `>` in a quoted curl body was taken
128-
# for a redirect whose "target" was the next fragment — a false BLOCK (#24812). All three
129-
# quote-aware scanners hand such a body to this ONE function, so they agree on where it ends.
127+
# --- an expansion inside "…" is its own quoting context --------------------------------
128+
# Inside "…", `$(`, a backtick or `${` opens a NEW quoting context: a `"` in the body starts a
129+
# string of its own and does not close the outer one. Each scanner below used to read that `"`
130+
# as the closer, so the rest of the body became bare shell, and a `>` in a quoted curl body was
131+
# taken for a redirect whose "target" was the next fragment — a false BLOCK (#24812); the same
132+
# reading of `echo "${x:-"a > b"}"` named `b}` as a target (#24901). All three quote-aware
133+
# scanners hand such a body to this ONE function, so they agree on where it ends.
134+
# guard-shared-stash.sh, guard-process-kill.sh and guard-tree-enum.sh segment commands the same
135+
# way and carry a byte-identical copy; their self-tests fail when a copy differs from this one.
130136
#
131-
# subst_end S I: S[I] is the `$` of a `$(`, or a backtick, met inside "…". Sets SUBST_END to
132-
# the index of the character that closes the substitution, reading the body with its own
133-
# quotes, escapes and nested `(…)` / `$(…)` / backticks; when the input ends first, to the
134-
# last index — the rest is one inert run (bash refuses such a command anyway). The body is
135-
# kept verbatim as part of the word, so a target holding it still fails open on its `$`.
136-
# Boundary: a `(` or `)` bash would not count (a `case` pattern, a comment) moves the end —
137-
# too early, and the rest is read as it was before this function existed; too late, and the
138-
# text swallowed with the body is inert, i.e. fails open.
137+
# subst_end S I: when S[I] opens such a context (the `$` of a `$(` or a `${`, or a backtick),
138+
# sets SUBST_END to the index of the character that closes it and returns 0; otherwise returns 1
139+
# and changes nothing. The body is read with its own quotes, escapes and nesting; when the input
140+
# ends first, SUBST_END is the last index — the rest is one inert run (bash refuses such a
141+
# command anyway). The body is kept verbatim as part of the word, so a target holding it still
142+
# fails open on its `$`.
143+
#
144+
# A `${…}` body reads like "…" with one more closer: `"`, `$(`, `${` and a backtick nest in it,
145+
# a backslash escapes, and `}` closes it. A `'` there is where shells part ways, measured: bash
146+
# 5.2 reads `'…'` as a quoted run while it looks for the `}` (`"${x:-'}'}"` prints `'}'`), and
147+
# `bash --posix` and dash read the `'` as a literal. The two readings agree only when the run
148+
# holds none of `"` `\` `$` `}` or a backtick, so such a run is skipped, and any other `'` makes
149+
# the rest one inert run: an extent that depends on the shell is not one this guard can read.
150+
#
151+
# Boundary: a `(` or `)` bash would not count (a `case` pattern, a comment, an unquoted `${…}`
152+
# holding one) moves the end — too early, and the rest is read as it was before this function
153+
# existed; too late, and the text swallowed with the body is inert, i.e. fails open.
139154
SUBST_END=0
140155
subst_end() {
141-
local s="$1" i="$2" n=${#1} st ch top
142-
if [ "${s:i:1}" = '`' ]; then st='`'; else st=')' ; i=$((i + 1)); fi
156+
local s="$1" i="$2" n=${#1} st ch top run
157+
case "${s:i:2}" in
158+
'$(') st=')' ; i=$((i + 1)) ;;
159+
'${') st='}' ; i=$((i + 1)) ;;
160+
'`'*) st='`' ;;
161+
*) return 1 ;;
162+
esac
143163
for ((i = i + 1; i < n; i++)); do
144164
ch="${s:i:1}" ; top="${st:${#st}-1:1}"
145165
if [ "$top" = "'" ]; then [ "$ch" = "'" ] && st="${st%?}"; continue; fi
146166
if [ "$ch" = '\' ]; then i=$((i + 1)); continue; fi
147-
if [ "$top" = '"' ]; then
167+
if [ "$top" = '"' ] || [ "$top" = '}' ]; then
148168
case "$ch" in
149-
'"') st="${st%?}" ;;
169+
'"') if [ "$top" = '"' ]; then st="${st%?}"; else st+='"'; fi ;;
150170
'`') st+='`' ;;
151-
'$') if [ "${s:i+1:1}" = '(' ]; then st+=')' ; i=$((i + 1)); fi ;;
171+
'$')
172+
case "${s:i+1:1}" in
173+
'(') st+=')' ; i=$((i + 1)) ;;
174+
'{') st+='}' ; i=$((i + 1)) ;;
175+
esac ;;
176+
'}') [ "$top" = '}' ] && st="${st%?}" ;;
177+
"'")
178+
if [ "$top" = '}' ]; then
179+
run="${s:i+1}"
180+
case "$run" in *"'"*) ;; *) SUBST_END=$((n - 1)); return 0 ;; esac
181+
run="${run%%"'"*}"
182+
case "$run" in *[\"\\\`\$\}]*) SUBST_END=$((n - 1)); return 0 ;; esac
183+
i=$((i + ${#run} + 1))
184+
fi ;;
152185
esac
186+
if [ -z "$st" ]; then SUBST_END=$i; return 0; fi
153187
continue
154188
fi
155189
case "$ch" in
@@ -158,9 +192,10 @@ subst_end() {
158192
')') [ "$top" = ')' ] && st="${st%?}" ;;
159193
'`') if [ "$top" = '`' ]; then st="${st%?}"; else st+='`'; fi ;;
160194
esac
161-
if [ -z "$st" ]; then SUBST_END=$i; return; fi
195+
if [ -z "$st" ]; then SUBST_END=$i; return 0; fi
162196
done
163197
SUBST_END=$((n - 1))
198+
return 0
164199
}
165200

166201
# --- heredoc bodies are text, not commands ---------------------------------------------
@@ -193,8 +228,8 @@ strip_line_comment() {
193228
ch="${s:i:1}"
194229
if [ -n "$q" ]; then
195230
if [ "$q" = '"' ] && [ "$ch" = '\' ] && [ $((i + 1)) -lt "$n" ]; then i=$((i + 1)); continue; fi
196-
if [ "$q" = '"' ] && { [ "$ch" = '`' ] || [ "${s:i:2}" = '$(' ]; }; then
197-
subst_end "$s" "$i" ; i=$SUBST_END ; continue
231+
if [ "$q" = '"' ] && { [ "$ch" = '$' ] || [ "$ch" = '`' ]; } && subst_end "$s" "$i"; then
232+
i=$SUBST_END ; continue
198233
fi
199234
[ "$ch" = "$q" ] && q=""
200235
continue
@@ -265,8 +300,8 @@ split_segments() {
265300
seg+="$ch" ; i=$((i + 1)) ; seg+="${s:i:1}" ; continue ;;
266301
esac
267302
fi
268-
if [ "$q" = '"' ] && { [ "$ch" = '`' ] || [ "${s:i:2}" = '$(' ]; }; then
269-
subst_end "$s" "$i" ; seg+="${s:i:SUBST_END-i+1}" ; i=$SUBST_END ; continue
303+
if [ "$q" = '"' ] && { [ "$ch" = '$' ] || [ "$ch" = '`' ]; } && subst_end "$s" "$i"; then
304+
seg+="${s:i:SUBST_END-i+1}" ; i=$SUBST_END ; continue
270305
fi
271306
seg+="$ch"
272307
[ "$ch" = "$q" ] && q=""
@@ -325,8 +360,8 @@ tokenize() {
325360
i=$((i + 1)) ; tok+="${s:i:1}" ; have=1 ; continue ;;
326361
esac
327362
fi
328-
if [ "$q" = '"' ] && { [ "$ch" = '`' ] || [ "${s:i:2}" = '$(' ]; }; then
329-
subst_end "$s" "$i" ; tok+="${s:i:SUBST_END-i+1}" ; i=$SUBST_END ; have=1 ; continue
363+
if [ "$q" = '"' ] && { [ "$ch" = '$' ] || [ "$ch" = '`' ]; } && subst_end "$s" "$i"; then
364+
tok+="${s:i:SUBST_END-i+1}" ; i=$SUBST_END ; have=1 ; continue
330365
fi
331366
if [ "$ch" = "$q" ]; then q=""; else tok+="$ch"; fi
332367
have=1

‎.claude/hooks/guard-process-kill.sh‎

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,63 @@ fi
142142
# (#11738 on the stash guard, #11131 on guard-main-checkout-bash.sh);
143143
# - INSIDE "…" the rule inverts: `\"` is a literal quote that leaves the region OPEN, and
144144
# reading it as closing splits where bash would not, turning the tail of a pure READ
145-
# into a segment judged on its own head word — a false BLOCK (#11804 / #10406).
145+
# into a segment judged on its own head word — a false BLOCK (#11804 / #10406);
146+
# - INSIDE "…" a `$(`, a backtick or a `${` opens a quoting context of its own, so a `"` in
147+
# its body nests instead of closing the outer string. Reading it as the closer split the
148+
# body: `echo "a $(echo "x; pkill -f node")"` and `echo "${x:-"a; killall node"}"` were
149+
# BLOCKED, measured on this repo's copy before the fix (#24901).
146150
#
147151
# Both characters are kept verbatim: this pass only SPLITS, and the classifiers below
148-
# re-read the words afterwards.
152+
# re-read the words afterwards. The self-test fails when this function differs from
153+
# guard-shared-stash.sh's, and when subst_end() differs from guard-main-checkout-bash.sh's,
154+
# whose comment above the function is its documentation.
155+
SUBST_END=0
156+
subst_end() {
157+
local s="$1" i="$2" n=${#1} st ch top run
158+
case "${s:i:2}" in
159+
'$(') st=')' ; i=$((i + 1)) ;;
160+
'${') st='}' ; i=$((i + 1)) ;;
161+
'`'*) st='`' ;;
162+
*) return 1 ;;
163+
esac
164+
for ((i = i + 1; i < n; i++)); do
165+
ch="${s:i:1}" ; top="${st:${#st}-1:1}"
166+
if [ "$top" = "'" ]; then [ "$ch" = "'" ] && st="${st%?}"; continue; fi
167+
if [ "$ch" = '\' ]; then i=$((i + 1)); continue; fi
168+
if [ "$top" = '"' ] || [ "$top" = '}' ]; then
169+
case "$ch" in
170+
'"') if [ "$top" = '"' ]; then st="${st%?}"; else st+='"'; fi ;;
171+
'`') st+='`' ;;
172+
'$')
173+
case "${s:i+1:1}" in
174+
'(') st+=')' ; i=$((i + 1)) ;;
175+
'{') st+='}' ; i=$((i + 1)) ;;
176+
esac ;;
177+
'}') [ "$top" = '}' ] && st="${st%?}" ;;
178+
"'")
179+
if [ "$top" = '}' ]; then
180+
run="${s:i+1}"
181+
case "$run" in *"'"*) ;; *) SUBST_END=$((n - 1)); return 0 ;; esac
182+
run="${run%%"'"*}"
183+
case "$run" in *[\"\\\`\$\}]*) SUBST_END=$((n - 1)); return 0 ;; esac
184+
i=$((i + ${#run} + 1))
185+
fi ;;
186+
esac
187+
if [ -z "$st" ]; then SUBST_END=$i; return 0; fi
188+
continue
189+
fi
190+
case "$ch" in
191+
"'" | '"') st+="$ch" ;;
192+
'(') st+=')' ;;
193+
')') [ "$top" = ')' ] && st="${st%?}" ;;
194+
'`') if [ "$top" = '`' ]; then st="${st%?}"; else st+='`'; fi ;;
195+
esac
196+
if [ -z "$st" ]; then SUBST_END=$i; return 0; fi
197+
done
198+
SUBST_END=$((n - 1))
199+
return 0
200+
}
201+
149202
segments=()
150203
split_segments() {
151204
local s="$1" seg="" q="" ch i n=${#1}
@@ -158,6 +211,9 @@ split_segments() {
158211
seg+="$ch" ; i=$((i + 1)) ; seg+="${s:i:1}" ; continue ;;
159212
esac
160213
fi
214+
if [ "$q" = '"' ] && { [ "$ch" = '$' ] || [ "$ch" = '`' ]; } && subst_end "$s" "$i"; then
215+
seg+="${s:i:SUBST_END-i+1}" ; i=$SUBST_END ; continue
216+
fi
161217
seg+="$ch"
162218
[ "$ch" = "$q" ] && q=""
163219
continue

‎.claude/hooks/guard-shared-stash.sh‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,63 @@ fi
114114
# that guard's `word` bookkeeping has no analogue here because this pass has no comment rule
115115
# to track word starts for. Both characters are kept verbatim — this pass only SPLITS, and
116116
# check_segment() re-reads the words afterwards.
117+
#
118+
# INSIDE "…" a `$(`, a backtick or a `${` opens a quoting context of its own, so a `"` in its
119+
# body nests instead of closing the outer string. Without that rule this pass read the inner
120+
# `"` as the closer, the `;` inside the body split, and a pure read became a stash command —
121+
# measured on this repo's copy before the fix: `echo "a $(echo "x; git stash pop")"` and
122+
# `echo "${x:-"a; git stash pop"}"` were BLOCKED (#24901). The body is kept verbatim inside
123+
# the segment, so a stash inside a QUOTED substitution stays the fail-open it always was.
124+
#
125+
# subst_end() is copied byte-for-byte from guard-main-checkout-bash.sh, whose comment above
126+
# the function is its documentation; this hook's self-test fails when the copy differs.
127+
SUBST_END=0
128+
subst_end() {
129+
local s="$1" i="$2" n=${#1} st ch top run
130+
case "${s:i:2}" in
131+
'$(') st=')' ; i=$((i + 1)) ;;
132+
'${') st='}' ; i=$((i + 1)) ;;
133+
'`'*) st='`' ;;
134+
*) return 1 ;;
135+
esac
136+
for ((i = i + 1; i < n; i++)); do
137+
ch="${s:i:1}" ; top="${st:${#st}-1:1}"
138+
if [ "$top" = "'" ]; then [ "$ch" = "'" ] && st="${st%?}"; continue; fi
139+
if [ "$ch" = '\' ]; then i=$((i + 1)); continue; fi
140+
if [ "$top" = '"' ] || [ "$top" = '}' ]; then
141+
case "$ch" in
142+
'"') if [ "$top" = '"' ]; then st="${st%?}"; else st+='"'; fi ;;
143+
'`') st+='`' ;;
144+
'$')
145+
case "${s:i+1:1}" in
146+
'(') st+=')' ; i=$((i + 1)) ;;
147+
'{') st+='}' ; i=$((i + 1)) ;;
148+
esac ;;
149+
'}') [ "$top" = '}' ] && st="${st%?}" ;;
150+
"'")
151+
if [ "$top" = '}' ]; then
152+
run="${s:i+1}"
153+
case "$run" in *"'"*) ;; *) SUBST_END=$((n - 1)); return 0 ;; esac
154+
run="${run%%"'"*}"
155+
case "$run" in *[\"\\\`\$\}]*) SUBST_END=$((n - 1)); return 0 ;; esac
156+
i=$((i + ${#run} + 1))
157+
fi ;;
158+
esac
159+
if [ -z "$st" ]; then SUBST_END=$i; return 0; fi
160+
continue
161+
fi
162+
case "$ch" in
163+
"'" | '"') st+="$ch" ;;
164+
'(') st+=')' ;;
165+
')') [ "$top" = ')' ] && st="${st%?}" ;;
166+
'`') if [ "$top" = '`' ]; then st="${st%?}"; else st+='`'; fi ;;
167+
esac
168+
if [ -z "$st" ]; then SUBST_END=$i; return 0; fi
169+
done
170+
SUBST_END=$((n - 1))
171+
return 0
172+
}
173+
117174
segments=()
118175
split_segments() {
119176
local s="$1" seg="" q="" ch i n=${#1}
@@ -126,6 +183,9 @@ split_segments() {
126183
seg+="$ch" ; i=$((i + 1)) ; seg+="${s:i:1}" ; continue ;;
127184
esac
128185
fi
186+
if [ "$q" = '"' ] && { [ "$ch" = '$' ] || [ "$ch" = '`' ]; } && subst_end "$s" "$i"; then
187+
seg+="${s:i:SUBST_END-i+1}" ; i=$SUBST_END ; continue
188+
fi
129189
seg+="$ch"
130190
[ "$ch" = "$q" ] && q=""
131191
continue

‎.claude/hooks/guard-tree-enum.sh‎

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,20 +96,87 @@ fi
9696
[ -n "$cmd" ] || exit 0
9797

9898
# --- split the command into shell segments, honouring quotes ---------------------------
99-
# Kept case-for-case identical to guard-shared-stash.sh's pass, including the #11738
100-
# repair: OUTSIDE quotes a backslash escapes the NEXT character, so an escaped `\"` opens
101-
# no quoted region. Without that branch a stray `\"` makes every separator behind it inert,
102-
# the command collapses into one segment, and the real work rides through as an argument.
99+
# Kept case-for-case identical to guard-shared-stash.sh's pass, and the self-test fails when
100+
# it is not. That covers the #11738 repair: OUTSIDE quotes a backslash escapes the NEXT
101+
# character, so an escaped `\"` opens no quoted region. Without that branch a stray `\"`
102+
# makes every separator behind it inert, the command collapses into one segment, and the
103+
# real work rides through as an argument.
104+
#
105+
# INSIDE "…" two more rules hold, and this copy carried neither until #24901, while the line
106+
# above already called it identical. A `\"` there is a literal quote that leaves the region
107+
# OPEN (#11804); and a `$(`, a backtick or a `${` opens a quoting context of its own, so a
108+
# `"` in its body nests instead of closing the outer string. Reading either `"` as the
109+
# closer split a pure read: measured on this repo's copy before the fix,
110+
# `echo "say \"; ls *.yml; git show origin/main:x\" ok"` and
111+
# `echo "a $(echo "x; ls *.yml")" && git show origin/main:README.md` were both BLOCKED.
112+
# subst_end() is copied byte-for-byte from guard-main-checkout-bash.sh, whose comment above
113+
# the function is its documentation; the self-test pins that copy too.
103114
#
104115
# `(` and `)` are separators, which is what makes command substitution fall out correctly
105116
# here: `for f in $(git ls-tree --name-only origin/main dir)` becomes `for f in $` — no
106117
# glob, so not an enumeration — plus a `git ls-tree` segment, which is the suppressor.
118+
SUBST_END=0
119+
subst_end() {
120+
local s="$1" i="$2" n=${#1} st ch top run
121+
case "${s:i:2}" in
122+
'$(') st=')' ; i=$((i + 1)) ;;
123+
'${') st='}' ; i=$((i + 1)) ;;
124+
'`'*) st='`' ;;
125+
*) return 1 ;;
126+
esac
127+
for ((i = i + 1; i < n; i++)); do
128+
ch="${s:i:1}" ; top="${st:${#st}-1:1}"
129+
if [ "$top" = "'" ]; then [ "$ch" = "'" ] && st="${st%?}"; continue; fi
130+
if [ "$ch" = '\' ]; then i=$((i + 1)); continue; fi
131+
if [ "$top" = '"' ] || [ "$top" = '}' ]; then
132+
case "$ch" in
133+
'"') if [ "$top" = '"' ]; then st="${st%?}"; else st+='"'; fi ;;
134+
'`') st+='`' ;;
135+
'$')
136+
case "${s:i+1:1}" in
137+
'(') st+=')' ; i=$((i + 1)) ;;
138+
'{') st+='}' ; i=$((i + 1)) ;;
139+
esac ;;
140+
'}') [ "$top" = '}' ] && st="${st%?}" ;;
141+
"'")
142+
if [ "$top" = '}' ]; then
143+
run="${s:i+1}"
144+
case "$run" in *"'"*) ;; *) SUBST_END=$((n - 1)); return 0 ;; esac
145+
run="${run%%"'"*}"
146+
case "$run" in *[\"\\\`\$\}]*) SUBST_END=$((n - 1)); return 0 ;; esac
147+
i=$((i + ${#run} + 1))
148+
fi ;;
149+
esac
150+
if [ -z "$st" ]; then SUBST_END=$i; return 0; fi
151+
continue
152+
fi
153+
case "$ch" in
154+
"'" | '"') st+="$ch" ;;
155+
'(') st+=')' ;;
156+
')') [ "$top" = ')' ] && st="${st%?}" ;;
157+
'`') if [ "$top" = '`' ]; then st="${st%?}"; else st+='`'; fi ;;
158+
esac
159+
if [ -z "$st" ]; then SUBST_END=$i; return 0; fi
160+
done
161+
SUBST_END=$((n - 1))
162+
return 0
163+
}
164+
107165
segments=()
108166
split_segments() {
109167
local s="$1" seg="" q="" ch i n=${#1}
110168
for ((i = 0; i < n; i++)); do
111169
ch="${s:i:1}"
112170
if [ -n "$q" ]; then
171+
if [ "$q" = '"' ] && [ "$ch" = '\' ] && [ $((i + 1)) -lt "$n" ]; then
172+
case "${s:i+1:1}" in
173+
'"' | '\' | '$' | '`')
174+
seg+="$ch" ; i=$((i + 1)) ; seg+="${s:i:1}" ; continue ;;
175+
esac
176+
fi
177+
if [ "$q" = '"' ] && { [ "$ch" = '$' ] || [ "$ch" = '`' ]; } && subst_end "$s" "$i"; then
178+
seg+="${s:i:SUBST_END-i+1}" ; i=$SUBST_END ; continue
179+
fi
113180
seg+="$ch"
114181
[ "$ch" = "$q" ] && q=""
115182
continue

0 commit comments

Comments
 (0)