README
¶
🏃⌨️ quicli
Zero-boilerplate CLI in Go
This struct:
type Opts struct {
Count int `cli:"how many times" default:"1"`
Say string `cli:"what to say" default:"hello"`
World bool `cli:"announce it"`
Format string `cli:"output format" choices:"text,json,yaml" default:"text"`
Tags []string `cli:"filter by tags"`
}
Is a complete CLI. Help text, short flags, env vars, shell completion, input validation, JSON Schema (for AI tool integration). All generated.
$ say-hello --help
Say Hello to the world
Usage: say-hello [flags]
--count -c how many times. (default: 1) [env: SAY_HELLO_COUNT]
--say -s what to say. (default: "hello") [env: SAY_HELLO_SAY]
--world -w announce it. (default: false) [env: SAY_HELLO_WORLD]
--format -f output format. (choices: text, json, yaml) (default: "text") [env: SAY_HELLO_FORMAT]
--tags -t filter by tags. (default: []) [env: SAY_HELLO_TAGS]
$ say-hello --count 3 --say "bonjour"
$ say-hello -w
$ SAY_HELLO_COUNT=5 say-hello # env var, same as --count 5
$ say-hello --completion zsh # shell completion
$ say-hello --json-schema # JSON Schema for AI tools
No init functions. No command registration. No flag pointers. Inspired by nim's cligen.
Getting started
Tag a struct, pass a function:
func main() {
quicli.RunFunc("say-hello [flags]", "Say Hello to the world", func(o Opts) {
for i := 0; i < o.Count; i++ {
msg := o.Say
if o.World { msg = "🌍 " + msg }
fmt.Println(msg)
}
})
}
That's it. Flags are parsed, validated, and passed to your function as a typed struct.
Supported types: int, string, bool, float64, []string, time.Duration, or any type implementing flag.Value.
Tags: cli:"desc" · default:"val" · short:"x" · env:"VAR" · required:"true" · choices:"a,b,c"
With subcommands
Give each subcommand its own struct. NewSubcommand infers the flags from it:
type ColorOpts struct {
Foreground bool `cli:"use foreground color"`
}
type WhisperOpts struct {
Say string `cli:"what to whisper" default:"psst"`
Times int `cli:"how many times" default:"1"`
}
func main() {
colorSub := quicli.NewSubcommand("color", "print in red", func(o ColorOpts) {
fmt.Println("foreground:", o.Foreground)
})
colorSub.Aliases = quicli.Aliases("co") // optional
quicli.Cli{
Usage: "say-hello [command] [flags]",
Description: "Say Hello to the world",
Function: func(cfg quicli.Config) { fmt.Println("hello") },
Subcommands: quicli.Subcommands{
colorSub,
quicli.NewSubcommand("whisper", "say quietly", func(o WhisperOpts) {
for i := 0; i < o.Times; i++ { fmt.Println(o.Say) }
}),
},
}.RunWithSubcommand()
}
$ say-hello color --foreground
$ say-hello co --foreground # alias works
$ say-hello w --say "shhh" # unambiguous prefix works too
$ say-hello whisper --say "shhh" --times 2
The one-liner way
Everything in one expression:
quicli.Run(quicli.Cli{
Usage: "say-hello [flags]",
Description: "Say Hello to the world",
Flags: quicli.Flags{
{Name: "count", Default: 1, Description: "how many times"},
{Name: "say", Default: "hello", Description: "what to say"},
{Name: "world", Description: "announce it"},
},
Function: func(cfg quicli.Config) {
count := cfg.GetIntFlag("count")
say := cfg.GetStringFlag("say")
world := cfg.GetBoolFlag("world")
for i := 0; i < count; i++ {
if world { fmt.Print("🌍 ") }
fmt.Println(say)
}
},
})
With subcommands
quicli.Cli{
Usage: "mytool [command] [flags]",
Description: "A tool that does things",
Flags: quicli.Flags{
{Name: "verbose", Description: "verbose output"},
{Name: "output", Default: "text", Description: "output format",
SharedSubcommand: quicli.SubcommandSet{"get", "list"}},
},
Function: Root,
Subcommands: quicli.Subcommands{
{Name: "get", Aliases: quicli.Aliases("g"), Description: "get a resource", Function: Get,
Flags: quicli.Flags{{Name: "id", Default: "", Description: "resource id"}}},
{Name: "list", Aliases: quicli.Aliases("ls"), Description: "list resources", Function: List},
{Name: "delete", Description: "delete a resource", Function: Delete},
},
}.RunWithSubcommand()
$ mytool --help
A tool that does things
Usage: mytool [command] [flags]
Available commands: get (g), list (ls), delete
--verbose -v verbose output. (default: false) [env: MYTOOL_VERBOSE]
Use "mytool <command> --help" for more information about a command.
$ mytool get --help
A tool that does things
Usage: mytool [command] [flags]
Command get: get a resource
--id -i resource id. (default: "") [env: MYTOOL_ID]
--output -o output format. (default: "text") [env: MYTOOL_OUTPUT]
Use "mytool --help" for more information about available commands.
$ mytool get --id abc123
$ mytool g --id abc123 # alias works
$ mytool ge --id abc123 # unambiguous prefix works too
$ mytool lst # quicli error: unknown subcommand 'lst', did you mean 'list'?
$ MYTOOL_VERBOSE=true mytool list
Validation & constraints
type DeployOpts struct {
Target string `cli:"deploy target" required:"true"`
Env string `cli:"environment" choices:"dev,staging,prod" default:"dev"`
Timeout time.Duration `cli:"deploy timeout" default:"5m"`
}
Required flags required:"true" must be explicitly provided (via CLI or env var). Help shows (required) instead of a default.
Choices choices:"a,b,c" restricts the flag to a set of valid values. Invalid input exits with a clear error.
Custom types time.Duration works out of the box ("30s", "5m", "1h"). Any type implementing flag.Value is supported too:
type LogLevel struct{ v string }
func (l *LogLevel) String() string { return l.v }
func (l *LogLevel) Set(s string) error { l.v = s; return nil }
type Opts struct {
Level LogLevel `cli:"log level" default:"info"`
}
Batteries included
Every quicli CLI gets these for free, no configuration needed:
Env var fallback PROGNAME_FLAGNAME is checked before the default. Shown in help.
SAY_HELLO_COUNT=10 ./say-hello # same as --count 10
Override per flag: EnvVar: "MY_CUSTOM_VAR" · Opt out: EnvVar: "-"
Short flags first letter auto-derived (--count -> -c). Override with ShortName: "n".
Shell completion one flag, three shells:
./say-hello --completion bash >> ~/.bash_completion
./say-hello --completion zsh > ~/.zsh/completions/_say-hello
./say-hello --completion fish > ~/.config/fish/completions/say-hello.fish
Prefix matching type just enough to be unambiguous:
$ mytool g --id abc123 # matches "get" (unique prefix)
$ mytool d # matches "delete" (unique prefix)
Typo detection suggests the closest subcommand on misspelling:
$ mytool delet
quicli error: unknown subcommand 'delet', did you mean 'delete'?
JSON Schema expose your CLI's contract for AI tools and code generation:
$ mytool --json-schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"description": "A tool that does things",
"properties": {
"verbose": {
"type": "boolean",
"description": "verbose output",
"x-quicli-env-var": "MYTOOL_VERBOSE"
}
},
"x-quicli-subcommands": {
"get": { "..." : "..." },
"list": { "..." : "..." }
}
}
Programmatic access: cli.JSONSchema(), cli.SubcommandSchemas(), cli.JSONSchemaString().
AI agent integration
quicli CLIs are AI-ready out of the box. Two approaches depending on your setup:
With MCP (recommended for Claude, Cursor, etc.)
Install the bridge:
go install github.com/ariary/quicli/cmd/quicli-mcp@latest
Add to your MCP client config (e.g. ~/.claude/claude_desktop_config.json):
{
"mcpServers": {
"my-tool": {
"command": "quicli-mcp",
"args": ["/path/to/my-tool"]
}
}
}
That's it. Your CLI's flags become typed tool parameters. Subcommands become separate tools. Env-only flags (EnvOnly: true) are passed as environment variables, not CLI args.
Without MCP (direct API integration)
Any quicli CLI exposes its contract via --json-schema. Feed it to any LLM with tool/function calling:
# 1. Get the schema
SCHEMA=$(./my-tool --json-schema)
# 2. Use it as a tool definition in your API call
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-d "{
\"model\": \"claude-sonnet-4-20250514\",
\"tools\": [{
\"name\": \"my-tool\",
\"description\": \"$(echo $SCHEMA | jq -r .description)\",
\"input_schema\": $SCHEMA
}],
\"messages\": [{\"role\": \"user\", \"content\": \"...\"}]
}"
# 3. When the model calls the tool, run:
./my-tool --count 5 --name hello
The schema works with Claude API tool_use, OpenAI function calling, or any agent framework that accepts JSON Schema.
Env-only flags for secrets
Keep API keys and tokens out of shell history:
type Opts struct {
Target string `cli:"target to scan" required:"true"`
APIKey string `cli:"API key" env:"only"`
}
--help won't show APIKey. --json-schema marks it as "x-quicli-input": "env-only". The MCP bridge passes it as an environment variable automatically.
Debug your flags
See where each flag's value comes from:
$ MYTOOL_NAME=world ./mytool --count 5 --debug-options
FLAG VALUE SOURCE
--count 5 cli
--name world env (MYTOOL_NAME)
--format json default
--secret *** env (MYTOOL_SECRET)
Get more examples
quicli is a thin wrapper around Go's
flagpackage. Use it to write CLIs fast, not to build complex command hierarchies.