Async-first declarative CLI builder for MoonBit, inspired by gunshi
Dependencies
import {
"totto2727/admiral@0.6.1",
"moonbitlang/async@0.20.3",
}
preferred_target = "native"import {
"totto2727/admiral" @admiral,
"moonbitlang/async",
}async fn main {
let name = @admiral.string(
"name",
short='n',
description="Name to greet",
env="ADMIRAL_NAME",
config="name",
required=true,
)
let verbose = @admiral.bool("verbose", short='v', description="Verbose output")
let count = @admiral.int("count", short='c', description="Repeat count", default=Some(1))
let app = @admiral.cli(
name="myapp",
version="1.0.0",
description="My CLI tool",
commands=[
@admiral.command(
name="greet",
description="Greet someone",
options=[name, verbose, count],
examples=["myapp greet --name Alice", "myapp greet -n Bob -v -c 3"],
run=Some(async fn(ctx) {
let name_value = try { ctx.get_string_required(name) } catch { _ => return }
let is_verbose = ctx.get_bool(verbose)
let count_value = match ctx.get_int(count) { Some(n) => n; None => 1 }
for i = 0; i < count_value; i = i + 1 {
if is_verbose {
println("Hello, " + name_value + "! (" + (i + 1).to_string() + ")")
} else {
println("Hello, " + name_value + "!")
}
}
}),
),
],
)
app.run()
}$ myapp greet --name Alice
Hello, Alice!
$ myapp greet -n Bob -v -c 3
Hello, Bob! (1)
Hello, Bob! (2)
Hello, Bob! (3)
$ myapp --help
Usage: myapp [command]
My CLI tool
Commands:
greet Greet someone
Options:
-h, --help Show help information.
-V, --version Show version information.// String option: --name value or -n value
@admiral.string("name", short='n', description="User name", env="MYAPP_NAME", config="name", required=true)
// Bool flag: --verbose or -v
@admiral.bool("verbose", short='v', description="Verbose output", env="MYAPP_VERBOSE", config="verbose")
// Int option: --port 8080 or -p 8080
@admiral.int("port", short='p', description="Port number", env="MYAPP_PORT", config="port", default=Some(3000))
// Scalar position: file
@admiral.position_string("file", description="Input file", config="input", required=true)
// Variadic position: file...
@admiral.position_strings("files", description="Input files")@admiral.string("name", env="MYAPP_NAME")
@admiral.bool("verbose", env="MYAPP_VERBOSE")
@admiral.int("port", env="MYAPP_PORT")app.run(
argv=Some(["serve"]),
env={
"MYAPP_PORT": "8080",
"MYAPP_VERBOSE": "true",
},
)
// An empty map prevents ambient process variables from affecting the parse.
app.run(argv=Some(["serve"]), env=Map([]))let project = @admiral.position_string(
"project",
required=true,
interactive=true,
)
let query = @admiral.string(
"query",
env="ADMIRAL_PROJECT_QUERY",
default=Some(""),
interactive=true,
)
let app = @admiral.cli(
name="project-search",
positionals=[project],
options=[query],
interactive=Some(input => {
let initial = input.to_context()
let selected = run_project_search_tui(
initial.get_string(project),
initial.get_string(query).unwrap_or(""),
)
input.set_string(project, selected)
}),
run=Some(ctx => println(ctx.get_string_required(project))),
)fn load_config() -> Map[String, Json] raise @admiral.ConfigLoadFailure {
{
"port": (7000).to_json(),
"verbose": (true).to_json(),
"tags": ["release", "signed"].to_json(),
}
}
let app = @admiral.cli(
name="myapp",
load_config=Some(load_config),
commands=[...],
)let verbose = @admiral.bool("verbose")
let name = @admiral.string("name", required=true)
let port = @admiral.int("port", required=true)
let input = @admiral.position_int("input", required=true)
// Register definitions with command(options=[verbose, name, port], positionals=[input]).
run=Some(async fn(ctx) {
// Bool — returns false if not specified
let is_verbose = ctx.get_bool(verbose)
// String — returns None if not specified
let name_value = ctx.get_string(name) // String?
// String (required) — raises if missing
let name_value = try { ctx.get_string_required(name) } catch { _ => return }
// Int — parses string value to Int, returns None if missing or invalid
let port_value = ctx.get_int(port) // Int?
// Int (required) — raises if missing or not a valid integer
let port_value = try { ctx.get_int_required(port) } catch { _ => return }
// The same getter accepts PositionDef[Int]
let input_value = try { ctx.get_int_required(input) } catch { _ => return }
})let dry_run = @admiral.bool("dry-run", description="Preview without applying")
let up_steps = @admiral.int("steps", short='s', description="Number of steps")
let down_steps = @admiral.int("steps", short='s', description="Steps to rollback", default=Some(1))
let seed_file = @admiral.string("file", short='f', description="Seed file", default=Some("seeds/default.sql"))
let app = @admiral.cli(
name="myapp",
commands=[
@admiral.command(
name="db",
description="Database commands",
subcommands=[
@admiral.command(
name="migrate",
description="Run migrations",
subcommands=[
@admiral.command(
name="up",
description="Apply pending migrations",
options=[dry_run, up_steps],
examples=[
"myapp db migrate up",
"myapp db migrate up --dry-run",
"myapp db migrate up --steps 5",
],
run=Some(async fn(ctx) {
if ctx.get_bool(dry_run) {
println("[DRY RUN] Would apply migrations")
} else {
match ctx.get_int(up_steps) {
Some(n) => println("Applying " + n.to_string() + " migrations...")
None => println("Applying all pending migrations...")
}
}
}),
),
@admiral.command(
name="down",
description="Rollback migrations",
options=[down_steps],
run=Some(async fn(ctx) {
let steps = match ctx.get_int(down_steps) { Some(n) => n; None => 1 }
println("Rolling back " + steps.to_string() + " migration(s)...")
}),
),
],
),
@admiral.command(
name="seed",
description="Seed the database",
options=[seed_file],
run=Some(async fn(ctx) {
let file = match ctx.get_string(seed_file) { Some(f) => f; None => "seeds/default.sql" }
println("Seeding from: " + file)
}),
),
],
),
],
)$ myapp db migrate up --dry-run
[DRY RUN] Would apply migrations
$ myapp db migrate down --steps 3
Rolling back 3 migration(s)...
$ myapp db seed --file custom.sql
Seeding from: custom.sqllet files = @admiral.position_strings("files", description="Files to concatenate")
@admiral.command(
name="cat",
description="Concatenate files",
positionals=[files],
run=Some(async fn(ctx) {
let file_values = ctx.get_strings(files)
for file in file_values {
println("Reading: " + file)
}
}),
)$ myapp cat a.txt b.txt c.txt
Reading: a.txt
Reading: b.txt
Reading: c.txt// In async tests, pass argv explicitly:
async test {
app.run(argv=Some(["greet", "--name", "alice"]))
}
// In production, omit argv to use process args:
app.run()println(app.render_schema()) // -> JSON string
let json = ToJson::to_json(app) // -> Json value{
"name": "myapp",
"version": "1.0.0",
"description": "My CLI tool",
"commands": {
"greet": {
"description": "Greet someone",
"options": {
"name": {
"type": "string",
"description": "Name to greet",
"required": true,
"short": "n",
"env": "ADMIRAL_NAME"
},
"verbose": { "type": "bool", "description": "Verbose output", "required": false, "short": "v" },
"count": { "type": "int", "description": "Repeat count", "required": false, "short": "c", "default": "1" }
},
"examples": ["myapp greet --name Alice", "myapp greet -n Bob -v -c 3"]
},
"db": {
"description": "Database commands",
"commands": {
"migrate": {
"description": "Run migrations",
"commands": {
"up": {
"description": "Apply pending migrations",
"options": {
"dry-run": { "type": "bool", "description": "Preview without applying", "required": false },
"steps": { "type": "int", "description": "Number of steps", "required": false, "short": "s" }
},
"examples": ["myapp db migrate up", "myapp db migrate up --dry-run"]
}
}
}
}
}
}
}// Bash
println(app.render_bash_completion())
// Zsh
println(app.render_zsh_completion())
// Fish
println(app.render_fish_completion())let shell = @admiral.string(
"shell",
short='s',
description="Shell type (bash, zsh, fish)",
required=true,
)
@admiral.command(
name="completion",
description="Generate shell completion script",
options=[shell],
run=Some(async fn(ctx) {
match ctx.get_string(shell) {
Some("bash") => println(app.render_bash_completion())
Some("zsh") => println(app.render_zsh_completion())
Some("fish") => println(app.render_fish_completion())
_ => println("Unsupported shell. Use: bash, zsh, fish")
}
}),
)# Bash: add to ~/.bashrc
eval "$(myapp completion --shell bash)"
# Zsh: add to ~/.zshrc
eval "$(myapp completion --shell zsh)"
# Fish: save to completions dir
myapp completion --shell fish > ~/.config/fish/completions/myapp.fish| Function | Description |
|---|---|
| string(name, short?, description?, env?, config?, required?, default?) | String option (--name value) |
| strings(name, short?, description?, env?, config?, required?) | Repeated string option |
| bool(name, short?, description?, env?, config?) | Boolean flag (--verbose) |
| int(name, short?, description?, env?, config?, required?, default?) | Integer option (--port 8080) |
| ints(name, short?, description?, env?, config?, required?) | Repeated integer option |
| int64(name, short?, description?, env?, config?, required?, default?) | 64-bit signed integer option |
| int64s(name, short?, description?, env?, config?, required?) | Repeated 64-bit signed integer option |
| uint(name, short?, description?, env?, config?, required?, default?) | Unsigned integer option |
| uints(name, short?, description?, env?, config?, required?) | Repeated unsigned integer option |
| uint64(name, short?, description?, env?, config?, required?, default?) | 64-bit unsigned integer option |
| uint64s(name, short?, description?, env?, config?, required?) | Repeated 64-bit unsigned integer option |
| double(name, short?, description?, env?, config?, required?, default?) | Double-precision floating-point option |
| doubles(name, short?, description?, env?, config?, required?) | Repeated double-precision floating-point option |
| Function | Result type |
|---|---|
| position_string(name, description?, config?, required?) | PositionDef[String] |
| position_strings(name, description?, config?, required?) | PositionDef[Array[String]] |
| position_int(name, description?, config?, required?) | PositionDef[Int] |
| position_ints(name, description?, config?, required?) | PositionDef[Array[Int]] |
| position_int64 / position_int64s | PositionDef[Int64] / PositionDef[Array[Int64]] |
| position_uint / position_uints | PositionDef[UInt] / PositionDef[Array[UInt]] |
| position_uint64 / position_uint64s | PositionDef[UInt64] / PositionDef[Array[UInt64]] |
| position_double / position_doubles | PositionDef[Double] / PositionDef[Array[Double]] |
| Function | Description |
|---|---|
| command(name, description?, options?, positionals?, examples?, subcommands?, run?) | Define a command or subcommand with an async run callback |
| cli(name, version?, description?, options?, commands?, load_config?) | Create a CLI app with global options |
| Method | Return | Description |
|---|---|---|
| get_bool(OptionDef[Bool]) | Bool | Flag value (default: false) |
| get_string(ArgDef[String, M]) | String? | First string value from an option or position |
| get_string_required(ArgDef[String, M]) | String raise | Required string value |
| get_int(ArgDef[Int, M]) | Int? | Parsed integer value from an option or position |
| get_int_required(ArgDef[Int, M]) | Int raise | Required parsed integer value |
| get_int64 / get_int64_required | Int64? / Int64 raise | 64-bit signed integer value |
| get_uint / get_uint_required | UInt? / UInt raise | Unsigned integer value |
| get_uint64 / get_uint64_required | UInt64? / UInt64 raise | 64-bit unsigned integer value |
| get_double / get_double_required | Double? / Double raise | Double-precision floating-point value |
| get_strings(ArgDef[Array[String], M]) | Array[String] | Repeated string values, empty when unavailable |
| get_ints(ArgDef[Array[Int], M]) | Array[Int] raise | Repeated parsed integer values |
| get_int64s / get_uints / get_uint64s / get_doubles | corresponding Array[T] raise | Repeated parsed numeric values |
| plural getter with _required suffix | NonEmptyArray[T] raise | Required non-empty repeated values |
| get_subcommand() | (String, Context)? | Selected subcommand name and context |
| Method | Return | Description |
|---|---|---|
| render_schema() | String | Full CLI definition as JSON string |
| ToJson::to_json(app) | Json | Full CLI definition as Json value |
| render_bash_completion() | String | Bash completion script |
| render_zsh_completion() | String | Zsh completion script |
| render_fish_completion() | String | Fish completion script |
app.run() // use process args
app.run(argv=Some(["greet", "--name", "x"])) // explicit args (for testing)
app.run(env={ "ADMIRAL_NAME": "Env" }) // explicit environment map
app.run(
argv=Some(["greet"]),
env={ "ADMIRAL_NAME": "Env" },
)impl Show for ConfigLoadFailureimpl ToStoredOption for ArgDef[T, OptionMetadata]impl ToStoredPosition for ArgDef[T, PositionMetadata]pub(all) struct CliApp {
name : String
version : String
description : String
root_options : Array[ArgDef[Unit, OptionMetadata]]
root_positionals : Array[ArgDef[Unit, PositionMetadata]]
commands : Array[CommandDef]
interactive : async (InteractiveContext) -> Unit?
run : async (Context) -> Unit?
load_config : () -> Map[String, Json] raise ConfigLoadFailure?
}pub(all) struct CommandDef {
name : String
description : String
options : Array[ArgDef[Unit, OptionMetadata]]
positionals : Array[ArgDef[Unit, PositionMetadata]]
examples : Array[String]
subcommands : Array[CommandDef]
interactive : async (InteractiveContext) -> Unit?
run : async (Context) -> Unit?
}pub(all) struct Context {
flags : HashMap[String, Bool]
values : HashMap[String, ReadOnlyArray[String]]
sources : HashMap[String, ValueSource]
config : HashMap[String, Json]
interactive_flags : HashMap[String, Bool]
interactive_values : HashMap[String, ReadOnlyArray[String]]
subcommand : (String, Context)?
}fn Context::get_bool(self : Context, option : ArgDef[Bool, OptionMetadata]) -> Bool raise JsonDecodeErrorfn[Metadata] Context::get_doubles_required(self : Context, argument : ArgDef[Array[Double], Metadata]) -> NonEmptyArray[Double] raisefn[Metadata] Context::get_int64s_required(self : Context, argument : ArgDef[Array[Int64], Metadata]) -> NonEmptyArray[Int64] raisefn[Metadata] Context::get_ints_required(self : Context, argument : ArgDef[Array[Int], Metadata]) -> NonEmptyArray[Int] raisefn[Metadata] Context::get_string(self : Context, argument : ArgDef[String, Metadata]) -> String? raise JsonDecodeErrorfn[Metadata] Context::get_strings(self : Context, argument : ArgDef[Array[String], Metadata]) -> ReadOnlyArray[String] raise JsonDecodeErrorfn[Metadata] Context::get_strings_required(self : Context, argument : ArgDef[Array[String], Metadata]) -> NonEmptyArray[String] raisefn[Metadata] Context::get_uint64s_required(self : Context, argument : ArgDef[Array[UInt64], Metadata]) -> NonEmptyArray[UInt64] raisefn InteractiveContext::set_bool(self : InteractiveContext, option : ArgDef[Bool, OptionMetadata], value : Bool) -> Unitfn[Metadata] InteractiveContext::set_double(self : InteractiveContext, argument : ArgDef[Double, Metadata], value : Double) -> Unitfn[Metadata] InteractiveContext::set_doubles(self : InteractiveContext, argument : ArgDef[Array[Double], Metadata], values : Array[Double]) -> Unitfn[Metadata] InteractiveContext::set_int(self : InteractiveContext, argument : ArgDef[Int, Metadata], value : Int) -> Unitfn[Metadata] InteractiveContext::set_int64(self : InteractiveContext, argument : ArgDef[Int64, Metadata], value : Int64) -> Unitfn[Metadata] InteractiveContext::set_int64s(self : InteractiveContext, argument : ArgDef[Array[Int64], Metadata], values : Array[Int64]) -> Unitfn[Metadata] InteractiveContext::set_ints(self : InteractiveContext, argument : ArgDef[Array[Int], Metadata], values : Array[Int]) -> Unitfn[Metadata] InteractiveContext::set_string(self : InteractiveContext, argument : ArgDef[String, Metadata], value : String) -> Unitfn[Metadata] InteractiveContext::set_strings(self : InteractiveContext, argument : ArgDef[Array[String], Metadata], values : Array[String]) -> Unitfn[Metadata] InteractiveContext::set_uint(self : InteractiveContext, argument : ArgDef[UInt, Metadata], value : UInt) -> Unitfn[Metadata] InteractiveContext::set_uint64(self : InteractiveContext, argument : ArgDef[UInt64, Metadata], value : UInt64) -> Unitfn[Metadata] InteractiveContext::set_uint64s(self : InteractiveContext, argument : ArgDef[Array[UInt64], Metadata], values : Array[UInt64]) -> Unitfn[Metadata] InteractiveContext::set_uints(self : InteractiveContext, argument : ArgDef[Array[UInt], Metadata], values : Array[UInt]) -> Unitpub struct OptionMetadata {
type_ : OptionType
short : Char
description : String
env : String?
required : Bool
default_value : String?
multiple : Bool
interactive : Bool
} derive(Debug)pub struct PositionMetadata {
type_ : OptionType
description : String
required : Bool
multiple : Bool
interactive : Bool
} derive(Debug)fn bool(name : String, short? : Char, description? : String, env? : String, config? : String, interactive? : Bool) -> ArgDef[Bool, OptionMetadata]fn cli(name~ : String, version? : String, description? : String, options? : Array[&ToStoredOption], positionals? : Array[&ToStoredPosition], commands? : Array[CommandDef], interactive? : async (InteractiveContext) -> Unit?, run? : async (Context) -> Unit?, load_config? : () -> Map[String, Json] raise ConfigLoadFailure?) -> CliAppfn command(name~ : String, description? : String, options? : Array[&ToStoredOption], positionals? : Array[&ToStoredPosition], examples? : Array[String], subcommands? : Array[CommandDef], interactive? : async (InteractiveContext) -> Unit?, run? : async (Context) -> Unit?) -> CommandDeffn double(name : String, short? : Char, description? : String, env? : String, config? : String, required? : Bool, default? : Double?, interactive? : Bool) -> ArgDef[Double, OptionMetadata]fn doubles(name : String, short? : Char, description? : String, env? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Array[Double], OptionMetadata]fn int(name : String, short? : Char, description? : String, env? : String, config? : String, required? : Bool, default? : Int?, interactive? : Bool) -> ArgDef[Int, OptionMetadata]fn int64(name : String, short? : Char, description? : String, env? : String, config? : String, required? : Bool, default? : Int64?, interactive? : Bool) -> ArgDef[Int64, OptionMetadata]fn int64s(name : String, short? : Char, description? : String, env? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Array[Int64], OptionMetadata]fn ints(name : String, short? : Char, description? : String, env? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Array[Int], OptionMetadata]fn position_double(name : String, description? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Double, PositionMetadata]fn position_doubles(name : String, description? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Array[Double], PositionMetadata]fn position_int(name : String, description? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Int, PositionMetadata]fn position_int64(name : String, description? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Int64, PositionMetadata]fn position_int64s(name : String, description? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Array[Int64], PositionMetadata]fn position_ints(name : String, description? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Array[Int], PositionMetadata]fn position_string(name : String, description? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[String, PositionMetadata]fn position_strings(name : String, description? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Array[String], PositionMetadata]fn position_uint(name : String, description? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[UInt, PositionMetadata]fn position_uint64(name : String, description? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[UInt64, PositionMetadata]fn position_uint64s(name : String, description? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Array[UInt64], PositionMetadata]fn position_uints(name : String, description? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Array[UInt], PositionMetadata]fn string(name : String, short? : Char, description? : String, env? : String, config? : String, required? : Bool, default? : String?, interactive? : Bool) -> ArgDef[String, OptionMetadata]fn strings(name : String, short? : Char, description? : String, env? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Array[String], OptionMetadata]fn uint(name : String, short? : Char, description? : String, env? : String, config? : String, required? : Bool, default? : UInt?, interactive? : Bool) -> ArgDef[UInt, OptionMetadata]fn uint64(name : String, short? : Char, description? : String, env? : String, config? : String, required? : Bool, default? : UInt64?, interactive? : Bool) -> ArgDef[UInt64, OptionMetadata]fn uint64s(name : String, short? : Char, description? : String, env? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Array[UInt64], OptionMetadata]fn uints(name : String, short? : Char, description? : String, env? : String, config? : String, required? : Bool, interactive? : Bool) -> ArgDef[Array[UInt], OptionMetadata]Async-first declarative CLI builder for MoonBit, inspired by gunshi
Dependencies