parser API

parser

package

API reference for the parser package.

S
struct

FlagMetadata

FlagMetadata holds information about a flag.

pkg/parser/node.go:8-16
type FlagMetadata struct

Fields

Name Type Description
Name string
Short string
Description string
Default string
Env string
Required bool
Field reflect.Value
S
struct

ArgMetadata

ArgMetadata holds information about a positional argument.

pkg/parser/node.go:19-24
type ArgMetadata struct

Fields

Name Type Description
Description string
Required bool
IsGreedy bool
Field reflect.Value
S
struct

CommandNode

CommandNode represents a node in the command tree.

pkg/parser/node.go:27-37
type CommandNode struct

Fields

Name Type Description
Name string
Description string
Aliases []string
Flags map[string]*FlagMetadata
ShortFlags map[string]string
Args []*ArgMetadata
Children map[string]*CommandNode
Value reflect.Value
Type reflect.Type
F
function

NewCommandNode

NewCommandNode creates a new CommandNode with initialized maps.

Parameters

name
string
description
string

Returns

pkg/parser/node.go:44-54
func NewCommandNode(name, description string, val reflect.Value) *CommandNode

{
	return &CommandNode{
		Name:        name,
		Description: description,
		Flags:       make(map[string]*FlagMetadata),
		ShortFlags:  make(map[string]string),
		Children:    make(map[string]*CommandNode),
		Value:       val,
		Type:        val.Type(),
	}
}

Example

node := parser.NewCommandNode("init", "Initialize project", reflect.ValueOf(&InitCmd{}))
F
function

Parse

Parse parses a struct and returns a CommandNode tree.

Parameters

name
string
root
any

Returns

error
pkg/parser/parser.go:19-38
func Parse(name string, root any) (*CommandNode, error)

{
	val := reflect.ValueOf(root)
	if !val.IsValid() {
		return nil, fmt.Errorf("invalid root value (nil)")
	}
	if val.Kind() == reflect.Ptr {
		if val.IsNil() {
			return nil, fmt.Errorf("root value is a nil pointer")
		}
		val = val.Elem()
	}

	node := NewCommandNode(name, "", val)

	if err := ParseStruct(node, val); err != nil {
		return nil, err
	}

	return node, nil
}

Example

root := &RootCmd{}
node, err := parser.Parse("apx", root)
F
function

ParseStruct

ParseStruct recursively parses the struct fields to build the command tree.

Parameters

Returns

error
pkg/parser/parser.go:47-228
func ParseStruct(node *CommandNode, val reflect.Value) error

