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")

Reply via email to