CLI dialect

CLI dialect: parse-args, help, and subcommands

Rye comes with a small, expressive CLI parsing dialect that lets you define flags, positional arguments, validation, and nested subcommands directly in Rye code. It also generates help text for you.

You’ll use these builtins:

  • parse-args - parse argv against a spec, returns a Dict
  • parse-args\ctx - like parse-args, but returns a Context (dot-access)
  • generate-help - generate help text from a spec
  • generate-help\command - generate help for a specific subcommand path
  • format-parse-errors - pretty-print errors from failed parsing

The dialect lives in the spec block you pass to parse-args. It shares some similarities with other Rye dialects, like validation dialect. It supports:

  • short and long flags via flagwords: -v|verbose flag,
  • short and long flags with an argument -o|output string required
  • positional args via setwords: input: string required, files: string many
  • nested subcommands: subcommand { 'init { ... } 'remote { subcommand { 'add { ... } } } }

Looking more into details, the dialect shares mechanisms and vocabulary with the validation dialect:

  • defaults for optional fields: -n|count integer optional 10
  • custom validation with check blocks: num: check { > 0 } "must be positive"
  • actions for supplied flags and options: -d|doit flag do { do-it }

Help is also generated from the dialect so it supports:

  • program metadata for help: program "mytool", description "..."
  • inline docs for help: doc "Enable verbose"

Basics

Our basic cli has a flag –verbose, flag with argument –output and a required positional argument at the end.

; argv simulated as a block for examples
args: { "--verbose" "-o" "out.txt" "in.txt" }

spec: {
  program "mytool"
  description "Example tool using Rye's CLI dialect"

  -v|verbose flag doc "Enable verbose output"
  -o|output  string required doc "Output file"
  input: string required doc "Input file"
}

result: parse-args args spec |^fix {
  print format-parse-errors result
}

print2 "verbose: " result."verbose"
print2 "output: "  result."output"
print2 "input: "   result."input"

Types and defaults

  • Types: string, integer, decimal, boolean, file, any
  • Optional with default: optional 42, optional 1.5, optional "stdout", etc.
  • Flags with values use the declared type; plain flag is boolean.

Like with validation dialect, values are converted according to the dialect.

res: parse-args\ctx { } {
  -n|count integer optional 3 doc "How many"
  -r|rate  decimal optional 1.5
  -q|quiet flag 
}
|do\in {
  probe count    ; [Integer: 3]
  probe rate     ; [Decimal: 1.5]
  probe quiet    ; [Boolean: false]
}  

Repeatable options and positional lists

Use list to collect repeated flags; use many for variadic positionals.

parse-args 
{ -I "/usr/include" -I "./include" } 
{ -I|include string list }
|-> "include" |length? |print  ; 2

parse-args 
{ "a.txt" "b.txt" "c.txt" } 
{ files: string many }
|-> "files" |length? |print    ; 3

Validation with check

Attach check { ... } "message" to a flag or positional. The block runs with the value injected; it succeeds on truthy boolean or positive integer.

spec: { -p|port integer check { >= 1024 } "port must be >= 1024" }

parse-args { --port 80 } spec 
|fix { 
  .format-parse-errors |print
}
; => Error: --port port must be >= 1024

Another example for positionals:

spec: { file: string check { .ends-with? ".txt" } "must be .txt file" }

parse-args { "notes.pdf" } spec 
|fix {
  .format-parse-errors |print
}

Subcommands (git-like and nested)

Define subcommands with 'name { ... }. Nest with subcommand { ... } inside another.

spec: {
  -v|verbose flag
  subcommand {
    'commit {
      -m|message string required doc "Commit message"
      -a|amend  flag doc "Amend last commit"
    }
    'remote {
      doc "Manage remotes"
      subcommand {
        'add {
          name: string required
          url:  string required
        }
      }
    }
  }
}

; commit
r1: parse-args { commit -m "Init" } spec
print r1."command"      ; "commit"
print r1."message"      ; "Init"

; nested: remote add
r2: parse-args { remote add origin "https://example.com" } spec
print r2."command"      ; "remote add"
print r2."command-path" ; ["remote" "add"]
print r2."name"         ; "origin"
print r2."url"          ; "https://example.com"

Global flags (declared at the top level) are available alongside subcommands.

Putting it together: a tiny CLI

args: rye .Args?         ; read argv as a Rye block

spec: {
  program "minitool"
  description "Demo CLI"
  -v|verbose flag doc "Verbose"
  -o|output string required doc "Output file"
  input: string required doc "Input file"
}

res: parse-args args spec
|fix {
  print generate-help spec
  print "" ,
  .format-parse-errors .print ,
  exit 1
}

if res."verbose" { print "Verbose mode ON" }
Write file res."output" Read file res."input"

Call functions directly from the spec with do

When flags select actions, you don’t need to query the parsed flags afterward. Attach a do { ... } block to each flag or option and call your functions directly from the CLI spec.

do-it: does { print "Just do it" }
greet: fn { name } { print join { "Hello, " name "!" } }

spec: {
  program "actions"
  description "Run actions directly from the CLI spec"

  -d|doit flag doc "Do it" do { do-it }
  -g|greet string doc "Greet someone"
    check { .length? > 0 } "name must not be empty"
    do { ::name greet name }
  -h|help flag doc "Show this help" do { print generate-help spec }
}

; Simulated argv; use rye .Args? for real command-line arguments.
parse-args { --doit --greet "Ada" } spec
; Just do it
; Hello, Ada!

parse-args { --help } spec
; Prints the help generated from spec.

There is no if result."doit" or separate dispatch step: parse-args runs the matching actions. It still returns the usual dictionary, but this example doesn’t need it.

Actions run after the entire command line passes parsing and validation, in argument order, once per supplied occurrence. Absent flags and default values don’t trigger actions. If any argument fails validation, none of the actions run-including a help action if another required argument is missing.

For options with values, the converted, validated value is injected into the action block. In the example, ::name captures that value before calling greet. Actions run in the caller’s context, so they can access your functions and spec. They also work with subcommand flags and parse-args\ctx.

Notes and tips

  • Long flag names become keys; if only a short flag exists, the short name is used.
  • list initializes to an empty list by default so you can append naturally.
  • Validation check blocks run with the value injected; return true/positive to accept.