Cobra generator now works within Go modules

Pretty major change in behavior, but with modules a change is needed.
Now cobra can initialize and add from within any Go module.
The experience is simplified and streamlined, but requires `go mod init` to happen first.
This commit is contained in:
Steve Francia 2021-07-01 17:47:45 -04:00
parent c9edb78acc
commit 9388e79fb4
4 changed files with 73 additions and 36 deletions

View File

@ -1,4 +1,4 @@
// Copyright © 2015 Steve Francia <spf@spf13.com>. // Copyright © 2021 Steve Francia <spf@spf13.com>.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -14,42 +14,41 @@
package cmd package cmd
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec"
"path" "path"
"path/filepath"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var ( var (
pkgName string
initCmd = &cobra.Command{ initCmd = &cobra.Command{
Use: "init [name]", Use: "init [path]",
Aliases: []string{"initialize", "initialise", "create"}, Aliases: []string{"initialize", "initialise", "create"},
Short: "Initialize a Cobra Application", Short: "Initialize a Cobra Application",
Long: `Initialize (cobra init) will create a new application, with a license Long: `Initialize (cobra init) will create a new application, with a license
and the appropriate structure for a Cobra-based CLI application. and the appropriate structure for a Cobra-based CLI application.
* If a name is provided, a directory with that name will be created in the current directory; Cobra init must be run inside of a go module (please run "go mod init <MODNAME>" first)
* If no name is provided, the current directory will be assumed;
`, `,
Run: func(_ *cobra.Command, args []string) { Run: func(_ *cobra.Command, args []string) {
projectPath, err := initializeProject(args) projectPath, err := initializeProject(args)
cobra.CheckErr(err) cobra.CheckErr(err)
cobra.CheckErr(goGet("github.com/spf13/cobra"))
if viper.GetBool("useViper") {
cobra.CheckErr(goGet("github.com/spf13/viper"))
}
fmt.Printf("Your Cobra application is ready at\n%s\n", projectPath) fmt.Printf("Your Cobra application is ready at\n%s\n", projectPath)
}, },
} }
) )
func init() {
initCmd.Flags().StringVar(&pkgName, "pkg-name", "", "fully qualified pkg name")
cobra.CheckErr(initCmd.MarkFlagRequired("pkg-name"))
}
func initializeProject(args []string) (string, error) { func initializeProject(args []string) (string, error) {
wd, err := os.Getwd() wd, err := os.Getwd()
if err != nil { if err != nil {
@ -62,13 +61,15 @@ func initializeProject(args []string) (string, error) {
} }
} }
modName := getModImportPath()
project := &Project{ project := &Project{
AbsolutePath: wd, AbsolutePath: wd,
PkgName: pkgName, PkgName: modName,
Legal: getLicense(), Legal: getLicense(),
Copyright: copyrightLine(), Copyright: copyrightLine(),
Viper: viper.GetBool("useViper"), Viper: viper.GetBool("useViper"),
AppName: path.Base(pkgName), AppName: path.Base(modName),
} }
if err := project.Create(); err != nil { if err := project.Create(); err != nil {
@ -77,3 +78,52 @@ func initializeProject(args []string) (string, error) {
return project.AbsolutePath, nil return project.AbsolutePath, nil
} }
func getModImportPath() string {
mod, cd := parseModInfo()
return path.Join(mod.Path, fileToURL(strings.TrimPrefix(cd.Dir, mod.Dir)))
}
func fileToURL(in string) string {
i := strings.Split(in, string(filepath.Separator))
return path.Join(i...)
}
func parseModInfo() (Mod, CurDir) {
var mod Mod
var dir CurDir
m := modInfoJSON("-m")
cobra.CheckErr(json.Unmarshal(m, &mod))
// Unsure why, but if no module is present Path is set to this string.
if mod.Path == "command-line-arguments" {
cobra.CheckErr("Please run `go mod init <MODNAME>` before `cobra init`")
}
e := modInfoJSON("-e")
cobra.CheckErr(json.Unmarshal(e, &dir))
return mod, dir
}
type Mod struct {
Path, Dir, GoMod string
}
type CurDir struct {
Dir string
}
func goGet(mod string) error {
cmd := exec.Command("go", "get", mod)
return cmd.Run()
}
func modInfoJSON(args ...string) []byte {
cmdArgs := append([]string{"list", "-json"}, args...)
out, err := exec.Command("go", cmdArgs...).Output()
cobra.CheckErr(err)
return out
}

View File

@ -16,8 +16,8 @@ func getProject() *Project {
AbsolutePath: fmt.Sprintf("%s/testproject", wd), AbsolutePath: fmt.Sprintf("%s/testproject", wd),
Legal: getLicense(), Legal: getLicense(),
Copyright: copyrightLine(), Copyright: copyrightLine(),
AppName: "testproject", AppName: "cmd",
PkgName: "github.com/spf13/testproject", PkgName: "github.com/spf13/cobra/cobra/cmd/cmd",
Viper: true, Viper: true,
} }
} }
@ -37,29 +37,16 @@ func TestGoldenInitCmd(t *testing.T) {
expectErr bool expectErr bool
}{ }{
{ {
name: "successfully creates a project with name", name: "successfully creates a project based on module",
args: []string{"testproject"}, args: []string{"testproject"},
pkgName: "github.com/spf13/testproject", pkgName: "github.com/spf13/testproject",
expectErr: false, expectErr: false,
}, },
{
name: "returns error when passing an absolute path for project",
args: []string{dir},
pkgName: "github.com/spf13/testproject",
expectErr: true,
},
{
name: "returns error when passing a relative path for project",
args: []string{"github.com/spf13/testproject"},
pkgName: "github.com/spf13/testproject",
expectErr: true,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
assertNoErr(t, initCmd.Flags().Set("pkg-name", tt.pkgName))
viper.Set("useViper", true) viper.Set("useViper", true)
viper.Set("license", "apache") viper.Set("license", "apache")
projectPath, err := initializeProject(tt.args) projectPath, err := initializeProject(tt.args)

View File

@ -15,7 +15,7 @@ limitations under the License.
*/ */
package main package main
import "github.com/spf13/testproject/cmd" import "github.com/spf13/cobra/cobra/cmd/cmd"
func main() { func main() {
cmd.Execute() cmd.Execute()

View File

@ -18,8 +18,8 @@ package cmd
import ( import (
"fmt" "fmt"
"os" "os"
"github.com/spf13/cobra"
"github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@ -27,7 +27,7 @@ var cfgFile string
// rootCmd represents the base command when called without any subcommands // rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "testproject", Use: "cmd",
Short: "A brief description of your application", Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example: examples and usage of using your application. For example:
@ -53,7 +53,7 @@ func init() {
// Cobra supports persistent flags, which, if defined here, // Cobra supports persistent flags, which, if defined here,
// will be global for your application. // will be global for your application.
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.testproject.yaml)") rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cmd.yaml)")
// Cobra also supports local flags, which will only run // Cobra also supports local flags, which will only run
// when this action is called directly. // when this action is called directly.
@ -70,10 +70,10 @@ func initConfig() {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
cobra.CheckErr(err) cobra.CheckErr(err)
// Search config in home directory with name ".testproject" (without extension). // Search config in home directory with name ".cmd" (without extension).
viper.AddConfigPath(home) viper.AddConfigPath(home)
viper.SetConfigType("yaml") viper.SetConfigType("yaml")
viper.SetConfigName(".testproject") viper.SetConfigName(".cmd")
} }
viper.AutomaticEnv() // read in environment variables that match viper.AutomaticEnv() // read in environment variables that match