Files
2026-01-31 11:47:30 +00:00

236 lines
4.6 KiB
Go

package cmd
import (
"encoding"
"fmt"
"reflect"
"strconv"
"github.com/bitmagnet-io/bitmagnet/pkg/env"
)
type instance struct {
Spec
Command
parent *instance
values map[string][]any
cmdValue reflect.Value
reflectValues map[string]reflect.Value
args []string
index int
subcmds []Command
helpRequested bool
}
func (i *instance) Subcommands() []Command {
if i.subcmds != nil {
return i.subcmds
}
i.subcmds = i.Command.Subcommands()
if i.subcmds == nil {
i.subcmds = make([]Command, 0)
}
return i.subcmds
}
func (i *instance) run(env env.Env) error {
cmd := i.Command
outer:
for {
token, ok := i.nextToken()
if !ok {
break
}
switch token.tokenType {
case tokenTypeKeyValue:
p, ok := i.param(token.key)
if !ok {
return fmt.Errorf("%w: %s: %w: %s", ErrInvalidArgs, i.name, ErrUnknownParam, token.key)
}
if err := i.handleParamValue(p, token.value); err != nil {
return fmt.Errorf("%w: %s: %w", ErrInvalidArgs, i.name, err)
}
case tokenTypeKey:
if token.key == helpParam {
i.helpRequested = true
}
p, ok := i.param(token.key)
if !ok {
return fmt.Errorf("%w: %s: %w: %s", ErrInvalidArgs, i.name, ErrUnknownParam, token.key)
}
if len(i.values[p.Name]) > 0 && !p.Multiple {
return fmt.Errorf("%w: %s: %w: %s", ErrInvalidArgs, i.name, ErrRepeatedParam, p.Name)
}
var arg string
if p.Type != paramTypeBool {
arg, ok = i.nextArg()
if !ok {
return fmt.Errorf("%w: %s: %w: %s", ErrInvalidArgs, i.name, ErrMissingParamValue, p.Name)
}
}
if err := i.handleParamValue(p, arg); err != nil {
return fmt.Errorf("%w: %s: %w", ErrInvalidArgs, i.name, err)
}
case tokenTypePositional:
i.index--
break outer
default:
panic("unexpected token")
}
}
for _, p := range i.Params() {
if p.Default != "" {
if _, ok := i.values[p.Name]; !ok {
if err := i.handleParamValue(p, p.Default); err != nil {
return fmt.Errorf("%w: %s: %w", ErrInvalidArgs, i.name, err)
}
}
}
}
i.applyValues()
if err := cmd.Setup(env); err != nil {
return fmt.Errorf("%w: %s: %w: %w", ErrExecution, i.name, ErrSetup, err)
}
if token, ok := i.nextToken(); ok {
if token.tokenType == tokenTypePositional {
for _, subCmd := range i.Subcommands() {
subCmdSpec, err := introspect(subCmd)
if err != nil {
return err
}
if subCmdSpec.name != token.value {
continue
}
subInstance := subCmdSpec.newInstance(subCmd, i.args[i.index:])
subInstance.parent = i
return subInstance.run(env)
}
}
return fmt.Errorf("%w: %s: %w: %s", ErrInvalidArgs, i.name, ErrUnexpectedArgument, token.arg)
}
var err error
if i.helpRequested {
err = cmd.Help(env)
} else {
err = cmd.Run(env)
}
if err != nil {
return fmt.Errorf("%w: %s: %w", ErrExecution, i.name, err)
}
err = cmd.Teardown(env)
if err != nil {
return fmt.Errorf("%w: %s: %w: %w", ErrExecution, i.name, ErrTeardown, err)
}
return nil
}
func (i *instance) nextToken() (token, bool) {
arg, ok := i.nextArg()
if !ok {
return token{}, false
}
return parseToken(arg), true
}
func (i *instance) nextArg() (string, bool) {
if i.index >= len(i.args) {
return "", false
}
arg := i.args[i.index]
i.index++
return arg, true
}
func (i *instance) handleParamValue(param Param, strValue string) error {
var values []any
switch param.Type {
case paramTypeTextUnmarshaler:
v := i.reflectValues[param.Name]
ptr := reflect.New(v.Type())
ptr.Elem().Set(v)
unmarshaler := ptr.Interface().(encoding.TextUnmarshaler)
err := unmarshaler.UnmarshalText([]byte(strValue))
if err != nil {
return fmt.Errorf("%w: %s: %w", ErrParseParamValue, param.Name, err)
}
values = append(values, unmarshaler)
case paramTypeBool:
if strValue == "" {
values = append(values, true)
} else {
value, err := strconv.ParseBool(strValue)
if err != nil {
return fmt.Errorf("%w: %s: %w", ErrParseParamValue, param.Name, err)
}
values = append(values, value)
}
case paramTypeString:
values = append(values, strValue)
}
i.values[param.Name] = append(i.values[param.Name], values...)
return nil
}
func (i *instance) applyValues() {
for name, values := range i.values {
param := i.params[name]
var value any
switch param.Type {
case paramTypeBool:
value = values[0].(bool)
case paramTypeString:
value = values[0].(string)
case paramTypeTextUnmarshaler:
value = reflect.ValueOf(values[0]).Elem().Interface()
default:
panic("unknown param type")
}
if rv, ok := i.reflectValues[name]; ok {
rv.Set(reflect.ValueOf(value))
}
}
i.cmdValue.Set(reflect.ValueOf(Cmd{
instance: i,
}))
}