Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package guile-arguments for openSUSE:Factory
checked in at 2026-06-17 16:23:38
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/guile-arguments (Old)
and /work/SRC/openSUSE:Factory/.guile-arguments.new.1981 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "guile-arguments"
Wed Jun 17 16:23:38 2026 rev:2 rq:1359919 version:0.2.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/guile-arguments/guile-arguments.changes
2026-06-15 19:54:50.824109065 +0200
+++
/work/SRC/openSUSE:Factory/.guile-arguments.new.1981/guile-arguments.changes
2026-06-17 16:24:31.294628931 +0200
@@ -1,0 +2,8 @@
+Wed Jun 17 08:44:52 UTC 2026 - Giacomo Leidi <[email protected]>
+
+- Update to version 0.2.0:
+ * Release 0.2.0.
+ * arguments: Add structured subcommand support.
+ * arguments: Document why record predicates are duplicated.
+
+-------------------------------------------------------------------
Old:
----
guile-arguments-0.1.1.tar.gz
New:
----
guile-arguments-0.2.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ guile-arguments.spec ++++++
--- /var/tmp/diff_new_pack.vaLuja/_old 2026-06-17 16:24:34.786774846 +0200
+++ /var/tmp/diff_new_pack.vaLuja/_new 2026-06-17 16:24:34.814776016 +0200
@@ -17,7 +17,7 @@
Name: guile-arguments
-Version: 0.1.1
+Version: 0.2.0
Release: 0
Summary: Parser for breaking down command line arguments into
structured objects
License: GPL-3.0-or-later
++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.vaLuja/_old 2026-06-17 16:24:35.198792061 +0200
+++ /var/tmp/diff_new_pack.vaLuja/_new 2026-06-17 16:24:35.246794067 +0200
@@ -1,6 +1,6 @@
<servicedata>
<service name="tar_scm">
<param
name="url">https://codeberg.org/fishinthecalculator/arguments</param>
- <param
name="changesrevision">0f4c00547fa634c6ddb6de0095e5f554edd7149c</param></service></servicedata>
+ <param
name="changesrevision">487558d8932cb67fa02b2dc3b27d45c75543d896</param></service></servicedata>
(No newline at EOF)
++++++ guile-arguments-0.1.1.tar.gz -> guile-arguments-0.2.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/arguments/.guix/modules/guile-arguments.scm
new/arguments/.guix/modules/guile-arguments.scm
--- old/arguments/.guix/modules/guile-arguments.scm 2026-05-06
01:50:10.000000000 +0200
+++ new/arguments/.guix/modules/guile-arguments.scm 2026-06-16
22:41:29.000000000 +0200
@@ -33,7 +33,7 @@
(define-public guile-arguments.git
(package
(name "guile-arguments.git")
- (version "0.1.1")
+ (version "0.2.0")
(source guile-arguments-git-source)
(build-system gnu-build-system)
(arguments
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/arguments/arguments.scm new/arguments/arguments.scm
--- old/arguments/arguments.scm 2026-05-06 01:50:10.000000000 +0200
+++ new/arguments/arguments.scm 2026-06-16 22:41:29.000000000 +0200
@@ -21,8 +21,8 @@
#:export (argument-parser
parser?
- flag option positional
- argument?
+ flag option positional subcommand
+ argument? subcommand?
parse-arguments
parsed-arguments?
@@ -84,23 +84,44 @@
(is-argument? value))
(define-record-type <parser>
- (%make-parser program description epilog arguments)
+ (%make-parser program description epilog arguments subcommands
+ subcommands-required?)
is-parser?
- (program parser-program)
- (description parser-description)
- (epilog parser-epilog)
- (arguments parser-arguments)) ;; list of <argument> in declaration order
+ (program parser-program)
+ (description parser-description)
+ (epilog parser-epilog)
+ (arguments parser-arguments) ;; <argument>s in declaration
order
+ (subcommands parser-subcommands) ;; <subcommand>s in declaration
order
+ (subcommands-required? parser-subcommands-required?))
+;; XXX: This procedure exists so that guile-documenta won't emit empty API
+;; documentation for the record predicate.
(define (parser? value)
"Returns #t if VALUE is a parser record, #f otherwise."
(is-parser? value))
+;; A subcommand wraps a prebuilt subparser and binds it to a name.
+(define-record-type <subcommand>
+ (%make-subcommand name parser help)
+ is-subcommand?
+ (name subcommand-name)
+ (parser subcommand-parser)
+ (help subcommand-help))
+
+;; XXX: This procedure exists so that guile-documenta won't emit empty API
+;; documentation for the record predicate.
+(define (subcommand? value)
+ "Returns #t if VALUE is a subcommand record, #f otherwise."
+ (is-subcommand? value))
+
(define-record-type <parsed-arguments>
(%make-parsed-arguments alist parser)
is-parsed-arguments?
(alist parsed-arguments-alist) ;; ((name . value) ...) in declaration
order
(parser parsed-arguments-parser)) ;; back-pointer for error reporting /
help
+;; XXX: This procedure exists so that guile-documenta won't emit empty API
+;; documentation for the record predicate.
(define (parsed-arguments? value)
"Returns #t if VALUE is a parsed-arguments record, #f otherwise."
(is-parsed-arguments? value))
@@ -124,6 +145,8 @@
(make-exception-with-message msg)
(make-exception-with-origin 'parse-arguments)))))
+;; XXX: This procedure exists so that guile-documenta won't emit empty API
+;; documentation for the record predicate.
(define (argument-error? exn)
"Returns #t if EXN is an exception of @code{&argument-error} type, #f
otherwise."
@@ -135,6 +158,8 @@
(define-exception-type &help-requested &exception
make-help-requested is-help-requested?)
+;; XXX: This procedure exists so that guile-documenta won't emit empty API
+;; documentation for the record predicate.
(define (help-requested? exn)
"Returns #t if EXN is an exception of @code{&help-requested} type, #f
otherwise. Usually this exception is raised when the user passed either
@@ -323,6 +348,38 @@
(or #,(clause-lookup alist 'metavar)
(default-metavar #,name))))))))
+(define-syntax subcommand
+ (lambda (stx)
+ "Declare a subcommand by attaching a prebuilt subparser. Subcommands
+let a single top-level parser dispatch to one of several verbs, each with its
+own argument set. Supported fields:
+
+@itemize
+@item @code{name} (type: @emph{symbol}): The name of the subcommand. It is
+used both to match the token on the command line and as the key the selected
+subcommand's parsed-arguments are stored under.
+@item @code{parser} (type: @emph{parser}): The prebuilt @code{<parser>} value
+to delegate to when this subcommand is selected.
+@item @code{help} (type: @emph{string}): The help string displayed in the
+@code{Commands:} section of the parent parser's help message.
+@end itemize"
+ (syntax-case stx ()
+ ((_ clause ...)
+ (let* ((clauses-syntax #'(clause ...))
+ (alist (clause-alist clauses-syntax))
+ (name (clause-lookup alist 'name))
+ (parser-clause (clause-lookup alist 'parser)))
+ (unless name
+ (argument-error "subcommands must have a name but none was set"))
+ (unless parser-clause
+ (argument-error "subcommands must have a parser but none was set"))
+ #`(let ((p #,parser-clause))
+ (unless (parser? p)
+ (argument-error
+ "subcommand parser must be a <parser>, got ~s" p))
+ (%make-subcommand #,name p
+ (or #,(clause-lookup alist 'help) ""))))))))
+
;;;
;;; Parser construction
;;;
@@ -377,6 +434,10 @@
@item @code{#:auto-help?} (type: @emph{boolean}): Whether or not to intercept
@code{-h} and @code{--help}, emit the auto generated help message and exit or
leave that up to the user to handle.
+@item @code{#:subcommands-required?} (type: @emph{boolean}): When the parser
+declares any subcommand, this keyword argument controls whether the user must
+select one. Defaults to @code{#t} when at least one subcommand is declared,
+@code{#f} otherwise.
@end itemize
Supported argument builders:
@@ -388,51 +449,76 @@
for more details.
@item @code{positional}: Builds positional arguments, refer to its own
documentation for more details.
+@item @code{subcommand}: Builds subcommand entries, refer to its own
+documentation for more details.
@end itemize"
(let loop ((items items)
(program #f)
(description #f)
(epilog #f)
(auto-help? #t)
- (arguments '()))
+ (subcommands-required? 'unset)
+ (arguments '())
+ (subcommands '()))
(match items
(()
- (build-parser program description epilog auto-help? (reverse arguments)))
+ (build-parser program description epilog auto-help?
+ subcommands-required?
+ (reverse arguments)
+ (reverse subcommands)))
(((? keyword? keyword) rest ...)
(when (null? rest)
(argument-error "keyword ~a missing its value" keyword))
(let ((value (car rest)))
(match keyword
(#:program
- (loop (cdr rest) value description epilog auto-help? arguments))
+ (loop (cdr rest) value description epilog auto-help?
+ subcommands-required? arguments subcommands))
(#:description
- (loop (cdr rest) program value epilog auto-help? arguments))
+ (loop (cdr rest) program value epilog auto-help?
+ subcommands-required? arguments subcommands))
(#:epilog
- (loop (cdr rest) program description value auto-help? arguments))
+ (loop (cdr rest) program description value auto-help?
+ subcommands-required? arguments subcommands))
(#:auto-help?
- (loop (cdr rest) program description epilog value arguments))
+ (loop (cdr rest) program description epilog value
+ subcommands-required? arguments subcommands))
+ (#:subcommands-required?
+ (loop (cdr rest) program description epilog auto-help?
+ value arguments subcommands))
(_
(argument-error "unknown keyword in argument-parser: ~a"
keyword)))))
(((? argument? arg) rest ...)
(loop rest program description epilog auto-help?
- (cons arg arguments)))
+ subcommands-required? (cons arg arguments) subcommands))
+ (((? subcommand? sub) rest ...)
+ (loop rest program description epilog auto-help?
+ subcommands-required? arguments (cons sub subcommands)))
(_
(argument-error "unexpected value in argument-parser: ~s" (car
items))))))
-(define (build-parser program description epilog auto-help? arguments)
+(define (build-parser program description epilog auto-help?
+ subcommands-required? arguments subcommands)
(let* ((all (if (and auto-help? (not (has-help-conflict? arguments)))
(cons %auto-help-flag arguments)
arguments))
(default-program
(if (pair? (command-line))
(basename (car (command-line)))
- "PROGRAM")))
+ "PROGRAM"))
+ (effective-required?
+ (if (eq? subcommands-required? 'unset)
+ (pair? subcommands)
+ subcommands-required?)))
(validate-arguments all)
(validate-positional-multiple all)
+ (validate-subcommands all subcommands)
(%make-parser (or program default-program)
(or description "")
(or epilog "")
- all)))
+ all
+ subcommands
+ effective-required?)))
(define (validate-arguments arguments)
;; Catch duplicate names, long forms and short forms eagerly so the
@@ -469,6 +555,33 @@
(argument-error
"only the last positional may be #:multiple? #t")))))
+(define (validate-subcommands arguments subcommands)
+ ;; Subcommands are dispatched on a bare positional token, so they cannot
+ ;; coexist with top-level positionals. Their names must not collide with
+ ;; each other, with any top-level argument name, or with the reserved
+ ;; 'help name.
+ (when (pair? subcommands)
+ (when (any (lambda (a) (eq? (argument-kind a) 'positional)) arguments)
+ (argument-error
+ "a parser with subcommands cannot have top-level positionals"))
+ (let loop ((seen '()) (rest subcommands))
+ (when (pair? rest)
+ (let ((n (subcommand-name (car rest))))
+ (when (eq? n 'help)
+ (argument-error
+ "'help is reserved and cannot be used as a subcommand name"))
+ (when (memq n seen)
+ (argument-error "duplicate subcommand name: ~s" n))
+ (loop (cons n seen) (cdr rest)))))
+ (let ((arg-names (map argument-name arguments)))
+ (for-each
+ (lambda (s)
+ (let ((n (subcommand-name s)))
+ (when (memq n arg-names)
+ (argument-error
+ "subcommand name ~s collides with an argument name" n))))
+ subcommands))))
+
;;;
;;; Parsing
;;;
@@ -485,6 +598,15 @@
(filter (lambda (a) (eq? (argument-kind a) 'positional))
(parser-arguments parser)))
+(define (find-subcommand parser token)
+ (find (lambda (s)
+ (string=? (symbol->string (subcommand-name s)) token))
+ (parser-subcommands parser)))
+
+(define (subcommand-names parser)
+ (map (lambda (s) (symbol->string (subcommand-name s)))
+ (parser-subcommands parser)))
+
(define (record-value alist arg value)
(let ((name (argument-name arg)))
(if (argument-multiple? arg)
@@ -527,9 +649,13 @@
(finalize parser alist))
(after-delimiter?
(call-with-values
- (lambda () (consume-positional alist queue (car rest)))
- (lambda (alist queue)
- (loop alist queue (cdr rest) #t))))
+ (lambda ()
+ (consume-positional-or-subcommand
+ parser alist queue (car rest) (cdr rest) handle-help?))
+ (lambda (alist queue new-rest dispatched?)
+ (if dispatched?
+ (finalize parser alist)
+ (loop alist queue new-rest #t)))))
(else
(let ((token (car rest)))
(cond
@@ -547,9 +673,13 @@
(loop alist queue new-rest #f))))
(else
(call-with-values
- (lambda () (consume-positional alist queue token))
- (lambda (alist queue)
- (loop alist queue (cdr rest) #f))))))))))))
+ (lambda ()
+ (consume-positional-or-subcommand
+ parser alist queue token (cdr rest) handle-help?))
+ (lambda (alist queue new-rest dispatched?)
+ (if dispatched?
+ (finalize parser alist)
+ (loop alist queue new-rest #f)))))))))))))
(define (handle-long parser alist token rest)
;; token looks like "--name" or "--name=value". If it's "--name" and
@@ -633,43 +763,79 @@
(values (record-value alist arg ((argument-type arg) token))
(cdr queue))))))))
+(define (consume-positional-or-subcommand parser alist queue token rest
+ handle-help?)
+ ;; If the parser has subcommands declared then the bare token must match
+ ;; one (top-level positionals are forbidden in that case). On a hit we
+ ;; recursively delegate the remainder of argv to the subparser, threading
+ ;; #:handle-help? through so the subparser owns its own help. Otherwise
+ ;; fall back to the regular positional consumer.
+ ;;
+ ;; Returned values: (alist queue rest dispatched?).
+ (if (pair? (parser-subcommands parser))
+ (let ((sub (find-subcommand parser token)))
+ (if sub
+ (let* ((inner (parse-arguments (subcommand-parser sub) rest
+ #:handle-help? handle-help?))
+ (name (subcommand-name sub)))
+ (values (acons name inner alist) queue '() #t))
+ (argument-error
+ "unknown subcommand: ~s (expected one of: ~a)"
+ token
+ (string-join (subcommand-names parser) ", "))))
+ (call-with-values
+ (lambda () (consume-positional alist queue token))
+ (lambda (alist queue)
+ (values alist queue rest #f)))))
+
(define (finalize parser alist)
;; Walk the declared arguments in order and build the final result.
;; Each missing argument either gets its default or, if required,
;; raises. We preserve declaration order in the resulting alist so
- ;; downstream tools (help, debugging) get a predictable layout.
- (let loop ((arguments (parser-arguments parser))
- (out '()))
- (cond
- ((null? arguments)
- (%make-parsed-arguments (reverse out) parser))
- (else
- (let* ((a (car arguments))
- (name (argument-name a))
- (required? (argument-required? a))
- (long (argument-long a))
- (metavar (argument-metavar a))
- (default (argument-default a))
- (kind (argument-kind a))
- (multiple? (argument-multiple? a)))
- (cond
- ((assq name alist)
- => (lambda (p) (loop (cdr arguments) (cons p out))))
- (required?
- (argument-error "missing required argument: ~a"
- (or long metavar
- (symbol->string name))))
- (else
- (loop (cdr arguments)
- (cons
- (cons name
- ;; default value
- (cond
- ((not (eq? default #f)) default)
- ((eq? kind 'flag) #f)
- (multiple? '())
- (else #f)))
- out)))))))))
+ ;; downstream tools (help, debugging) get a predictable layout. When
+ ;; subcommands are declared, the selected one (if any) is appended at
+ ;; the end of the result alist; an unselected subcommand is absent.
+ (let* ((subs (parser-subcommands parser))
+ (sub-entries
+ (filter (lambda (p)
+ (any (lambda (s) (eq? (car p) (subcommand-name s))) subs))
+ alist)))
+ (when (and (pair? subs)
+ (parser-subcommands-required? parser)
+ (null? sub-entries))
+ (argument-error
+ "missing required subcommand (expected one of: ~a)"
+ (string-join (subcommand-names parser) ", ")))
+ (let loop ((arguments (parser-arguments parser))
+ (out '()))
+ (if (null? arguments)
+ (%make-parsed-arguments (append (reverse out) sub-entries) parser)
+ (let* ((a (car arguments))
+ (name (argument-name a))
+ (required? (argument-required? a))
+ (long (argument-long a))
+ (metavar (argument-metavar a))
+ (default (argument-default a))
+ (kind (argument-kind a))
+ (multiple? (argument-multiple? a)))
+ (cond
+ ((assq name alist)
+ => (lambda (p) (loop (cdr arguments) (cons p out))))
+ (required?
+ (argument-error "missing required argument: ~a"
+ (or long metavar
+ (symbol->string name))))
+ (else
+ (loop (cdr arguments)
+ (cons
+ (cons name
+ ;; default value
+ (cond
+ ((not (eq? default #f)) default)
+ ((eq? kind 'flag) #f)
+ (multiple? '())
+ (else #f)))
+ out)))))))))
;;;
;;; Accessors
@@ -749,11 +915,13 @@
(opts (filter (lambda (a)
(memq (argument-kind a) '(flag option)))
(parser-arguments parser)))
- (pos (positionals parser)))
+ (pos (positionals parser))
+ (subs (parser-subcommands parser)))
(string-append
"Usage: " prog
(if (pair? opts) " [OPTIONS]" "")
(string-concatenate (map positional-usage-fragment pos))
+ (if (pair? subs) " COMMAND [ARGS...]" "")
"\n")))
(define (format-positional-line a)
@@ -798,6 +966,9 @@
(string-append " " left (make-string pad #\space) right "\n")))
entries))))
+(define (format-subcommand-line s)
+ (cons (symbol->string (subcommand-name s)) (subcommand-help s)))
+
(define (format-help parser)
"Serializes PARSER, an argument parser, into an help message by visiting all
arguments, obtaining their documentation and joining them."
@@ -805,6 +976,7 @@
(memq (argument-kind a) '(flag option)))
(parser-arguments parser)))
(pos (positionals parser))
+ (subs (parser-subcommands parser))
(desc (parser-description parser))
(epi (parser-epilog parser)))
(string-append
@@ -818,6 +990,10 @@
(string-append "\nOptions:\n"
(format-arg-list (map format-option-line opts)))
"")
+ (if (pair? subs)
+ (string-append "\nCommands:\n"
+ (format-arg-list (map format-subcommand-line subs)))
+ "")
(if (string-null? epi) "" (string-append "\n" epi "\n")))))
(define* (print-help parser #:optional (port (current-error-port)))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/arguments/configure.ac new/arguments/configure.ac
--- old/arguments/configure.ac 2026-05-06 01:50:10.000000000 +0200
+++ new/arguments/configure.ac 2026-06-16 22:41:29.000000000 +0200
@@ -1,7 +1,7 @@
dnl -*- Autoconf -*-
-AC_INIT(guile-arguments, 0.1.1)
-AC_SUBST(HVERSION, "\"0.1.1\"")
+AC_INIT(guile-arguments, 0.2.0)
+AC_SUBST(HVERSION, "\"0.2.0\"")
AC_SUBST(AUTHOR, "\"Giacomo Leidi\"")
AC_SUBST(COPYRIGHT, "'(2026)")
AC_SUBST(LICENSE, gpl3+)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/arguments/doc/api/index.texi
new/arguments/doc/api/index.texi
--- old/arguments/doc/api/index.texi 2026-05-06 01:50:10.000000000 +0200
+++ new/arguments/doc/api/index.texi 2026-06-16 22:41:29.000000000 +0200
@@ -176,6 +176,34 @@
@c %end of fragment
+@c %start of fragment
+
+@defmac subcommand stx
+Declare a subcommand by attaching a prebuilt subparser. Subcommands let
+a single top-level parser dispatch to one of several verbs, each with
+its own argument set. Supported fields:
+
+@itemize @bullet
+@item
+@code{name} (type: @emph{symbol}): The name of the subcommand. It is
+used both to match the token on the command line and as the key the
+selected subcommand's parsed-arguments are stored under.
+
+@item
+@code{parser} (type: @emph{parser}): The prebuilt @code{<parser>} value
+to delegate to when this subcommand is selected.
+
+@item
+@code{help} (type: @emph{string}): The help string displayed in the
+@code{Commands:} section of the parent parser's help message.
+
+@end itemize
+
+@end defmac
+
+
+@c %end of fragment
+
@c %end of fragment
@c %start of fragment
@@ -281,6 +309,12 @@
@code{-h} and @code{--help}, emit the auto generated help message and
exit or leave that up to the user to handle.
+@item
+@code{#:subcommands-required?} (type: @emph{boolean}): When the parser
+declares any subcommand, this keyword argument controls whether the user
+must select one. Defaults to @code{#t} when at least one subcommand is
+declared, @code{#f} otherwise.
+
@end itemize
Supported argument builders:
@@ -298,6 +332,10 @@
@code{positional}: Builds positional arguments, refer to its own
documentation for more details.
+@item
+@code{subcommand}: Builds subcommand entries, refer to its own
+documentation for more details.
+
@end itemize
@end deffn
@@ -423,6 +461,16 @@
@end deffn
+
+@c %end of fragment
+
+@c %start of fragment
+
+@deffn Procedure subcommand? value
+Returns #t if VALUE is a subcommand record, #f otherwise.
+
+@end deffn
+
@c %end of fragment
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/arguments/doc/arguments.texi
new/arguments/doc/arguments.texi
--- old/arguments/doc/arguments.texi 2026-05-06 01:50:10.000000000 +0200
+++ new/arguments/doc/arguments.texi 2026-06-16 22:41:29.000000000 +0200
@@ -215,12 +215,73 @@
If you declare your own @code{--help} or @code{-h}, the auto-help
flag is silently omitted so there's no clash.
+@section Subcommands
+
+Some programs expose several distinct subcommands, each with its own argument
+set: @command{git commit}, @command{guix system reconfigure},
+@command{docker run}. Declare each verb with @code{subcommand} and attach a
+prebuilt subparser to it:
+
+@lisp
+(define build
+ (argument-parser
+ #:program "tool build"
+ (option (name 'target) (short #\t))
+ (positional (name 'project))))
+
+(define top
+ (argument-parser
+ #:program "tool"
+ (flag (name 'verbose?) (short #\v))
+ (subcommand
+ (name 'build)
+ (parser build)
+ (help "Build the project"))))
+@end lisp
+
+A few rules apply when subcommands are declared:
+
+@itemize
+@item Each @code{subcommand} carries a @code{name} (a symbol) and an attached
+@code{parser}. The optional @code{help} string is displayed in the parent
+parser's @code{Commands:} section.
+@item A parser that declares subcommands cannot also declare top-level
+positionals. Use options for global state and let the subparser own the
+positionals.
+@item Subcommand names cannot collide with each other, with any argument
+name in the same parser, or with the reserved name @code{help}. Such
+mistakes raise @code{&argument-error} at parser construction time.
+@item By default, a parser that declares at least one subcommand requires
+that one be selected. Pass @code{#:subcommands-required? #f} on the parent
+parser to permit calling without one.
+@item Each subparser owns its own auto-help: @command{tool build --help}
+prints the build subparser's help message.
+@end itemize
+
+The selected subcommand appears in the result alist under its own name, with
+the subparser's @code{<parsed-arguments>} as its value. An unselected
+subcommand is simply absent. The natural dispatch idiom is:
+
+@lisp
+(define args (parse-arguments top))
+
+(cond
+ ((arguments-ref args 'build)
+ => (lambda (sub)
+ (match-arguments sub (target project)
+ ...))))
+@end lisp
+
+Subcommands nest naturally: a subparser may itself declare further
+subcommands, and the parsed-arguments chain is walked one
+@code{arguments-ref} hop at a time.
+
@node Design notes
@chapter Design notes
This chapter spells out the choices behind the API. None of them are
-necessary to know for using the library: feel free to skip ahead to @ref{API}
+necessary to know for using the library: feel free to skip ahead to @pxref{API}
if you just want to look up a procedure.
@section No @code{#:arguments (list @dots{})} wrapper
@@ -318,39 +379,50 @@
@node Examples
@chapter Examples
-@section A subcommand dispatcher
+@section Subcommands
-Use a positional with @code{(multiple? #t)} to forward unknown tokens
-to a sub-parser:
+Each verb of the program gets its own parser, then is attached with
+@code{subcommand}:
@lisp
+(define build
+ (argument-parser
+ #:program "tool build"
+ (option (name 'target) (short #\t))
+ (positional (name 'project))))
+
+(define test
+ (argument-parser
+ #:program "tool test"
+ (option (name 'filter))))
+
(define top
(argument-parser
- #:program "tool"
- (flag
- (name 'verbose?)
- (short #\v))
- (positional
- (name 'subcommand))
- (positional
- (name 'rest)
- (multiple? #t)
- (required? #f))))
+ #:program "tool"
+ (flag (name 'verbose?) (short #\v))
+ (subcommand (name 'build) (parser build) (help "Build the project"))
+ (subcommand (name 'test) (parser test) (help "Run tests"))))
(define arguments (parse-arguments top))
-(match-arguments arguments (subcommand rest)
- (case (string->symbol subcommand)
- ((build) (run-build rest))
- ((test) (run-tests rest))
- (else (error "unknown subcommand" subcommand))))
-@end lisp
-The @code{--} separator lets you pass option-looking tokens through:
-
-@lisp
-$ tool -v build -- --weird-option ./project
+(cond
+ ((arguments-ref arguments 'build)
+ => (lambda (sub)
+ (match-arguments sub (target project)
+ (run-build target project))))
+ ((arguments-ref arguments 'test)
+ => (lambda (sub)
+ (match-arguments sub (filter)
+ (run-tests filter)))))
@end lisp
+The @code{--} separator still terminates option parsing at the level where
+it appears, but subcommand dispatch carries through it. So
+@command{tool -- build --weird} dispatches to @code{build}, which then
+rejects @code{--weird} as an unknown option. You can use @code{--} again
+inside the subcommand to forward option-looking tokens to a greedy
+positional declared there.
+
@section Repeatable include paths
@lisp
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/arguments/examples/tool.scm
new/arguments/examples/tool.scm
--- old/arguments/examples/tool.scm 1970-01-01 01:00:00.000000000 +0100
+++ new/arguments/examples/tool.scm 2026-06-16 22:41:29.000000000 +0200
@@ -0,0 +1,60 @@
+;;; SPDX-License-Identifier: GPL-3.0-or-later
+;;; Copyright © 2026 Giacomo Leidi <[email protected]>
+
+;; (arguments) Subcommands
+
+;; Run with:
+
+;; guix shell guile -f guix.scm -- guile -s examples/tool.scm -h
+;; Usage: tool [OPTIONS] COMMAND [ARGS...]
+
+;; Options:
+;; -h, --help Show this help message and exit
+;; -v, --verbose
+
+;; Commands:
+;; build Build the project
+;; test Run tests
+
+(use-modules (arguments)
+ (ice-9 format))
+
+(define build
+ (argument-parser
+ #:program "tool build"
+ (option (name 'target) (short #\t))
+ (positional (name 'project))))
+
+(define (run-build target project)
+ (format (current-error-port)
+ "Building~a project ~a...~%"
+ (if target (string-append " target " target " for") "") project))
+
+(define test
+ (argument-parser
+ #:program "tool test"
+ (option (name 'filter))))
+
+(define (run-tests filter)
+ (format (current-error-port)
+ "Running tests~a...~%"
+ (if filter (string-append " excluding " filter) "")))
+
+(define top
+ (argument-parser
+ #:program "tool"
+ (flag (name 'verbose?) (short #\v))
+ (subcommand (name 'build) (parser build) (help "Build the project"))
+ (subcommand (name 'test) (parser test) (help "Run tests"))))
+
+(define arguments (parse-arguments top))
+
+(cond
+ ((arguments-ref arguments 'build)
+ => (lambda (sub)
+ (match-arguments sub (target project)
+ (run-build target project))))
+ ((arguments-ref arguments 'test)
+ => (lambda (sub)
+ (match-arguments sub (filter)
+ (run-tests filter)))))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/arguments/hall.scm new/arguments/hall.scm
--- old/arguments/hall.scm 2026-05-06 01:50:10.000000000 +0200
+++ new/arguments/hall.scm 2026-06-16 22:41:29.000000000 +0200
@@ -5,7 +5,7 @@
(name "arguments")
(prefix "guile")
(postfix "")
- (version "0.1.1")
+ (version "0.2.0")
(author "Giacomo Leidi")
(email "[email protected]")
(copyright (2026))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/arguments/tests/test-arguments.scm
new/arguments/tests/test-arguments.scm
--- old/arguments/tests/test-arguments.scm 2026-05-06 01:50:10.000000000
+0200
+++ new/arguments/tests/test-arguments.scm 2026-06-16 22:41:29.000000000
+0200
@@ -328,4 +328,244 @@
(test-assert "help shows the user flag"
(string-contains h "--verbose")))))
+;;;
+;;; Subcommands
+;;;
+
+(test-group "subcommand construction"
+ (let* ((build (mk (option (name 'target) (short #\t))
+ (positional (name 'project))))
+ (p (mk (flag (name 'verbose?) (short #\v))
+ (subcommand (name 'build) (parser build) (help "Build it")))))
+ (test-assert "parser with subcommand constructs" (parser? p))
+ (test-assert "subcommand? on subcommand record"
+ (subcommand? (subcommand (name 'x) (parser build))))
+ (test-assert "subcommand? on non-subcommand is #f"
+ (not (subcommand? 42)))
+ (test-assert "subcommand? on argument is #f"
+ (not (subcommand? (flag (name 'foo))))))
+
+ (let* ((build (mk (option (name 'target))))
+ (test-sp (mk (option (name 'filter))))
+ (p (mk (subcommand (name 'build) (parser build))
+ (flag (name 'verbose?) (short #\v))
+ (subcommand (name 'test) (parser test-sp)))))
+ (test-assert "interleaved subcommands and flags" (parser? p))))
+
+(test-group "subcommand construction errors"
+ (let ((sub (mk (option (name 'x)))))
+ (test-assert "duplicate subcommand names"
+ (raises-arg-error?
+ (lambda ()
+ (mk (subcommand (name 'build) (parser sub))
+ (subcommand (name 'build) (parser sub))))))
+
+ (test-assert "subcommand name collides with argument name"
+ (raises-arg-error?
+ (lambda ()
+ (mk (flag (name 'build))
+ (subcommand (name 'build) (parser sub))))))
+
+ (test-assert "subcommand named 'help is rejected"
+ (raises-arg-error?
+ (lambda ()
+ (mk (subcommand (name 'help) (parser sub))))))
+
+ (test-assert "top-level positional + subcommand rejected"
+ (raises-arg-error?
+ (lambda ()
+ (mk (positional (name 'x))
+ (subcommand (name 'build) (parser sub))))))
+
+ (test-assert "subcommand with non-parser is rejected"
+ (raises-arg-error?
+ (lambda ()
+ (mk (subcommand (name 'build) (parser "not-a-parser"))))))))
+
+;;;
+;;; Subcommand parsing
+;;;
+
+(test-group "subcommand parsing"
+ (let* ((build (mk (option (name 'target) (short #\t))
+ (positional (name 'project))))
+ (test-sp (mk (option (name 'filter))))
+ (p (mk (flag (name 'verbose?) (short #\v))
+ (subcommand (name 'build) (parser build) (help "Build"))
+ (subcommand (name 'test) (parser test-sp) (help "Test")))))
+
+ (test-assert "selected subcommand is stored as parsed-arguments"
+ (parsed-arguments?
+ (arguments-ref (parse p '("build" "--target" "foo" "myproj")) 'build)))
+
+ (test-equal "inner option value accessible via two arguments-ref hops"
+ "foo"
+ (arguments-ref
+ (arguments-ref (parse p '("build" "--target" "foo" "myproj")) 'build)
+ 'target))
+
+ (test-equal "inner positional value accessible"
+ "myproj"
+ (arguments-ref
+ (arguments-ref (parse p '("build" "--target" "foo" "myproj")) 'build)
+ 'project))
+
+ (test-equal "top-level flag before subcommand"
+ #t
+ (arguments-ref (parse p '("-v" "build" "--target" "x" "myproj"))
'verbose?))
+
+ (test-assert "unselected subcommand absent from arguments-alist"
+ (not (assq 'test
+ (arguments-alist
+ (parse p '("build" "--target" "x" "myproj"))))))
+
+ (test-equal "unselected subcommand ref returns user-supplied default"
+ 'absent
+ (arguments-ref (parse p '("build" "--target" "x" "myproj"))
+ 'test 'absent))
+
+ (test-equal "short option after dispatch goes to subparser"
+ "foo"
+ (arguments-ref
+ (arguments-ref (parse p '("build" "-t" "foo" "myproj")) 'build)
+ 'target))))
+
+(test-group "subcommand parsing errors"
+ (let* ((build (mk (option (name 'target))))
+ (p (mk (flag (name 'verbose?) (short #\v))
+ (subcommand (name 'build) (parser build)))))
+ (test-assert "unknown subcommand name errors"
+ (raises-arg-error?
+ (lambda () (parse p '("nope")))))
+ (test-assert "missing required subcommand errors (default required)"
+ (raises-arg-error?
+ (lambda () (parse p '())))))
+
+ (let* ((build (mk (option (name 'target))))
+ (p (argument-parser
+ #:auto-help? #f
+ #:subcommands-required? #f
+ (flag (name 'verbose?))
+ (subcommand (name 'build) (parser build)))))
+ (test-assert "subcommands-required? #f permits no subcommand"
+ (parsed-arguments? (parse p '())))
+ (test-assert "no subcommand selected -> no subcommand entry"
+ (not (assq 'build (arguments-alist (parse p '()))))))
+
+ (let* ((build (mk (option (name 'target) (required? #t))))
+ (p (mk (subcommand (name 'build) (parser build)))))
+ (test-assert "required option missing inside subcommand bubbles up"
+ (raises-arg-error?
+ (lambda () (parse p '("build")))))))
+
+;;;
+;;; Subcommand help
+;;;
+
+(test-group "subcommand help"
+ (let* ((build (argument-parser
+ #:program "build"
+ (option (name 'target))))
+ (p (argument-parser
+ #:program "demo"
+ (subcommand (name 'build) (parser build)))))
+ (test-assert "top-level --help raises &help-requested"
+ (call-with-current-continuation
+ (lambda (k)
+ (with-exception-handler
+ (lambda (exn) (k (help-requested? exn)))
+ (lambda ()
+ (parse-arguments p '("--help") #:handle-help? #f)
+ #f)
+ #:unwind? #t))))
+
+ (test-assert "subcommand --help raises &help-requested"
+ (call-with-current-continuation
+ (lambda (k)
+ (with-exception-handler
+ (lambda (exn) (k (help-requested? exn)))
+ (lambda ()
+ (parse-arguments p '("build" "--help") #:handle-help? #f)
+ #f)
+ #:unwind? #t)))))
+
+ (let* ((build (argument-parser
+ #:program "tool build"
+ (option (name 'target))))
+ (test-sp (argument-parser
+ #:program "tool test"
+ (option (name 'filter))))
+ (p (argument-parser
+ #:program "tool"
+ (subcommand (name 'build) (parser build) (help "Build the
project"))
+ (subcommand (name 'test) (parser test-sp) (help "Run tests")))))
+ (let ((u (format-usage p))
+ (h (format-help p)))
+ (test-assert "usage mentions COMMAND"
+ (string-contains u "COMMAND"))
+ (test-assert "help shows Commands: section"
+ (string-contains h "Commands:"))
+ (test-assert "help lists build subcommand"
+ (string-contains h "build"))
+ (test-assert "help lists test subcommand"
+ (string-contains h "test"))
+ (test-assert "help shows subcommand help text"
+ (string-contains h "Build the project")))))
+
+;;;
+;;; Nested subcommands
+;;;
+
+(test-group "nested subcommands"
+ (let* ((leaf (mk (option (name 'baz))))
+ (mid (mk (subcommand (name 'bar) (parser leaf))))
+ (top (mk (subcommand (name 'foo) (parser mid)))))
+ (let ((args (parse top '("foo" "bar" "--baz" "value"))))
+ (test-assert "top selects foo"
+ (parsed-arguments? (arguments-ref args 'foo)))
+ (test-assert "mid selects bar"
+ (parsed-arguments?
+ (arguments-ref (arguments-ref args 'foo) 'bar)))
+ (test-equal "leaf option reaches innermost level"
+ "value"
+ (arguments-ref
+ (arguments-ref (arguments-ref args 'foo) 'bar)
+ 'baz)))))
+
+;;;
+;;; `--` interaction
+;;;
+
+(test-group "subcommand and -- interaction"
+ (let* ((build (mk (option (name 'target))))
+ (p (mk (flag (name 'verbose?) (short #\v))
+ (subcommand (name 'build) (parser build)))))
+ (test-assert "-- still allows subcommand dispatch"
+ (parsed-arguments?
+ (arguments-ref (parse p '("--" "build" "--target" "x")) 'build)))
+ (test-equal "subcommand option after `-- build` parsed normally"
+ "x"
+ (arguments-ref
+ (arguments-ref (parse p '("--" "build" "--target" "x")) 'build)
+ 'target))))
+
+;;;
+;;; Name scoping across parser levels
+;;;
+
+(test-group "subcommand and top-level name scoping"
+ (let* ((build (mk (flag (name 'verbose?) (short #\v))))
+ (p (mk (flag (name 'verbose?) (short #\v))
+ (subcommand (name 'build) (parser build)))))
+ (let ((args (parse p '("-v" "build"))))
+ (test-equal "top-level -v set when before subcommand"
+ #t (arguments-ref args 'verbose?))
+ (test-equal "subparser -v unset when -v is before subcommand"
+ #f (arguments-ref (arguments-ref args 'build) 'verbose?)))
+ (let ((args (parse p '("build" "-v"))))
+ (test-equal "top-level -v unset when -v is after subcommand"
+ #f (arguments-ref args 'verbose?))
+ (test-equal "subparser -v set when -v is after subcommand"
+ #t (arguments-ref (arguments-ref args 'build) 'verbose?)))))
+
(test-end "arguments")