A reflection-based, generic, type-safe CLI parser for Go that reads struct tags and generates help text, with subcommands, grouped commands, and LLM-friendly output.
- Features
- Install
- Quick Start (Single Command)
- Subcommands (Global + Command Flags)
- Command Groups ("gh repo" style)
- Help Generation (Human + LLM)
- Help metadata fields
- Flag Tags
- Supported Flag Types
- Optional flags via pointers
- Port validation
- Positional Argument Schemas
- Aliases
- Partial Parsing (Known Flags)
- Registry & Introspection
- Remaining Args (
--) - Errors
- API Reference
- Development
- License
- Type-safe flag parsing with Go generics.
- Global + subcommand flag separation (including mixed ordering).
- Positional argument schemas with validation and auto-population.
- Automatic help text (human and LLM optimized).
- Command groups ("docker run"-style) and aliases.
- Flags can appear anywhere; supports
--passthrough. - Optional flags via pointer types; defaults via struct tags.
- Built-in
Porttype with optional range validation. - Partial parsing for "known flags" and consume-only workflows.
- Registry-based command introspection for advanced dispatching.
go get github.com/shayne/yargs@latestUse ParseFlags when you have no subcommands.
package main
import (
"fmt"
"log"
"os"
"github.com/shayne/yargs"
)
type Flags struct {
Verbose bool `flag:"verbose" short:"v" help:"Enable verbose output"`
Output string `flag:"output" short:"o" help:"Output file"`
}
func main() {
result, err := yargs.ParseFlags[Flags](os.Args[1:])
if err != nil {
log.Fatal(err)
}
fmt.Printf("verbose=%v output=%s args=%v\n", result.Flags.Verbose, result.Flags.Output, result.Args)
}Use ParseWithCommand or ParseAndHandleHelp when you have subcommands.
package main
import (
"context"
"errors"
"fmt"
"os"
"github.com/shayne/yargs"
)
type GlobalFlags struct {
Verbose bool `flag:"verbose" short:"v" help:"Enable verbose output"`
}
type StatusFlags struct {
Short bool `flag:"short" short:"s" help:"Show short status"`
}
type CommitFlags struct {
Message string `flag:"message" short:"m" help:"Commit message"`
Amend bool `flag:"amend" help:"Amend the last commit"`
}
type CloneFlags struct {
Depth int `flag:"depth" help:"Create a shallow clone with history truncated"`
Branch string `flag:"branch" short:"b" help:"Checkout a specific branch"`
}
type CloneArgs struct {
Repo string `pos:"0" help:"Repository URL"`
Dir string `pos:"1?" help:"Target directory"`
}
var helpConfig = yargs.HelpConfig{
Command: yargs.CommandInfo{
Name: "git",
Description: "The stupid content tracker",
},
SubCommands: map[string]yargs.SubCommandInfo{
"status": {Name: "status", Description: "Show working tree status"},
"commit": {Name: "commit", Description: "Record changes to the repository"},
"clone": {Name: "clone", Description: "Clone a repository into a new directory"},
},
}
func handleStatus(args []string) error {
return runWithParse[StatusFlags, struct{}](args, func(result *yargs.TypedParseResult[GlobalFlags, StatusFlags, struct{}]) error {
fmt.Printf("short=%v verbose=%v\n", result.SubCommandFlags.Short, result.GlobalFlags.Verbose)
return nil
})
}
func handleCommit(args []string) error {
return runWithParse[CommitFlags, struct{}](args, func(result *yargs.TypedParseResult[GlobalFlags, CommitFlags, struct{}]) error {
fmt.Printf("message=%q amend=%v\n", result.SubCommandFlags.Message, result.SubCommandFlags.Amend)
return nil
})
}
func handleClone(args []string) error {
return runWithParse[CloneFlags, CloneArgs](args, func(result *yargs.TypedParseResult[GlobalFlags, CloneFlags, CloneArgs]) error {
fmt.Printf("repo=%s dir=%s depth=%d branch=%s\n",
result.Args.Repo,
result.Args.Dir,
result.SubCommandFlags.Depth,
result.SubCommandFlags.Branch,
)
return nil
})
}
func runWithParse[S any, A any](args []string, fn func(*yargs.TypedParseResult[GlobalFlags, S, A]) error) error {
result, err := yargs.ParseAndHandleHelp[GlobalFlags, S, A](args, helpConfig)
if errors.Is(err, yargs.ErrShown) {
return nil
}
if err != nil {
return err
}
return fn(result)
}
func main() {
handlers := map[string]yargs.SubcommandHandler{
"status": func(_ context.Context, args []string) error { return handleStatus(args) },
"commit": func(_ context.Context, args []string) error { return handleCommit(args) },
"clone": func(_ context.Context, args []string) error { return handleClone(args) },
}
if err := yargs.RunSubcommands(context.Background(), os.Args[1:], helpConfig, GlobalFlags{}, handlers); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}Use RunSubcommandsWithGroups and Group/GroupInfo for grouped commands.
config := yargs.HelpConfig{
Command: yargs.CommandInfo{Name: "gh", Description: "GitHub CLI"},
Groups: map[string]yargs.GroupInfo{
"repo": {
Name: "repo",
Description: "Manage repositories",
Commands: map[string]yargs.SubCommandInfo{
"create": {Name: "create", Description: "Create a repository"},
"view": {Name: "view", Description: "View a repository"},
},
},
"issue": {
Name: "issue",
Description: "Manage issues",
Commands: map[string]yargs.SubCommandInfo{
"list": {Name: "list", Description: "List issues"},
"create": {Name: "create", Description: "Create an issue"},
},
},
},
}
groups := map[string]yargs.Group{
"repo": {
Description: "Manage repositories",
Commands: map[string]yargs.SubcommandHandler{
"create": handleRepoCreate,
"view": handleRepoView,
},
},
"issue": {
Description: "Manage issues",
Commands: map[string]yargs.SubcommandHandler{
"list": handleIssueList,
"create": handleIssueCreate,
},
},
}
_ = yargs.RunSubcommandsWithGroups(context.Background(), os.Args[1:], config, GlobalFlags{}, nil, groups)Yargs can emit human help or LLM-optimized help from the same metadata.
- Global:
GenerateGlobalHelp - Group:
GenerateGroupHelp - Subcommand:
GenerateSubCommandHelp - Dispatcher:
RunSubcommandsandRunSubcommandsWithGroups
- Global:
GenerateGlobalHelpLLM - Group:
GenerateGroupHelpLLM - Subcommand:
GenerateSubCommandHelpLLM - Flags:
--help-llm
ParseWithCommandAndHelp and ParseAndHandleHelp will detect help, -h,
--help, and --help-llm and return the right error sentinel.
ParseAndHandleHelp prints help automatically and returns ErrShown.
The help subcommand is supported as app help or app help <command>.
You control help output with these fields:
CommandInfo:Name,Description,Examples,LLMInstructionsSubCommandInfo:Name,Description,Usage,Examples,Aliases,Hidden,LLMInstructionsGroupInfo:Name,Description,Commands,Hidden,LLMInstructions
type Flags struct {
Verbose bool `flag:"verbose" short:"v" help:"Enable verbose output"`
Output string `flag:"output" default:"out.txt" help:"Output path"`
Rate int `flag:"rate" help:"Requests per second"`
}Supported struct tags:
flag:"name"- long flag name. Defaults to lowercased field name.short:"x"- single-character alias.help:"text"- help text for auto-generation.default:"value"- default if flag not provided.port:"min-max"- range validation forPortfields.pos:"N"- positional argument schema (see below).
string,boolint,int8,int16,int32,int64uint,uint8,uint16,uint32,uint64float32,float64time.Durationurl.URL,*url.URLyargs.Port(uint16 alias with optional range validation)- Pointers to any of the above (for optional flags)
[]string(comma-separated or repeated flags)
type Flags struct {
Token *string `flag:"token" help:"Optional auth token"`
}
// Token is nil if not provided.type Flags struct {
HTTPPort yargs.Port `flag:"http" port:"1-65535" help:"HTTP port"`
AdminPort *yargs.Port `flag:"admin" port:"8000-9000" help:"Admin port"`
}Define positional arguments using pos:"N" tags. Yargs will validate counts
and populate the struct.
type Args struct {
Service string `pos:"0" help:"Service name"`
Image string `pos:"1" help:"Image or payload"`
Tags []string `pos:"2*" help:"Optional tags"`
}Positional tag variants:
pos:"0"required argument at index 0pos:"0?"optional argument at index 0pos:"0*"variadic (0 or more) at index 0pos:"0+"variadic (1 or more) at index 0
Use Aliases on SubCommandInfo to register alternative command names.
ApplyAliases will rewrite the args to canonical names and is used by the
built-in dispatchers.
config := yargs.HelpConfig{
SubCommands: map[string]yargs.SubCommandInfo{
"status": {Name: "status", Aliases: []string{"st", "stat"}},
},
}
args := yargs.ApplyAliases(os.Args[1:], config)Aliases also work for grouped commands (group-local aliases).
When you only want a subset of flags and want to preserve unknown args:
type Flags struct {
Host string `flag:"host"`
Tags []string `flag:"tags" short:"t"`
}
res, err := yargs.ParseKnownFlags[Flags](os.Args[1:], yargs.KnownFlagsOptions{
SplitCommaSlices: true,
})
// res.Flags contains only known flags; res.RemainingArgs preserves the rest.Or use the lower-level ConsumeFlagsBySpec:
specs := map[string]yargs.ConsumeSpec{
"host": {Kind: reflect.String},
"tags": {Kind: reflect.Slice, SplitComma: true},
}
remaining, values := yargs.ConsumeFlagsBySpec(os.Args[1:], specs)Use Registry for schema-aware command resolution and positional metadata.
reg := yargs.Registry{
Command: yargs.CommandInfo{Name: "app"},
SubCommands: map[string]yargs.CommandSpec{
"run": {
Info: yargs.SubCommandInfo{Name: "run"},
ArgsSchema: RunArgs{},
},
},
}
resolved, ok, err := yargs.ResolveCommandWithRegistry(os.Args[1:], reg)
if ok {
if spec, ok := resolved.PArg(0); ok {
fmt.Printf("arg0 name=%s required=%v\n", spec.Name, spec.Required)
}
}Everything after -- is preserved in RemainingArgs.
result, _ := yargs.ParseFlags[Flags]([]string{"-v", "arg1", "--", "--raw", "x"})
// result.Args == []string{"arg1"}
// result.RemainingArgs == []string{"--raw", "x"}Yargs exposes structured errors for control flow and diagnostics:
ErrHelp,ErrSubCommandHelp,ErrHelpLLMfor help requests.ErrShownfromParseAndHandleHelpwhen it already printed output.InvalidFlagErrorfor unknown flags.InvalidArgsErrorfor bad positional args.FlagValueErrorfor type conversion or validation issues.
ParseAndHandleHelp will print a clean message for users, and if the global
flags struct has a Verbose bool field set to true, it will also print the full
error chain for FlagValueError.
For full, generated API docs, see:
https://pkg.go.dev/github.com/shayne/yargs
This repo uses mise for tool and task management.
mise install
mise run testmise run fmtmise run tidymise run testmise run check
MIT. See LICENSE.