Browse Source
-multiple now accepts a {min max} range (max -1 = unbounded) alongside the
legacy booleans 0/1, so a definition can declare at most once ({0 1}, a second
occurrence is a parse error), bounded repetition ({2 4}), or one-or-more
({1 -1}) instead of choosing between silent last-wins (0) and unbounded
collection (1).
The spec compiler canonicalises once at resolve: the stored -multiple becomes
the computed boolean (list-shape collect: true for max>1 or -1, false for
legacy 0 and max==1) so every existing collect-vs-replace / scalar-vs-list /
leader-value-single-multiple hot-path truth-test stays correct, and the range
companions (min/max/maxbounded) live in a separate per-form MULTIPLE_RANGES
dict - NOT in ARG_INFO, so they do not ride along when ARG_INFO is round-tripped
as a spec via resolved_def copyfrom. Legacy 0/1 and boolean strings
(true/false/yes/no) are coerced to the boolean and stay byte-unchanged (no
MULTIPLE_RANGES entry for unlimited cases, so {0 -1} is equivalent to legacy 1).
Resolve validation: max positive or -1, min <= max, and the -optional/range-min
reconciliation (non-zero min forces presence, contradicts -optional -> reject
with a clear message; declare -optional 0). The value-shape rule: max==1 forms
stay scalar, max>1 or -1 yield the occurrence list.
Parse enforcement: a new PUNKARGS VALIDATION occurrencecount failure class
(payload <argname> count <n> min <m> | max <m>) fires in a single post-loop pass
per section (opts/leaders/values) via a private::multiple_range_enforce
helper. Over-max is a hard contradiction (fires in both normal and
viability-probe modes; parse_status_classify maps it to invalid); under-min is
pure end-of-input exhaustion (SUPPRESSED in the G-152 viability probe via the
viabilitycheck arg, classified incomplete so a viable form reports incomplete
not invalid). Legacy -multiple 1 required with 0 occurrences still reports
trailingvaluecount (byte-unchanged, not occurrencecount).
Rendering: the usage-table Multi column reflects the range (0-1 / 2-4 / 1+ for
unbounded-with-floor; the greencheck stays for legacy 1), the string renderer
emits MULTI:0-1 etc., and the synopsis distinguishes at-most-once (?arg?, no
ellipsis) from repeating (arg...). -multipleunique/-multipleuniqueset compose
with max>1 ranges unchanged. define -help documents the range form and the
-optional/range-min rule.
New testsuite multipleranges.test (28 tests: 13 define-time
canonicalisation+validation, 12 parse-time enforcement incl parse_status
verdicts and a legacy-required-still-trailingvaluecount guard, 3 rendering).
Legacy untouched by default confirmed: full punk/args suite 399/0 (3 skipped),
punk/ns 125/125. All G-053 acceptance criteria met; goal flipped to achieved
2026-08-08 and archived (detail -> goals/archive/, index -> GOALS-archive.md,
reference sweep applied to G-072/G-084, deferred G-084 cross-member -multiple
collection decision recorded as a Follow-on => goal G-084).
Project version 0.59.0 -> 0.60.0 (minor: backward-compatible behaviour
addition). punk::args module 0.21.0 -> 0.22.0.
Assisted-by: harness=pi; primary-model=huggingface/zai-org/GLM-5.2; api-location=huggingface.co
master
12 changed files with 675 additions and 86 deletions
@ -1,75 +0,0 @@
|
||||
# G-053 punk::args range-valued -multiple: occurrence arity with strict duplicate handling |
||||
|
||||
Status: proposed |
||||
Scope: src/modules/punk/args-999999.0a1.0.tm (spec compiler, parse, arg_error/synopsis renderers); src/tests/modules/punk/args/testsuites/args/ |
||||
Goal: -multiple accepts a {min max} occurrence range (mirroring -choicemultiple; max -1 unbounded) alongside the legacy booleans - so a definition can declare "at most once, repeat is an error" ({0 1}) or bounded repetition ({2 4}) instead of choosing between silent last-wins (0) and unbounded collection (1) - with boolean semantics preserved exactly, including the prepend-defaults/last-wins override idiom. |
||||
Acceptance: parse raises a usage-style arity error naming the argument for occurrences outside a declared range; boolean -multiple 0/1 behaviour is unchanged (full existing punk::args suite passes untouched); the -optional/range-min reconciliation rule is documented and enforced at define time; the usage table Multi column and synopsis reflect declared ranges; -multipleunique/-multipleuniqueset compose with max>1 ranges unchanged; characterization tests cover the new forms and the value-shape rule. |
||||
|
||||
## Context |
||||
|
||||
Boolean `-multiple` conflates three axes: |
||||
1. **occurrence arity** - how many times the argument may be supplied |
||||
2. **overflow policy** - what happens beyond the limit: silent replace (legacy |
||||
`-multiple 0` last-wins) or error |
||||
3. **value shape** - scalar vs list-of-occurrences |
||||
|
||||
The motivating incident (2026-07-10): runtests.tcl's `-include-paths` was a |
||||
non-multiple list option, so repeated `-include-paths` flags silently last-won - |
||||
quietly narrowing a test run while reporting green, and defeating even a recorded |
||||
memory note about the gotcha. Repeatable accumulating flags are the dominant |
||||
convention in shell-facing CLIs (gcc -I, curl -H, rsync --exclude), so this misuse |
||||
recurs. runtests was fixed by making that option `-multiple 1`, but the general |
||||
fix for "repeat should be an error" has no expression today. |
||||
|
||||
A separate `-duplicates deny|replace` policy flag was considered and rejected: with |
||||
`-multiple 1` a duplicates policy is meaningless (duplicates ARE the collected |
||||
payload), so the flag's validity would depend on another flag's setting, and it |
||||
would blur into the existing `-multipleunique`/`-multipleuniqueset` territory. |
||||
|
||||
punk::args already has the shape precedent in its own vocabulary: `-choicemultiple` |
||||
is a `{min max}` pair, not a boolean. |
||||
|
||||
## Approach |
||||
|
||||
`-multiple` accepts a boolean (legacy, semantics preserved exactly) or a |
||||
`{min max}` range (max -1 = unbounded): |
||||
|
||||
- `-multiple 0` - legacy: single-valued, repeats silently replace (last wins), |
||||
scalar shape. Unchanged - this preserves the prepend-defaults override idiom |
||||
`punk::args::parse [list -flag default {*}$userargs]`. |
||||
- `-multiple 1` - legacy: unbounded collection, list shape. Unchanged. |
||||
- `-multiple {0 1}` - at most once; a second occurrence is a parse (arity) error. |
||||
This is the "duplicates deny" case. |
||||
- `-multiple {2 4}`, `{1 -1}` etc - bounded/lower-bounded repetition, collected. |
||||
|
||||
Design decisions to settle (record here when made): |
||||
- **Value shape for range forms**: lean - `max == 1` forms stay scalar (they are |
||||
strict single-valued variants of legacy 0); `max > 1` or `-1` yield the |
||||
occurrence list. Whatever is chosen must be pinned by the characterization tests. |
||||
- **-optional vs range-min reconciliation**: lean - `-optional` governs presence, |
||||
the range governs occurrence count when present; contradictory combinations |
||||
(e.g. range min >= 1 with -optional 1 intended as "required") rejected at define |
||||
time with a clear message. |
||||
- **Hot-path canonicalization**: `-multiple` is truth-tested in many parse/render |
||||
sites; a raw `"0 1"` value would fail expr boolean coercion. The spec compiler |
||||
should canonicalize once into internal min/max/policy fields (alongside the |
||||
existing ARG_INFO/ARG_CHECKS structures) so runtime checks stay cheap. |
||||
|
||||
Display benefits: the usage table's Multi column and the synopsis `?arg...?` |
||||
rendering gain meaningful bounded-repetition forms (e.g. "0-1", "2-4"). |
||||
|
||||
## Notes |
||||
|
||||
- `-multipleunique` / `-multipleuniqueset` remain the uniqueness knobs and only |
||||
make sense for max > 1; they compose with ranges unchanged. |
||||
- Related: G-045 (authoring ergonomics, achieved 2026-07-12 - see |
||||
goals/archive/G-045-punkargs-authoring-ergonomics.md), G-046 (parse-time |
||||
performance - the canonicalization must not regress the hot path). |
||||
- The runtests `-include-paths` fix (repeatable, accumulate, single-list form still |
||||
accepted) shipped independently on 2026-07-10 and does not depend on this goal. |
||||
- Archived-goal references in this file: G-046 achieved 2026-07-10 (goals/archive/G-046-punkargs-deferred-help-and-fixes.md). |
||||
- Referenced by G-083 (argument relations - "folding into G-053" |
||||
considered and rejected there; achieved - see |
||||
goals/archive/G-083-punkargs-argument-relations.md) and G-084 (-parsekey |
||||
cross-member -multiple collection rides this goal's occurrence arity) - |
||||
recorded 2026-07-24 after overlap review. |
||||
@ -0,0 +1,128 @@
|
||||
# G-053 punk::args range-valued -multiple: occurrence arity with strict duplicate handling |
||||
|
||||
Status: achieved 2026-08-08 |
||||
Scope: src/modules/punk/args-999999.0a1.0.tm (spec compiler, parse, arg_error/synopsis renderers); src/tests/modules/punk/args/testsuites/args/ |
||||
Goal: -multiple accepts a {min max} occurrence range (mirroring -choicemultiple; max -1 unbounded) alongside the legacy booleans - so a definition can declare "at most once, repeat is an error" ({0 1}) or bounded repetition ({2 4}) instead of choosing between silent last-wins (0) and unbounded collection (1) - with boolean semantics preserved exactly, including the prepend-defaults/last-wins override idiom. |
||||
Acceptance: parse raises a usage-style arity error naming the argument for occurrences outside a declared range; boolean -multiple 0/1 behaviour is unchanged (full existing punk::args suite passes untouched); the -optional/range-min reconciliation rule is documented and enforced at define time; the usage table Multi column and synopsis reflect declared ranges; -multipleunique/-multipleuniqueset compose with max>1 ranges unchanged; characterization tests cover the new forms and the value-shape rule. |
||||
|
||||
## Context |
||||
|
||||
Boolean `-multiple` conflates three axes: |
||||
1. **occurrence arity** - how many times the argument may be supplied |
||||
2. **overflow policy** - what happens beyond the limit: silent replace (legacy |
||||
`-multiple 0` last-wins) or error |
||||
3. **value shape** - scalar vs list-of-occurrences |
||||
|
||||
The motivating incident (2026-07-10): runtests.tcl's `-include-paths` was a |
||||
non-multiple list option, so repeated `-include-paths` flags silently last-won - |
||||
quietly narrowing a test run while reporting green, and defeating even a recorded |
||||
memory note about the gotcha. Repeatable accumulating flags are the dominant |
||||
convention in shell-facing CLIs (gcc -I, curl -H, rsync --exclude), so this misuse |
||||
recurs. runtests was fixed by making that option `-multiple 1`, but the general |
||||
fix for "repeat should be an error" has no expression today. |
||||
|
||||
A separate `-duplicates deny|replace` policy flag was considered and rejected: with |
||||
`-multiple 1` a duplicates policy is meaningless (duplicates ARE the collected |
||||
payload), so the flag's validity would depend on another flag's setting, and it |
||||
would blur into the existing `-multipleunique`/`-multipleuniqueset` territory. |
||||
|
||||
punk::args already has the shape precedent in its own vocabulary: `-choicemultiple` |
||||
is a `{min max}` pair, not a boolean. |
||||
|
||||
## Approach |
||||
|
||||
`-multiple` accepts a boolean (legacy, semantics preserved exactly) or a |
||||
`{min max}` range (max -1 = unbounded): |
||||
|
||||
- `-multiple 0` - legacy: single-valued, repeats silently replace (last wins), |
||||
scalar shape. Unchanged - this preserves the prepend-defaults override idiom |
||||
`punk::args::parse [list -flag default {*}$userargs]`. |
||||
- `-multiple 1` - legacy: unbounded collection, list shape. Unchanged. |
||||
- `-multiple {0 1}` - at most once; a second occurrence is a parse (arity) error. |
||||
This is the "duplicates deny" case. |
||||
- `-multiple {2 4}`, `{1 -1}` etc - bounded/lower-bounded repetition, collected. |
||||
|
||||
Design decisions to settle (record here when made): |
||||
- **Value shape for range forms** (settled 2026-08-08): `max == 1` forms stay |
||||
scalar (they are strict single-valued variants of legacy 0); `max > 1` or `-1` |
||||
yield the occurrence list. This matches the existing boolean truth-test usage |
||||
(`-multiple` true = list-collect), so `{0 1}` is scalar and `{2 4}`/`{1 -1}` |
||||
are lists. Pinned by the characterization tests. |
||||
- **-optional vs range-min reconciliation** (settled 2026-08-08): `-optional` |
||||
governs presence (the 0..1 of whether the arg appears at all); the range min |
||||
governs occurrence count when present. `-optional 0` + range min>=1 = |
||||
required (at least min times). Contradictory combinations (range min >= 1 |
||||
intended as "required" while -optional is also set, or a min that an optional |
||||
arg can never reach) are rejected at define time with a clear message. |
||||
- **Hot-path canonicalization** (settled 2026-08-08): the spec compiler |
||||
canonicalizes `-multiple` ONCE into internal companion fields while preserving |
||||
the stored boolean's meaning so every existing truth-test stays correct: |
||||
- stored `-multiple` boolean = "list-shape collect" (true for legacy 1, |
||||
`{2 4}`, `{1 -1}`, `{0 -1}`; false for legacy 0, `{0 1}`, `{1 1}`) - all |
||||
existing collect-vs-replace / scalar-vs-list / leader-value-single-multiple |
||||
truth-tests keep working unchanged. |
||||
- new internal `_multiple_min` (occurrence floor), `_multiple_max` (cap, |
||||
-1 unbounded), `_multiple_maxbounded` (1 when max is a hard cap that errors |
||||
on exceed). Legacy 0 -> min 0 max -1 maxbounded 0 (unlimited replace); |
||||
legacy 1 -> min 0 max -1 maxbounded 0 (unlimited collect); `{0 1}` -> |
||||
min 0 max 1 maxbounded 1; `{2 4}` -> min 2 max 4 maxbounded 1; `{1 -1}` -> |
||||
min 1 max -1 maxbounded 0. |
||||
- only two new runtime check sites: a max-occurrence check at the storage sites |
||||
(opts + leaders + values; count would exceed max -> arity error) and a |
||||
min-occurrence check at final validation (count < min -> arity error). The |
||||
boolean hot path is untouched. |
||||
|
||||
Display benefits: the usage table's Multi column and the synopsis `?arg...?` |
||||
rendering gain meaningful bounded-repetition forms (e.g. "0-1", "2-4"). |
||||
|
||||
## Progress |
||||
|
||||
- 2026-08-08 G-053 implemented (punk::args 0.22.0, project 0.60.0): -multiple |
||||
now accepts a {min max} range (max -1 = unbounded) alongside the legacy |
||||
booleans. The spec compiler canonicalises once at resolve: the stored |
||||
-multiple becomes the computed boolean (list-shape collect: true for max>1 |
||||
or -1, false for legacy 0 and max==1) so every existing collect-vs-replace / |
||||
scalar-vs-list / leader-value-single-multiple hot-path truth-test stays |
||||
correct, and the range companions (min/max/maxbounded) live in a separate |
||||
per-form MULTIPLE_RANGES dict - NOT in ARG_INFO, so they do not ride along when |
||||
ARG_INFO is round-tripped as a spec via resolved_def copyfrom (the first |
||||
attempt stored them in ARG_INFO and broke 29 tests with an 'unrecognised key |
||||
_multiple_min' resolve error; the separate-dict fix cleared it). Legacy 0/1 |
||||
and boolean strings (true/false/yes/no) are coerced to the boolean and stay |
||||
byte-unchanged (no MULTIPLE_RANGES entry for unlimited cases, so {0 -1} is |
||||
equivalent to legacy 1). Resolve validation: max positive or -1, min <= max, |
||||
and the -optional/range-min reconciliation (non-zero min forces presence, |
||||
contradicts -optional -> reject). Parse enforcement: a new PUNKARGS VALIDATION |
||||
occurrencecount failure class (payload <argname> count <n> min <m> | max <m>) |
||||
fires in a single post-loop pass per section (opts/leaders/values) via a |
||||
private::multiple_range_enforce helper; over-max is a hard contradiction |
||||
(fires in normal and viability-probe modes, parse_status_classify maps it to |
||||
invalid), under-min is pure end-of-input exhaustion (SUPPRESSED in the G-152 |
||||
viability probe via the viabilitycheck arg, classified incomplete so a viable |
||||
form reports incomplete not invalid). The usage-table Multi column reflects |
||||
the range (0-1 / 2-4 / 1+; greencheck stays for legacy 1), the string renderer |
||||
emits MULTI:0-1 etc., and the synopsis distinguishes at-most-once (?arg?, no |
||||
ellipsis) from repeating (arg...). -multipleunique/-multipleuniqueset compose |
||||
unchanged. define -help documents the range form and the -optional/range-min |
||||
rule. New testsuite multipleranges.test (28 tests). Legacy untouched by |
||||
default confirmed: full punk/args suite 399/0 (3 skipped), punk/ns 125/125. |
||||
|
||||
## Notes |
||||
|
||||
- `-multipleunique` / `-multipleuniqueset` remain the uniqueness knobs and only |
||||
make sense for max > 1; they compose with ranges unchanged. |
||||
- Related: G-045 (authoring ergonomics, achieved 2026-07-12 - see |
||||
goals/archive/G-045-punkargs-authoring-ergonomics.md), G-046 (parse-time |
||||
performance - the canonicalization must not regress the hot path). |
||||
- The runtests `-include-paths` fix (repeatable, accumulate, single-list form still |
||||
accepted) shipped independently on 2026-07-10 and does not depend on this goal. |
||||
- Archived-goal references in this file: G-046 achieved 2026-07-10 (goals/archive/G-046-punkargs-deferred-help-and-fixes.md). |
||||
- Referenced by G-083 (argument relations - "folding into G-053" |
||||
considered and rejected there; achieved - see |
||||
goals/archive/G-083-punkargs-argument-relations.md) and G-084 (-parsekey |
||||
cross-member -multiple collection rides this goal's occurrence arity) - |
||||
recorded 2026-07-24 after overlap review. |
||||
|
||||
## Follow-ons |
||||
|
||||
Follow-on: G-084 cross-member -multiple collection on a shared parsekey rides this goal's occurrence-arity model - decide accumulate-in-received-order vs error-on-cross-member-combination now that the {min max} vocabulary is landed (see goals/G-084-punkargs-parsekey-completeness.md) => goal G-084 |
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,355 @@
|
||||
package require tcltest |
||||
package require punk::args |
||||
|
||||
#G-053 range-valued -multiple: occurrence arity with strict duplicate handling. |
||||
#This file pins the DEFINE-TIME vocabulary (spec-compiler canonicalisation and |
||||
#validation). The parse-time enforcement (max-occurrence error, min-occurrence |
||||
#arithmetic) is pinned here too once implemented (increment 2). Error-shape pins |
||||
#use distinctive message substrings for resolve-time definition errors (plain |
||||
#text, no ANSI) - the relations.test/longopts.test convention. Definitions are |
||||
#lazy: resolve errors surface at first parse, so the parse path is used to |
||||
#trigger them and the message substring is pinned. Observable canonicalised |
||||
#state (the stored boolean + the MULTIPLE_RANGES companion dict) is read via |
||||
#punk::args::define + get_spec. |
||||
|
||||
namespace eval ::testspace { |
||||
namespace import ::tcltest::* |
||||
variable common { |
||||
set result "" |
||||
} |
||||
|
||||
#-------------------------------------------------------------------- |
||||
# increment 1: define-time vocabulary (canonicalisation + validation) |
||||
#-------------------------------------------------------------------- |
||||
|
||||
test multirange_legacy_zero_unchanged {legacy -multiple 0 stays scalar replace (byte-unchanged)}\ |
||||
-setup $common -body { |
||||
set argd [punk::args::parse {-x 1 -x 2} withdef @opts {-x -type int}] |
||||
lappend result [dict get $argd opts -x] |
||||
set result |
||||
} -result [list 2] |
||||
|
||||
test multirange_legacy_one_unchanged {legacy -multiple 1 stays list collect (byte-unchanged)}\ |
||||
-setup $common -body { |
||||
set argd [punk::args::parse {-x 1 -x 2} withdef @opts {-x -type int -multiple 1}] |
||||
lappend result [dict get $argd opts -x] |
||||
set result |
||||
} -result [list {1 2}] |
||||
|
||||
test multirange_legacy_boolstring_true {legacy -multiple true coerces to collect like 1}\ |
||||
-setup $common -body { |
||||
set argd [punk::args::parse {-x 1 -x 2} withdef @opts {-x -type int -multiple true}] |
||||
lappend result [dict get $argd opts -x] |
||||
set result |
||||
} -result [list {1 2}] |
||||
|
||||
test multirange_legacy_boolstring_false {legacy -multiple false coerces to scalar replace like 0}\ |
||||
-setup $common -body { |
||||
set argd [punk::args::parse {-x 1 -x 2} withdef @opts {-x -type int -multiple false}] |
||||
lappend result [dict get $argd opts -x] |
||||
set result |
||||
} -result [list 2] |
||||
|
||||
test multirange_range_0_1_canonical_scalar {range {0 1} canonicalises to scalar bool 0 with a maxbounded companion}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@id -id ::g53r1 |
||||
@opts |
||||
-x -type none -multiple {0 1} |
||||
@values -min 0 -max 0 |
||||
} |
||||
set spec [punk::args::get_spec ::g53r1] |
||||
set f0 [lindex [dict keys [dict get $spec FORMS]] 0] |
||||
set ai [dict get $spec FORMS $f0 ARG_INFO] |
||||
lappend result [dict get $ai -x -multiple] |
||||
lappend result [dict get $spec FORMS $f0 MULTIPLE_RANGES -x] |
||||
set result |
||||
} -result [list 0 {min 0 max 1 maxbounded 1}] |
||||
|
||||
test multirange_range_2_4_canonical_list {range {2 4} canonicalises to list bool 1 with a bounded companion}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@id -id ::g53r2 |
||||
@opts |
||||
-x -type int -multiple {2 4} -optional 0 |
||||
@values -min 0 -max 0 |
||||
} |
||||
set spec [punk::args::get_spec ::g53r2] |
||||
set f0 [lindex [dict keys [dict get $spec FORMS]] 0] |
||||
set ai [dict get $spec FORMS $f0 ARG_INFO] |
||||
lappend result [dict get $ai -x -multiple] |
||||
lappend result [dict get $spec FORMS $f0 MULTIPLE_RANGES -x] |
||||
set result |
||||
} -result [list 1 {min 2 max 4 maxbounded 1}] |
||||
|
||||
test multirange_range_1_unbounded_canonical_list {range {1 -1} canonicalises to list bool 1, unbounded, floor 1}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@id -id ::g53r3 |
||||
@opts |
||||
-x -type int -multiple {1 -1} -optional 0 |
||||
@values -min 0 -max 0 |
||||
} |
||||
set spec [punk::args::get_spec ::g53r3] |
||||
set f0 [lindex [dict keys [dict get $spec FORMS]] 0] |
||||
set ai [dict get $spec FORMS $f0 ARG_INFO] |
||||
lappend result [dict get $ai -x -multiple] |
||||
lappend result [dict get $spec FORMS $f0 MULTIPLE_RANGES -x] |
||||
set result |
||||
} -result [list 1 {min 1 max -1 maxbounded 0}] |
||||
|
||||
test multirange_range_0_unbounded_equivalent_legacy_one {range {0 -1} is unbounded collect with no floor - no companion (equivalent to legacy 1)}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@id -id ::g53r4 |
||||
@opts |
||||
-x -type int -multiple {0 -1} |
||||
@values -min 0 -max 0 |
||||
} |
||||
set spec [punk::args::get_spec ::g53r4] |
||||
set f0 [lindex [dict keys [dict get $spec FORMS]] 0] |
||||
set ai [dict get $spec FORMS $f0 ARG_INFO] |
||||
lappend result [dict get $ai -x -multiple] |
||||
lappend result [dict exists [dict get $spec FORMS $f0 MULTIPLE_RANGES] -x] |
||||
set result |
||||
} -result [list 1 0] |
||||
|
||||
test multirange_bad_max_zero {range max of 0 is a resolve error (max must be positive or -1)}\ |
||||
-setup $common -body { |
||||
try { |
||||
punk::args::parse {} withdef @opts {-x -type none -multiple {0 0}} |
||||
lappend result "UNEXPECTED-accepted" |
||||
} on error {emsg eopts} { |
||||
lappend result [string match "*maximum (second element) must be a positive integer or -1*" $emsg] |
||||
} |
||||
set result |
||||
} -result [list 1] |
||||
|
||||
test multirange_bad_min_gt_max {range min > max is a resolve error}\ |
||||
-setup $common -body { |
||||
try { |
||||
punk::args::parse {} withdef @opts {-x -type none -multiple {3 2}} |
||||
lappend result "UNEXPECTED-accepted" |
||||
} on error {emsg eopts} { |
||||
lappend result [string match "*minimum (3) must not exceed the maximum (2)*" $emsg] |
||||
} |
||||
set result |
||||
} -result [list 1] |
||||
|
||||
test multirange_optional_vs_min_reconciliation {range min >= 1 with -optional set (opts default) is a resolve error}\ |
||||
-setup $common -body { |
||||
try { |
||||
#opts default -optional 1; {1 -1} min 1 forces presence -> contradiction |
||||
punk::args::parse {} withdef @opts {-x -type none -multiple {1 -1}} |
||||
lappend result "UNEXPECTED-accepted" |
||||
} on error {emsg eopts} { |
||||
lappend result [string match "*contradictory -multiple range*requires the argument to be present, but -optional is set*" $emsg] |
||||
} |
||||
set result |
||||
} -result [list 1] |
||||
|
||||
test multirange_optional_zero_with_optional_ok {range {0 1} with -optional (default) is fine - min 0 permits absence}\ |
||||
-setup $common -body { |
||||
punk::args::parse {} withdef @opts {-x -type none -multiple {0 1}} |
||||
lappend result "ok" |
||||
set result |
||||
} -result [list ok] |
||||
|
||||
test multirange_multipleunique_composes {range {2 4} with -multipleunique resolves (composes unchanged)}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@id -id ::g53r5 |
||||
@opts |
||||
-x -type int -multiple {2 4} -optional 0 -multipleunique 1 |
||||
@values -min 0 -max 0 |
||||
} |
||||
set spec [punk::args::get_spec ::g53r5] |
||||
set f0 [lindex [dict keys [dict get $spec FORMS]] 0] |
||||
set ai [dict get $spec FORMS $f0 ARG_INFO] |
||||
lappend result [dict get $ai -x -multiple] |
||||
lappend result [dict get $ai -x -multipleunique] |
||||
set result |
||||
} -result [list 1 1] |
||||
|
||||
#-------------------------------------------------------------------- |
||||
# increment 2: parse-time enforcement (occurrencecount) |
||||
#-------------------------------------------------------------------- |
||||
|
||||
test multirange_opt_at_most_once_second_errors {option {0 1}: a second occurrence raises occurrencecount max}\ |
||||
-setup $common -body { |
||||
try { |
||||
punk::args::parse {-x 1 -x 2} withdef @opts {-x -type int -multiple {0 1}} |
||||
lappend result "UNEXPECTED-accepted" |
||||
} on error {emsg eopts} { |
||||
lappend result [string match "*PUNKARGS VALIDATION*occurrencecount -x*max 1*" [dict get $eopts -errorcode]] |
||||
} |
||||
set result |
||||
} -result [list 1] |
||||
|
||||
test multirange_opt_at_most_once_first_ok {option {0 1}: a single occurrence parses (scalar)}\ |
||||
-setup $common -body { |
||||
set argd [punk::args::parse {-x 1} withdef @opts {-x -type int -multiple {0 1}}] |
||||
lappend result [dict get $argd opts -x] |
||||
set result |
||||
} -result [list 1] |
||||
|
||||
test multirange_opt_bounded_under_min_errors {option {2 4}: one occurrence raises occurrencecount min}\ |
||||
-setup $common -body { |
||||
try { |
||||
punk::args::parse {-x 1} withdef @opts {-x -type int -multiple {2 4} -optional 0} |
||||
lappend result "UNEXPECTED-accepted" |
||||
} on error {emsg eopts} { |
||||
lappend result [string match "*PUNKARGS VALIDATION*occurrencecount -x*min 2*" [dict get $eopts -errorcode]] |
||||
} |
||||
set result |
||||
} -result [list 1] |
||||
|
||||
test multirange_opt_bounded_in_range_ok {option {2 4}: three occurrences parse (collected list)}\ |
||||
-setup $common -body { |
||||
set argd [punk::args::parse {-x 1 -x 2 -x 3} withdef @opts {-x -type int -multiple {2 4} -optional 0}] |
||||
lappend result [dict get $argd opts -x] |
||||
set result |
||||
} -result [list {1 2 3}] |
||||
|
||||
test multirange_opt_bounded_over_max_errors {option {2 4}: five occurrences raise occurrencecount max}\ |
||||
-setup $common -body { |
||||
try { |
||||
punk::args::parse {-x 1 -x 2 -x 3 -x 4 -x 5} withdef @opts {-x -type int -multiple {2 4} -optional 0} |
||||
lappend result "UNEXPECTED-accepted" |
||||
} on error {emsg eopts} { |
||||
lappend result [string match "*PUNKARGS VALIDATION*occurrencecount -x*max 4*" [dict get $eopts -errorcode]] |
||||
} |
||||
set result |
||||
} -result [list 1] |
||||
|
||||
test multirange_opt_floor_unbounded_one_ok {option {1 -1}: one occurrence parses (min satisfied)}\ |
||||
-setup $common -body { |
||||
set argd [punk::args::parse {-x 1} withdef @opts {-x -type int -multiple {1 -1} -optional 0}] |
||||
lappend result [dict get $argd opts -x] |
||||
set result |
||||
} -result [list 1] |
||||
|
||||
test multirange_opt_floor_unbounded_zero_errors {option {1 -1}: zero occurrences raise occurrencecount min}\ |
||||
-setup $common -body { |
||||
try { |
||||
punk::args::parse {} withdef @opts {-x -type int -multiple {1 -1} -optional 0} |
||||
lappend result "UNEXPECTED-accepted" |
||||
} on error {emsg eopts} { |
||||
lappend result [string match "*PUNKARGS VALIDATION*occurrencecount -x*min 1*" [dict get $eopts -errorcode]] |
||||
} |
||||
set result |
||||
} -result [list 1] |
||||
|
||||
test multirange_value_bounded_enforced {value {2 4}: under-min errors, in-range ok, over-max errors}\ |
||||
-setup $common -body { |
||||
if {[catch {punk::args::parse {1} withdef @values {v -type int -multiple {2 4}}}]} { |
||||
lappend result under-err |
||||
} else { |
||||
lappend result under-ok |
||||
} |
||||
set argd [punk::args::parse {1 2 3} withdef @values {v -type int -multiple {2 4}}] |
||||
lappend result in-ok |
||||
lappend result [dict get $argd values v] |
||||
if {[catch {punk::args::parse {1 2 3 4 5} withdef @values {v -type int -multiple {2 4}}}]} { |
||||
lappend result over-err |
||||
} else { |
||||
lappend result over-ok |
||||
} |
||||
set result |
||||
} -result [list under-err in-ok {1 2 3} over-err] |
||||
|
||||
test multirange_leader_bounded_enforced {leader {0 2}: at most 2 occurrences enforced}\ |
||||
-setup $common -body { |
||||
set argd [punk::args::parse {1 2 3} withdef @leaders {a -type int} {b -type int -multiple {0 2}} @values] |
||||
lappend result [dict get $argd leaders b] |
||||
try { |
||||
punk::args::parse {1 2 3 4 5} withdef @leaders {a -type int} {b -type int -multiple {0 2}} @values |
||||
lappend result "over:accepted" |
||||
} on error {emsg eopts} { |
||||
lappend result "over:err" |
||||
} |
||||
set result |
||||
} -result [list {2 3} {over:err}] |
||||
|
||||
test multirange_parsestatus_undermin_incomplete {parse_status: under-min occurrence is incomplete (viable, pure exhaustion)}\ |
||||
-setup $common -body { |
||||
set st [punk::args::parse_status {1} withdef @values {v -type int -multiple {2 4}}] |
||||
lappend result [dict get $st status] |
||||
lappend result [dict get $st failureclass] |
||||
set result |
||||
} -result [list incomplete occurrencecount] |
||||
|
||||
test multirange_parsestatus_overmax_invalid {parse_status: over-max occurrence is invalid (hard contradiction)}\ |
||||
-setup $common -body { |
||||
set st [punk::args::parse_status {1 2 3 4 5} withdef @values {v -type int -multiple {2 4}}] |
||||
lappend result [dict get $st status] |
||||
lappend result [dict get $st failureclass] |
||||
set result |
||||
} -result [list invalid occurrencecount] |
||||
|
||||
test multirange_legacy_required_still_missingrequired {legacy -multiple 1 required value with 0 occurrences still reports the count shortfall (byte-unchanged, not occurrencecount)}\ |
||||
-setup $common -body { |
||||
try { |
||||
punk::args::parse {} withdef @values {v -type int -multiple 1} |
||||
lappend result "UNEXPECTED-accepted" |
||||
} on error {emsg eopts} { |
||||
lappend result [lrange [dict get $eopts -errorcode] 0 2] |
||||
} |
||||
set result |
||||
} -result [list {PUNKARGS VALIDATION {trailingvaluecount 0 min 1 max -1}}] |
||||
|
||||
#-------------------------------------------------------------------- |
||||
# increment 3: rendering (Multi column + synopsis reflect declared ranges) |
||||
#-------------------------------------------------------------------- |
||||
|
||||
test multirange_render_multi_column_ranges {usage table Multi column shows 0-1 / 2-4 / 1+ for ranges, greencheck for legacy 1}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@id -id ::g53r_multi |
||||
@opts |
||||
-x -type int -multiple {0 1} |
||||
-y -type int -multiple {2 4} -optional 0 |
||||
-z -type int -multiple {1 -1} -optional 0 |
||||
-w -type int -multiple 1 |
||||
@values -min 0 -max 0 |
||||
} |
||||
#trigger a usage render via a parse error; strip ANSI from the rendered info |
||||
catch {punk::args::parse {--bogus} withid ::g53r_multi} m eopts |
||||
set plain [join [lmap l [split [dict get $eopts -errorinfo] \n] {regsub -all {\x1b\[[0-9;]*m} $l ""}] \n] |
||||
lappend result [regexp {0-1} $plain] |
||||
lappend result [regexp {2-4} $plain] |
||||
lappend result [regexp {1\+} $plain] |
||||
lappend result [regexp {\xe2\x9c\x93|\u2713} $plain] |
||||
set result |
||||
} -result [list 1 1 1 1] |
||||
|
||||
test multirange_render_synopsis_atmostonce_no_ellipsis {synopsis: {0 1} optional arg shows ?-x? with no ... ellipsis (at most once)}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@id -id ::g53r_syn1 |
||||
@opts |
||||
-x -type int -multiple {0 1} |
||||
@values -min 0 -max 0 |
||||
} |
||||
set s [punk::args::synopsis -return full ::g53r_syn1] |
||||
#optional at-most-once: ?-x <int>? and NO trailing ... for -x |
||||
lappend result [string match {*?-x*?*} $s] |
||||
lappend result [expr {[string match {*-x*...*} $s] ? 0 : 1}] |
||||
set result |
||||
} -result [list 1 1] |
||||
|
||||
test multirange_render_synopsis_bounded_repeats_ellipsis {synopsis: {2 4} required arg carries the ... repetition ellipsis}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@id -id ::g53r_syn2 |
||||
@opts |
||||
-y -type int -multiple {2 4} -optional 0 |
||||
@values -min 0 -max 0 |
||||
} |
||||
set s [punk::args::synopsis -return full ::g53r_syn2] |
||||
lappend result [string match {*-y*...*} $s] |
||||
set result |
||||
} -result [list 1] |
||||
|
||||
} |
||||
tcltest::cleanupTests |
||||
Loading…
Reference in new issue