Skip to content

Commit

Permalink
refactor(cli): Migrate from alecthomas/kingpin to spf13/cobra.
Browse files Browse the repository at this point in the history
This change migrates all of the current CLI code (and related tests) to
now use cobra. All functionality should stay identical with the flags.

The only changes present within the tests are the formatting of specific
errors, and how they differ between kingpin/cobra.

One potential breaking change that could impact users is that all
`gunk help...` commands now print to stdout, even if invalid. This is
not true for other errors, so it probably should not break any real
world applications.

This change can be linked back to spf13/cobra#1002.
  • Loading branch information
hhhapz committed Nov 18, 2021
1 parent cb866bd commit 81d328e
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 136 deletions.
10 changes: 5 additions & 5 deletions go.mod
Expand Up @@ -3,26 +3,26 @@ module github.com/gunk/gunk
go 1.17

require (
github.com/alecthomas/kingpin v2.2.6+incompatible
github.com/emicklei/proto v1.9.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.6.0
github.com/gunk/opt v0.1.0
github.com/karelbilek/dirchanges v0.0.0-20210218071031-880a92f1a313
github.com/kenshaw/ini v0.5.1
github.com/kenshaw/snaker v0.1.6
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c
github.com/spf13/cobra v1.2.1
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1
golang.org/x/tools v0.1.8-0.20211102182255-bb4add04ddef
google.golang.org/genproto v0.0.0-20211116182654-e63d96a377c4
google.golang.org/genproto v0.0.0-20211117155847-120650a500bb
google.golang.org/protobuf v1.27.1
mvdan.cc/gofumpt v0.2.0
)

require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/mod v0.5.1 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
Expand Down
202 changes: 188 additions & 14 deletions go.sum

Large diffs are not rendered by default.

196 changes: 108 additions & 88 deletions main.go
Expand Up @@ -4,116 +4,136 @@ import (
"fmt"
"os"

"github.com/alecthomas/kingpin"
"github.com/gunk/gunk/convert"
"github.com/gunk/gunk/dump"
"github.com/gunk/gunk/format"
"github.com/gunk/gunk/generate"
"github.com/gunk/gunk/generate/downloader"
"github.com/gunk/gunk/log"
"github.com/gunk/gunk/vetconfig"
"github.com/spf13/cobra"
)

var (
version = "v0.8.7"
app = kingpin.New("gunk", "The modern frontend and syntax for Protocol Buffers.").UsageTemplate(kingpin.CompactUsageTemplate)
gen = app.Command("generate", "Generate code from Gunk packages.")
genPatterns = gen.Arg("patterns", "patterns of Gunk packages").Strings()
conv = app.Command("convert", "Convert Proto file to Gunk file.")
convProtoFilesOrFolders = conv.Arg("files_or_folders", "Proto files or folders to convert to Gunk").Strings()
convOverwriteGunkFile = conv.Flag("overwrite", "overwrite the converted Gunk file if it exists.").Bool()
frmt = app.Command("format", "Format Gunk code.")
frmtPatterns = frmt.Arg("patterns", "patterns of Gunk packages").Strings()
dmp = app.Command("dump", "Write a FileDescriptorSet, defined in descriptor.proto")
dmpPatterns = dmp.Arg("patterns", "patterns of Gunk packages").Strings()
dmpFormat = dmp.Flag("format", "output format: proto (default), or json").String()
download = app.Command("download", "Download required tools for Gunk, e.g., protoc")
dlAll = download.Command("all", "download all required tools")
dlProtoc = download.Command("protoc", "download protoc")
dlProtocPath = dlProtoc.Flag("path", "path to check for protoc binary, or where to download it to").String()
dlProtocVer = dlProtoc.Flag("version", "version of protoc to use").String()
ver = app.Command("version", "Show Gunk version.")
vet = app.Command("vet", "Vet gunk config files")
)
var version = "v0.8.7"

func main() {
os.Exit(main1())
}

func main1() (code int) {
// Replace kingpin's use of os.Exit, as testscript requires that we
// return exit codes instead of exiting the entire program. Use a panic,
// as we need to completely halt kingpin when it calls our terminate.
// But also make sure to not use `recover` and re-panic, as that does not print the correct
// stacktrace
var tCode int
var didTerminate bool
didPanic := true
app.Terminate(func(c int) {
didTerminate = true
tCode = c
panic("terminated")
})
defer func() {
if didPanic {
if didTerminate {
recover()
code = tCode
}
}
}()
c := main2()
didPanic = false
return c
os.Exit(run())
}

