diff --git a/example/nonposix.yaml b/example/nonposix.yaml index de5bc1b..a974d08 100644 --- a/example/nonposix.yaml +++ b/example/nonposix.yaml @@ -2,7 +2,7 @@ name: nonposix description: flags: -opt, -optarg?: both nonposix - -styled=: nonpoxis shorthand + -styled=: nonposix shorthand -mx, --mixed*: mixed repeatable completion: flag: diff --git a/example_test.go b/example_test.go index 6ae623c..cc993e8 100644 --- a/example_test.go +++ b/example_test.go @@ -6,6 +6,8 @@ import ( "github.com/rsteube/carapace" "github.com/rsteube/carapace/pkg/sandbox" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" ) //go:embed example/runnable.yaml @@ -26,3 +28,13 @@ func TestRunnable(t *testing.T) { Tag("commands")) }) } + +func sandboxSpec(t *testing.T, spec string) (f func(func(s *sandbox.Sandbox))) { + var command Command + if err := yaml.Unmarshal([]byte(spec), &command); err != nil { + panic(err.Error()) + } + return sandbox.Command(t, func() *cobra.Command { + return command.ToCobra() + }) +} diff --git a/flag.go b/flag.go index 791be7d..e0d5399 100644 --- a/flag.go +++ b/flag.go @@ -9,15 +9,15 @@ import ( ) type flag struct { - longhand string - shorthand string - usage string - slice bool - optarg bool - value bool - nonposix bool - hidden bool - required bool + longhand string + shorthand string + usage string + slice bool + optarg bool + value bool + nameAsShorthand bool + hidden bool + required bool } func parseFlag(s, usage string) (*flag, error) { @@ -30,8 +30,8 @@ func parseFlag(s, usage string) (*flag, error) { f := &flag{} f.longhand = strings.TrimLeft(matches["longhand"], "-") - f.nonposix = matches["longhand"] != "" && !strings.HasPrefix(matches["longhand"], "--") f.shorthand = strings.TrimPrefix(matches["shorthand"], "-") + f.nameAsShorthand = (matches["longhand"] != "" && !strings.HasPrefix(matches["longhand"], "--")) f.usage = usage f.slice = strings.Contains(matches["modifier"], "*") f.optarg = strings.Contains(matches["modifier"], "?") @@ -57,57 +57,57 @@ func (f flag) addTo(fset *pflag.FlagSet) error { if f.longhand != "" && f.shorthand != "" { if f.value { if f.slice { - if !f.nonposix { - fs.StringSliceP(f.longhand, f.shorthand, []string{}, f.usage) - } else { + if f.nameAsShorthand { fs.StringSliceN(f.longhand, f.shorthand, []string{}, f.usage) + } else { + fs.StringSliceP(f.longhand, f.shorthand, []string{}, f.usage) } } else { - if !f.nonposix { - fs.StringP(f.longhand, f.shorthand, "", f.usage) - } else { + if f.nameAsShorthand { fs.StringN(f.longhand, f.shorthand, "", f.usage) + } else { + fs.StringP(f.longhand, f.shorthand, "", f.usage) } } } else { if f.slice { - if !f.nonposix { - fs.CountP(f.longhand, f.shorthand, f.usage) - } else { + if f.nameAsShorthand { fs.CountN(f.longhand, f.shorthand, f.usage) + } else { + fs.CountP(f.longhand, f.shorthand, f.usage) } } else { - if !f.nonposix { - fs.BoolP(f.longhand, f.shorthand, false, f.usage) - } else { + if f.nameAsShorthand { fs.BoolN(f.longhand, f.shorthand, false, f.usage) + } else { + fs.BoolP(f.longhand, f.shorthand, false, f.usage) } } } } else if f.longhand != "" { if f.value { if f.slice { - if !f.nonposix { - fs.StringSlice(f.longhand, []string{}, f.usage) - } else { + if f.nameAsShorthand { fs.StringSliceS(f.longhand, f.longhand, []string{}, f.usage) + } else { + fs.StringSlice(f.longhand, []string{}, f.usage) } } else { - if !f.nonposix { - fs.String(f.longhand, "", f.usage) - } else { + if f.nameAsShorthand { fs.StringS(f.longhand, f.longhand, "", f.usage) + } else { + fs.String(f.longhand, "", f.usage) } } } else { if f.slice { - if !f.nonposix { - fs.Count(f.longhand, f.usage) - } else { + if f.nameAsShorthand { fs.CountS(f.longhand, f.longhand, f.usage) + } else { + fs.Count(f.longhand, f.usage) } } else { - if !f.nonposix { + if !f.nameAsShorthand { fs.Bool(f.longhand, false, f.usage) } else { fs.BoolS(f.longhand, f.longhand, false, f.usage) diff --git a/flag_test.go b/flag_test.go index 6d4e93c..91d1a4e 100644 --- a/flag_test.go +++ b/flag_test.go @@ -107,15 +107,15 @@ func TestParseFlag(t *testing.T) { }) test("nonposix both", "-short, -long*", &flag{ - shorthand: "short", - longhand: "long", - slice: true, - nonposix: true, + shorthand: "short", + longhand: "long", + slice: true, + nameAsShorthand: true, }) test("nonposix mixed", "-short, --long", &flag{ - shorthand: "short", - longhand: "long", - nonposix: false, + shorthand: "short", + longhand: "long", + nameAsShorthand: false, }) } diff --git a/go.mod b/go.mod index 0205a2e..fa2c341 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/invopop/jsonschema v0.7.0 - github.com/rsteube/carapace v0.39.1 + github.com/rsteube/carapace v0.39.2 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index f950a88..56b0825 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,8 @@ github.com/invopop/jsonschema v0.7.0 h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy77 github.com/invopop/jsonschema v0.7.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rsteube/carapace v0.39.1 h1:ZEBhfRn5BvRyZkR8tdwvXP2/35ZMj6rZ/K2anCPbSFU= -github.com/rsteube/carapace v0.39.1/go.mod h1:jkLt41Ne2TD2xPuMdX/2O05Smhy8vMgG7O2TYvC0yOc= +github.com/rsteube/carapace v0.39.2 h1:Fy577GqW96r3l/mfcSJf022Ed3SUsn9gNRFu86KLO6Q= +github.com/rsteube/carapace v0.39.2/go.mod h1:jkLt41Ne2TD2xPuMdX/2O05Smhy8vMgG7O2TYvC0yOc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= diff --git a/internal/pflagfork/flag.go b/internal/pflagfork/flag.go new file mode 100644 index 0000000..ae4676c --- /dev/null +++ b/internal/pflagfork/flag.go @@ -0,0 +1,176 @@ +package pflagfork + +import ( + "fmt" + "reflect" + "strings" + + "github.com/rsteube/carapace/pkg/style" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// mode defines how flags are represented. +type mode int + +const ( + Default mode = iota // default behaviour + ShorthandOnly // only the shorthand should be used + NameAsShorthand // non-posix mode where the name is also added as shorthand (single `-` prefix) +) + +type Flag struct { + *pflag.Flag +} + +func (f Flag) Nargs() int { + if field := reflect.ValueOf(f.Flag).Elem().FieldByName("Nargs"); field.IsValid() && field.Kind() == reflect.Int { + return int(field.Int()) + } + return 0 +} + +func (f Flag) Mode() mode { + if field := reflect.ValueOf(f.Flag).Elem().FieldByName("Mode"); field.IsValid() && field.Kind() == reflect.Int { + return mode(field.Int()) + } + return Default +} + +func (f Flag) OptargDelimiter() rune { + if field := reflect.ValueOf(f.Flag).Elem().FieldByName("OptargDelimiter"); field.IsValid() && field.Kind() == reflect.Int32 { + return (rune(field.Int())) + } + return '=' +} + +func (f Flag) IsRepeatable() bool { + if strings.Contains(f.Value.Type(), "Slice") || + strings.Contains(f.Value.Type(), "Array") || + f.Value.Type() == "count" { + return true + } + return false +} + +func (f Flag) Split(arg string) (prefix, optarg string) { + delimiter := string(f.OptargDelimiter()) + splitted := strings.SplitN(arg, delimiter, 2) + return splitted[0] + delimiter, splitted[1] +} + +func (f Flag) Matches(arg string, posix bool) bool { + if !strings.HasPrefix(arg, "-") { // not a flag + return false + } + + switch { + + case strings.HasPrefix(arg, "--"): + name := strings.TrimPrefix(arg, "--") + name = strings.SplitN(name, string(f.OptargDelimiter()), 2)[0] + + switch f.Mode() { + case ShorthandOnly, NameAsShorthand: + return false + default: + return name == f.Name + } + + case !posix: + name := strings.TrimPrefix(arg, "-") + name = strings.SplitN(name, string(f.OptargDelimiter()), 2)[0] + + if name == "" { + return false + } + + switch f.Mode() { + case ShorthandOnly: + return name == f.Shorthand + default: + return name == f.Name || name == f.Shorthand + } + + default: + if f.Shorthand != "" { + return strings.HasSuffix(arg, f.Shorthand) + } + return false + } +} + +func (f Flag) TakesValue() bool { + switch f.Value.Type() { + case "bool", "boolSlice", "count": + return false + default: + return true + } +} + +func (f Flag) IsOptarg() bool { + return f.NoOptDefVal != "" +} + +func (f Flag) Style() string { + switch { + case !f.TakesValue(): + return style.Carapace.FlagNoArg + case f.IsOptarg(): + return style.Carapace.FlagOptArg + case f.Nargs() != 0: + return style.Carapace.FlagMultiArg + default: + return style.Carapace.FlagArg + } +} + +func (f Flag) Required() bool { + if annotation := f.Annotations[cobra.BashCompOneRequiredFlag]; len(annotation) == 1 && annotation[0] == "true" { + return true + } + return false +} + +func (f Flag) Definition() string { + var definition string + switch f.Mode() { + case ShorthandOnly: + definition = fmt.Sprintf("-%v", f.Shorthand) + case NameAsShorthand: + definition = fmt.Sprintf("-%v, -%v", f.Shorthand, f.Name) + default: + switch f.Shorthand { + case "": + definition = fmt.Sprintf("--%v", f.Name) + default: + definition = fmt.Sprintf("-%v, --%v", f.Shorthand, f.Name) + } + } + + if f.Hidden { + definition += "&" + } + + if f.Required() { + definition += "!" + } + + if f.IsRepeatable() { + definition += "*" + } + + switch { + case f.IsOptarg(): + switch f.Value.Type() { + case "bool", "boolSlice", "count": + default: + definition += "?" + } + case f.TakesValue(): + definition += "=" + } + + return definition +} diff --git a/scrape.go b/scrape.go index 442140b..bcfe85d 100644 --- a/scrape.go +++ b/scrape.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" + "github.com/rsteube/carapace-spec/internal/pflagfork" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -104,17 +105,27 @@ func scrape(cmd *cobra.Command, tmpDir string) { return } - isShortHand := f.Shorthand != "" - persistentPrefix := "" if cmd.PersistentFlags().Lookup(f.Name) != nil { persistentPrefix = "Persistent" } - if isShortHand { - fmt.Fprintf(out, ` %vCmd.%vFlags().%vP("%v", "%v", %v, "%v")`+"\n", cmdVarName(cmd), persistentPrefix, flagType(f), f.Name, f.Shorthand, flagValue(f), formatUsage(f.Usage)) - } else { - fmt.Fprintf(out, ` %vCmd.%vFlags().%v("%v", %v, "%v")`+"\n", cmdVarName(cmd), persistentPrefix, flagType(f), f.Name, flagValue(f), formatUsage(f.Usage)) + switch (pflagfork.Flag{Flag: f}).Mode() { + case pflagfork.ShorthandOnly: + fmt.Fprintf(out, ` %vCmd.%vFlags().%vS("%v", "%v", %v, "%v")`+"\n", cmdVarName(cmd), persistentPrefix, flagType(f), f.Name, f.Shorthand, flagValue(f), formatUsage(f.Usage)) + case pflagfork.NameAsShorthand: + fmt.Fprintf(out, ` %vCmd.%vFlags().%vN("%v", "%v", %v, "%v")`+"\n", cmdVarName(cmd), persistentPrefix, flagType(f), f.Name, f.Shorthand, flagValue(f), formatUsage(f.Usage)) + case pflagfork.Default: + switch { + case f.Shorthand != "" && f.Value.Type() == "count": + fmt.Fprintf(out, ` %vCmd.%vFlags().%vP("%v", "%v", "%v")`+"\n", cmdVarName(cmd), persistentPrefix, flagType(f), f.Name, f.Shorthand, formatUsage(f.Usage)) + case f.Shorthand != "" && f.Value.Type() != "count": + fmt.Fprintf(out, ` %vCmd.%vFlags().%vP("%v", "%v", %v, "%v")`+"\n", cmdVarName(cmd), persistentPrefix, flagType(f), f.Name, f.Shorthand, flagValue(f), formatUsage(f.Usage)) + case f.Value.Type() == "count": + fmt.Fprintf(out, ` %vCmd.%vFlags().%v("%v", "%v")`+"\n", cmdVarName(cmd), persistentPrefix, flagType(f), f.Name, formatUsage(f.Usage)) + default: + fmt.Fprintf(out, ` %vCmd.%vFlags().%v("%v", %v, "%v")`+"\n", cmdVarName(cmd), persistentPrefix, flagType(f), f.Name, flagValue(f), formatUsage(f.Usage)) + } } }) @@ -123,7 +134,7 @@ func scrape(cmd *cobra.Command, tmpDir string) { return } - if f.Value.Type() != "bool" && f.NoOptDefVal != "" { + if f.Value.Type() != "bool" && f.Value.Type() != "count" && f.NoOptDefVal != "" { fmt.Fprintf(out, ` %vCmd.Flag("%v").NoOptDefVal = "%v"`+"\n", cmdVarName(cmd), f.Name, f.NoOptDefVal) } diff --git a/spec_test.go b/spec_test.go index 01b5df6..927382c 100644 --- a/spec_test.go +++ b/spec_test.go @@ -1,13 +1,13 @@ package spec import ( - "bytes" _ "embed" - "strings" "testing" + "github.com/rsteube/carapace" + "github.com/rsteube/carapace/pkg/sandbox" + "github.com/rsteube/carapace/pkg/style" "github.com/spf13/pflag" - "gopkg.in/yaml.v3" ) //go:embed example/example.yaml @@ -16,103 +16,114 @@ var example string //go:embed example/nonposix.yaml var nonposix string -func TestSpec(t *testing.T) { - if out := execute(t, example, "example", "sub1", "--styled", ""); !strings.Contains(out, "cyan") { - t.Error(out) - } - - if out := execute(t, example, "example", "sub1", "--optarg="); !strings.Contains(out, "second") { - t.Error(out) - } - - if out := execute(t, example, "example", "sub1", "--list", "a,b,"); !strings.Contains(out, "a,b,c") { - t.Error(out) - } - - if out := execute(t, example, "example", "sub1", "--repeatable", "--repeatable", ""); !strings.Contains(out, "pos1A") { - t.Error(out) - } - - if out := execute(t, example, "example", "sub1", "--", ""); !strings.Contains(out, "dash1") { - t.Error(out) - } - - if out := execute(t, example, "example", "sub1", "--persistent", ""); !strings.Contains(out, "pos1") { - t.Error(out) - } - - if out := execute(t, example, "example", "sub1", "--env", "C_"); !strings.Contains(out, "C_VALUE=C_") { - t.Error(out) - } - - if out := execute(t, example, "example", "sub1", "--sty"); !strings.Contains(out, "--styled") { - t.Error(out) - } - - if out := execute(t, example, "example", "sub1", "--repeatable", "--sty"); strings.Contains(out, "--styled") { - t.Error(out) - } - - if out := execute(t, example, "example", "sub1", "", ""); !strings.Contains(out, "action.go") { - t.Error(out) - } - - if out := execute(t, example, "example", ""); !strings.Contains(out, "sub1") { - t.Error(out) - } +func TestPosix(t *testing.T) { + sandboxSpec(t, example)(func(s *sandbox.Sandbox) { + s.Run("sub1", "--styled", "c"). + Expect(carapace.ActionStyledValuesDescribed( + "cyan", "cyan", style.Cyan, + ).Usage("styled values")) + + s.Run("sub1", "--optarg="). + Expect(carapace.ActionValues( + "first", + "second", + "third", + ).Prefix("--optarg="). + Usage("optarg flag")) + + s.Run("sub1", "--list", "a,b,"). + Expect(carapace.ActionValues( + "a", + "b", + "c", + "d", + ).Prefix("a,b,"). + NoSpace(). + Usage("list flag")) + + s.Run("sub1", "--repeatable", "--repeatable", ""). + Expect(carapace.Batch( + carapace.ActionValues( + "pos1A", + "pos1B", + ), + carapace.ActionStyledValuesDescribed( + "subsub1", "sub sub command", style.Blue, + ).Tag("group3 commands"), + ).ToA()) + + s.Run("sub1", "--", ""). + Expect(carapace.ActionValues( + "dash1", + "dash2", + )) + + s.Run("sub1", "--persistent", "p"). + Expect(carapace.ActionValues( + "pos1A", + "pos1B", + )) + + s.Run("sub1", "--env", "C_"). + Expect(carapace.ActionValues( + "C_VALUE=C_", + ).Usage("env")) + + s.Run("sub1", "--sty"). + Expect(carapace.ActionValuesDescribed( + "--styled", "styled values", + ).NoSpace('.'). + Style(style.Carapace.FlagArg). + Tag("flags")) + + s.Run("hidden", ""). + Expect(carapace.ActionValues( + "hPos1", + "hPos2", + )) + + s.Run("hidden", "--hidden", ""). + Expect(carapace.ActionValues( + "first", + "second", + "third", + ).Usage("hidden flag")) + }) } -func TestSpecNonposix(t *testing.T) { +func skipNonFork(t *testing.T) { if fs := (flagSet{pflag.NewFlagSet("test", pflag.PanicOnError)}); !fs.IsFork() { t.Skip("skip nonposix tests with spf13/pflag") } - - if out := execute(t, nonposix, "nonposix", ""); !strings.Contains(out, "a") { - t.Error(out) - } - - if out := execute(t, nonposix, "nonposix", "-"); !strings.Contains(out, "-styled") { - t.Error(out) - } - - if out := execute(t, nonposix, "nonposix", "-mx", "--"); !strings.Contains(out, "--mixed") { - t.Error(out) - } - - if out := execute(t, nonposix, "nonposix", "-opt="); !strings.Contains(out, "1") { - t.Error(out) - } } -func TestHidden(t *testing.T) { - if out := execute(t, example, "example", ""); strings.Contains(out, "hidden") { - t.Error(out) - } - - if out := execute(t, example, "example", "hidden", ""); !strings.Contains(out, "hPos1") { - t.Error(out) - } - - if out := execute(t, example, "example", "hidden", "--hidden", ""); !strings.Contains(out, "first") { - t.Error(out) - } -} - -func execute(t *testing.T, spec string, args ...string) string { - var stdout, stderr bytes.Buffer - var c Command - if err := yaml.Unmarshal([]byte(spec), &c); err != nil { - t.Error(err.Error()) - } - cmd, err := c.ToCobraE() - if err != nil { - t.Error(err.Error()) - } - cmd.SetOut(&stdout) - cmd.SetErr(&stderr) - cmd.SetArgs(append([]string{"_carapace", "export"}, args...)) - if err := cmd.Execute(); err != nil { - t.Error(err.Error()) - } - return stdout.String() +func TestNonposix(t *testing.T) { + skipNonFork(t) + + sandboxSpec(t, nonposix)(func(s *sandbox.Sandbox) { + s.Run("a"). + Expect(carapace.ActionValues("a")) + + s.Run("-s"). + Expect(carapace.ActionValuesDescribed( + "-styled", "nonposix shorthand"). + NoSpace('.'). + Style(style.Carapace.FlagArg). + Tag("flags")) + + s.Run("--m"). + Expect(carapace.ActionValuesDescribed( + "--mixed", "mixed repeatable", + ).NoSpace('.'). + Style(style.Carapace.FlagNoArg). + Tag("flags")) + + s.Run("-opt="). + Expect(carapace.ActionValues( + "1", + "2", + "3", + ).Prefix("-opt="). + Usage("both nonposix")) + }) }