Skip to content

dowdiness/margs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

61 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

margs

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

Current Status

Fully implemented (Phase 1 & 2):

  • Typed options: String, Int, Bool flags, and repeated String lists
  • Positional arguments and nested subcommands
  • Auto-generated help text (main parser + subcommands)
  • Built-in --version/-V handling 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)

Installation

Add to moon.mod.json:

{
  "deps": {
    "dowdiness/margs": "*"
  }
}

Or install via MoonBit tooling:

moon install dowdiness/margs

Quick Start

fn 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()
}

Core API

  • 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

Hook Semantics

  • before_hooks run before handler dispatch.
  • after_hooks are success-only: they run only if all before_hooks and the handler complete without raising.
  • after_hooks are not finally hooks; do cleanup in the handler (or a dedicated wrapper) if cleanup must always run.

Test Helper Semantics

  • run_for_test is for asserting parse/help/error behavior and exit-code mapping.
  • CliTestResult.output includes only framework-managed output (help/version/error text).
  • run_for_test does not capture handler println/stdout on successful execution.

Value Precedence

When an option value is specified in multiple places, margs resolves it using this precedence order (highest to lowest):

  1. Command-line arguments (highest priority)

    mytool --port 3000
  2. Environment variables

    MY_APP_PORT=3000 mytool
  3. Configuration file

    # ~/.mytoolrc.json
    { "port": 3000 }
  4. 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)

Environment Variable Support

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=9000

Config File Support

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

Metadata Generation

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

Structured Output

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

Example App

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-app

Development

moon check        # fast type-check
moon build        # build module
moon test         # run test suite
moon test -v      # verbose tests
moon run src/example -- --help

Project layout:

  • src/margs/: library implementation (types.mbt, builder.mbt, parser.mbt, help.mbt, validators.mbt, exit_codes.mbt)
  • src/example/: demo CLI using the library
  • docs/: reports and planning docs

Documents

  • docs/margs_cli_library_report.md: architecture review, API analysis, and improvement proposals.

Future Plans (Roadmap)

Planned direction (merged with what's already implemented):

  1. Done: wrapper API, alias/nested command dispatch, and example CLI.
  2. Phase 1: CLI test helpers and async-friendly handlers.
  3. Phase 2: env/config integration, structured output helpers, metadata versioning.
  4. Phase 3: shell completion, interactive prompts, i18n support.
  5. Phase 4: plugin system and code generation/scaffolding.

These roadmap items are proposals and are not fully implemented in the current codebase.

About

Arg parser for MoonBit.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages