Commitdb3d3933Recorded27 Jul 2026Repositorysigil-args
Add structured argument passthrough
Changed
README.md | 5 +++++
package.sgl | 4 ++--
src/sigil/args.sgl | 48 ++++++++++++++++++++++++++++++++++++------------
test/test-args.sgl | 29 ++++++++++++++++++++++++++++-
4 files changed, 71 insertions(+), 15 deletions(-)Diff
README.mdmodified
@@ -30,6 +30,11 @@ Defines CLIs as composable record values: options, commands, subcommands, with a
30
31
Supports short flags (`-v`, `-vvv`, `-vf`), long flags (`--verbose`, `--no-verbose`), values (`-o val`, `--output=val`), `--` to stop option parsing, env-var fallback, choice restriction, multi-value (repeatable) options, negatable booleans, custom parsers, and required flags. 32
+33
Commands that forward argv can declare `passthrough: #t`. Their handler takes+34
`(opts args passthrough)`: `args` contains operands before `--`, while+35
`passthrough` contains the exact unparsed tail. It is `#f` when the separator is+36
absent and `()` for an explicitly empty tail.+37
38
See [`docs/args.md`](docs/args.md) for the full reference. 39
40
## Buildingpackage.sglmodified
@@ -4,8 +4,8 @@
4
5
(package 6
name: "sigil-args"−7
version: "0.16.0"−8
sigil: "^0.16"+7
version: "0.17.0"+8
sigil: "^0.17" 9
description: "Command-line argument parsing for Sigil" 10
url: "https://codeberg.org/sigil/sigil-args" 11
license: "BSD-3-Clause"src/sigil/args.sglmodified
@@ -81,10 +81,11 @@
81
82
command command? 83
command-name command-description command-options−84
command-handler command-subcommands+84
command-handler command-subcommands command-passthrough 85
86
parse-result parse-result? 87
parse-result-opts parse-result-args parse-result-errors parse-result-subcommand+88
parse-result-passthrough 89
90
;; Parsing 91
parse-args@@ -158,7 +159,10 @@
159
;;; Handler procedure `(lambda (opts args) ...)` called when command runs. 160
(handler default: #f) 161
;;; List of command records for nested subcommands.−161
(subcommands default: '()))+162
(subcommands default: '())+163
;;; If #t, preserve argv after `--` separately and pass it as the third+164
;;; handler argument. Existing two-argument handlers remain unchanged.+165
(passthrough default: #f)) 166
167
168
;; ========== Parse Result Record ==========@@ -175,7 +179,9 @@
179
;;; Error messages as a list of strings. Empty if parsing succeeded. 180
(errors default: '()) 181
;;; Matched subcommand command record, or `#f` if none.−178
(subcommand default: #f))+182
(subcommand default: #f)+183
;;; Exact arguments following `--`, or #f when no separator occurred.+184
(passthrough default: #f)) 185
186
187
;; ========== Utilities ==========@@ -248,7 +254,16 @@
254
(: command? list? -> parse-result?) 255
(let ((options (command-options cmd)) 256
(subcommands (command-subcommands cmd)))−251
(parse-args-loop options subcommands argv '() '() '() #f #f)))+257
(let* ((result (parse-args-loop options subcommands argv '() '() '() #f #f))+258
(target (or (parse-result-subcommand result) cmd))+259
(tail (parse-result-passthrough result)))+260
(if (or (command-passthrough target) (not tail)) result+261
(parse-result+262
opts: (parse-result-opts result)+263
args: (append (parse-result-args result) tail)+264
errors: (parse-result-errors result)+265
subcommand: (parse-result-subcommand result)+266
passthrough: #f))))) 267
268
;;; Main parsing loop 269
;;; options - List of option specs to match against@@ -262,7 +277,7 @@
277
(define (parse-args-loop options subcommands argv opts args errors stop? subcmd) 278
(if (null? argv) 279
;; Done parsing - apply defaults and check required−265
(finalize-parse options opts args errors subcmd)+280
(finalize-parse options opts args errors subcmd #f) 281
(let ((arg (car argv)) 282
(rest (cdr argv))) 283
(cond@@ -278,14 +293,15 @@
293
opts: (parse-result-opts sub-result) 294
args: (parse-result-args sub-result) 295
errors: (parse-result-errors sub-result)−281
subcommand: matched-subcmd))+296
subcommand: matched-subcmd+297
passthrough: (parse-result-passthrough sub-result))) 298
;; Not a subcommand - it's a positional arg 299
(parse-args-loop options subcommands rest opts 300
(append args (list arg)) errors stop? subcmd)))) 301
302
;; -- means stop parsing options 303
((string=? arg "--")−288
(parse-args-loop options subcommands rest opts args errors #t subcmd))+304
(finalize-parse options opts args errors subcmd rest)) 305
306
;; Long option: --name or --name=value 307
((string-prefix? "--" arg)@@ -444,14 +460,15 @@
460
#f))) 461
462
;;; Finalize parsing: apply env fallback, defaults, check required, validate choices−447
(define (finalize-parse options opts args errors subcmd)+463
(define (finalize-parse options opts args errors subcmd passthrough) 464
(let loop ((remaining options) (final-opts opts) (final-errors errors)) 465
(if (null? remaining) 466
(parse-result 467
opts: (reverse final-opts) 468
args: args 469
errors: (reverse final-errors)−454
subcommand: subcmd)+470
subcommand: subcmd+471
passthrough: passthrough) 472
(let* ((opt (car remaining)) 473
(name (option-name opt)) 474
(existing (alist-get name final-opts))@@ -508,7 +525,9 @@
525
"Usage: " name 526
(if (not (null? opts)) " [options]" "") 527
(if (not (null? subs)) " <command>" "")−511
" [args...]\n"+528
" [args...]"+529
(if (command-passthrough cmd) " [-- args...]" "")+530
"\n" 531
;; Description 532
(if (string=? desc "") "" (string-append "\n" desc "\n")) 533
;; Options@@ -625,6 +644,7 @@
644
(args (parse-result-args result)) 645
(errors (parse-result-errors result)) 646
(subcmd (parse-result-subcommand result))+647
(passthrough (parse-result-passthrough result)) 648
(help-requested? (or (alist-get 'help opts) 649
(and (not (null? argv)) 650
(or (string=? (car argv) "--help")@@ -658,7 +678,9 @@
678
(subcmd 679
(let ((handler (command-handler subcmd))) 680
(if handler−661
(handler opts args)+681
(if (command-passthrough subcmd)+682
(handler opts args passthrough)+683
(handler opts args)) 684
(begin 685
(print-help subcmd) 686
(exit 0)))))@@ -667,7 +689,9 @@
689
(else 690
(let ((handler (command-handler cmd))) 691
(if handler−670
(handler opts args)+692
(if (command-passthrough cmd)+693
(handler opts args passthrough)+694
(handler opts args)) 695
;; No handler and has subcommands - show help 696
(if (not (null? (command-subcommands cmd))) 697
(begintest/test-args.sglmodified
@@ -125,7 +125,30 @@
125
options: (list (option name: 'verbose short: #\v)))) 126
(result (parse-args cmd '("--" "-v" "--help")))) 127
(assert-false (alist-get 'verbose (parse-result-opts result)))−128
(assert-equal '("-v" "--help") (parse-result-args result)))))+128
(assert-equal '("-v" "--help") (parse-result-args result))))+129
+130
(test "passthrough command preserves the separator boundary"+131
(let* ((cmd (command name: "runner" passthrough: #t))+132
(result (parse-args cmd '("app" "--" "-q" "--verbose"))))+133
(assert-equal '("app") (parse-result-args result))+134
(assert-equal '("-q" "--verbose")+135
(parse-result-passthrough result))))+136
+137
(test "passthrough distinguishes absent and empty tails"+138
(let ((cmd (command name: "runner" passthrough: #t)))+139
(assert-false (parse-result-passthrough (parse-args cmd '("app"))))+140
(assert-equal '()+141
(parse-result-passthrough (parse-args cmd '("app" "--"))))))+142
+143
(test "passthrough tail does not select a subcommand"+144
(let* ((child (command name: "child"))+145
(cmd (command name: "runner" passthrough: #t+146
subcommands: (list child)))+147
(result (parse-args cmd '("--" "child" "--" "again"))))+148
(assert-false (parse-result-subcommand result))+149
(assert-null (parse-result-args result))+150
(assert-equal '("child" "--" "again")+151
(parse-result-passthrough result))))) 152
153
;; ============================================================ 154
;; Subcommands@@ -266,4 +289,8 @@
289
(help (generate-help cmd))) 290
(assert-true (string-find help "[a|b|c]"))))) 291
+292
(test "help shows passthrough argv"+293
(let ((help (generate-help (command name: "run" passthrough: #t))))+294
(assert-true (string-find help "[-- args...]"))))+295
296
(run-tests)