func main2() (code int) {
app.HelpFlag.Short('h') // allow -h as well as --help
gen.Flag("print-commands", "print the commands").Short('x').BoolVar(&log.PrintCommands)
gen.Flag("verbose", "print the names of packages as they are generated").Short('v').BoolVar(&log.Verbose)
download.Flag("verbose", "print details of downloaded tools").Short('v').BoolVar(&log.Verbose)
func run() int {
app := cobra.Command{
Use: "gunk",
Short: "The modern frontend and syntax for Protocol Buffers.",
Version: version,
}
// version commmand
ver := &cobra.Command{
Use: "version",
Short: "Print the version number of gundk",
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintln(os.Stdout, "gunk", version)
},
}
app.AddCommand(ver)
// generate command
gen := &cobra.Command{
Use: "generate [patterns]",
Short: "Generate code from Gunk packages",
RunE: func(cmd *cobra.Command, args []string) error {
return generate.Run("", args...)
},
}
gen.Flags().BoolVarP(&log.PrintCommands, "print-commands", "x", false, "Print the commands")
gen.Flags().BoolVarP(&log.Verbose, "verbose", "v", false, "Print the names of packages are they are generated")
app.AddCommand(gen)
// convert command
var overwrite bool
conv := &cobra.Command{
Use: "convert [-overwrite] [file | directory]...",
Short: "Convert Proto file to Gunk file.",
RunE: func(cmd *cobra.Command, args []string) error {
return convert.Run(args, overwrite)
},
}
conv.Flags().BoolVarP(&overwrite, "overwrite", "w", false, "Overwrite the converted Gunk file if it exists.")
// format command
app.AddCommand(conv)
frmt := &cobra.Command{
Use: "format [patterns]",
Short: "Format Gunk code",
RunE: func(cmd *cobra.Command, args []string) error {
return format.Run("", args...)
},
}
app.AddCommand(frmt)
// dump command
var dmpFormat string
dmp := &cobra.Command{
Use: "dump [patterns]",
Short: "Write a FileDescriptorSet, defined in descriptor.proto",
RunE: func(cmd *cobra.Command, args []string) error {
return dump.Run(dmpFormat, "", args...)
},
}
dmp.Flags().StringVarP(&dmpFormat, "format", "f", "proto", "output format: [proto | json]")
app.AddCommand(dmp)
// download list
// TODO(hhhapz): add protoc-java, and protoc-ts, etc.
downloadSubcommands := []func() error{
downloadProtoc,
func() error { return downloadProtoc("", "") },
}
command, err := app.Parse(os.Args[1:])
if code != 0 {
// simulate the os.Exit that would have happened
return code
} else if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return 1
// download command
download := cobra.Command{
Use: "download [protoc | protoc]",
Short: "Download the necessary tools for Gunk",
}
download.Flags().BoolVarP(&log.Verbose, "verbose", "v", false, "Print details of downloaded tools")
dlAll := cobra.Command{
Use: "all",
Short: "Download all required tools for Gunk, e.g., protoc",
RunE: func(cmd *cobra.Command, args []string) error {
for _, f := range downloadSubcommands {
if err := f(); err != nil {
return err
}
}
return nil
},
}
var dlProtocPath, dlProtocVer string
dlProtoc := cobra.Command{
Use: "protoc",
Short: "Download protoc",
RunE: func(cmd *cobra.Command, args []string) error {
return downloadProtoc(dlProtocPath, dlProtocVer)
},
}
switch command {
case ver.FullCommand():
fmt.Fprintf(os.Stdout, "gunk %s\n", version)
case gen.FullCommand():
err = generate.Run("", *genPatterns...)
case vet.FullCommand():
err = vetconfig.Run(".")
case conv.FullCommand():
err = convert.Run(*convProtoFilesOrFolders, *convOverwriteGunkFile)
case frmt.FullCommand():
err = format.Run("", *frmtPatterns...)
case dmp.FullCommand():
err = dump.Run(*dmpFormat, "", *dmpPatterns...)
case dlAll.FullCommand():
for _, dl := range downloadSubcommands {
err = dl()
if err != nil {
break
dlProtoc.Flags().StringVar(&dlProtocPath, "path", "", "Path to check for protoc binary, or where to download it to")
dlProtoc.Flags().BoolVarP(&log.Verbose, "verbose", "v", false, "Print details of download tools")
dlProtoc.Flags().StringVar(&dlProtocVer, "version", "", "Version of protoc to use")
download.AddCommand(&dlAll, &dlProtoc)
app.AddCommand(&download)
// vet command
vet := cobra.Command{
Use: "vet [path]",
Short: "Vet gunk config files",
RunE: func(cmd *cobra.Command, args []string) error {
path := "."
if len(args) > 0 {
path = args[0]
}
}
case dlProtoc.FullCommand():
err = downloadProtoc()
return vetconfig.Run(path)
},
}
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
app.AddCommand(&vet)
// run app
if err := app.Execute(); err != nil {
return 1
}
return 0
}

func downloadProtoc() error {
_, err := downloader.CheckOrDownloadProtoc(*dlProtocPath, *dlProtocVer)
func downloadProtoc(path, version string) error {
_, err := downloader.CheckOrDownloadProtoc(path, version)
return err
}
2 changes: 1 addition & 1 deletion main_test.go
Expand Up @@ -40,7 +40,7 @@ func TestMain(m *testing.M) {
}
}
os.Exit(testscript.RunMain(m, map[string]func() int{
"gunk": main1,
"gunk": run,
}))
}

Expand Down
38 changes: 19 additions & 19 deletions testdata/scripts/cmds.txt
@@ -1,43 +1,43 @@
gunk
stderr '^usage: gunk \['
! stderr 'command not specified'
! stdout .
stdout 'Usage:\n gunk \[command\]'
! stdout 'command not specified'
! stderr .

gunk version
stdout '^gunk v0.*'
! stderr .

gunk -h
stderr -count=1 '^usage: gunk \['
! stdout .
stdout 'Usage:\n gunk \[command\]'
! stderr .

gunk --help
stderr '^usage: gunk \['
! stdout .
stdout 'Usage:\n gunk \[command\]'
! stderr .

gunk help
stderr '^usage: gunk \['
! stdout .
stdout 'Usage:\n gunk \[command\]'
! stderr .

gunk help generate
stderr '^usage: gunk generate'
! stdout .
stdout 'Usage:\n gunk generate \[patterns\] \[flags\]'
! stderr .

! gunk help missing
stderr 'expected command'
gunk help missing
stderr 'Unknown help topic \[`missing`\]'

! gunk missing
stderr 'expected command'
stderr 'unknown command "missing"'

gunk generate -h
stderr '^usage: gunk generate'
! stdout .
stdout 'Usage:\n gunk generate \[patterns\] \[flags\]'
! stderr .

gunk generate --help
stderr '^usage: gunk generate'
! stdout .
stdout 'Usage:\n gunk generate \[patterns\] \[flags\]'
! stderr .

! gunk generate --missing
stderr 'unknown long flag'
stderr 'unknown flag: --missing'
! stdout .

4 changes: 2 additions & 2 deletions testdata/scripts/convert_duplicates.txt
Expand Up @@ -2,7 +2,7 @@ gunk convert util.proto
cmp util.gunk util.gunk.golden

! gunk convert util2.proto
stderr 'error: util2.proto:13:1: Event_Source redeclared in this block'
stderr 'Error: util2.proto:13:1: Event_Source redeclared in this block'

gunk convert util3.proto
cmp util3.gunk util3.gunk.golden
Expand Down Expand Up @@ -89,4 +89,4 @@ const (
)

type Bar struct {
}
}
2 changes: 1 addition & 1 deletion testdata/scripts/convert_errors.txt
@@ -1,5 +1,5 @@
! gunk convert util.proto
stderr 'error: util.proto:5:1: "hello" is an unhandled proto file option'
stderr 'Error: util.proto:5:1: "hello" is an unhandled proto file option'

