Move genflags tool to cmd/ and pin to previous release

to alleviate problems caused by the circular dependency of using the
same code as a library that is potentially being generated to adhere to
a different API.
This commit is contained in:
Dan Buch 2022-08-14 10:02:07 -04:00
parent 45a1375078
commit 2ca91434a8
Signed by: meatballhat
GPG Key ID: A12F782281063434
17 changed files with 268 additions and 214 deletions

View File

@ -46,6 +46,9 @@ jobs:
- name: test - name: test
run: go run internal/build/build.go 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 - name: check-binary-size
run: go run internal/build/build.go check-binary-size run: go run internal/build/build.go check-binary-size

1
.gitignore vendored
View File

@ -6,5 +6,6 @@ internal/*/built-example
coverage.txt coverage.txt
/.local/ /.local/
/site/ /site/
/cmd/urfave-cli-genflags/urfave-cli-genflags
*.exe *.exe

32
cli.go
View File

@ -1,23 +1,25 @@
// Package cli provides a minimal framework for creating and organizing command line // 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 // Go applications. cli is designed to be easy to understand and write, the most simple
// cli application can be written as follows: // 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: // 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 package cli
//go:generate go run internal/genflags/cmd/genflags/main.go //go:generate go run cmd/urfave-cli-genflags/main.go

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -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=

View File

@ -9,12 +9,14 @@ import (
"os/exec" "os/exec"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"syscall" "syscall"
"text/template" "text/template"
"github.com/urfave/cli/v2" "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" "gopkg.in/yaml.v3"
) )
@ -22,6 +24,16 @@ const (
defaultPackageName = "cli" 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) { func sh(ctx context.Context, exe string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, exe, args...) cmd := exec.CommandContext(ctx, exe, args...)
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@ -92,7 +104,7 @@ func runGenFlags(cCtx *cli.Context) error {
return err return err
} }
spec := &genflags.Spec{} spec := &Spec{}
if err := yaml.Unmarshal(specBytes, spec); err != nil { if err := yaml.Unmarshal(specBytes, spec); err != nil {
return err return err
} }
@ -123,12 +135,12 @@ func runGenFlags(cCtx *cli.Context) error {
spec.UrfaveCLITestNamespace = "cli." spec.UrfaveCLITestNamespace = "cli."
} }
genTmpl, err := template.New("gen").Parse(genflags.TemplateString) genTmpl, err := template.New("gen").Parse(TemplateString)
if err != nil { if err != nil {
return err return err
} }
genTestTmpl, err := template.New("gen_test").Parse(genflags.TestTemplateString) genTestTmpl, err := template.New("gen_test").Parse(TestTemplateString)
if err != nil { if err != nil {
return err return err
} }
@ -161,3 +173,120 @@ func runGenFlags(cCtx *cli.Context) error {
return nil 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
}

View File

@ -1,29 +1,63 @@
package genflags_test package main_test
import ( import (
"fmt"
"reflect" "reflect"
"testing" "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) { func TestSpec_SortedFlagTypes(t *testing.T) {
spec := &genflags.Spec{ spec := &main.Spec{
FlagTypes: map[string]*genflags.FlagTypeConfig{ FlagTypes: map[string]*main.FlagTypeConfig{
"nerf": &genflags.FlagTypeConfig{}, "nerf": &main.FlagTypeConfig{},
"gerf": nil, "gerf": nil,
}, },
} }
actual := spec.SortedFlagTypes() actual := spec.SortedFlagTypes()
expected := []*genflags.FlagType{ expected := []*main.FlagType{
{ {
GoType: "gerf", GoType: "gerf",
Config: nil, Config: nil,
}, },
{ {
GoType: "nerf", GoType: "nerf",
Config: &genflags.FlagTypeConfig{}, Config: &main.FlagTypeConfig{},
}, },
} }
if !reflect.DeepEqual(expected, actual) { if !reflect.DeepEqual(expected, actual) {
@ -31,12 +65,12 @@ func TestSpec_SortedFlagTypes(t *testing.T) {
} }
} }
func genFlagType() *genflags.FlagType { func genFlagType() *main.FlagType {
return &genflags.FlagType{ return &main.FlagType{
GoType: "blerf", GoType: "blerf",
Config: &genflags.FlagTypeConfig{ Config: &main.FlagTypeConfig{
SkipInterfaces: []string{"fmt.Stringer"}, SkipInterfaces: []string{"fmt.Stringer"},
StructFields: []*genflags.FlagStructField{ StructFields: []*main.FlagStructField{
{ {
Name: "Foibles", Name: "Foibles",
Type: "int", Type: "int",

View File

@ -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.: line help system which may be consulted for further information, e.g.:
```sh ```sh
go run internal/genflags/cmd/genflags/main.go --help go run cmd/urfave-cli-genflags/main.go --help
``` ```
#### docs output #### docs output

View File

@ -1,6 +1,6 @@
# NOTE: this file is used by the tool defined in # NOTE: this file is used by the tool defined in
# ./internal/genflags/cmd/genflags/main.go which uses the # ./cmd/urfave-cli-genflags/main.go which uses the
# `genflags.Spec` type that maps to this file structure. # `Spec` type that maps to this file structure.
flag_types: flag_types:
bool: bool:

View File

@ -94,7 +94,7 @@ func main() {
}, },
&cli.StringSliceFlag{ &cli.StringSliceFlag{
Name: "packages", Name: "packages",
Value: cli.NewStringSlice("cli", "altsrc", "internal/build", "internal/genflags"), Value: cli.NewStringSlice("cli", "altsrc", "internal/build"),
}, },
} }

View File

@ -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"
}

View File

@ -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)
}
},
)
}
}

View File

@ -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
}