diff --git a/README.md b/README.md index 074e3979f..a5bffca7f 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Cobra provides: * Fully POSIX-compliant flags (including short & long versions) * Nested subcommands * Global, local and cascading flags -* Easy generation of applications & commands with `cobra init appname` & `cobra add cmdname` +* Easy generation of applications & commands with `cobra init` & `cobra add cmdname` * Intelligent suggestions (`app srver`... did you mean `app server`?) * Automatic help generation for commands and flags * Automatic help flag recognition of `-h`, `--help`, etc. @@ -54,7 +54,7 @@ Cobra provides: * Automatically generated man pages for your application * Command aliases so you can change things without breaking them * The flexibility to define your own help, usage, etc. -* Optional tight integration with [viper](http://github.com/spf13/viper) for 12-factor apps +* Optional seamless integration with [viper](http://github.com/spf13/viper) for 12-factor apps # Concepts @@ -88,7 +88,7 @@ have children commands and optionally run an action. In the example above, 'server' is the command. -[More about cobra.Command](https://godoc.org/github.com/spf13/cobra#Command) +[More about cobra.Command](https://pkg.go.dev/github.com/spf13/cobra#Command) ## Flags @@ -117,8 +117,12 @@ import "github.com/spf13/cobra" ``` # Usage +Cobra provides its own program that will create your application and add any +commands you want. It's the easiest way to incorporate Cobra into your application. -See [User Guide](user_guide.md). +For complete details on using the Cobra generator, please read [The Cobra Generator README](https://github.com/spf13/cobra/blob/master/cobra/README.md) + +For complete details on using the Cobra library, please read the [The Cobra User Guide](user_guide.md). # License diff --git a/cobra/README.md b/cobra/README.md index dcaf0c5f1..f1a60565b 100644 --- a/cobra/README.md +++ b/cobra/README.md @@ -3,41 +3,78 @@ Cobra provides its own program that will create your application and add any commands you want. It's the easiest way to incorporate Cobra into your application. -In order to use the cobra command, compile it using the following command: +Install the cobra generator with the command `go install github.com/spf13/cobra/cobra`. +Go will automatically install it in your `$GOPATH/bin` directory which should be in your $PATH. - go get github.com/spf13/cobra/cobra +Once installed you should have the `cobra` command available. Confirm by typing `cobra` at a +command line. -This will create the cobra executable under your `$GOPATH/bin` directory. +There are only two operations currently supported by the Cobra generator: ### cobra init The `cobra init [app]` command will create your initial application code for you. It is a very powerful application that will populate your program with -the right structure so you can immediately enjoy all the benefits of Cobra. It -will also automatically apply the license you specify to your application. +the right structure so you can immediately enjoy all the benefits of Cobra. +It can also apply the license you specify to your application. -Cobra init is pretty smart. You can either run it in your current application directory -or you can specify a relative path to an existing project. If the directory does not exist, it will be created for you. +With the introduction of Go modules, the Cobra generator has been simplified to +take advantage of modules. The Cobra generator works from within a Go module. -Updates to the Cobra generator have now decoupled it from the GOPATH. -As such `--pkg-name` is required. +#### Initalizing a module -**Note:** init will no longer fail on non-empty directories. +__If you already have a module, skip this step.__ +If you want to initialize a new Go module: + + 1. Create a new directory + 2. `cd` into that directory + 3. run `go mod init ` + +e.g. ``` -mkdir -p newApp && cd newApp -cobra init --pkg-name github.com/spf13/newApp +cd $HOME/code +mkdir myapp +cd myapp +go mod init github.com/spf13/myapp ``` -or +#### Initalizing an Cobra CLI application + +From within a Go module run `cobra init`. This will create a new barebones project +for you to edit. + +You should be able to run your new application immediately. Try it with +`go run main.go`. + +You will want to open up and edit 'cmd/root.go' and provide your own description and logic. +e.g. ``` -cobra init --pkg-name github.com/spf13/newApp path/to/newApp +cd $HOME/code/myapp +cobra init +go run main.go ``` -### cobra add +Cobra init can also be run from a subdirectory such as how the [cobra generator itself is organized](https://github.com/spf13/cobra). +This is useful if you want to keep your application code separate from your library code. + +#### Optional flags: +You can provide it your author name with the `--author` flag. +e.g. `cobra init --author "Steve Francia spf@spf13.com"` + +You can provide a license to use with `--license` +e.g. `cobra init --license apache` + +Use the `--viper` flag to automatically setup [viper](https://github.com/spf13/viper) + +Viper is a companion to Cobra intended to provide easy handling of environment variables and config files and seamlessly connecting them to the application flags. + +### Add commands to a project + +Once a cobra application is initialized you can continue to use the Cobra generator to +add additional commands to your application. The command to do this is `cobra add`. -Once an application is initialized, Cobra can create additional commands for you. Let's say you created an app and you wanted the following commands for it: * app serve @@ -52,6 +89,14 @@ cobra add config cobra add create -p 'configCmd' ``` +`cobra add` supports all the same optional flags as `cobra init` does (described above). + +You'll notice that this final command has a `-p` flag. This is used to assign a +parent command to the newly added command. In this case, we want to assign the +"create" command to the "config" command. All commands have a default parent of rootCmd if not specified. + +By default `cobra` will append `Cmd` to the name provided and uses this name for the internal variable name. When specifying a parent, be sure to match the variable name used in the code. + *Note: Use camelCase (not snake_case/kebab-case) for command names. Otherwise, you will encounter errors. For example, `cobra add add-user` is incorrect, but `cobra add addUser` is valid.* @@ -62,9 +107,10 @@ the following: ``` ▾ app/ ▾ cmd/ - serve.go config.go create.go + serve.go + root.go main.go ``` @@ -72,8 +118,11 @@ At this point you can run `go run main.go` and it would run your app. `go run main.go serve`, `go run main.go config`, `go run main.go config create` along with `go run main.go help serve`, etc. would all work. -Obviously you haven't added your own code to these yet. The commands are ready -for you to give them their tasks. Have fun! +You now have a basic Cobra-based application up and running. Next step is to edit the files in cmd and customize them for your application. + +For complete details on using the Cobra library, please read the [The Cobra User Guide](https://github.com/spf13/cobra/blob/master/user_guide.md#using-the-cobra-library). + +Have fun! ### Configuring the cobra generator @@ -86,6 +135,7 @@ An example ~/.cobra.yaml file: ```yaml author: Steve Francia license: MIT +viper: true ``` You can also use built-in licenses. For example, **GPLv2**, **GPLv3**, **LGPL**, diff --git a/cobra/cmd/add_test.go b/cobra/cmd/add_test.go index 0b32ca67e..20bc34f2e 100644 --- a/cobra/cmd/add_test.go +++ b/cobra/cmd/add_test.go @@ -4,9 +4,13 @@ import ( "fmt" "os" "testing" + + "github.com/spf13/viper" ) func TestGoldenAddCmd(t *testing.T) { + viper.Set("useViper", true) + viper.Set("license", "apache") command := &Command{ CmdName: "test", CmdParent: parentName, diff --git a/cobra/cmd/init.go b/cobra/cmd/init.go index 8c0e617ab..dbdc25bba 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,51 @@ 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 { + return exec.Command("go", "get", mod).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 e364f4098..930313c59 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,30 +37,18 @@ 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) defer func() { if projectPath != "" { diff --git a/cobra/cmd/licenses.go b/cobra/cmd/licenses.go index 2b3a42438..30c7b242d 100644 --- a/cobra/cmd/licenses.go +++ b/cobra/cmd/licenses.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. @@ -53,7 +53,7 @@ func init() { } // getLicense returns license specified by user in flag or in config. -// If user didn't specify the license, it returns Apache License 2.0. +// If user didn't specify the license, it returns none // // TODO: Inspect project for existing license func getLicense() License { @@ -73,8 +73,8 @@ func getLicense() License { return findLicense(viper.GetString("license")) } - // If user didn't set any license, use Apache 2.0 by default. - return Licenses["apache"] + // If user didn't set any license, use none by default + return Licenses["none"] } func copyrightLine() string { diff --git a/cobra/cmd/root.go b/cobra/cmd/root.go index 350df1034..51bba2cc5 100644 --- a/cobra/cmd/root.go +++ b/cobra/cmd/root.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. @@ -46,11 +46,11 @@ func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)") rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "author name for copyright attribution") rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "name of license for the project") - rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration") + rootCmd.PersistentFlags().Bool("viper", false, "use Viper for configuration") cobra.CheckErr(viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))) cobra.CheckErr(viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))) viper.SetDefault("author", "NAME HERE ") - viper.SetDefault("license", "apache") + viper.SetDefault("license", "none") rootCmd.AddCommand(addCmd) rootCmd.AddCommand(initCmd) diff --git a/cobra/cmd/testdata/main.go.golden b/cobra/cmd/testdata/main.go.golden index f69d34aa1..0af77e17f 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 32c1529e7..2adeaea53 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 diff --git a/cobra/tpl/main.go b/cobra/tpl/main.go index ed3a98e14..d2f774247 100644 --- a/cobra/tpl/main.go +++ b/cobra/tpl/main.go @@ -1,3 +1,16 @@ +// 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. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package tpl func MainTemplate() []byte { @@ -23,10 +36,12 @@ func RootTemplate() []byte { package cmd import ( +{{- if .Viper }} "fmt" "os" +{{ end }} "github.com/spf13/cobra" -{{ if .Viper }} +{{- if .Viper }} "github.com/spf13/viper"{{ end }} ) diff --git a/user_guide.md b/user_guide.md index 311abce28..923392b73 100644 --- a/user_guide.md +++ b/user_guide.md @@ -32,7 +32,7 @@ func main() { Cobra provides its own program that will create your application and add any commands you want. It's the easiest way to incorporate Cobra into your application. -[Here](https://github.com/spf13/cobra/blob/master/cobra/README.md) you can find more information about it. +For complete details on using the Cobra generator, please read [The Cobra Generator README](https://github.com/spf13/cobra/blob/master/cobra/README.md) ## Using the Cobra Library