-- util.proto --
syntax = "proto3";
Expand Down
4 changes: 2 additions & 2 deletions testdata/scripts/convert_import_missing_gopkg.txt
@@ -1,5 +1,5 @@
! gunk convert util.proto
stderr 'error: imported file must contain go_package option imported/imported.proto'
stderr 'Error: imported file must contain go_package option imported/imported.proto'

-- .gunkconfig --

Expand Down Expand Up @@ -41,4 +41,4 @@ package imported;

message Type {
string Name = 1;
}
}
4 changes: 2 additions & 2 deletions testdata/scripts/convert_import_no_gunkconfig.txt
@@ -1,5 +1,5 @@
! gunk convert util.proto
stderr 'error: util.proto:9:2: imported.Type is undefined'
stderr 'Error: util.proto:9:2: imported.Type is undefined'

-- util.proto --
syntax = "proto3";
Expand Down Expand Up @@ -41,4 +41,4 @@ option go_package= "github.com/gunk/gunk/imported";

message Type {
string Name = 1;
}
}
4 changes: 2 additions & 2 deletions testdata/scripts/repeated_function_param.txt
@@ -1,8 +1,8 @@
! gunk generate ./p1
stderr 'error: .*p1/p1.gunk:9:2: parameter type should not be repeated'
stderr 'Error: .*p1/p1.gunk:9:2: parameter type should not be repeated'

! gunk generate ./p2
stderr 'error: .*p2/p2.gunk:9:2: parameter type should not be repeated'
stderr 'Error: .*p2/p2.gunk:9:2: parameter type should not be repeated'

-- go.mod --
module testdata.tld/util
Expand Down

0 comments on commit 81d328e

Please sign in to comment.