diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 1e4a51f..0a3e7c3 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -46,6 +46,9 @@ jobs: - name: test run: go run internal/build/build.go test + - name: test urfave-cli-genflags + run: make -C cmd/urfave-cli-genflags + - name: check-binary-size run: go run internal/build/build.go check-binary-size diff --git a/.gitignore b/.gitignore index c04fcc5..4296f6c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ internal/*/built-example coverage.txt /.local/ /site/ +/cmd/urfave-cli-genflags/urfave-cli-genflags *.exe diff --git a/cli.go b/cli.go index 2a11c5a..c0c5d9a 100644 --- a/cli.go +++ b/cli.go @@ -1,23 +1,25 @@ // Package cli provides a minimal framework for creating and organizing command line // Go applications. cli is designed to be easy to understand and write, the most simple // cli application can be written as follows: -// func main() { -// (&cli.App{}).Run(os.Args) -// } +// +// func main() { +// (&cli.App{}).Run(os.Args) +// } // // Of course this application does not do much, so let's make this an actual application: -// func main() { -// app := &cli.App{ -// Name: "greet", -// Usage: "say a greeting", -// Action: func(c *cli.Context) error { -// fmt.Println("Greetings") -// return nil -// }, -// } // -// app.Run(os.Args) -// } +// func main() { +// app := &cli.App{ +// Name: "greet", +// Usage: "say a greeting", +// Action: func(c *cli.Context) error { +// fmt.Println("Greetings") +// return nil +// }, +// } +// +// app.Run(os.Args) +// } package cli -//go:generate go run internal/genflags/cmd/genflags/main.go +//go:generate go run cmd/urfave-cli-genflags/main.go diff --git a/cmd/urfave-cli-genflags/Makefile b/cmd/urfave-cli-genflags/Makefile new file mode 100644 index 0000000..3b11415 --- /dev/null +++ b/cmd/urfave-cli-genflags/Makefile @@ -0,0 +1,21 @@ +GOTEST_FLAGS ?= -v --coverprofile main.coverprofile --covermode count --cover github.com/urfave/cli/v2/cmd/urfave-cli-genflags +GOBUILD_FLAGS ?= -x + +.PHONY: all +all: test build smoke-test + +.PHONY: test +test: + go test $(GOTEST_FLAGS) ./... + +.PHONY: build +build: + go build $(GOBUILD_FLAGS) ./... + +.PHONY: smoke-test +smoke-test: build + ./urfave-cli-genflags --help + +.PHONY: show-cover +show-cover: + go tool cover -func main.coverprofile diff --git a/cmd/urfave-cli-genflags/README.md b/cmd/urfave-cli-genflags/README.md new file mode 100644 index 0000000..9424234 --- /dev/null +++ b/cmd/urfave-cli-genflags/README.md @@ -0,0 +1,15 @@ +# urfave-cli-genflags + +This is a tool that is used internally by [urfave/cli] to generate +flag types and methods from a YAML input. It intentionally pins +usage of `github.com/urfave/cli/v2` to a *release* rather than +using the adjacent code so that changes don't result in *this* tool +refusing to compile. It's almost like dogfooding? + +## support warning + +This tool is maintained as a sub-project and is not covered by the +API and backward compatibility guaranteed by releases of +[urfave/cli]. + +[urfave/cli]: https://github.com/urfave/cli diff --git a/internal/genflags/generated.gotmpl b/cmd/urfave-cli-genflags/generated.gotmpl similarity index 100% rename from internal/genflags/generated.gotmpl rename to cmd/urfave-cli-genflags/generated.gotmpl diff --git a/internal/genflags/generated_test.gotmpl b/cmd/urfave-cli-genflags/generated_test.gotmpl similarity index 100% rename from internal/genflags/generated_test.gotmpl rename to cmd/urfave-cli-genflags/generated_test.gotmpl diff --git a/cmd/urfave-cli-genflags/go.mod b/cmd/urfave-cli-genflags/go.mod new file mode 100644 index 0000000..af40aaa --- /dev/null +++ b/cmd/urfave-cli-genflags/go.mod @@ -0,0 +1,15 @@ +module github.com/urfave/cli/v2/cmd/urfave-cli-genflags + +go 1.18 + +require ( + github.com/urfave/cli/v2 v2.11.2 + golang.org/x/text v0.3.7 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect +) diff --git a/cmd/urfave-cli-genflags/go.sum b/cmd/urfave-cli-genflags/go.sum new file mode 100644 index 0000000..e59916d --- /dev/null +++ b/cmd/urfave-cli-genflags/go.sum @@ -0,0 +1,14 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.11.2 h1:FVfNg4m3vbjbBpLYxW//WjxUoHvJ9TlppXcqY9Q9ZfA= +github.com/urfave/cli/v2 v2.11.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/genflags/cmd/genflags/main.go b/cmd/urfave-cli-genflags/main.go similarity index 53% rename from internal/genflags/cmd/genflags/main.go rename to cmd/urfave-cli-genflags/main.go index 4212e60..835216a 100644 --- a/internal/genflags/cmd/genflags/main.go +++ b/cmd/urfave-cli-genflags/main.go @@ -9,12 +9,14 @@ import ( "os/exec" "os/signal" "path/filepath" + "sort" "strings" "syscall" "text/template" "github.com/urfave/cli/v2" - "github.com/urfave/cli/v2/internal/genflags" + "golang.org/x/text/cases" + "golang.org/x/text/language" "gopkg.in/yaml.v3" ) @@ -22,6 +24,16 @@ const ( defaultPackageName = "cli" ) +var ( + //go:embed generated.gotmpl + TemplateString string + + //go:embed generated_test.gotmpl + TestTemplateString string + + titler = cases.Title(language.Und, cases.NoLower) +) + func sh(ctx context.Context, exe string, args ...string) (string, error) { cmd := exec.CommandContext(ctx, exe, args...) cmd.Stderr = os.Stderr @@ -92,7 +104,7 @@ func runGenFlags(cCtx *cli.Context) error { return err } - spec := &genflags.Spec{} + spec := &Spec{} if err := yaml.Unmarshal(specBytes, spec); err != nil { return err } @@ -123,12 +135,12 @@ func runGenFlags(cCtx *cli.Context) error { spec.UrfaveCLITestNamespace = "cli." } - genTmpl, err := template.New("gen").Parse(genflags.TemplateString) + genTmpl, err := template.New("gen").Parse(TemplateString) if err != nil { return err } - genTestTmpl, err := template.New("gen_test").Parse(genflags.TestTemplateString) + genTestTmpl, err := template.New("gen_test").Parse(TestTemplateString) if err != nil { return err } @@ -161,3 +173,120 @@ func runGenFlags(cCtx *cli.Context) error { return nil } + +func TypeName(goType string, fc *FlagTypeConfig) string { + if fc != nil && strings.TrimSpace(fc.TypeName) != "" { + return strings.TrimSpace(fc.TypeName) + } + + dotSplit := strings.Split(goType, ".") + goType = dotSplit[len(dotSplit)-1] + + if strings.HasPrefix(goType, "[]") { + return titler.String(strings.TrimPrefix(goType, "[]")) + "SliceFlag" + } + + return titler.String(goType) + "Flag" +} + +type Spec struct { + FlagTypes map[string]*FlagTypeConfig `yaml:"flag_types"` + PackageName string `yaml:"package_name"` + TestPackageName string `yaml:"test_package_name"` + UrfaveCLINamespace string `yaml:"urfave_cli_namespace"` + UrfaveCLITestNamespace string `yaml:"urfave_cli_test_namespace"` +} + +func (gfs *Spec) SortedFlagTypes() []*FlagType { + typeNames := []string{} + + for name := range gfs.FlagTypes { + if strings.HasPrefix(name, "[]") { + name = strings.TrimPrefix(name, "[]") + "Slice" + } + + typeNames = append(typeNames, name) + } + + sort.Strings(typeNames) + + ret := make([]*FlagType, len(typeNames)) + + for i, typeName := range typeNames { + ret[i] = &FlagType{ + GoType: typeName, + Config: gfs.FlagTypes[typeName], + } + } + + return ret +} + +type FlagTypeConfig struct { + SkipInterfaces []string `yaml:"skip_interfaces"` + StructFields []*FlagStructField `yaml:"struct_fields"` + TypeName string `yaml:"type_name"` + ValuePointer bool `yaml:"value_pointer"` +} + +type FlagStructField struct { + Name string + Type string +} + +type FlagType struct { + GoType string + Config *FlagTypeConfig +} + +func (ft *FlagType) StructFields() []*FlagStructField { + if ft.Config == nil || ft.Config.StructFields == nil { + return []*FlagStructField{} + } + + return ft.Config.StructFields +} + +func (ft *FlagType) ValuePointer() bool { + if ft.Config == nil { + return false + } + + return ft.Config.ValuePointer +} + +func (ft *FlagType) TypeName() string { + return TypeName(ft.GoType, ft.Config) +} + +func (ft *FlagType) GenerateFmtStringerInterface() bool { + return ft.skipInterfaceNamed("fmt.Stringer") +} + +func (ft *FlagType) GenerateFlagInterface() bool { + return ft.skipInterfaceNamed("Flag") +} + +func (ft *FlagType) GenerateRequiredFlagInterface() bool { + return ft.skipInterfaceNamed("RequiredFlag") +} + +func (ft *FlagType) GenerateVisibleFlagInterface() bool { + return ft.skipInterfaceNamed("VisibleFlag") +} + +func (ft *FlagType) skipInterfaceNamed(name string) bool { + if ft.Config == nil { + return true + } + + lowName := strings.ToLower(name) + + for _, interfaceName := range ft.Config.SkipInterfaces { + if strings.ToLower(interfaceName) == lowName { + return false + } + } + + return true +} diff --git a/internal/genflags/spec_test.go b/cmd/urfave-cli-genflags/main_test.go similarity index 52% rename from internal/genflags/spec_test.go rename to cmd/urfave-cli-genflags/main_test.go index 25a9c8b..b5c9fee 100644 --- a/internal/genflags/spec_test.go +++ b/cmd/urfave-cli-genflags/main_test.go @@ -1,29 +1,63 @@ -package genflags_test +package main_test import ( + "fmt" "reflect" "testing" - "github.com/urfave/cli/v2/internal/genflags" + main "github.com/urfave/cli/v2/cmd/urfave-cli-genflags" ) +func TestTypeName(t *testing.T) { + for _, tc := range []struct { + gt string + fc *main.FlagTypeConfig + expected string + }{ + {gt: "int", fc: nil, expected: "IntFlag"}, + {gt: "int", fc: &main.FlagTypeConfig{}, expected: "IntFlag"}, + {gt: "int", fc: &main.FlagTypeConfig{TypeName: "VeryIntyFlag"}, expected: "VeryIntyFlag"}, + {gt: "[]bool", fc: nil, expected: "BoolSliceFlag"}, + {gt: "[]bool", fc: &main.FlagTypeConfig{}, expected: "BoolSliceFlag"}, + {gt: "[]bool", fc: &main.FlagTypeConfig{TypeName: "ManyTruthsFlag"}, expected: "ManyTruthsFlag"}, + {gt: "time.Rumination", fc: nil, expected: "RuminationFlag"}, + {gt: "time.Rumination", fc: &main.FlagTypeConfig{}, expected: "RuminationFlag"}, + {gt: "time.Rumination", fc: &main.FlagTypeConfig{TypeName: "PonderFlag"}, expected: "PonderFlag"}, + } { + t.Run( + fmt.Sprintf("type=%s,cfg=%v", tc.gt, func() string { + if tc.fc != nil { + return tc.fc.TypeName + } + return "nil" + }()), + func(ct *testing.T) { + actual := main.TypeName(tc.gt, tc.fc) + if tc.expected != actual { + ct.Errorf("expected %q, got %q", tc.expected, actual) + } + }, + ) + } +} + func TestSpec_SortedFlagTypes(t *testing.T) { - spec := &genflags.Spec{ - FlagTypes: map[string]*genflags.FlagTypeConfig{ - "nerf": &genflags.FlagTypeConfig{}, + spec := &main.Spec{ + FlagTypes: map[string]*main.FlagTypeConfig{ + "nerf": &main.FlagTypeConfig{}, "gerf": nil, }, } actual := spec.SortedFlagTypes() - expected := []*genflags.FlagType{ + expected := []*main.FlagType{ { GoType: "gerf", Config: nil, }, { GoType: "nerf", - Config: &genflags.FlagTypeConfig{}, + Config: &main.FlagTypeConfig{}, }, } if !reflect.DeepEqual(expected, actual) { @@ -31,12 +65,12 @@ func TestSpec_SortedFlagTypes(t *testing.T) { } } -func genFlagType() *genflags.FlagType { - return &genflags.FlagType{ +func genFlagType() *main.FlagType { + return &main.FlagType{ GoType: "blerf", - Config: &genflags.FlagTypeConfig{ + Config: &main.FlagTypeConfig{ SkipInterfaces: []string{"fmt.Stringer"}, - StructFields: []*genflags.FlagStructField{ + StructFields: []*main.FlagStructField{ { Name: "Foibles", Type: "int", diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index b0d9bfc..4462899 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -95,7 +95,7 @@ The built-in `go generate` command is used to run the commands specified in line help system which may be consulted for further information, e.g.: ```sh -go run internal/genflags/cmd/genflags/main.go --help +go run cmd/urfave-cli-genflags/main.go --help ``` #### docs output diff --git a/flag-spec.yaml b/flag-spec.yaml index 3fa2d10..7199c19 100644 --- a/flag-spec.yaml +++ b/flag-spec.yaml @@ -1,6 +1,6 @@ # NOTE: this file is used by the tool defined in -# ./internal/genflags/cmd/genflags/main.go which uses the -# `genflags.Spec` type that maps to this file structure. +# ./cmd/urfave-cli-genflags/main.go which uses the +# `Spec` type that maps to this file structure. flag_types: bool: diff --git a/internal/build/build.go b/internal/build/build.go index d94dae7..51b560c 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -94,7 +94,7 @@ func main() { }, &cli.StringSliceFlag{ Name: "packages", - Value: cli.NewStringSlice("cli", "altsrc", "internal/build", "internal/genflags"), + Value: cli.NewStringSlice("cli", "altsrc", "internal/build"), }, } diff --git a/internal/genflags/package.go b/internal/genflags/package.go deleted file mode 100644 index 4e5de41..0000000 --- a/internal/genflags/package.go +++ /dev/null @@ -1,34 +0,0 @@ -package genflags - -import ( - _ "embed" - "strings" - - "golang.org/x/text/cases" - "golang.org/x/text/language" -) - -var ( - //go:embed generated.gotmpl - TemplateString string - - //go:embed generated_test.gotmpl - TestTemplateString string - - titler = cases.Title(language.Und, cases.NoLower) -) - -func TypeName(goType string, fc *FlagTypeConfig) string { - if fc != nil && strings.TrimSpace(fc.TypeName) != "" { - return strings.TrimSpace(fc.TypeName) - } - - dotSplit := strings.Split(goType, ".") - goType = dotSplit[len(dotSplit)-1] - - if strings.HasPrefix(goType, "[]") { - return titler.String(strings.TrimPrefix(goType, "[]")) + "SliceFlag" - } - - return titler.String(goType) + "Flag" -} diff --git a/internal/genflags/package_test.go b/internal/genflags/package_test.go deleted file mode 100644 index 3920540..0000000 --- a/internal/genflags/package_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package genflags_test - -import ( - "fmt" - "testing" - - "github.com/urfave/cli/v2/internal/genflags" -) - -func TestTypeName(t *testing.T) { - for _, tc := range []struct { - gt string - fc *genflags.FlagTypeConfig - expected string - }{ - {gt: "int", fc: nil, expected: "IntFlag"}, - {gt: "int", fc: &genflags.FlagTypeConfig{}, expected: "IntFlag"}, - {gt: "int", fc: &genflags.FlagTypeConfig{TypeName: "VeryIntyFlag"}, expected: "VeryIntyFlag"}, - {gt: "[]bool", fc: nil, expected: "BoolSliceFlag"}, - {gt: "[]bool", fc: &genflags.FlagTypeConfig{}, expected: "BoolSliceFlag"}, - {gt: "[]bool", fc: &genflags.FlagTypeConfig{TypeName: "ManyTruthsFlag"}, expected: "ManyTruthsFlag"}, - {gt: "time.Rumination", fc: nil, expected: "RuminationFlag"}, - {gt: "time.Rumination", fc: &genflags.FlagTypeConfig{}, expected: "RuminationFlag"}, - {gt: "time.Rumination", fc: &genflags.FlagTypeConfig{TypeName: "PonderFlag"}, expected: "PonderFlag"}, - } { - t.Run( - fmt.Sprintf("type=%s,cfg=%v", tc.gt, func() string { - if tc.fc != nil { - return tc.fc.TypeName - } - return "nil" - }()), - func(ct *testing.T) { - actual := genflags.TypeName(tc.gt, tc.fc) - if tc.expected != actual { - ct.Errorf("expected %q, got %q", tc.expected, actual) - } - }, - ) - } -} diff --git a/internal/genflags/spec.go b/internal/genflags/spec.go deleted file mode 100644 index 5b1d4d3..0000000 --- a/internal/genflags/spec.go +++ /dev/null @@ -1,105 +0,0 @@ -package genflags - -import ( - "sort" - "strings" -) - -type Spec struct { - FlagTypes map[string]*FlagTypeConfig `yaml:"flag_types"` - PackageName string `yaml:"package_name"` - TestPackageName string `yaml:"test_package_name"` - UrfaveCLINamespace string `yaml:"urfave_cli_namespace"` - UrfaveCLITestNamespace string `yaml:"urfave_cli_test_namespace"` -} - -func (gfs *Spec) SortedFlagTypes() []*FlagType { - typeNames := []string{} - - for name := range gfs.FlagTypes { - if strings.HasPrefix(name, "[]") { - name = strings.TrimPrefix(name, "[]") + "Slice" - } - - typeNames = append(typeNames, name) - } - - sort.Strings(typeNames) - - ret := make([]*FlagType, len(typeNames)) - - for i, typeName := range typeNames { - ret[i] = &FlagType{ - GoType: typeName, - Config: gfs.FlagTypes[typeName], - } - } - - return ret -} - -type FlagTypeConfig struct { - SkipInterfaces []string `yaml:"skip_interfaces"` - StructFields []*FlagStructField `yaml:"struct_fields"` - TypeName string `yaml:"type_name"` - ValuePointer bool `yaml:"value_pointer"` - NoDefaultText bool `yaml:"no_default_text"` -} - -type FlagStructField struct { - Name string - Type string -} - -type FlagType struct { - GoType string - Config *FlagTypeConfig -} - -func (ft *FlagType) StructFields() []*FlagStructField { - if ft.Config == nil || ft.Config.StructFields == nil { - return []*FlagStructField{} - } - - return ft.Config.StructFields -} - -func (ft *FlagType) ValuePointer() bool { - if ft.Config == nil { - return false - } - - return ft.Config.ValuePointer -} - -func (ft *FlagType) TypeName() string { - return TypeName(ft.GoType, ft.Config) -} - -func (ft *FlagType) GenerateFmtStringerInterface() bool { - return ft.skipInterfaceNamed("fmt.Stringer") -} - -func (ft *FlagType) GenerateFlagInterface() bool { - return ft.skipInterfaceNamed("Flag") -} - -func (ft *FlagType) GenerateDefaultText() bool { - return !ft.Config.NoDefaultText -} - -func (ft *FlagType) skipInterfaceNamed(name string) bool { - if ft.Config == nil { - return true - } - - lowName := strings.ToLower(name) - - for _, interfaceName := range ft.Config.SkipInterfaces { - if strings.ToLower(interfaceName) == lowName { - return false - } - } - - return true -}