{
	v := val
	for v.Kind() == reflect.Ptr {
		if v.IsNil() {
			return nil
		}
		v = v.Elem()
	}

	if v.Kind() != reflect.Struct {
		return nil
	}

	typ := v.Type()

	for i := 0; i < v.NumField(); i++ {
		field := typ.Field(i)
		fieldVal := v.Field(i)

		if field.Tag.Get("internal") == "ignore" {
			continue
		}

		cmdTag, ok := field.Tag.Lookup("cmd")

		// Pre-process for map merging
		if ok && cmdTag == "*" {
			fVal := fieldVal
			for fVal.Kind() == reflect.Ptr {
				if fVal.IsNil() {
					break
				}
				fVal = fVal.Elem()
			}

			if fVal.Kind() == reflect.Map {
				if !fVal.IsNil() {
					if node.Children == nil {
						node.Children = make(map[string]*CommandNode)
					}

					iter := fVal.MapRange()
					for iter.Next() {
						cmdName := iter.Key().String()
						val := iter.Value()

						// Unwrap pointer to reach the command struct
						startVal := val
						for startVal.Kind() == reflect.Ptr {
							if startVal.IsNil() {
								break
							}
							startVal = startVal.Elem()
						}

						if startVal.Kind() == reflect.Ptr && startVal.IsNil() {
							continue
						}

						helpPrefix := field.Tag.Get("help")
						description := ""
						if helpPrefix != "" {
							description = fmt.Sprintf("pr:%s.%s", helpPrefix, cmdName)
						}

						childNode := NewCommandNode(cmdName, description, startVal)
						node.Children[cmdName] = childNode

						if err := ParseStruct(childNode, startVal); err != nil {
							return err
						}
					}
				}
			}
			continue
		}

		if ok {
			startVal := fieldVal
			for startVal.Kind() == reflect.Ptr {
				if startVal.IsNil() {
					if startVal.CanSet() {
						startVal.Set(reflect.New(startVal.Type().Elem()))
					} else {
						break
					}
				}
				startVal = startVal.Elem()
			}

			cmdName := strings.ToLower(field.Name)
			if cmdTag != "" {
				cmdName = cmdTag
			}

			var aliases []string
			if aliasTag, ok := field.Tag.Lookup("aliases"); ok {
				aliases = strings.Split(aliasTag, ",")
			}

			description := field.Tag.Get("help")

			childNode := NewCommandNode(cmdName, description, startVal)
			childNode.Aliases = aliases

			node.Children[cmdName] = childNode
			for _, alias := range aliases {
				node.Children[alias] = childNode
			}

			if err := ParseStruct(childNode, startVal); err != nil {
				return err
			}
			continue
		}

		if cliTag, ok := field.Tag.Lookup("cli"); ok {
			parts := strings.Split(cliTag, ",")
			name := parts[0]
			short := ""
			if len(parts) > 1 {
				short = parts[1]
			}

			required := false
			if reqTag, ok := field.Tag.Lookup("required"); ok && reqTag == "true" {
				required = true
			}

			flagMeta := &FlagMetadata{
				Name:        name,
				Short:       short,
				Description: field.Tag.Get("help"),
				Default:     field.Tag.Get("default"),
				Env:         field.Tag.Get("env"),
				Required:    required,
				Field:       fieldVal,
			}

			node.Flags[name] = flagMeta
			if short != "" {
				node.ShortFlags[short] = name
			}
			continue
		}

		if flagTag, ok := field.Tag.Lookup("flag"); ok {
			meta := parseFlagTag(flagTag, fieldVal)

			name := meta.Name
			if name == "" {
				name = strings.ToLower(field.Name)
			}

			node.Flags[name] = meta
			if meta.Short != "" {
				node.ShortFlags[meta.Short] = name
			}
			continue
		}

		if _, ok := field.Tag.Lookup("arg"); ok {
			required := false
			if reqTag, ok := field.Tag.Lookup("required"); ok && reqTag == "true" {
				required = true
			}

			isGreedy := field.Type.Kind() == reflect.Slice && field.Type.Elem().Kind() == reflect.String

			argMeta := &ArgMetadata{
				Description: field.Tag.Get("help"),
				Required:    required,
				IsGreedy:    isGreedy,
				Field:       fieldVal,
			}

			node.Args = append(node.Args, argMeta)
			continue
		}
	}
	return nil
}

Example

node := parser.NewCommandNode("init", "Initialize project", reflect.ValueOf(&InitCmd{}))
val := reflect.ValueOf(&InitCmd{})
err := parser.ParseStruct(node, val)
F
function

parseFlagTag

parseFlagTag parses the flag:“short:x, long:y, name:z” format.

Parameters

tag
string
fieldVal

Returns

pkg/parser/parser.go:231-249
func parseFlagTag(tag string, fieldVal reflect.Value) *FlagMetadata

{
	meta := &FlagMetadata{Field: fieldVal}

	parsed := flagTagParser.Parse(tag)

	if vals, ok := parsed["short"]; ok && len(vals) > 0 {
		meta.Short = vals[0]
	}

	if vals, ok := parsed["long"]; ok && len(vals) > 0 {
		meta.Name = vals[0]
	}

	if vals, ok := parsed["name"]; ok && len(vals) > 0 {
		meta.Description = vals[0]
	}

	return meta
}
S
struct

rootNoSub

pkg/parser/parser_test.go:8-8
type rootNoSub struct
S
struct

rootWithSub

pkg/parser/parser_test.go:10-12
type rootWithSub struct

Fields

Name Type Description
Add AddCmd cmd:"" help:"Add a new item"
S
struct

AddCmd

pkg/parser/parser_test.go:14-16
type AddCmd struct

Fields

Name Type Description
Item string arg:"" required:"true" help:"Item to add"
S
struct

