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
## Building
package.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
(begin
test/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)