2024-10-24 19:00:12 +08:00
|
|
|
package command
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
2024-11-16 15:15:27 +08:00
|
|
|
"fmt"
|
|
|
|
"os"
|
2024-10-24 19:00:12 +08:00
|
|
|
"reflect"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2024-11-16 15:15:27 +08:00
|
|
|
"text/template"
|
2024-10-24 19:00:12 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
type Option struct {
|
|
|
|
Name string
|
|
|
|
LongName string
|
|
|
|
ShortName string
|
|
|
|
Description string
|
|
|
|
OType string
|
|
|
|
DefaultValue interface{}
|
|
|
|
}
|
2024-11-16 15:15:27 +08:00
|
|
|
|
|
|
|
var UsageTemplate, _ = template.New("Usage").Parse(`Usage:
|
|
|
|
{{if .ParentCommand }} {{.ParentCommand.Name}} {{end}} {{.Name}} [OPTIONS] [ARGS]
|
|
|
|
{{ if .Description }}
|
|
|
|
Description:
|
|
|
|
{{.Description}}
|
|
|
|
{{ end }}
|
|
|
|
{{ if .Args }}
|
|
|
|
Options:
|
|
|
|
{{range .Args}}
|
|
|
|
-{{.ShortName}}|--{{.LongName}} {{.OType}} {{.Description}} default:{{.DefaultValue}}
|
|
|
|
{{end}}
|
|
|
|
{{ end }}
|
|
|
|
{{ if .SubCommands }}
|
|
|
|
Sub Commands:
|
|
|
|
{{range .SubCommands}}
|
|
|
|
{{.Name}} {{.Description}}
|
|
|
|
{{end}}
|
|
|
|
{{end}}
|
|
|
|
`)
|
|
|
|
|
2024-10-24 19:00:12 +08:00
|
|
|
type Command struct {
|
|
|
|
Name string
|
|
|
|
Description string
|
|
|
|
Args []*Option
|
|
|
|
SubCommands []*Command
|
2024-11-16 15:15:27 +08:00
|
|
|
ParentCommand *Command
|
2024-10-24 19:00:12 +08:00
|
|
|
subcommand string
|
|
|
|
globalOptions map[string]interface{}
|
|
|
|
subcommandOptions map[string]interface{}
|
2024-11-16 15:15:27 +08:00
|
|
|
remainArgs []string
|
2024-10-24 19:00:12 +08:00
|
|
|
}
|
|
|
|
|
2024-11-16 15:15:27 +08:00
|
|
|
func (c *Command) GetGlobalOptions() map[string]interface{} {
|
|
|
|
return c.globalOptions
|
|
|
|
}
|
|
|
|
func (c *Command) GetSubCommandOptions() map[string]interface{} {
|
|
|
|
return c.subcommandOptions
|
|
|
|
}
|
|
|
|
func (c *Command) GetSubCommand() string {
|
|
|
|
return c.subcommand
|
|
|
|
}
|
2024-10-24 19:00:12 +08:00
|
|
|
func (c *Command) AddArg(name string, shortName string, description string, defaultValue interface{}) {
|
|
|
|
oType := reflect.TypeOf(defaultValue).String()
|
|
|
|
c.Args = append(c.Args, &Option{
|
|
|
|
Name: name,
|
|
|
|
LongName: ToKebabCase(name),
|
|
|
|
ShortName: shortName,
|
|
|
|
Description: description,
|
|
|
|
OType: oType,
|
|
|
|
DefaultValue: defaultValue,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Command) AddSubCommand(name string, description string) *Command {
|
2024-11-16 15:15:27 +08:00
|
|
|
if c.ParentCommand != nil {
|
2024-10-24 19:00:12 +08:00
|
|
|
panic("Sub commands can only be added to top level commands")
|
|
|
|
}
|
|
|
|
command := &Command{
|
|
|
|
Name: name,
|
|
|
|
Description: description,
|
2024-11-16 15:15:27 +08:00
|
|
|
ParentCommand: c,
|
2024-10-24 19:00:12 +08:00
|
|
|
}
|
|
|
|
c.SubCommands = append(c.SubCommands, command)
|
|
|
|
return command
|
|
|
|
}
|
|
|
|
func (c *Command) Usage() {
|
2024-11-16 15:15:27 +08:00
|
|
|
UsageTemplate.Execute(os.Stdout, c)
|
2024-10-24 19:00:12 +08:00
|
|
|
}
|
|
|
|
func (c *Command) GetOption(name string) *Option {
|
|
|
|
if len(name) > 1 {
|
|
|
|
for _, arg := range c.Args {
|
|
|
|
if arg.LongName == name {
|
|
|
|
return arg
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, arg := range c.Args {
|
|
|
|
if arg.ShortName == name {
|
|
|
|
return arg
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2024-11-16 15:15:27 +08:00
|
|
|
func (c *Command) parseError(errMsg string) {
|
|
|
|
fmt.Println(errMsg)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
func (c *Command) showHelpWithOption(args []string) {
|
|
|
|
if len(args) > 0 && (args[0] == "-h" || args[0] == "--help") {
|
|
|
|
c.Usage()
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
func (c *Command) showSubCommandHelp(args []string) {
|
|
|
|
if len(args) == 2 && args[0] == "help" {
|
|
|
|
cmd := c.findSubcommand(args[1])
|
|
|
|
if cmd == nil {
|
|
|
|
c.parseError("Unknown subcommand " + args[1])
|
|
|
|
}
|
|
|
|
cmd.Usage()
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
func (c *Command) Parse(args []string) ([]string, error) {
|
|
|
|
c.showHelpWithOption(args)
|
|
|
|
c.showSubCommandHelp(args)
|
2024-10-24 19:00:12 +08:00
|
|
|
if len(args) == 0 {
|
2024-11-16 15:15:27 +08:00
|
|
|
if len(c.Args) != 0 || len(c.SubCommands) != 0 || c.ParentCommand == nil {
|
2024-10-24 19:00:12 +08:00
|
|
|
c.Usage()
|
|
|
|
}
|
2024-11-16 15:15:27 +08:00
|
|
|
if c.ParentCommand != nil {
|
|
|
|
c.ParentCommand.subcommand = c.Name
|
2024-10-24 19:00:12 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-16 15:15:27 +08:00
|
|
|
vargs := args
|
|
|
|
for {
|
|
|
|
if len(vargs) == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
var parsed bool
|
|
|
|
var err error
|
|
|
|
vargs, parsed, err = c.longOption(vargs)
|
|
|
|
if err != nil {
|
|
|
|
c.parseError(err.Error())
|
|
|
|
}
|
|
|
|
if parsed {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
vargs, parsed, err = c.shortOption(vargs)
|
|
|
|
if err != nil {
|
|
|
|
c.parseError(err.Error())
|
|
|
|
}
|
|
|
|
if parsed {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if len(c.SubCommands) > 0 {
|
|
|
|
cmd := c.findSubcommand(args[0])
|
|
|
|
if cmd != nil {
|
|
|
|
c.subcommand = cmd.Name
|
|
|
|
vargs = vargs[1:]
|
|
|
|
vargs, err = cmd.Parse(vargs)
|
|
|
|
if err != nil {
|
|
|
|
c.parseError(err.Error())
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
c.parseError("Unknown subcommand " + args[0])
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if c.ParentCommand != nil {
|
|
|
|
c.ParentCommand.remainArgs = append(c.ParentCommand.remainArgs, vargs[0])
|
|
|
|
|
|
|
|
} else {
|
|
|
|
c.remainArgs = append(c.remainArgs, vargs[0])
|
|
|
|
}
|
|
|
|
vargs = vargs[1:]
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return vargs, nil
|
|
|
|
}
|
|
|
|
func (c *Command) findSubcommand(cmd string) *Command {
|
|
|
|
for _, subCmd := range c.SubCommands {
|
|
|
|
if subCmd.Name == cmd {
|
|
|
|
return subCmd
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
2024-10-24 19:00:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Command) longOption(args []string) ([]string, bool, error) {
|
|
|
|
if !strings.HasPrefix(args[0], "--") {
|
|
|
|
return args, false, nil
|
|
|
|
}
|
|
|
|
optName := strings.TrimPrefix(args[0], "--")
|
|
|
|
if len(optName) < 2 {
|
|
|
|
return args, false, errors.New("Invalid option name")
|
|
|
|
}
|
|
|
|
opt := c.GetOption(optName)
|
|
|
|
if opt != nil {
|
|
|
|
rargs, _, err := c.getOptValue(args, opt, false)
|
|
|
|
if err == nil {
|
|
|
|
return rargs, true, nil
|
|
|
|
} else {
|
|
|
|
return args, false, err
|
|
|
|
}
|
|
|
|
}
|
2024-11-16 15:15:27 +08:00
|
|
|
if c.ParentCommand != nil {
|
|
|
|
return c.ParentCommand.longOption(args)
|
2024-10-24 19:00:12 +08:00
|
|
|
}
|
|
|
|
return args, false, errors.New("Unknown option " + args[0])
|
|
|
|
}
|
|
|
|
func (c *Command) shortOption(args []string) ([]string, bool, error) {
|
|
|
|
if !strings.HasPrefix(args[0], "-") {
|
|
|
|
return args, false, nil
|
|
|
|
}
|
|
|
|
optName := strings.TrimPrefix(args[0], "-")
|
|
|
|
last := len(optName) - 1
|
|
|
|
for i, s := range optName {
|
|
|
|
opt := c.GetOption(string(s))
|
|
|
|
if opt != nil {
|
|
|
|
if i != last {
|
|
|
|
_, _, err := c.getOptValue(args[i:], opt, true)
|
|
|
|
if err == nil {
|
|
|
|
return args, true, nil
|
|
|
|
} else {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
rargs, _, err := c.getOptValue(args[i:], opt, false)
|
|
|
|
if err == nil {
|
|
|
|
return rargs, true, nil
|
|
|
|
} else {
|
|
|
|
return args, false, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
2024-11-16 15:15:27 +08:00
|
|
|
if c.ParentCommand != nil {
|
|
|
|
opt = c.ParentCommand.GetOption(string(s))
|
2024-10-24 19:00:12 +08:00
|
|
|
if opt != nil {
|
2024-11-16 15:15:27 +08:00
|
|
|
rargs, _, err := c.ParentCommand.getOptValue(args[i:], opt, i != last)
|
2024-10-24 19:00:12 +08:00
|
|
|
if err == nil {
|
|
|
|
return rargs, true, nil
|
|
|
|
} else {
|
|
|
|
return args, false, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return args, false, errors.New("Unknown option " + args[0])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return args, false, errors.New("Unknown option " + args[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Command) getOptValue(args []string, opt *Option, isFixBool bool) ([]string, interface{}, error) {
|
|
|
|
|
|
|
|
paramMap := c.globalOptions
|
2024-11-16 15:15:27 +08:00
|
|
|
if c.ParentCommand != nil {
|
|
|
|
paramMap = c.ParentCommand.subcommandOptions
|
2024-10-24 19:00:12 +08:00
|
|
|
}
|
|
|
|
if isFixBool {
|
|
|
|
if opt.OType != "bool" {
|
|
|
|
return args, nil, errors.New("Invalid type for boolean option")
|
|
|
|
}
|
|
|
|
paramMap[opt.Name] = true
|
|
|
|
return args, true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if opt.OType == "bool" {
|
|
|
|
nextStr := strings.ToLower(args[1])
|
|
|
|
if nextStr == "f" || nextStr == "false" {
|
|
|
|
paramMap[opt.Name] = false
|
|
|
|
return args[2:], true, nil
|
|
|
|
} else if nextStr == "t" || nextStr == "true" {
|
|
|
|
paramMap[opt.Name] = true
|
|
|
|
return args[2:], true, nil
|
|
|
|
} else {
|
|
|
|
paramMap[opt.Name] = true
|
|
|
|
return args[1:], true, nil
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
valStr := args[1]
|
|
|
|
switch opt.OType {
|
|
|
|
case "int":
|
|
|
|
val, err := strconv.Atoi(valStr)
|
|
|
|
if err != nil {
|
|
|
|
return args, nil, errors.New("Invalid value for option " + opt.Name)
|
|
|
|
} else {
|
|
|
|
paramMap[opt.Name] = val
|
|
|
|
return args[2:], true, nil
|
|
|
|
}
|
|
|
|
case "string":
|
|
|
|
paramMap[opt.Name] = valStr
|
|
|
|
return args[2:], true, nil
|
|
|
|
case "float64":
|
|
|
|
val, err := strconv.ParseFloat(valStr, 64)
|
|
|
|
if err != nil {
|
|
|
|
return args, nil, errors.New("Invalid value for option " + opt.Name)
|
|
|
|
} else {
|
|
|
|
paramMap[opt.Name] = val
|
|
|
|
return args[2:], true, nil
|
|
|
|
}
|
|
|
|
case "default":
|
2024-11-16 15:15:27 +08:00
|
|
|
return args, nil, errors.New("unsupported type")
|
2024-10-24 19:00:12 +08:00
|
|
|
}
|
|
|
|
}
|
2024-11-16 15:15:27 +08:00
|
|
|
return args, nil, errors.New("unsupported type")
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewCommand(name string, desc string) *Command {
|
|
|
|
return &Command{
|
|
|
|
Name: name,
|
|
|
|
Description: desc,
|
|
|
|
globalOptions: map[string]interface{}{},
|
|
|
|
subcommandOptions: map[string]interface{}{},
|
|
|
|
remainArgs: []string{},
|
|
|
|
}
|
2024-10-24 19:00:12 +08:00
|
|
|
}
|