How to provide nested command usage message with `command-line`

In Racket, we can use command-line to create single command with some flags.

I use

(define args (command-line ...))
(match args
  [(list "command1") ...]
  [(list "command2") ...]
  [(list "command3") ...])

to create program with nested command. The problem is usage text will not mention these nested commands then.

Is that possible to use command-line to complete this purpose and with proper usage help?

I think this style is good enough to be the answer.

Here's a version I've extracted from some unpublished code that is a bit less manual than Resyntax's CLI pattern, but that means there's more happening implicitly (IOW: tradeoffs apply):

#lang racket

(module+ main
  (define (make-new-command-name command)
    (and (current-command-name)
         (format "~a ~a" (current-command-name) command)))

  (define (run name c)
    (parameterize ([current-command-name (make-new-command-name name)])
      ((command-invoke c))))

  (define padding-length
    (add1 (apply max (map symbol-length (hash-keys command-table)))))

  (parse-command-line
   (short-program+command-name)
   (current-command-line-arguments)
   `((usage-help "This tool is used to <do stuff>."
                 "Command is one of:"
                 ,@(for/list ([(name c) (in-hash command-table)])
                     (format "  ~a:~a\t~a"
                             name
                             (make-padding-string (symbol-length name) padding-length)
                             (command-description c)))))
   (λ (_acc name-string . args)
     (define name (string->symbol name-string))
     (cond
       [(hash-has-key? command-table name)
        (parameterize ([current-command-line-arguments (list->vector args)])
          (run name (hash-ref command-table name)))]
       [else
        (eprintf "~a: unknown command ~a\n" (short-program+command-name) name)
        (eprintf "Expected one of: ~a\n"
                 (string-join (map symbol->string (hash-keys command-table)) ", "))
        (exit 1)]))
   '("command" "command-arguments")))

(require raco/command-name
         syntax/parse/define
         file/glob
         db
         axio
         threading)

;; planet/private/command implements an svn-style command-line macro; for now,
;; we'll stick with something a bit simpler
(struct command (description invoke))
(define command-table (make-hash))
(define-syntax-parser define/command
  [(_ name:id description:string body:expr ...+)
   (syntax/loc this-syntax
     (begin
       (define (name)
         body ...)
       (hash-set! command-table 'name (command description name))))])

(define/command init "Initialize the <thing>"
  (define opt-result "default")
  (command-line
   #:program (short-program+command-name)
   #:once-each
   [("--opt") --opt-value "Help for --opt [default]"
              (set! opt-result --opt-value)]
   #:args (positional)
   (exit (do-init opt-result positional))))

(define (symbol-length s)
  (string-length (symbol->string s)))

(define (make-padding-string occupied-length padding-length)
  (make-string (- padding-length occupied-length) #\space))

;; vim: lispwords+=define/command

This code was used in a custom raco tool, so the raco/command-name and its exports may not be relevant for your use case.

1 Like