diff --git a/os/gcmd/gcmd.go b/os/gcmd/gcmd.go index bac250a06..60d2b2468 100644 --- a/os/gcmd/gcmd.go +++ b/os/gcmd/gcmd.go @@ -18,8 +18,9 @@ import ( ) const ( - CtxKeyParser gctx.StrKey = `CtxKeyParser` - CtxKeyCommand gctx.StrKey = `CtxKeyCommand` + CtxKeyParser gctx.StrKey = `CtxKeyParser` + CtxKeyCommand gctx.StrKey = `CtxKeyCommand` + CtxKeyArguments gctx.StrKey = `CtxKeyArguments` ) const ( diff --git a/os/gcmd/gcmd_command.go b/os/gcmd/gcmd_command.go index 259a1058d..50ac66b98 100644 --- a/os/gcmd/gcmd_command.go +++ b/os/gcmd/gcmd_command.go @@ -20,13 +20,12 @@ type Command struct { Usage string // A brief line description about its usage, eg: gf build main.go [OPTION] Brief string // A brief info that describes what this command will do. Description string // A detailed description. - Options []Option // Option array, configuring how this command act. + Arguments []Argument // Argument array, configuring how this command act. Func Function // Custom function. FuncWithValue FuncWithValue // Custom function with output parameters that can interact with command caller. HelpFunc Function // Custom help function Examples string // Usage examples. Additional string // Additional info about this command, which will be appended to the end of help info. - NeedArgs bool // NeedArgs specifies this command needs arguments. Strict bool // Strict parsing options, which means it returns error if invalid option given. Config string // Config node name, which also retrieves the values from config component along with command line. parent *Command // Parent command for internal usage. @@ -39,18 +38,18 @@ type Function func(ctx context.Context, parser *Parser) (err error) // FuncWithValue is similar like Func but with output parameters that can interact with command caller. type FuncWithValue func(ctx context.Context, parser *Parser) (out interface{}, err error) -// Option is the command value that is specified by a name or shor name. -// An Option can have or have no value bound to it. -type Option struct { +// Argument is the command value that are used by certain command. +type Argument struct { Name string // Option name. Short string // Option short. Brief string // Brief info about this Option, which is used in help info. + IsArg bool // IsArg marks this argument taking value from command line argument instead of option. Orphan bool // Whether this Option having or having no value bound to it. } var ( // defaultHelpOption is the default help option that will be automatically added to each command. - defaultHelpOption = Option{ + defaultHelpOption = Argument{ Name: `help`, Short: `h`, Brief: `more information about this command`, diff --git a/os/gcmd/gcmd_command_help.go b/os/gcmd/gcmd_command_help.go index d1a983d97..69196f822 100644 --- a/os/gcmd/gcmd_command_help.go +++ b/os/gcmd/gcmd_command_help.go @@ -18,14 +18,14 @@ import ( // Print prints help info to stdout for current command. func (c *Command) Print() { var ( - prefix = gstr.Repeat(" ", 4) - buffer = bytes.NewBuffer(nil) - options = make([]Option, len(c.Options)) + prefix = gstr.Repeat(" ", 4) + buffer = bytes.NewBuffer(nil) + arguments = make([]Argument, len(c.Arguments)) ) // Copy options for printing. - copy(options, c.Options) + copy(arguments, c.Arguments) // Add built-in help option, just for info only. - options = append(options, defaultHelpOption) + arguments = append(arguments, defaultHelpOption) // Usage. if c.Usage != "" || c.Name != "" { @@ -42,7 +42,11 @@ func (c *Command) Print() { name = p.parent.Name + " " + name p = p.parent } - buffer.WriteString(fmt.Sprintf(`%s ARGUMENT [OPTION]`, name)) + if c.hasArgumentFromIndex() { + buffer.WriteString(fmt.Sprintf(`%s ARGUMENT [OPTION]`, name)) + } else { + buffer.WriteString(fmt.Sprintf(`%s [OPTION]`, name)) + } } buffer.WriteString("\n\n") } @@ -73,32 +77,67 @@ func (c *Command) Print() { buffer.WriteString("\n") } + // Argument. + if len(arguments) > 0 { + buffer.WriteString("ARGUMENT\n") + var ( + maxSpaceLength = 0 + ) + for _, arg := range arguments { + if !arg.IsArg { + continue + } + if len(arg.Name) > maxSpaceLength { + maxSpaceLength = len(arg.Name) + } + } + for _, arg := range arguments { + if !arg.IsArg { + continue + } + var ( + spaceLength = maxSpaceLength - len(arg.Name) + lineStr = fmt.Sprintf("%s%s%s%s\n", prefix, arg.Name, gstr.Repeat(" ", spaceLength+4), arg.Brief) + wordwrapPrefix = gstr.Repeat(" ", len(prefix+arg.Name)+spaceLength+4) + ) + lineStr = gstr.WordWrap(lineStr, maxLineChars, "\n"+wordwrapPrefix) + buffer.WriteString(lineStr) + } + buffer.WriteString("\n") + } + // Option. - if len(options) > 0 { + if len(arguments) > 0 { buffer.WriteString("OPTION\n") var ( nameStr string maxSpaceLength = 0 ) - for _, option := range options { - if option.Short != "" { - nameStr = fmt.Sprintf("-%s,\t--%s", option.Short, option.Name) + for _, arg := range arguments { + if arg.IsArg { + continue + } + if arg.Short != "" { + nameStr = fmt.Sprintf("-%s,\t--%s", arg.Short, arg.Name) } else { - nameStr = fmt.Sprintf("-/--%s", option.Name) + nameStr = fmt.Sprintf("-/--%s", arg.Name) } if len(nameStr) > maxSpaceLength { maxSpaceLength = len(nameStr) } } - for _, option := range options { - if option.Short != "" { - nameStr = fmt.Sprintf("-%s,\t--%s", option.Short, option.Name) + for _, arg := range arguments { + if arg.IsArg { + continue + } + if arg.Short != "" { + nameStr = fmt.Sprintf("-%s,\t--%s", arg.Short, arg.Name) } else { - nameStr = fmt.Sprintf("-/--%s", option.Name) + nameStr = fmt.Sprintf("-/--%s", arg.Name) } var ( spaceLength = maxSpaceLength - len(nameStr) - lineStr = fmt.Sprintf("%s%s%s%s\n", prefix, nameStr, gstr.Repeat(" ", spaceLength+4), option.Brief) + lineStr = fmt.Sprintf("%s%s%s%s\n", prefix, nameStr, gstr.Repeat(" ", spaceLength+4), arg.Brief) wordwrapPrefix = gstr.Repeat(" ", len(prefix+nameStr)+spaceLength+4) ) lineStr = gstr.WordWrap(lineStr, maxLineChars, "\n"+wordwrapPrefix) diff --git a/os/gcmd/gcmd_command_object.go b/os/gcmd/gcmd_command_object.go index af4282b21..fa3a98f5a 100644 --- a/os/gcmd/gcmd_command_object.go +++ b/os/gcmd/gcmd_command_object.go @@ -27,7 +27,7 @@ const ( tagNameDc = `dc` tagNameAd = `ad` tagNameEg = `eg` - tagNameArgs = `args` + tagNameArg = `arg` tagNameRoot = `root` ) @@ -80,8 +80,8 @@ func NewFromObject(object interface{}) (rootCmd *Command, err error) { if rootCmd.FuncWithValue == nil { rootCmd.FuncWithValue = methodCommand.FuncWithValue } - if len(rootCmd.Options) == 0 { - rootCmd.Options = methodCommand.Options + if len(rootCmd.Arguments) == 0 { + rootCmd.Arguments = methodCommand.Arguments } } else { subCommands = append(subCommands, methodCommand) @@ -115,9 +115,6 @@ func newCommandFromObjectMeta(object interface{}) (command *Command, err error) ) return } - if !command.NeedArgs { - command.NeedArgs = gconv.Bool(metaData[tagNameArgs]) - } if command.Description == "" { command.Description = metaData[tagNameDc] } @@ -201,11 +198,13 @@ func newCommandFromMethod(object interface{}, method reflect.Value) (command *Co } // Options creating. - if command.Options, err = newOptionsFromInput(inputObject.Interface()); err != nil { + if command.Arguments, err = newArgumentsFromInput(inputObject.Interface()); err != nil { return } + // ============================================================================================= // Create function that has value return. + // ============================================================================================= command.FuncWithValue = func(ctx context.Context, parser *Parser) (out interface{}, err error) { ctx = context.WithValue(ctx, CtxKeyParser, parser) @@ -221,15 +220,26 @@ func newCommandFromMethod(object interface{}, method reflect.Value) (command *Co var ( data = gconv.Map(parser.GetOptAll()) + argIndex = 0 + arguments = gconv.Strings(ctx.Value(CtxKeyArguments)) inputValues = []reflect.Value{reflect.ValueOf(ctx)} ) if data == nil { data = map[string]interface{}{} } // Handle orphan options. - for _, option := range command.Options { - if option.Orphan && parser.ContainsOpt(option.Name) { - data[option.Name] = "true" + for _, arg := range command.Arguments { + if arg.IsArg { + // Read argument from command line index. + if argIndex < len(arguments) { + data[arg.Name] = arguments[argIndex] + argIndex++ + } + } else { + // Read argument from command line option name. + if arg.Orphan && parser.ContainsOpt(arg.Name) { + data[arg.Name] = "true" + } } } // Default values from struct tag. @@ -268,7 +278,7 @@ func newCommandFromMethod(object interface{}, method reflect.Value) (command *Co return } -func newOptionsFromInput(object interface{}) (options []Option, err error) { +func newArgumentsFromInput(object interface{}) (args []Argument, err error) { var ( fields []gstructs.Field ) @@ -278,28 +288,31 @@ func newOptionsFromInput(object interface{}) (options []Option, err error) { }) for _, field := range fields { var ( - option = Option{} + arg = Argument{} metaData = field.TagMap() ) - if err = gconv.Scan(metaData, &option); err != nil { + if err = gconv.Scan(metaData, &arg); err != nil { return nil, err } - if option.Name == "" { - option.Name = field.Name() + if arg.Name == "" { + arg.Name = field.Name() } - if option.Name == helpOptionName { + if arg.Name == helpOptionName { return nil, gerror.Newf( `option name "%s" is already token by built-in options`, - option.Name, + arg.Name, ) } - if option.Short == helpOptionNameShort { + if arg.Short == helpOptionNameShort { return nil, gerror.Newf( `short option name "%s" is already token by built-in options`, - option.Short, + arg.Short, ) } - options = append(options, option) + if v, ok := metaData[tagNameArg]; ok { + arg.IsArg = gconv.Bool(v) + } + args = append(args, arg) } return } diff --git a/os/gcmd/gcmd_command_run.go b/os/gcmd/gcmd_command_run.go index 8cc3bbb2d..888bf4e5e 100644 --- a/os/gcmd/gcmd_command_run.go +++ b/os/gcmd/gcmd_command_run.go @@ -40,8 +40,8 @@ func (c *Command) RunWithValue(ctx context.Context) (value interface{}, err erro args = args[1:] // Find the matched command and run it. - if subCommand := c.searchCommand(args); subCommand != nil { - return subCommand.doRun(ctx, parser) + if subCommand, newCtx := c.searchCommand(ctx, args); subCommand != nil { + return subCommand.doRun(newCtx, parser) } // Print error and help command if no command found. @@ -86,20 +86,23 @@ func (c *Command) doRun(ctx context.Context, parser *Parser) (value interface{}, // reParse re-parses the arguments using option configuration of current command. func (c *Command) reParse(ctx context.Context, parser *Parser) (*Parser, error) { - if len(c.Options) == 0 { + if len(c.Arguments) == 0 { return parser, nil } var ( optionKey string supportedOptions = make(map[string]bool) ) - for _, option := range c.Options { - if option.Short != "" { - optionKey = fmt.Sprintf(`%s,%s`, option.Name, option.Short) - } else { - optionKey = option.Name + for _, arg := range c.Arguments { + if arg.IsArg { + continue } - supportedOptions[optionKey] = !option.Orphan + if arg.Short != "" { + optionKey = fmt.Sprintf(`%s,%s`, arg.Name, arg.Short) + } else { + optionKey = arg.Name + } + supportedOptions[optionKey] = !arg.Orphan } parser, err := Parse(supportedOptions, c.Strict) if err != nil { @@ -128,24 +131,36 @@ func (c *Command) reParse(ctx context.Context, parser *Parser) (*Parser, error) } // searchCommand recursively searches the command according given arguments. -func (c *Command) searchCommand(args []string) *Command { +func (c *Command) searchCommand(ctx context.Context, args []string) (*Command, context.Context) { if len(args) == 0 { - return nil + return nil, ctx } for _, cmd := range c.commands { - // If this command needs argument, - // it then gives all its left arguments to it. - if cmd.NeedArgs { - return cmd - } + // Recursively searching the command. if cmd.Name == args[0] { leftArgs := args[1:] - if len(leftArgs) == 0 { - return cmd + // If this command needs argument, + // it then gives all its left arguments to it. + if cmd.hasArgumentFromIndex() { + ctx = context.WithValue(ctx, CtxKeyArguments, leftArgs) + return cmd, ctx } - return cmd.searchCommand(leftArgs) + // Recursively searching. + if len(leftArgs) == 0 { + return cmd, ctx + } + return cmd.searchCommand(ctx, leftArgs) } } - return nil + return nil, ctx +} + +func (c *Command) hasArgumentFromIndex() bool { + for _, arg := range c.Arguments { + if arg.IsArg { + return true + } + } + return false } diff --git a/os/gcmd/gcmd_z_unit_feature_object1_test.go b/os/gcmd/gcmd_z_unit_feature_object1_test.go index 61e41021f..c6c62bd7d 100644 --- a/os/gcmd/gcmd_z_unit_feature_object1_test.go +++ b/os/gcmd/gcmd_z_unit_feature_object1_test.go @@ -150,7 +150,9 @@ type TestObjectForNeedArgsEnvInput struct { type TestObjectForNeedArgsEnvOutput struct{} type TestObjectForNeedArgsTestInput struct { - g.Meta `name:"test" args:"true"` + g.Meta `name:"test"` + Arg1 string `arg:"true" brief:"arg1 for test command"` + Arg2 string `arg:"true" brief:"arg2 for test command"` Name string `v:"required" short:"n" orphan:"false" brief:"name for test command"` } type TestObjectForNeedArgsTestOutput struct { @@ -162,9 +164,8 @@ func (TestObjectForNeedArgs) Env(ctx context.Context, in TestObjectForNeedArgsEn } func (TestObjectForNeedArgs) Test(ctx context.Context, in TestObjectForNeedArgsTestInput) (out *TestObjectForNeedArgsTestOutput, err error) { - parser := gcmd.ParserFromCtx(ctx) out = &TestObjectForNeedArgsTestOutput{ - Args: parser.GetArgAll(), + Args: []string{in.Arg1, in.Arg2, in.Name}, } return } @@ -177,9 +178,13 @@ func Test_Command_NeedArgs(t *testing.T) { cmd, err := gcmd.NewFromObject(TestObjectForNeedArgs{}) t.AssertNil(err) + //os.Args = []string{"root", "test", "a", "b", "c", "-h"} + //value, err := cmd.RunWithValue(ctx) + //t.AssertNil(err) + os.Args = []string{"root", "test", "a", "b", "c", "-n=john"} value, err := cmd.RunWithValue(ctx) t.AssertNil(err) - t.Assert(value, `{"Args":["root","test","a","b","c"]}`) + t.Assert(value, `{"Args":["a","b","john"]}`) }) } diff --git a/os/gcmd/gcmd_z_unit_test.go b/os/gcmd/gcmd_z_unit_test.go index e3589c764..7117bdef0 100644 --- a/os/gcmd/gcmd_z_unit_test.go +++ b/os/gcmd/gcmd_z_unit_test.go @@ -99,7 +99,7 @@ gf get github.com/gogf/gf@latest gf get github.com/gogf/gf@master gf get golang.org/x/sys `, - Options: []gcmd.Option{ + Arguments: []gcmd.Argument{ { Name: "my-option", Short: "o",