margs is a type-safe CLI toolkit for MoonBit.
It includes:
- A low-level parser/builder API (
parser,subcommand, option constructors) - A high-level wrapper API (
create_cli,command) with handler-based dispatch
Fully implemented (Phase 1 & 2):
- Typed options:
String,Int,Boolflags, and repeatedStringlists - Positional arguments and nested subcommands
- Auto-generated help text (main parser + subcommands)
- Built-in
--version/-Vhandling when version is configured - Built-in validators (
port_option,file_option,url_option,verbose_flag,quiet_flag) - Typo suggestions for unknown options/commands
- Structured parse errors and exit-code mapping
- High-level command wrapper API with global options and per-command handlers
- Middleware hooks (
add_before_hook/add_after_hook) on CLI and commands - In-process test helper (
run_for_test) for CLI assertions - Environment variable defaults with precedence chain
- Config file support (JSON format)
- Structured output helpers (logging, success/failure, key-value formatting)
- JSON metadata generation for shell completion
Not implemented yet (Phase 3+):
- Shell completion generation (bash/zsh/fish)
- Interactive prompts for missing values
- i18n support for help/error strings
- Async handler support (blocked by Moon compiler)
Add to moon.mod.json:
{
"deps": {
"dowdiness/margs": "*"
}
}Or install via MoonBit tooling:
moon install dowdiness/margsfn main {
@margs.create_cli("hello", description="Greeting CLI", version="0.1.1")
.add_command(
@margs.command("greet", handler=fn(args) {
let name = match args.get_string("name") {
Some(v) => v
None => "World"
}
println("Hello, \{name}!")
})
.add_option(@margs.str_option("name", short='n', long="name", default="World")),
)
.run()
}- Wrapper API:
@margs.create_cli,@margs.command,Cli::add_command,Cli::add_before_hook,Cli::add_after_hook,Cli::run,Cli::run_for_test - Parser/builders:
@margs.parser,@margs.subcommand - Option constructors:
@margs.str_option,@margs.int_option,@margs.flag,@margs.str_list_option,@margs.positional - Validator helpers:
@margs.port_option,@margs.file_option,@margs.url_option,@margs.verbose_flag,@margs.quiet_flag - Help output:
@margs.generate_help,@margs.generate_subcommand_help - Metadata generation:
@margs.generate_metadata - Parsed accessors:
get_string,get_int,get_bool,get_string_list,get_positional,require_string,require_int - Output helpers:
@margs.log_info,@margs.log_warn,@margs.log_error,@margs.log_debug,@margs.success,@margs.failure,@margs.step,@margs.section,@margs.kv
before_hooksrun before handler dispatch.after_hooksare success-only: they run only if allbefore_hooksand the handler complete without raising.after_hooksare notfinallyhooks; do cleanup in the handler (or a dedicated wrapper) if cleanup must always run.
run_for_testis for asserting parse/help/error behavior and exit-code mapping.CliTestResult.outputincludes only framework-managed output (help/version/error text).run_for_testdoes not capture handlerprintln/stdout on successful execution.
When an option value is specified in multiple places, margs resolves it using this precedence order (highest to lowest):
-
Command-line arguments (highest priority)
mytool --port 3000
-
Environment variables
MY_APP_PORT=3000 mytool
-
Configuration file
# ~/.mytoolrc.json { "port": 3000 }
-
Option default value (lowest priority)
int_option("port", default=8080)
Current status:
- ✅ CLI arguments and defaults
- ✅ Environment variables (strings, ints, bools)
- ✅ Config file support (JSON)
Options can fall back to environment variables before using defaults:
let cli = create_cli("myapp")
.add_option(str_option(
"host",
long="host",
env="MYAPP_HOST", // Falls back to $MYAPP_HOST
default="localhost",
help="Server hostname"
))
.add_option(int_option(
"port",
long="port",
env="MYAPP_PORT", // Falls back to $MYAPP_PORT
default=8080,
help="Server port"
))
.add_option(flag(
"verbose",
short='v',
env="MYAPP_VERBOSE", // Accepts: 1, true, yes, on
help="Enable verbose logging"
))Precedence: CLI arguments > environment variables > config file > default values
# Uses default
myapp
# → host=localhost, port=8080
# Uses env var
MYAPP_PORT=3000 myapp
# → host=localhost, port=3000
# CLI overrides env var
MYAPP_PORT=3000 myapp --port 9000
# → host=localhost, port=9000Load default values from a JSON configuration file:
let cli = create_cli("myapp")
.with_config_file(".myapprc.json")
.add_option(str_option("host", env="MYAPP_HOST", default="localhost"))
.add_option(int_option("port", env="MYAPP_PORT", default=8080))Config file format (.myapprc.json):
{
"host": "prod.example.com",
"port": "3000",
"verbose": "true"
}Precedence chain in action:
# Config: host=prod.example.com, port=3000
# Env: MYAPP_PORT=5000
# CLI: --host custom.com
myapp
# → host=custom.com (CLI), port=5000 (env)Features:
- Gracefully handles missing config files
- Simple flat JSON structure
- Works with all option types (string, int, bool, list)
- Auto-discovery with
discover_config_file("myapp")(checks.myapprc,.myapprc.json)
Generate JSON metadata from your CLI structure for shell completion scripts:
let cli = create_cli("myapp", version="1.0.0")
.add_option(str_option("host", long="host", help="Server hostname"))
.add_option(int_option("port", short='p', help="Server port"))
.add_command(
command("serve", handler=fn(_) { () })
.add_option(flag("daemon", short='d', help="Run as daemon"))
)
let metadata = generate_metadata(cli.to_parser())
println(metadata)Output (formatted JSON):
{
"name": "myapp",
"version": "1.0.0",
"options": [
{
"key": "host",
"type": "string",
"long": "host",
"help": "Server hostname",
"metavar": "VALUE",
"required": false
},
{
"key": "port",
"type": "int",
"short": "p",
"help": "Server port",
"metavar": "NUM",
"required": false
}
],
"commands": [
{
"name": "serve",
"options": [
{
"key": "daemon",
"type": "bool",
"short": "d",
"help": "Run as daemon",
"required": false
}
],
"subcommands": []
}
]
}Use cases:
- Generate shell completion scripts (bash, zsh, fish)
- Create documentation automatically
- Build IDE/editor integrations
- Generate man pages
Use the output helpers for consistent CLI formatting:
command("deploy", handler=fn(args) {
section("Deployment")
step("Building application...")
// build logic here
step("Uploading to server...")
// upload logic here
success("Deployment completed")
kv("Status", "deployed")
kv("Version", "1.2.3")
kv("URL", "https://app.example.com")
})Output:
=== Deployment ===
• Building application...
• Uploading to server...
✓ Deployment completed
Status: deployed
Version: 1.2.3
URL: https://app.example.com
A complete demo CLI is in src/example/main.mbt.
Run it:
moon run src/example -- --help
moon run src/example -- serve --help
moon run src/example -- build -o out -t wasm -t js
moon run src/example -- init my-appmoon check # fast type-check
moon build # build module
moon test # run test suite
moon test -v # verbose tests
moon run src/example -- --helpProject layout:
src/margs/: library implementation (types.mbt,builder.mbt,parser.mbt,help.mbt,validators.mbt,exit_codes.mbt)src/example/: demo CLI using the librarydocs/: reports and planning docs
docs/margs_cli_library_report.md: architecture review, API analysis, and improvement proposals.
Planned direction (merged with what's already implemented):
- Done: wrapper API, alias/nested command dispatch, and example CLI.
- Phase 1: CLI test helpers and async-friendly handlers.
- Phase 2: env/config integration, structured output helpers, metadata versioning.
- Phase 3: shell completion, interactive prompts, i18n support.
- Phase 4: plugin system and code generation/scaffolding.
These roadmap items are proposals and are not fully implemented in the current codebase.