7.1.53. Command-Line Argument Parsing (clargs)
This tutorial covers daslib/clargs — a structure macro that generates a
type-safe CLI argument parser from an annotated struct. Declare your flags as
struct fields; the macro generates a parse_args function and runtime
reflection helpers automatically.
Prerequisites: familiarity with structs, enums, and arrays.
options gen2
require daslib/clargs
7.1.53.1. Defining a CLI args struct
Annotate any struct with [CommandLineArgs]. The macro generates three
functions for it:
parse_args(var dst; args : array<string>) : string— parse a provided listparse_args(var dst) : string— parse from the process command line (post---slice; see Reading process arguments below)get_command_info(type<T>) : CommandInfo— runtime flag metadata
Field names map to flag names with underscores converted to dashes
(output_file → --output-file).
[CommandLineArgs]
struct Config {
name : string // --name
count : int // --count
verbose : bool // --verbose
timeout : float // --timeout
}
var cfg = Config()
let err = parse_args(cfg, ["--name", "Alice", "--count=42", "--verbose", "--timeout=1.5"])
// err == "" (success)
// cfg.name == "Alice"
// cfg.count == 42
// cfg.verbose == true
// cfg.timeout == 1.5
Each flag accepts two forms: --flag value (space-separated) and
--flag=value (equals sign, no space).
7.1.53.2. Supported field types
Field type |
Accepted flag values |
|---|---|
|
Any string |
|
Decimal integer (optional leading |
|
Decimal float with optional exponent |
|
Bare flag (true), |
|
Enum entry name as a string (e.g. |
|
Flag may appear multiple times |
7.1.53.3. Bool flags
A bare --verbose sets the field to true. Use =true or =false
to be explicit:
parse_args(cfg, ["--verbose"]) // verbose = true
parse_args(cfg, ["--verbose=false"]) // verbose = false
7.1.53.4. Enum flags
Pass the enum entry name as a string. An unknown name returns an error:
enum LogLevel { Debug; Info; Warning; Error }
[CommandLineArgs]
struct LogConfig {
level : LogLevel // --level (accepts "Debug", "Info", "Warning", "Error")
}
var cfg = LogConfig()
parse_args(cfg, ["--level", "Warning"])
// cfg.level == LogLevel.Warning
let err = parse_args(cfg, ["--level", "Verbose"])
// err == "--level: invalid enum value 'Verbose'"
7.1.53.5. Array flags
An array<string> field collects every occurrence of the flag into the
array. Both forms (--tag value and --tag=value) are supported:
[CommandLineArgs]
struct BuildConfig {
tags : array<string>
}
var cfg = BuildConfig()
parse_args(cfg, ["--tags=debug", "--tags", "release", "--tags=profile"])
// cfg.tags == ["debug", "release", "profile"]
7.1.53.6. Required flags
@clarg_required makes a flag mandatory. parse_args returns an error if
the flag is absent:
[CommandLineArgs]
struct DeployConfig {
host : string
@clarg_required
token : string
}
var cfg = DeployConfig()
let err1 = parse_args(cfg, ["--host=prod.example.com"])
// err1 == "--token: missing required flag"
let err2 = parse_args(cfg, ["--host=prod.example.com", "--token=secret"])
// err2 == "" (success)
7.1.53.7. Field-level attributes
Three field annotations fine-tune parsing behaviour:
@clarg_name = "flag"Overrides the auto-generated flag name.
@clarg_short = "X"Attaches a single-character short flag (see Short flags below).
@clarg_doc = "text"Attaches a description used by help generators (see Help rendering below).
@clarg_skipExcludes the field from CLI parsing entirely (set it in code directly).
[CommandLineArgs]
struct AppConfig {
@clarg_name = "output-dir"
@clarg_doc = "Directory to write output files"
out_path : string // flag is --output-dir, not --out-path
@clarg_doc = "Number of parallel workers (default: 1)"
workers : int
@clarg_skip
internal_id : int // not a CLI flag
}
var cfg = AppConfig()
cfg.internal_id = 99
parse_args(cfg, ["--output-dir=/tmp/out", "--workers=4"])
// cfg.out_path == "/tmp/out"
// cfg.workers == 4
// cfg.internal_id == 99
7.1.53.8. Error handling
parse_args returns an empty string on success or a descriptive error message
on the first failure:
let err = parse_args(cfg, ["--count", "not_a_number"])
// err == "--count: invalid int value 'not_a_number'"
if (err != "") {
print("usage error: {err}\n")
return
}
Common error forms:
"--flag: invalid int value 'abc'""--flag: invalid float value 'abc'""--flag: invalid enum value 'Unknown'""--flag: missing required flag""--flag: invalid bool value: 'yes'"
7.1.53.9. Short flags
@clarg_short = "X" attaches a single-character short flag. Both the long
and short forms are recognised by parse_args, with identical value syntax
(-X value, -X=value, or bare -X for booleans):
[CommandLineArgs]
struct ServerConfig {
@clarg_short = "p"
@clarg_doc = "listen port"
port : int
@clarg_short = "v"
@clarg_doc = "verbose logging"
verbose : bool
@clarg_short = "t"
@clarg_doc = "tag (repeated)"
tags : array<string>
}
var cfg = ServerConfig()
parse_args(cfg, ["-p", "8080", "-v", "-t=alpha", "-t=beta"])
// cfg.port == 8080
// cfg.verbose == true
// cfg.tags == ["alpha", "beta"]
Mixing long and short occurrences of an array flag preserves command-line order:
--tags=a -t b --tags=c collects ["a", "b", "c"].
Two fields cannot share a short flag. @clarg_short must be exactly one
character; both are compile-time errors from the macro.
7.1.53.10. Introspection with get_command_info
get_command_info(type<T>) returns a CommandInfo value containing a
CommandArgumentInfo entry for each parsed flag — the same data the
help renderer below uses, but exposed for programmatic inspection (custom
help formats, validation rules, configuration dumps, etc.):
let info <- get_command_info(type<ServerConfig>)
for (arg in info.args) {
print(" {arg.short_flag_name}, {arg.flag_name} ({arg.value_type}) {arg.doc_string}\n")
}
// output:
// -p, --port (tInt) listen port
// -v, --verbose (tBool) verbose logging
// -t, --tags (tString) tag (repeated)
CommandArgumentInfo fields:
Field |
Type |
Description |
|---|---|---|
|
|
Full flag string (e.g. |
|
|
|
|
|
Struct field name |
|
|
|
|
|
|
|
|
|
|
|
Base type ( |
|
|
Entry names for enum fields, empty otherwise |
7.1.53.11. Help rendering
The library ships a --help renderer over CommandInfo:
print_help(info, prog_name)— writes the formatted help to stdout.format_help(info, prog_name) : string— returns the same text, useful in tests or when redirecting into a logger.
parse_args does not auto-recognise --help — declare an explicit
help field and check it after parsing. This keeps parse_args a pure
parser and leaves the exit policy to the caller:
[CommandLineArgs]
struct DemoConfig {
@clarg_short = "n"
@clarg_doc = "user's display name"
name : string
@clarg_doc = "iteration count"
count : int
@clarg_short = "v"
@clarg_doc = "verbose logging"
verbose : bool
@clarg_short = "h"
@clarg_doc = "show this help and exit"
help : bool
}
[export]
def main() : int {
var cfg = DemoConfig()
let err = parse_args(cfg)
if (err != "") {
print("error: {err}\n")
return 1
}
if (cfg.help) {
print_help(get_command_info(type<DemoConfig>), "demo")
return 0
}
return 0
}
The rendered output:
Usage: demo [flags]
Flags:
-n, --name=STRING user's display name
--count=INT iteration count
-v, --verbose verbose logging
-h, --help show this help and exit
Format rules:
Per-flag line:
-X, --long=PLACEHOLDER doc_string. Fields with no@clarg_shortindent the short slot blank to keep the long flags vertically aligned.=PLACEHOLDERis the uppercased type name (STRING/INT/FLOAT/ENUM). Bool flags omit it.Enum values render inline as
(V1|V2|V3).(required)/(repeated)markers are appended to the doc column for required flags and array flags respectively.Defaults are not shown —
CommandInfodoes not currently carry them.
7.1.53.12. Reading process arguments
Two helpers feed argv into parse_args, depending on how the program is
invoked. Each has a zero-argument form (operating on the live process command
line) and a one-argument form (taking an explicit argv array, useful in
tests):
get_cli_arguments() / get_cli_arguments(argv)— script-styleReturns the slice after the
--separator in argv (or empty if no--). This is what daslang script invocations look like, where daslang itself owns argv up to the--and the script gets everything after:daslang.exe my_script.das -- --name Alice --count 5
The no-argument
parse_args(cfg)overload generated by the macro calls this internally.get_program_args() / get_program_args(argv)— standalone-tool styleReturns
argv[1..]— the full argv with the program name stripped. Use this for AOT’d binaries that own the full argv themselves and have no--separator (das-fmt, daspkg, lint, aot-style tools):[export] def main() : int { var cfg = FmtConfig() let err = parse_args(cfg, get_program_args()) if (err != "") { print("error: {err}\n") return 1 } return 0 }
The explicit-argv overloads make the splitting logic unit-testable without touching the live process state:
let scripted <- get_cli_arguments(["host", "script.das", "--", "--foo", "bar"])
// scripted == ["--foo", "bar"]
let standalone <- get_program_args(["fmt.exe", "--write", "file.das"])
// standalone == ["--write", "file.das"]
See also
Full source: tutorials/language/53_clargs.das
Previous tutorial: Option<T> and Result<T, E>
Structs, Enumerations and Bitfields, Arrays, Annotations and Options