Browse Source
An unquoted trailing -& element on a definition record line continues the
record on the next line. Implemented in
private::split_definition_records: the token is dropped and the next line
joins after a single space with its leading whitespace collapsed - exactly
how the Tcl parser joins backslash-newline continuations before a braced
definition ever reaches the splitter - so a -& record assembles
byte-identical to its backslash-continued equivalent. Motivation:
constructed (string-built) definitions cannot author backslash-newline
ergonomically (the building code's own quoting consumes it); -& is plain
text and survives any construction.
Collision rules (analysis and decision recorded in the goal detail file):
bare word preceded by whitespace (or whole line), trailing whitespace
tolerated; braced/quoted -& is data ({-&} escapes a literal trailing
value); mid-line -&, word-suffix abc-&, and -& inside still-open
braced/quoted values are data. Element-count disambiguation rejected
(action-at-a-distance; positional rule is locally decidable). Backslash
authoring unchanged - continuation is additive.
Tests: new recordcontinuation.test (6 tests) incl. backslash-twin
parse+render byte-equality and the constructed-def chaining case. Full
punk::args suite 181 pass / 1 pre-existing skip / 0 fail; full source-tree
suite 801 pass / 13 skip / 1 fail = exec-14.3 only (known baseline) - zero
regressions (tclsh 9.0.3). define doc documents -& alongside backslash
continuation.
Also recorded: tclparser considered and rejected for the splitter (ANSI
unbalanced-bracket data, dialect semantics outside Tcl grammar, binary
dependency vs G-004); dev-time parse cross-check lint flagged as candidate.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
6 changed files with 298 additions and 11 deletions
@ -1,4 +1,4 @@
|
||||
[project] |
||||
name = "punkshell" |
||||
version = "0.12.5" |
||||
version = "0.12.6" |
||||
license = "BSD-2-Clause" |
||||
|
||||
@ -0,0 +1,182 @@
|
||||
package require tcltest |
||||
|
||||
package require punk::args |
||||
package require punk::ansi |
||||
|
||||
#Record-continuation token -& (G-045): an unquoted trailing -& element continues a |
||||
#definition record on the next line, rewritten internally to a backslash |
||||
#line-continuation so the assembled record is byte-identical to its |
||||
#backslash-continued equivalent. Key properties pinned here: |
||||
# - equivalence: a -& definition parses AND renders identically to its |
||||
# backslash-continuation twin (continuation is additive - backslash authoring |
||||
# unchanged) |
||||
# - the motivating case: constructed (string-built) definitions can use -& where |
||||
# a backslash-newline would be consumed by the building code's own quoting |
||||
# - collision/escape rules: a braced trailing {-&} is data; -& mid-line is data; |
||||
# a word merely ending in the characters -& is data; -& on a line inside a |
||||
# still-open braced value is data |
||||
|
||||
namespace eval ::testspace { |
||||
namespace import ::tcltest::* |
||||
variable common { |
||||
set result "" |
||||
} |
||||
|
||||
proc render_table {id} { |
||||
return [punk::ansi::ansistrip [punk::args::arg_error "" [punk::args::get_spec $id] -aserror 0]] |
||||
} |
||||
|
||||
#added 2026-07-12 (agent, G-045) |
||||
#the two ids are deliberately the same length: the id appears in the rendered |
||||
#synopsis, so differing lengths would change table geometry and defeat the |
||||
#render-equality comparison after the id string map |
||||
test recordcontinuation_amp_equivalent_to_backslash {a definition using trailing -& parses and renders identically to its backslash-continuation equivalent}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@id -id ::testspace::rc_bsl |
||||
@cmd -name testspace::rc_fixture -summary\ |
||||
"Fixture."\ |
||||
-help\ |
||||
"CFIRST line. |
||||
CFLUSH line." |
||||
@opts |
||||
-o1 -type string -default od1 -help\ |
||||
"opt help line1 |
||||
opt help line2" |
||||
@values -min 1 -max 1 |
||||
v1 -type string -help\ |
||||
"val help." |
||||
} |
||||
punk::args::define { |
||||
@id -id ::testspace::rc_amp |
||||
@cmd -name testspace::rc_fixture -summary -& |
||||
"Fixture." -& |
||||
-help -& |
||||
"CFIRST line. |
||||
CFLUSH line." |
||||
@opts |
||||
-o1 -type string -default od1 -help -& |
||||
"opt help line1 |
||||
opt help line2" |
||||
@values -min 1 -max 1 |
||||
v1 -type string -help -& |
||||
"val help." |
||||
} |
||||
set r_b [punk::args::parse {-o1 X VAL1} withid ::testspace::rc_bsl] |
||||
set r_a [punk::args::parse {-o1 X VAL1} withid ::testspace::rc_amp] |
||||
lappend result [expr {[string map {rc_amp rc_bsl} $r_a] eq $r_b}] |
||||
set t_b [render_table ::testspace::rc_bsl] |
||||
set t_a [render_table ::testspace::rc_amp] |
||||
lappend result [expr {[string map {rc_amp rc_bsl} $t_a] eq $t_b}] |
||||
}\ |
||||
-cleanup { |
||||
punk::args::undefine ::testspace::rc_bsl 1 |
||||
punk::args::undefine ::testspace::rc_amp 1 |
||||
}\ |
||||
-result [list 1 1] |
||||
|
||||
#added 2026-07-12 (agent, G-045) - the motivating case: string-built definitions |
||||
#cannot use backslash-newline (consumed by the builder's own quoting) but can use -& |
||||
test recordcontinuation_amp_constructed_definition {a constructed (string-built) definition using -& chains record lines, including into a multi-line quoted value}\ |
||||
-setup $common -body { |
||||
set def "" |
||||
append def "@id -id ::testspace::rc_chain" \n |
||||
append def "@cmd -name testspace::rc_chain -summary \"Chain.\" -help \"chain help\"" \n |
||||
append def "@opts" \n |
||||
append def "-o1 -type string -&" \n |
||||
append def "-default HDEF -& " \n |
||||
append def "-help \"h line1" \n |
||||
append def "h line2\"" \n |
||||
append def "@values -min 0 -max 0" |
||||
punk::args::define $def |
||||
set argd [punk::args::parse {} withid ::testspace::rc_chain] |
||||
lappend result [dict get [dict get $argd opts] -o1] |
||||
set t [render_table ::testspace::rc_chain] |
||||
lappend result [expr {[string first "h line1" $t] >= 0}] |
||||
lappend result [expr {[string first "h line2" $t] >= 0}] |
||||
}\ |
||||
-cleanup { |
||||
punk::args::undefine ::testspace::rc_chain 1 |
||||
}\ |
||||
-result [list HDEF 1 1] |
||||
|
||||
#added 2026-07-12 (agent, G-045) |
||||
test recordcontinuation_amp_braced_escape_is_data {a braced trailing {-&} is a literal value - the record ends and the next line is a separate record}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@id -id ::testspace::rc_braced |
||||
@cmd -name testspace::rc_braced -summary "Braced." -help "braced help" |
||||
@opts |
||||
-o1 -type string -default {-&} |
||||
-o2 -type string -default d2 |
||||
@values -min 0 -max 0 |
||||
} |
||||
set argd [punk::args::parse {} withid ::testspace::rc_braced] |
||||
lappend result [dict get [dict get $argd opts] -o1] |
||||
lappend result [dict get [dict get $argd opts] -o2] |
||||
}\ |
||||
-cleanup { |
||||
punk::args::undefine ::testspace::rc_braced 1 |
||||
}\ |
||||
-result [list -& d2] |
||||
|
||||
#added 2026-07-12 (agent, G-045) |
||||
test recordcontinuation_amp_midline_is_data {an unquoted -& that is not the last element on the line is an ordinary value}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@id -id ::testspace::rc_midline |
||||
@cmd -name testspace::rc_midline -summary "Midline." -help "midline help" |
||||
@opts |
||||
-o1 -default -& -type string |
||||
@values -min 0 -max 0 |
||||
} |
||||
set argd [punk::args::parse {} withid ::testspace::rc_midline] |
||||
lappend result [dict get [dict get $argd opts] -o1] |
||||
}\ |
||||
-cleanup { |
||||
punk::args::undefine ::testspace::rc_midline 1 |
||||
}\ |
||||
-result [list -&] |
||||
|
||||
#added 2026-07-12 (agent, G-045) |
||||
test recordcontinuation_amp_wordend_is_data {a word merely ending in the characters -& is not a continuation token}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@id -id ::testspace::rc_wordend |
||||
@cmd -name testspace::rc_wordend -summary "Wordend." -help "wordend help" |
||||
@opts |
||||
-o1 -type string -default abc-& |
||||
-o2 -type string -default d2 |
||||
@values -min 0 -max 0 |
||||
} |
||||
set argd [punk::args::parse {} withid ::testspace::rc_wordend] |
||||
lappend result [dict get [dict get $argd opts] -o1] |
||||
lappend result [dict get [dict get $argd opts] -o2] |
||||
}\ |
||||
-cleanup { |
||||
punk::args::undefine ::testspace::rc_wordend 1 |
||||
}\ |
||||
-result [list abc-& d2] |
||||
|
||||
#added 2026-07-12 (agent, G-045) |
||||
test recordcontinuation_amp_inside_braces_is_data {a line ending in -& inside a still-open braced value stays literal data}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@id -id ::testspace::rc_inbrace |
||||
@cmd -name testspace::rc_inbrace -summary "Inbrace." -help "inbrace help" |
||||
@values -min 1 -max 1 |
||||
v1 -type string -help {AFIRST -& |
||||
ASECOND line} |
||||
} |
||||
set t [render_table ::testspace::rc_inbrace] |
||||
lappend result [expr {[string first "AFIRST -&" $t] >= 0}] |
||||
lappend result [expr {[string first "ASECOND line" $t] >= 0}] |
||||
}\ |
||||
-cleanup { |
||||
punk::args::undefine ::testspace::rc_inbrace 1 |
||||
}\ |
||||
-result [list 1 1] |
||||
|
||||
cleanupTests |
||||
} |
||||
namespace delete ::testspace |
||||
Loading…
Reference in new issue