From 8e59e88cb81e00d3ae2dcb4f6694c6506e004c4b Mon Sep 17 00:00:00 2001 From: John Guo Date: Wed, 27 Mar 2024 09:34:29 +0800 Subject: [PATCH] fix #3417 --- internal/command/command.go | 10 +- os/gcmd/gcmd.go | 6 +- os/gcmd/gcmd_command_object.go | 42 +++----- os/gcmd/gcmd_command_run.go | 22 ++-- os/gcmd/gcmd_parser.go | 21 +++- os/gcmd/gcmd_z_unit_issue_test.go | 161 ++++++++++++++++++++++++++++-- util/gconv/gconv_struct.go | 10 ++ 7 files changed, 211 insertions(+), 61 deletions(-) diff --git a/internal/command/command.go b/internal/command/command.go index 688201eb133..7cac390279b 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -17,7 +17,7 @@ import ( var ( defaultParsedArgs = make([]string, 0) defaultParsedOptions = make(map[string]string) - argumentRegex = regexp.MustCompile(`^\-{1,2}([\w\?\.\-]+)(=){0,1}(.*)$`) + argumentOptionRegex = regexp.MustCompile(`^\-{1,2}([\w\?\.\-]+)(=){0,1}(.*)$`) ) // Init does custom initialization. @@ -41,22 +41,22 @@ func ParseUsingDefaultAlgorithm(args ...string) (parsedArgs []string, parsedOpti parsedArgs = make([]string, 0) parsedOptions = make(map[string]string) for i := 0; i < len(args); { - array := argumentRegex.FindStringSubmatch(args[i]) + array := argumentOptionRegex.FindStringSubmatch(args[i]) if len(array) > 2 { if array[2] == "=" { parsedOptions[array[1]] = array[3] } else if i < len(args)-1 { if len(args[i+1]) > 0 && args[i+1][0] == '-' { - // Eg: gf gen -d -n 1 + // Example: gf gen -d -n 1 parsedOptions[array[1]] = array[3] } else { - // Eg: gf gen -n 2 + // Example: gf gen -n 2 parsedOptions[array[1]] = args[i+1] i += 2 continue } } else { - // Eg: gf gen -h + // Example: gf gen -h parsedOptions[array[1]] = array[3] } } else { diff --git a/os/gcmd/gcmd.go b/os/gcmd/gcmd.go index 41b7eff03a2..b73644e913e 100644 --- a/os/gcmd/gcmd.go +++ b/os/gcmd/gcmd.go @@ -18,9 +18,9 @@ import ( ) const ( - CtxKeyParser gctx.StrKey = `CtxKeyParser` - CtxKeyCommand gctx.StrKey = `CtxKeyCommand` - CtxKeyArguments gctx.StrKey = `CtxKeyArguments` + CtxKeyParser gctx.StrKey = `CtxKeyParser` + CtxKeyCommand gctx.StrKey = `CtxKeyCommand` + CtxKeyArgumentsIndex gctx.StrKey = `CtxKeyArgumentsIndex` ) const ( diff --git a/os/gcmd/gcmd_command_object.go b/os/gcmd/gcmd_command_object.go index 17ee1eecf00..43ecc4f9103 100644 --- a/os/gcmd/gcmd_command_object.go +++ b/os/gcmd/gcmd_command_object.go @@ -251,11 +251,8 @@ func newCommandFromMethod( return } - // Options creating. - var paramKeyToAttrMap map[string]string - if paramKeyToAttrMap, err = newTagToAttrMappingFromInput(inputObject.Interface()); err != nil { - return - } + // For input struct converting using priority tag. + var priorityTag = gstr.Join([]string{tagNameName, tagNameShort}, ",") // ============================================================================================= // Create function that has value return. @@ -265,9 +262,16 @@ func newCommandFromMethod( var ( data = gconv.Map(parser.GetOptAll()) argIndex = 0 - arguments = gconv.Strings(ctx.Value(CtxKeyArguments)) + arguments = parser.GetArgAll() inputValues = []reflect.Value{reflect.ValueOf(ctx)} ) + if value := ctx.Value(CtxKeyArgumentsIndex); value != nil { + argIndex = value.(int) + // Use the left args to assign to input struct object. + if argIndex < len(arguments) { + arguments = arguments[argIndex:] + } + } if data == nil { data = map[string]interface{}{} } @@ -310,9 +314,9 @@ func newCommandFromMethod( return fmt.Sprintf(`input command data map: %s`, gjson.MustEncode(data)) }) if inputObject.Kind() == reflect.Ptr { - err = gconv.Scan(data, inputObject.Interface(), paramKeyToAttrMap) + err = gconv.StructTag(data, inputObject.Interface(), priorityTag) } else { - err = gconv.Struct(data, inputObject.Addr().Interface(), paramKeyToAttrMap) + err = gconv.StructTag(data, inputObject.Addr().Interface(), priorityTag) } intlog.PrintFunc(ctx, func() string { return fmt.Sprintf(`input object assigned data: %s`, gjson.MustEncode(inputObject.Interface())) @@ -342,28 +346,6 @@ func newCommandFromMethod( return } -func newTagToAttrMappingFromInput(object interface{}) (paramKeyToAttrMap map[string]string, err error) { - var fields []gstructs.Field - fields, err = gstructs.Fields(gstructs.FieldsInput{ - Pointer: object, - RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag, - }) - paramKeyToAttrMap = make(map[string]string) - for _, field := range fields { - var ( - nameValue = field.Tag(tagNameName) - shortValue = field.Tag(tagNameShort) - ) - if nameValue != "" { - paramKeyToAttrMap[nameValue] = field.Name() - } - if shortValue != "" { - paramKeyToAttrMap[shortValue] = field.Name() - } - } - return -} - func newArgumentsFromInput(object interface{}) (args []Argument, err error) { var ( fields []gstructs.Field diff --git a/os/gcmd/gcmd_command_run.go b/os/gcmd/gcmd_command_run.go index 499da83dfee..80e29842ded 100644 --- a/os/gcmd/gcmd_command_run.go +++ b/os/gcmd/gcmd_command_run.go @@ -93,7 +93,7 @@ func (c *Command) RunWithSpecificArgs(ctx context.Context, args []string) (value parsedArgs = parsedArgs[1:] // Find the matched command and run it. - lastCmd, foundCmd, newCtx := c.searchCommand(ctx, parsedArgs) + lastCmd, foundCmd, newCtx := c.searchCommand(ctx, parsedArgs, 0) if foundCmd != nil { return foundCmd.doRun(newCtx, args, parser) } @@ -215,25 +215,27 @@ func (c *Command) reParse(ctx context.Context, args []string, parser *Parser) (* } // searchCommand recursively searches the command according given arguments. -func (c *Command) searchCommand(ctx context.Context, args []string) (lastCmd, foundCmd *Command, newCtx context.Context) { +func (c *Command) searchCommand( + ctx context.Context, args []string, fromArgIndex int, +) (lastCmd, foundCmd *Command, newCtx context.Context) { if len(args) == 0 { return c, nil, ctx } for _, cmd := range c.commands { // Recursively searching the command. + // String comparison case-sensitive. if cmd.Name == args[0] { leftArgs := args[1:] // If this command needs argument, - // it then gives all its left arguments to it. - if cmd.hasArgumentFromIndex() { - ctx = context.WithValue(ctx, CtxKeyArguments, leftArgs) + // it then gives all its left arguments to it using arg index marks. + // + // Note that the args here (using default args parsing) could be different with the args + // that are parsed in command. + if cmd.hasArgumentFromIndex() || len(leftArgs) == 0 { + ctx = context.WithValue(ctx, CtxKeyArgumentsIndex, fromArgIndex+1) return c, cmd, ctx } - // Recursively searching. - if len(leftArgs) == 0 { - return c, cmd, ctx - } - return cmd.searchCommand(ctx, leftArgs) + return cmd.searchCommand(ctx, leftArgs, fromArgIndex+1) } } return c, nil, ctx diff --git a/os/gcmd/gcmd_parser.go b/os/gcmd/gcmd_parser.go index 49cbcca8502..2dcf9479e05 100644 --- a/os/gcmd/gcmd_parser.go +++ b/os/gcmd/gcmd_parser.go @@ -185,11 +185,24 @@ func (p *Parser) isOptionNeedArgument(name string) bool { // setOptionValue sets the option value for name and according alias. func (p *Parser) setOptionValue(name, value string) { + // Accurate option name match. for optionName := range p.passedOptions { - array := gstr.SplitAndTrim(optionName, ",") - for _, v := range array { - if strings.EqualFold(v, name) { - for _, v := range array { + optionNameAndShort := gstr.SplitAndTrim(optionName, ",") + for _, optionNameItem := range optionNameAndShort { + if optionNameItem == name { + for _, v := range optionNameAndShort { + p.parsedOptions[v] = value + } + return + } + } + } + // Fuzzy option name match. + for optionName := range p.passedOptions { + optionNameAndShort := gstr.SplitAndTrim(optionName, ",") + for _, optionNameItem := range optionNameAndShort { + if strings.EqualFold(optionNameItem, name) { + for _, v := range optionNameAndShort { p.parsedOptions[v] = value } return diff --git a/os/gcmd/gcmd_z_unit_issue_test.go b/os/gcmd/gcmd_z_unit_issue_test.go index c3f9814f479..88ba6714aee 100644 --- a/os/gcmd/gcmd_z_unit_issue_test.go +++ b/os/gcmd/gcmd_z_unit_issue_test.go @@ -45,12 +45,12 @@ func (c Issue3390TestCase1) Index(ctx context.Context, in Issue3390Case1Input) ( } func Test_Issue3390_Case1(t *testing.T) { + root, err := gcmd.NewFromObject(Issue3390TestCase1{}) + if err != nil { + panic(err) + } + command := &Issue3390CommandCase1{root} gtest.C(t, func(t *gtest.T) { - root, err := gcmd.NewFromObject(Issue3390TestCase1{}) - if err != nil { - panic(err) - } - command := &Issue3390CommandCase1{root} value, err := command.RunWithSpecificArgs( gctx.New(), []string{"main", "-a", "aaa", "-b", "bbb"}, @@ -85,17 +85,160 @@ func (c Issue3390TestCase2) Index(ctx context.Context, in Issue3390Case2Input) ( return } func Test_Issue3390_Case2(t *testing.T) { + root, err := gcmd.NewFromObject(Issue3390TestCase2{}) + if err != nil { + panic(err) + } + command := &Issue3390CommandCase2{root} + gtest.C(t, func(t *gtest.T) { + value, err := command.RunWithSpecificArgs( + gctx.New(), + []string{"main", "-a", "aaa", "-b", "bbb"}, + ) + t.AssertNil(err) + t.Assert(value.(*Issue3390Case2Output).Content, `{"A":"bbb","Be":"aaa"}`) + }) +} + +type Issue3390CommandCase3 struct { + *gcmd.Command +} + +type Issue3390TestCase3 struct { + g.Meta `name:"index" ad:"test"` +} + +type Issue3390Case3Input struct { + g.Meta `name:"index"` + A string `short:"b"` + Be string `short:"a"` +} + +type Issue3390Case3Output struct { + Content string +} + +func (c Issue3390TestCase3) Index(ctx context.Context, in Issue3390Case3Input) (out *Issue3390Case3Output, err error) { + out = &Issue3390Case3Output{ + Content: gjson.MustEncodeString(in), + } + return +} +func Test_Issue3390_Case3(t *testing.T) { + root, err := gcmd.NewFromObject(Issue3390TestCase3{}) + if err != nil { + panic(err) + } + command := &Issue3390CommandCase3{root} gtest.C(t, func(t *gtest.T) { - root, err := gcmd.NewFromObject(Issue3390TestCase2{}) + value, err := command.RunWithSpecificArgs( + gctx.New(), + []string{"main", "-a", "aaa", "-b", "bbb"}, + ) + t.AssertNil(err) + t.Assert(value.(*Issue3390Case3Output).Content, `{"A":"bbb","Be":"aaa"}`) + }) +} + +type Issue3390CommandCase4 struct { + *gcmd.Command +} + +type Issue3390TestCase4 struct { + g.Meta `name:"index" ad:"test"` +} + +type Issue3390Case4Input struct { + g.Meta `name:"index"` + A string `short:"a"` + Be string `short:"b"` +} + +type Issue3390Case4Output struct { + Content string +} + +func (c Issue3390TestCase4) Index(ctx context.Context, in Issue3390Case4Input) (out *Issue3390Case4Output, err error) { + out = &Issue3390Case4Output{ + Content: gjson.MustEncodeString(in), + } + return +} + +func Test_Issue3390_Case4(t *testing.T) { + root, err := gcmd.NewFromObject(Issue3390TestCase4{}) + if err != nil { + panic(err) + } + command := &Issue3390CommandCase4{root} + gtest.C(t, func(t *gtest.T) { + value, err := command.RunWithSpecificArgs( + gctx.New(), + []string{"main", "-a", "aaa", "-b", "bbb"}, + ) + t.AssertNil(err) + t.Assert(value.(*Issue3390Case4Output).Content, `{"A":"aaa","Be":"bbb"}`) + }) +} + +type Issue3417Test struct { + g.Meta `name:"root"` +} + +type Issue3417BuildInput struct { + g.Meta `name:"build" config:"gfcli.build"` + File string `name:"FILE" arg:"true" brief:"building file path"` + Name string `short:"n" name:"name" brief:"output binary name"` + Version string `short:"v" name:"version" brief:"output binary version"` + Arch string `short:"a" name:"arch" brief:"output binary architecture, multiple arch separated with ','"` + System string `short:"s" name:"system" brief:"output binary system, multiple os separated with ','"` + Output string `short:"o" name:"output" brief:"output binary path, used when building single binary file"` + Path string `short:"p" name:"path" brief:"output binary directory path, default is '.'" d:"."` + Extra string `short:"e" name:"extra" brief:"extra custom \"go build\" options"` + Mod string `short:"m" name:"mod" brief:"like \"-mod\" option of \"go build\", use \"-m none\" to disable go module"` + Cgo bool `short:"c" name:"cgo" brief:"enable or disable cgo feature, it's disabled in default" orphan:"true"` + VarMap g.Map `short:"r" name:"varMap" brief:"custom built embedded variable into binary"` + PackSrc string `short:"ps" name:"packSrc" brief:"pack one or more folders into one go file before building"` + PackDst string `short:"pd" name:"packDst" brief:"temporary go file path for pack, this go file will be automatically removed after built" d:"internal/packed/build_pack_data.go"` + ExitWhenError bool `short:"ew" name:"exitWhenError" brief:"exit building when any error occurs, specially for multiple arch and system buildings. default is false" orphan:"true"` + DumpENV bool `short:"de" name:"dumpEnv" brief:"dump current go build environment before building binary" orphan:"true"` +} + +type Issue3417BuildOutput struct { + Content string +} + +func (c *Issue3417Test) Build(ctx context.Context, in Issue3417BuildInput) (out *Issue3417BuildOutput, err error) { + out = &Issue3417BuildOutput{ + Content: gjson.MustEncodeString(in), + } + return +} + +func Test_Issue3417(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + command, err := gcmd.NewFromObject(Issue3417Test{}) if err != nil { panic(err) } - command := &Issue3390CommandCase2{root} value, err := command.RunWithSpecificArgs( gctx.New(), - []string{"main", "-a", "aaa", "-b", "bbb"}, + []string{ + "gf", "build", + "-mod", "vendor", + "-v", "0.0.19", + "-n", "detect_hardware_os", + "-a", "amd64,arm64", + "-s", "linux", + "-p", "./bin", + "-e", "-trimpath -ldflags", + "cmd/v3/main.go", + }, ) t.AssertNil(err) - t.Assert(value.(*Issue3390Case2Output).Content, `{"A":"bbb","Be":"aaa"}`) + t.Assert( + value.(*Issue3417BuildOutput).Content, + `{"File":"cmd/v3/main.go","Name":"detect_hardware_os","Version":"0.0.19","Arch":"amd64,arm64","System":"linux","Output":"","Path":"./bin","Extra":"-trimpath -ldflags","Mod":"vendor","Cgo":false,"VarMap":null,"PackSrc":"","PackDst":"internal/packed/build_pack_data.go","ExitWhenError":false,"DumpENV":false}`, + ) }) } diff --git a/util/gconv/gconv_struct.go b/util/gconv/gconv_struct.go index adb4533758b..a1136ccc060 100644 --- a/util/gconv/gconv_struct.go +++ b/util/gconv/gconv_struct.go @@ -241,6 +241,16 @@ func doStruct( } // To convert value base on custom parameter key to attribute name map. + if priorityTag != "" { + if paramKeyToAttrMap == nil { + paramKeyToAttrMap = make(map[string]string) + } + if len(tagToAttrNameMap) > 0 { + for k, v := range tagToAttrNameMap { + paramKeyToAttrMap[k] = v + } + } + } err = doStructBaseOnParamKeyToAttrMap( pointerElemReflectValue, paramsMap,