commit ff579dba448a3b8ec282c0c032df1aac513a8d6e Author: Tristan Ancelet Date: Fri Feb 27 14:47:12 2026 -0600 Made change 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) +}