rootWithFlags

pkg/parser/parser_test.go:18-22
type rootWithFlags struct

Fields

Name Type Description
Verbose bool cli:"verbose,v" help:"Enable verbose" env:"VERBOSE"
Name string cli:"name" help:"Your name" default:"world"
Count int cli:"count" help:"Count" required:"true"
S
struct

rootFlagTag

pkg/parser/parser_test.go:24-26
type rootFlagTag struct

Fields

Name Type Description
Verbose bool flag:"short:v,long:verbose,name:Enable verbose output"
S
struct

rootAliases

pkg/parser/parser_test.go:28-30
type rootAliases struct

Fields

Name Type Description
Add AddCmd cmd:"" aliases:"a,ad" help:"Add a new item"
S
struct

rootWildcard

pkg/parser/parser_test.go:32-34
type rootWildcard struct

Fields

Name Type Description
Commands map[string]*WildcardCmd cmd:"*" help:"dynamic"
S
struct

WildcardCmd

pkg/parser/parser_test.go:36-38
type WildcardCmd struct

Fields

Name Type Description
Arg string arg:"" help:"arg"
S
struct

rootNested

pkg/parser/parser_test.go:40-44
type rootNested struct

Fields

Name Type Description
Config struct { Show ShowCmd `cmd:"" help:"Show config"` } cmd:"config" help:"Config commands"
S
struct

ShowCmd

pkg/parser/parser_test.go:46-48
type ShowCmd struct

Fields

Name Type Description
All bool cli:"all" help:"Show all"
S
struct

rootInternal

pkg/parser/parser_test.go:50-53
type rootInternal struct

Fields

Name Type Description
Verbose bool cli:"verbose,v" help:"Enable verbose"
Hidden string internal:"ignore"
F
function

TestParse_RootOnly

Parameters

pkg/parser/parser_test.go:55-69
func TestParse_RootOnly(t *testing.T)

{
	node, err := Parse("app", &rootNoSub{})
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}
	if node.Name != "app" {
		t.Errorf("got name %q, want %q", node.Name, "app")
	}
	if len(node.Children) != 0 {
		t.Errorf("expected no children, got %d", len(node.Children))
	}
	if len(node.Flags) != 0 {
		t.Errorf("expected no flags, got %d", len(node.Flags))
	}
}
F
function

TestParse_Subcommand

Parameters

pkg/parser/parser_test.go:71-86
func TestParse_Subcommand(t *testing.T)

{
	node, err := Parse("app", &rootWithSub{})
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}
	if len(node.Children) != 1 {
		t.Fatalf("expected 1 child, got %d", len(node.Children))
	}
	child, ok := node.Children["add"]
	if !ok {
		t.Fatal("expected child named 'add'")
	}
	if child.Description == "" {
		t.Error("expected non-empty description for add")
	}
}
F
function

TestParse_CLIFlags

Parameters

pkg/parser/parser_test.go:88-118
func TestParse_CLIFlags(t *testing.T)

{
	node, err := Parse("app", &rootWithFlags{})
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}
	if len(node.Flags) != 3 {
		t.Fatalf("expected 3 flags, got %d", len(node.Flags))
	}

	verbose, ok := node.Flags["verbose"]
	if !ok {
		t.Fatal("expected flag 'verbose'")
	}
	if verbose.Short != "v" {
		t.Errorf("got short %q, want %q", verbose.Short, "v")
	}
	if verbose.Env != "VERBOSE" {
		t.Errorf("got env %q, want %q", verbose.Env, "VERBOSE")
	}
	if verbose.Default != "" {
		t.Errorf("expected empty default for verbose")
	}

	count, ok := node.Flags["count"]
	if !ok {
		t.Fatal("expected flag 'count'")
	}
	if !count.Required {
		t.Error("count should be required")
	}
}
F
function

TestParse_FlagTag

Parameters

pkg/parser/parser_test.go:120-135
func TestParse_FlagTag(t *testing.T)

