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