From ff579dba448a3b8ec282c0c032df1aac513a8d6e Mon Sep 17 00:00:00 2001 From: Tristan Ancelet Date: Fri, 27 Feb 2026 14:47:12 -0600 Subject: [PATCH] Made change --- app.go | 174 +++++++++++++++++++++++++++++++++++ argtype.go | 15 ++++ command.go | 250 +++++++++++++++++++++++++++++++++++++++++++++++++++ context.go | 88 ++++++++++++++++++ flag.go | 175 ++++++++++++++++++++++++++++++++++++ go.mod | 5 ++ go.sum | 2 + matchtype.go | 12 +++ regex.go | 5 ++ templates.go | 40 +++++++++ utils.go | 28 ++++++ 11 files changed, 794 insertions(+) create mode 100644 app.go create mode 100644 argtype.go create mode 100644 command.go create mode 100644 context.go create mode 100644 flag.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 matchtype.go create mode 100644 regex.go create mode 100644 templates.go create mode 100644 utils.go diff --git a/app.go b/app.go new file mode 100644 index 0000000..9da3272 --- /dev/null +++ b/app.go @@ -0,0 +1,174 @@ +package parser + +import ( + "os" + "fmt" + "git.arcanium.tech/tristan/logging" +) + +type App struct { + Name string + Commands []*Command + Flags []*ArgFlag +} + +func (a *App) addRootCommand(cmd *Command){ + a.Commands = append(a.Commands, cmd) +} + +func (a *App) NewRootCommand(info CommandOptions) *Command { + cmd := &Command{ + Name: info.Name, + Description: info.Description, + } + a.addRootCommand( cmd ) + return cmd +} + +func (a *App) Help() { + appHelpTemplate.Execute(os.Stdout, a) +} + +func (a *App) getFlag(name string) (*ArgFlag, error) { + for _, flag := range a.Flags { + if flag.Matches(name) { + return flag, nil + } + } + return nil, fmt.Errorf("App(%s) : ERROR : there is no flag by name %s", a.Name, name) +} + +func (a *App) isCommand(name string) bool { + for _, command := range a.Commands { + if command.Name == name { + return true + } + } + return false +} + +func (a *App) getCommand(name string) (*Command, error) { + for _, command := range a.Commands { + if command.Name == name { + return command, nil + } + } + + return nil, fmt.Errorf("App(%s) : ERROR : No command named \"%s\"", a.Name, name) +} + +func (a *App) determineMatch(arg string) (matchType, error) { + if arg == "-h" || arg == "--help" || arg == "help" { + return helpMatch, nil + } + flag_match, err := isFlag(arg) + if err != nil { + return noMatch, err + } + + if flag_match { + return flagMatch, nil + } + + command := a.isCommand(arg) + if command == true { + return commandMatch, nil + } + + return noMatch, nil +} + +func (a *App) doChecks() error { + for _, command := range a.Commands{ + err := command.Check() + if err != nil { + return err + } + } + + for _, flag := range a.Flags { + err := flag.Check() + if err != nil { + return err + } + } + + return nil +} + +func (a *App) registerSelf(commands []*Command) { + for _, command := range commands { + command.App = a + if len(command.SubCommands) > 0 { + a.registerSelf(command.SubCommands) + } + } +} + + +func (a *App) Run(args []string) error { + logging.Debug("App(%s) : Beginning Run", a.Name) + a.registerSelf(a.Commands) + err := a.doChecks() + if err != nil { + return err + } + + if len(args) == 0 { + a.Help() + return nil + } + for i := 0; i < len(args); i++ { + arg := args[i] + logging.Debug("App(%s) : Iterating on provided args (i: %d, arg: %s)", a.Name, i, arg) + remaining_args := args[i+1:] + match, err := a.determineMatch(arg) + if err != nil { + return err + } + + switch match { + case noMatch: + return fmt.Errorf("No match found for Arg(%s) for App(%s)", arg, a.Name) + case helpMatch: + a.Help() + return nil + case flagMatch: + flag, err := a.getFlag(arg) + if err != nil { + return err + } + if flag.requireArg(){ + if len(args[i:]) <= 1 { + return fmt.Errorf("Command(%s) : ArgFlag(%s) : Required argument not found", a.Name, flag.Name) + } + arglist := args[i:i+2] + i++ + // want to call the flag with itself & the argument it wants + err = flag.CheckArg(arglist) + if err != nil { + return err + } + } else { + // Since we want to preserve that they were called at all. (particularly for BoolType) + flag.Args = args[i:i+1] + } + + if flag.OnMatch != nil { + err := flag.OnMatch() + if err != nil { + return err + } + } + + case commandMatch: + command, err := a.getCommand(arg) + if err != nil { + return err + } + return command.Run(remaining_args) + } + } + + return nil +} diff --git a/argtype.go b/argtype.go new file mode 100644 index 0000000..5ff22cc --- /dev/null +++ b/argtype.go @@ -0,0 +1,15 @@ +package parser + +type ArgType int + +const ( + NoneType ArgType = iota + StringType + IntType + BoolType + Float32Type + Float64Type + MatchType +) + + diff --git a/command.go b/command.go new file mode 100644 index 0000000..97a46d5 --- /dev/null +++ b/command.go @@ -0,0 +1,250 @@ +package parser + +import ( + "os" + "fmt" + "errors" + "git.arcanium.tech/tristan/go_logging" +) + + +type RunHook func(*Command) +type WorkFunc func(*Context) error + +var commands []*Command + +type CommandOptions struct { + Name string + Description string +} + +type Command struct { + Name string + Description string + SubCommands []*Command + Flags []*ArgFlag + App *App + Work WorkFunc + preRunHooks []RunHook + postRunHooks []RunHook +} + +func (c *Command) getFlag(name string) (*ArgFlag, error) { + for _, flag := range c.Flags { + if flag.Matches(name) { + return flag, nil + } + } + return nil, fmt.Errorf("Command(%s) : ERROR : there is no flag by name %s", c.Name, name) +} + +func (c *Command) Run(args []string) error { + if len(c.preRunHooks) > 0 { + for _, hook := range c.preRunHooks { + hook(c) + } + } + + err := c.run(args) + if err != nil { + return err + } + + if len(c.postRunHooks) > 0 { + for _, hook := range c.postRunHooks { + hook(c) + } + } + + return nil +} + +func (c *Command) isSubCommand(name string) (bool) { + for _, subcommand := range c.SubCommands { + if subcommand.Name == name { + return true + } + } + return false +} + +func (c *Command) getSubCommand(name string) (*Command, error) { + for _, subcommand := range c.SubCommands { + if subcommand.Name == name { + return subcommand, nil + } + } + + return nil, fmt.Errorf("ERROR : Command(%s) has no subcommand named %s", c.Name, name) +} + +func (c *Command) determineMatch(arg string) (matchType, error) { + if arg == "-h" || arg == "--help" || arg == "help" { + return helpMatch, nil + } + flag_match, err := isFlag(arg) + if err != nil { + return noMatch, err + } + + if flag_match { + _, err := c.getFlag(arg) + if err != nil { + return unknownFlagMatch, nil + } + return flagMatch, nil + } + + subcommand := c.isSubCommand(arg) + if subcommand == true { + return commandMatch, nil + } + + return noMatch, nil +} + +func (c *Command) run (args []string) error { + logging.Debug("Command(%s) : Entering Run", c.Name) + + if c.Work == nil && len(args) == 0 { + logging.Debug("Command(%s) : No arguments. Assuming help", c.Name) + c.Help() + return nil + } + + for i := 0; i < len(args); i++ { + arg := args[i] + + logging.Debug("Command(%s) : Iterating on provided args (i: %d, arg: %s)", c.Name, i, arg) + + match_type, err := c.determineMatch(arg) + if err != nil { + return err + } + + switch match_type { + case noMatch: + return fmt.Errorf("No match found for Arg(%s) for Command(%s)", arg, c.Name) + case helpMatch: + c.Help() + return nil + case unknownFlagMatch: + return fmt.Errorf("Command(%s) : ERROR : Unconfigured Flag(%s) detected", c.Name, arg) + case flagMatch: + flag, err := c.getFlag(arg) + if err != nil { + return err + } + if flag.requireArg(){ + if len(args[i:]) <= 1 { + return fmt.Errorf("Command(%s) : ArgFlag(%s) : Required argument not found", c.Name, flag.Name) + } + arglist := args[i:i+2] + i++ + // want to call the flag with itself & the argument it wants + err = flag.CheckArg(arglist) + if err != nil { + return err + } + } else { + // Since we want to preserve that they were called at all. (particularly for BoolType) + flag.Args = args[i:i+1] + } + + if flag.OnMatch != nil { + err := flag.OnMatch() + if err != nil { + return err + } + } + + case commandMatch: + remaining_args := args[i+1:] + subcommand, err := c.getSubCommand(arg) + if err != nil { + return err + } + return subcommand.Run(remaining_args) + default: + return nil + } + } + + if c.Work != nil { + flagmap := make(map[string]*ArgFlag) + for _, flag := range c.Flags { + flagmap[flag.Name] = flag + } + + ctx := Context{ + Command: c, + Flags: flagmap, + App: c.App, + Args: args, + } + return c.Work(&ctx) + } + return nil +} + +func (c *Command) Help () error { + commandHelpTemplate.Execute(os.Stdout, c) + return nil +} + +func (c *Command) Check() error { + if c.Name == "" { + return errors.New("ERROR : Command Name wasn't set") + } + + if c.Description == "" { + return fmt.Errorf("Command(%s) : ERROR : Description wasn't set", c.Name) + } + + if len(c.SubCommands) == 0 && c.Work == nil { + return fmt.Errorf("Command(%s) : ERROR : Command was configured with neither commands (SubCommands) nor a work function (Work)", c.Name) + } + + if len(c.SubCommands) > 0 && c.Work != nil { + return fmt.Errorf("Command(%s) : ERROR : Command was defined with both subcommands & a work function", c.Name) + } + + subcommands := make(map[string]bool) + for _, subcommand := range c.SubCommands { + err := subcommand.Check() + if err != nil { + return err + } + if _, exist := subcommands[subcommand.Name]; exist { + return fmt.Errorf("Command(%s) : SubCommand(%s) : SubCommand Name was found as duplicate", c.Name, subcommand.Name) + } + subcommands[subcommand.Name]=true + } + for _, flag := range c.Flags { + err := flag.Check() + if err != nil { + return err + } + } + + shorts := make(map[string]bool) + longs := make(map[string]bool) + names := make(map[string]bool) + for _, flag := range c.Flags { + if _, exist := shorts[flag.Short]; exist { + return fmt.Errorf("Command(%s) : ArgFlag(%s) : flag short duplicate found (%s)", c.Name, flag.Name, flag.Short) + } + shorts[flag.Short] = true + if _, exist := longs[flag.Long]; exist { + return fmt.Errorf("Command(%s) : ArgFlag(%s) : flag long duplicate found (%s)", c.Name, flag.Name, flag.Long) + } + longs[flag.Long] = true + if _, exist := names[flag.Name]; exist { + return fmt.Errorf("Command(%s) : ArgFlag(Long: %s) : flag name duplicate found (%s)", c.Name, flag.Long, flag.Name) + } + names[flag.Name] = true + } + + + return nil +} diff --git a/context.go b/context.go new file mode 100644 index 0000000..2b3771a --- /dev/null +++ b/context.go @@ -0,0 +1,88 @@ +package parser + +import "fmt" + +type Context struct { + Command *Command + App *App + Flags map[string]*ArgFlag + Args []string +} + +func (c *Context) GetFlag(name string) (*ArgFlag, error) { + flag, exists := c.Flags[name] + if ! exists { + return nil, fmt.Errorf("Context : ERROR : Flag(%s) not found", name) + } + return flag, nil +} + +func (c *Context) GetValueInt(name string) (int64, error){ + flag, err := c.GetFlag(name) + if err != nil { + return 0, err + } + + value, err := flag.GetValueInt() + if err != nil { + return 0, err + } + + return value, nil +} + +func (c *Context) GetValueBool(name string) (bool, error){ + flag, err := c.GetFlag(name) + if err != nil { + return false, err + } + + value, err := flag.GetValueBool() + if err != nil { + return false, err + } + + return value, nil +} + +func (c *Context) GetValueFloat32(name string) (float32, error){ + flag, err := c.GetFlag(name) + if err != nil { + return 0, err + } + + value, err := flag.GetValueFloat32() + if err != nil { + return 0.0, err + } + + return value, nil +} + +func (c *Context) GetValueFloat64(name string) (float64, error){ + flag, err := c.GetFlag(name) + if err != nil { + return 0.0, err + } + + value, err := flag.GetValueFloat64() + if err != nil { + return 0.0, err + } + + return value, nil +} + +func (c *Context) GetValueString(name string) (string, error){ + flag, err := c.GetFlag(name) + if err != nil { + return "", err + } + + value, err := flag.GetValueString() + if err != nil { + return "", err + } + + return value, nil +} diff --git a/flag.go b/flag.go new file mode 100644 index 0000000..c39eaa1 --- /dev/null +++ b/flag.go @@ -0,0 +1,175 @@ +package parser + +import ( + "fmt" + "strconv" + "errors" +) + +type ArgFlag struct { + Name string + Short string + Long string + Description string + Type ArgType + Arg string + Args []string + OnMatch func () error +} + +var flags = make(map[string]*ArgFlag) + +func (f *ArgFlag) requireArg() bool { + return f.Type != NoneType && f.Type != BoolType && f.Type != MatchType +} + +func (f *ArgFlag) invalid() bool { + return f.Name != "" +} + +func (f *ArgFlag) Matches(identifier string) bool { + if f.Name == identifier || f.Short == identifier || f.Long == identifier { + return true + } + return false +} + +func (f *ArgFlag) GetIndex() (int, error) { + for i, arg := range f.Args { + if arg == f.Short || arg == f.Long { + return i, nil + } + } + + return 0, fmt.Errorf("ERROR : Flag(%s) not found in arguments", f.Name) +} + +func (f *ArgFlag) GetValueInt() (int64, error) { + if f.Type != IntType { + return 0, fmt.Errorf("ERROR : Flag(%s) is not a IntType flag", f.symbol()) + } + + return strconv.ParseInt(f.Arg, 10, 64) +} + +func (f *ArgFlag) GetValueString() (string, error) { + if f.Type != StringType { + return "", fmt.Errorf("ERROR : Flag(%s) is not a StringType flag", f.Name) + } + + return f.Arg, nil +} + +func (f *ArgFlag) symbol() string { + index, err := f.GetIndex() + if err != nil { + return f.Name + } + + return f.Args[index] +} + +func (f *ArgFlag) GetValueFloat32() (float32, error) { + if f.Type != Float32Type { + return 0.0, fmt.Errorf("ERROR : Flag(%s) is not a Float32Type flag", f.symbol()) + } + + float, err := strconv.ParseFloat(f.Arg, 32) + + if err != nil { + return 0.0, err + } + + return float32(float), nil +} + +func (f *ArgFlag) GetValueFloat64() (float64, error) { + if f.Type != Float64Type { + return 0.0, fmt.Errorf("ERROR : Flag(%s) is not a Float64Type flag", f.symbol()) + } + return strconv.ParseFloat(f.Arg, 64) +} + +func (f *ArgFlag) GetValueBool() (bool, error) { + if f.Type != BoolType { + return false, fmt.Errorf("ERROR : Flag(%s) is not a BoolType flag", f.symbol()) + } + + _, err := f.GetIndex() + if err != nil { + return false, nil + } else { + return true, nil + } +} + +type CheckFunc func(string) (bool,error) + +func (f *ArgFlag) doCheck(arg string, check CheckFunc, error_string string) error { + is_true, err := check(arg) + if err != nil { + return err + } + if ! is_true { + return fmt.Errorf(error_string, f.symbol()) + } + return nil +} + +func (f *ArgFlag) CheckArg(args []string) error { + // Just checking if the argument provided to this flag is a valid one based on the ArgType + f.Args = args + if f.Type == NoneType { + return fmt.Errorf("ArgFlag(%s) : ERROR : Flag not configured with a type", f.symbol()) + } + + if len(args) < 2 { + return fmt.Errorf("ArgFlag(%s) : ERROR : Not enough arguments", f.Name) + } + + arg := args[1] + + switch f.Type { + case StringType: + is_flag, err := isFlag(arg) + if err != nil { + return err + } + if is_flag { + return fmt.Errorf("Flag(%s) : ERROR : Argument is a flag", f.symbol()) + } + case IntType: + err := f.doCheck(arg, isInt, "Flag(%s) : ERROR : Argument is not a valid number") + if err != nil { + return err + } + case Float32Type, Float64Type: + err := f.doCheck(arg, isFloat, "Flag(%s) : ERROR : Argument is not a valid float") + if err != nil { + return err + } + } + + f.Arg = arg + + return nil +} + +func (f *ArgFlag) Check() error { + if f.Name == "" { + return errors.New("ERROR : Flag was not configured with Name") + } + if f.Type == NoneType { + return fmt.Errorf("ArgFlag(%s) : ERROR : No type has been set", f.Name) + } + + if f.Description == "" { + return fmt.Errorf("ArgFlag(%s) : ERROR : No Description has been set", f.Name) + } + + if f.Short == "" && f.Long == "" { + return fmt.Errorf("ArgFlag(%s) : ERROR : No Short or Long flags were provided", f.Name) + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..268dbfc --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.arcanium.tech/tristan/go_parser + +go 1.25.7 + +require git.arcanium.tech/tristan/logging v0.0.0-20260227204521-e3cf814e9bbc // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..93b11f8 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +git.arcanium.tech/tristan/logging v0.0.0-20260227204521-e3cf814e9bbc h1:xYUrw+admjakl6rYrz5Rgmq/Q4vLa441pSwNaNQMfQs= +git.arcanium.tech/tristan/logging v0.0.0-20260227204521-e3cf814e9bbc/go.mod h1:EfwBbpW5osQp3nlbKhQRY/jogmh2yPW1Y+5/DgWxe0Q= diff --git a/matchtype.go b/matchtype.go new file mode 100644 index 0000000..8e2c3ad --- /dev/null +++ b/matchtype.go @@ -0,0 +1,12 @@ +package parser + +type matchType int +const ( + noMatch matchType = iota + flagMatch + unknownFlagMatch + commandMatch + helpMatch +) + + diff --git a/regex.go b/regex.go new file mode 100644 index 0000000..cc1c255 --- /dev/null +++ b/regex.go @@ -0,0 +1,5 @@ +package parser + +var flagRegex = "^-+[a-z-]+$" +var intRegex = "^[0-9]+$" +var floatRegex = "^[0-9]+(.[0-9]+)?$" diff --git a/templates.go b/templates.go new file mode 100644 index 0000000..f4e19f2 --- /dev/null +++ b/templates.go @@ -0,0 +1,40 @@ +package parser + +import "text/template" + +const commandHelpTemplateText = ` +App: {{.App.Name}} +Command Usage: {{.Name}} [OPTIONS] + +SubCommmands: +----------------------------------------------------------- + help : Show this help output +{{range .SubCommands}} {{.Name | printf "%-12s" }} : {{.Description}} +{{end}} + +Flags: +----------------------------------------------------------- +-h --help : Show this help output +{{range .Flags}}{{if .Short}}{{.Short}}{{end}} {{if .Long}}{{.Long}}{{end}} : {{.Description}} +{{end}} +` + +var commandHelpTemplate = template.Must(template.New("command_help").Parse(commandHelpTemplateText)) + +const appHelpTemplateText = ` +App Usage: {{.Name}} [OPTIONS] + +Commmands: +----------------------------------------------------------- + help : Show this help output +{{range .Commands}} {{.Name | printf "%-12s" }} : {{.Description}} +{{end}} + +Flags: +----------------------------------------------------------- +-h --help : Show this help output +{{range .Flags}}{{if .Short}}{{.Short}}{{end}}{{if .Long}} {{.Long}}{{end}} : {{.Description}} +{{end}} +` + +var appHelpTemplate = template.Must(template.New("app_help").Parse(appHelpTemplateText)) diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..1585c4f --- /dev/null +++ b/utils.go @@ -0,0 +1,28 @@ +package parser + +import "regexp" +func doCheck(pattern string, arg string) (bool, error){ + match, err := regexp.MatchString(pattern, arg) + if err != nil { + return false, err + } + + if match { + return true, nil + } else { + return false, nil + } +} + +func isFlag(arg string) (bool, error) { + return doCheck(flagRegex, arg) +} + + +func isInt(arg string) (bool, error) { + return doCheck(intRegex, arg) +} + +func isFloat(arg string) (bool, error) { + return doCheck(floatRegex, arg) +}