{
	node, err := Parse("app", &rootFlagTag{})
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}
	if len(node.Flags) != 1 {
		t.Fatalf("expected 1 flag, got %d", len(node.Flags))
	}
	flag, ok := node.Flags["verbose"]
	if !ok {
		t.Fatal("expected flag 'verbose'")
	}
	if flag.Short != "v" {
		t.Errorf("got short %q, want %q", flag.Short, "v")
	}
}
F
function

TestParse_PositionalArgs

Parameters

pkg/parser/parser_test.go:137-152
func TestParse_PositionalArgs(t *testing.T)

{
	node, err := Parse("add", &AddCmd{})
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}
	if len(node.Args) != 1 {
		t.Fatalf("expected 1 arg, got %d", len(node.Args))
	}
	arg := node.Args[0]
	if !arg.Required {
		t.Error("arg should be required")
	}
	if arg.IsGreedy {
		t.Error("arg should not be greedy")
	}
}
F
function

TestParse_Aliases

Parameters

pkg/parser/parser_test.go:154-178
func TestParse_Aliases(t *testing.T)

{
	node, err := Parse("app", &rootAliases{})
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}
	if len(node.Children) < 1 {
		t.Fatal("expected at least 1 child")
	}

	add, ok := node.Children["add"]
	if !ok {
		t.Fatal("expected child 'add'")
	}
	if len(add.Aliases) != 2 {
		t.Fatalf("expected 2 aliases, got %d", len(add.Aliases))
	}

	aChild, ok := node.Children["a"]
	if !ok {
		t.Fatal("expected alias 'a' to map to same node")
	}
	if aChild != add {
		t.Error("alias 'a' should point to same CommandNode as 'add'")
	}
}
F
function

TestParse_DynamicCommands

Parameters

pkg/parser/parser_test.go:180-209
func TestParse_DynamicCommands(t *testing.T)

{
	cmds := map[string]*WildcardCmd{
		"start": {Arg: "start-val"},
		"stop":  {Arg: "stop-val"},
	}
	root := &rootWildcard{Commands: cmds}
	node, err := Parse("app", root)
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}

	start, ok := node.Children["start"]
	if !ok {
		t.Fatal("expected dynamic child 'start'")
	}
	if start.Name != "start" {
		t.Errorf("got name %q, want %q", start.Name, "start")
	}

	stop, ok := node.Children["stop"]
	if !ok {
		t.Fatal("expected dynamic child 'stop'")
	}
	if stop.Name != "stop" {
		t.Errorf("got name %q, want %q", stop.Name, "stop")
	}
	if len(start.Args) != 1 {
		t.Fatalf("expected 1 arg in start child, got %d", len(start.Args))
	}
}
F
function

TestParse_NestedCommands

Parameters

pkg/parser/parser_test.go:211-231
func TestParse_NestedCommands(t *testing.T)

{
	node, err := Parse("app", &rootNested{})
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}
	config, ok := node.Children["config"]
	if !ok {
		t.Fatal("expected child 'config'")
	}
	show, ok := config.Children["show"]
	if !ok {
		t.Fatal("expected child 'show' under config")
	}
	if show.Description == "" {
		t.Error("expected non-empty description for show")
	}
	_, hasAll := show.Flags["all"]
	if !hasAll {
		t.Error("expected flag 'all' on show")
	}
}
F
function

TestParse_InternalIgnore

Parameters

pkg/parser/parser_test.go:233-244
func TestParse_InternalIgnore(t *testing.T)

{
	node, err := Parse("app", &rootInternal{})
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}
	if len(node.Flags) != 1 {
		t.Fatalf("expected 1 flag (verbose), got %d", len(node.Flags))
	}
	if node.Value.Type().Kind() != reflect.Struct {
		t.Fatal("expected struct value")
	}
}
F
function

TestParse_NonStructType

Parameters

pkg/parser/parser_test.go:246-254
func TestParse_NonStructType(t *testing.T)

{
	node, err := Parse("app", "string-root")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if node == nil {
		t.Fatal("expected non-nil node")
	}
}
F
function

TestParse_NilPointer

Parameters

pkg/parser/parser_test.go:256-262
func TestParse_NilPointer(t *testing.T)

{
	var cmd *AddCmd
	_, err := Parse("add", cmd)
	if err == nil {
		t.Error("expected error for nil pointer")
	}
}