From 9388e79fb48d6af993c8d4fb90a7de9c7533725b Mon Sep 17 00:00:00 2001 From: Steve Francia Date: Thu, 1 Jul 2021 17:47:45 -0400 Subject: [PATCH] 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. --- cobra/cmd/init.go | 78 +++++++++++++++++++++++++------ cobra/cmd/init_test.go | 19 ++------ cobra/cmd/testdata/main.go.golden | 2 +- cobra/cmd/testdata/root.go.golden | 10 ++-- 4 files changed, 73 insertions(+), 36 deletions(-) diff --git a/cobra/cmd/init.go b/cobra/cmd/init.go index 8c0e617a..6e4c4f17 100644 --- a/cobra/cmd/init.go +++ b/cobra/cmd/init.go @@ -1,4 +1,4 @@ -// Copyright © 2015 Steve Francia . +// Copyright © 2021 Steve Francia . // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,42 +14,41 @@ package cmd import ( + "encoding/json" "fmt" "os" + "os/exec" "path" + "path/filepath" + "strings" "github.com/spf13/cobra" "github.com/spf13/viper" ) var ( - pkgName string - initCmd = &cobra.Command{ - Use: "init [name]", + Use: "init [path]", Aliases: []string{"initialize", "initialise", "create"}, Short: "Initialize a Cobra Application", Long: `Initialize (cobra init) will create a new application, with a license 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; - * If no name is provided, the current directory will be assumed; +Cobra init must be run inside of a go module (please run "go mod init " first) `, Run: func(_ *cobra.Command, args []string) { - projectPath, err := initializeProject(args) 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) }, } ) -func init() { - initCmd.Flags().StringVar(&pkgName, "pkg-name", "", "fully qualified pkg name") - cobra.CheckErr(initCmd.MarkFlagRequired("pkg-name")) -} - func initializeProject(args []string) (string, error) { wd, err := os.Getwd() if err != nil { @@ -62,13 +61,15 @@ func initializeProject(args []string) (string, error) { } } + modName := getModImportPath() + project := &Project{ AbsolutePath: wd, - PkgName: pkgName, + PkgName: modName, Legal: getLicense(), Copyright: copyrightLine(), Viper: viper.GetBool("useViper"), - AppName: path.Base(pkgName), + AppName: path.Base(modName), } if err := project.Create(); err != nil { @@ -77,3 +78,52 @@ func initializeProject(args []string) (string, error) { 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 ` 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 +} diff --git a/cobra/cmd/init_test.go b/cobra/cmd/init_test.go index ea80250c..930313c5 100644 --- a/cobra/cmd/init_test.go +++ b/cobra/cmd/init_test.go @@ -16,8 +16,8 @@ func getProject() *Project { AbsolutePath: fmt.Sprintf("%s/testproject", wd), Legal: getLicense(), Copyright: copyrightLine(), - AppName: "testproject", - PkgName: "github.com/spf13/testproject", + AppName: "cmd", + PkgName: "github.com/spf13/cobra/cobra/cmd/cmd", Viper: true, } } @@ -37,29 +37,16 @@ func TestGoldenInitCmd(t *testing.T) { expectErr bool }{ { - name: "successfully creates a project with name", + name: "successfully creates a project based on module", args: []string{"testproject"}, pkgName: "github.com/spf13/testproject", 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 { t.Run(tt.name, func(t *testing.T) { - assertNoErr(t, initCmd.Flags().Set("pkg-name", tt.pkgName)) viper.Set("useViper", true) viper.Set("license", "apache") projectPath, err := initializeProject(tt.args) diff --git a/cobra/cmd/testdata/main.go.golden b/cobra/cmd/testdata/main.go.golden index f69d34aa..0af77e17 100644 --- a/cobra/cmd/testdata/main.go.golden +++ b/cobra/cmd/testdata/main.go.golden @@ -15,7 +15,7 @@ limitations under the License. */ package main -import "github.com/spf13/testproject/cmd" +import "github.com/spf13/cobra/cobra/cmd/cmd" func main() { cmd.Execute() diff --git a/cobra/cmd/testdata/root.go.golden b/cobra/cmd/testdata/root.go.golden index 32c1529e..2adeaea5 100644 --- a/cobra/cmd/testdata/root.go.golden +++ b/cobra/cmd/testdata/root.go.golden @@ -18,8 +18,8 @@ package cmd import ( "fmt" "os" - "github.com/spf13/cobra" + "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -27,7 +27,7 @@ var cfgFile string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "testproject", + Use: "cmd", Short: "A brief description of your application", Long: `A longer description that spans multiple lines and likely contains examples and usage of using your application. For example: @@ -53,7 +53,7 @@ func init() { // Cobra supports persistent flags, which, if defined here, // 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 // when this action is called directly. @@ -70,10 +70,10 @@ func initConfig() { home, err := os.UserHomeDir() 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.SetConfigType("yaml") - viper.SetConfigName(".testproject") + viper.SetConfigName(".cmd") } viper.AutomaticEnv() // read in environment variables that match