How does cobra commander for go (golang) work? - go

I am trying to understand how to create costume commands for go using the cobra (and viper) libraries and be able to use them.
Specifically I was trying to understand and make the example they provide work. The example is the following:
import(
"github.com/spf13/cobra"
"fmt"
"strings"
)
func main() {
var echoTimes int
var cmdPrint = &cobra.Command{
Use: "print [string to print]",
Short: "Print anything to the screen",
Long: `print is for printing anything back to the screen.
For many years people have printed back to the screen.
`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Print: " + strings.Join(args, " "))
},
}
var cmdEcho = &cobra.Command{
Use: "echo [string to echo]",
Short: "Echo anything to the screen",
Long: `echo is for echoing anything back.
Echo works a lot like print, except it has a child command.
`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Print: " + strings.Join(args, " "))
},
}
var cmdTimes = &cobra.Command{
Use: "times [# times] [string to echo]",
Short: "Echo anything to the screen more times",
Long: `echo things multiple times back to the user by providing
a count and a string.`,
Run: func(cmd *cobra.Command, args []string) {
for i:=0; i < echoTimes; i++ {
fmt.Println("Echo: " + strings.Join(args, " "))
}
},
}
cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")
var rootCmd = &cobra.Command{Use: "app"}
rootCmd.AddCommand(cmdPrint, cmdEcho)
cmdEcho.AddCommand(cmdTimes)
rootCmd.Execute()
}
However, I have made the file for it, in package main and I can't seem to make it work properly. I do understand that the rootCmd is not suppose to be executable. However, it seems to me that the Use field behaves inconsistently (as far as I can tell). I was trying to understand how commands actually run in the command line and compile the file in different ways to experiment this.
For example I compiled this file I named as cobra.go with go build cobra.go and now I have an executable cobra.
If I do ./name it behaves the same way as ./name help and prints:
Usage:
app [command]
Available Commands:
print [string to print] Print anything to the screen
echo [string to echo] Echo anything to the screen
help [command] Help about any command
Available Flags:
--help=false: help for app
Use "app help [command]" for more information about that command.
However the description incorrectly says that to use it you must do "app help [command]".
If I do that ./app help print (with only the cobra executable of course) it prints:
zsh: no such file or directory: ./app
if I do app help print it also doesn't work (since it isn't found in PATH):
zsh: command not found: app
So my understanding is that, unless I build it with the same name as root, cobra will act funny.
So it means that the use field has to match the file name for the output of help to make sense and for us to be able to run its sub commands properly, right?
Also, how is one able to have (compile) multiple different "root" commands in the same project, if only one package main is allowed? If one has multiple "root" commands under main, it shouldn't work, right? I will try some of these ideas soon.

You could check how cobra is used is other projects (go-search).
For instance, the bitballoon-cli project defines multiple commands, each in their own file, each in the main package. Like create.go:
package main
import (
"github.com/BitBalloon/bitballoon-go"
"github.com/spf13/cobra"
"log"
)
var createCmd = &cobra.Command{
Use: "create",
Short: "create a new BitBalloon site",
Long: "creates a new BitBalloon site and returns an ID you can deploy to",
}
var siteName, customDomain, password, notificationEmail string
func init() {
createCmd.Run = create
createCmd.Flags().StringVarP(&siteName, "name", "n", "", "Name of the site (must be a valid subdomain: <name>.bitballoon.com)")
createCmd.Flags().StringVarP(&customDomain, "domain", "d", "", "Custom domain for the site (only works for premium sites)")
createCmd.Flags().StringVarP(&password, "password", "p", "", "Password for the site")
createCmd.Flags().StringVarP(&notificationEmail, "email", "e", "", "Notification email for form submissions (only works for premium sites)")
}
func create(cmd *cobra.Command, args []string) {
// your function for this command
}
You could check if that way of defining and adding a command to your project works in your case.
4 years later, see the video "justforfunc #32: CLI tools with Cobra", by Francesc Campoy.

Related

Require a flag as the first argument in a Cobra command

I'm trying to create a Cobra command that uses a flag to inform the action of the command, specifically a configuration command that can either add or remove a configured setting. For example
cli> prog_name config --set config_var var_vlue
cli> prog_name config --unset config_var var_value
Is there a way to do this in Cobra? I have been reading through the documentation and haven't found any way to validate that a flag is the first value in the command. I've seen information about positional arguments, but from what I've read it sounds like flags aren't considered arguments, so they wouldn't be covered by positional arguments.
I'd imagine I can do this in my PreRunE function and do the validation manually, but if there's a way to set this in Cobra I think that'd most likely be better, since I'd prefer Cobra to be doing that parsing and matching rather than me have to be comparing specific values in os.Args to "--set" and "--unset" or something similar.
It seems like the best option is to just use a subcommand for this instead of a flag.
You can solve this by consulting this link.
In short what you need is the Flags() function. You can find documentation here.
package main
import (
"fmt"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "testprog",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("rootCmd called")
},
}
var subCmd = &cobra.Command{
Use: "sub",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(args)
},
}
func main() {
rootCmd.AddCommand(subCmd)
flags := subCmd.Flags()
// not necessary in your case
flags.SetInterspersed(false)
// Bool defines a bool flag with specified name,
// default value, and usage string. The return value
// is the address of a bool variable that stores
// the value of the flag.
flags.Bool("test", false, "test flag")
rootCmd.Execute()
}
Let's see what happens in the terminal:
> ./cobraApp sub --test a
> [a]

ValidArgsFunction dynamic autocomplete not working with Golang Cobra cli program

I'm trying to get autocomplete working with Cobra but nothing happens after pressing the tab key - on something that I think should autocomplete. Am I missing something?
var HelloCmd = &cobra.Command{
Use: "hello <name>",
Short: "Say hello to someone",
Long: `Say hello to someone`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(args)
fmt.Println("hello " + args[0])
cmd.Help()
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return []string{"steve", "john"}, cobra.ShellCompDirectiveNoFileComp
},
}
And then I call go build . followed by ./program hello s[TAB] or ./program hello j[TAB] it suggests nothing. I want to it suggest the names "steve" and "john". If I type ./program hello [TAB] it suggests the files in the directory.
Pls help I've been ripping my hairs out all morning to fix this!
you need to generate autocomplete script and add it to your shell profile follow this section:
Creating your own completion command
and see [cmd] completion -h for more help.

Running root command logic + sub-command logic

I have the following command + subcommand:
aws.go
// s3Cmd represents the out command
var s3Cmd = &cobra.Command{
Use: "s3",
Short: "Uploads saved tarballs to an s3 bucket in aws",
Long: `Uploads files to S3 using the credentials passed as arguments
s3Bucket and the aws key and secret.`,
RunE: func(cmd *cobra.Command, args []string) error {
// some logic
},
}
func init() {
outCmd.AddCommand(s3Cmd)
}
out.go
// outCmd represents the out command
var outCmd = &cobra.Command{
Use: "out",
Short: "Consumes data out from RabbitMQ and stores to tarballs",
Long: `Select your output directory and batchsize of the tarballs.
When there are no more messages in the queue, press CTRL + c, to interrupt
the consumption and save the last message buffers.`,
RunE: func(cmd *cobra.Command, args []string) error {
//logic
},
}
func init() {
RootCmd.AddCommand(outCmd)
}
When I execute go run main.go out --args s3 --args
The above runs the logic inside s3Command but doesn't run what's inside outCmd, is there a way to run the outCommand logic first then s3Cmd first?
The go-cobra commands and subcommands are meant to be run individually. There are some hacky ways to run multiple, but generally that means requiring a special format to your args and handling batching up the runs yourself. See the discussion at https://github.com/spf13/cobra/issues/726 for an example of one way to do it and pointers to a few related issues.

GO: Run cli command with wrong args

I use cobra to create CLI command tool.
everything is looking OK except the error handling
what I want that if command was sent by mistake (wrong args or wrong input) return std.err instead of std.out
to simplify the secnario I've created this which demonstrate my use-case
package main
import (
"errors"
"fmt"
"os"
"github.com/spf13/cobra"
)
var (
RootCmd = &cobra.Command{
Use: "myApp",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("ROOT verbose = %d, args = %v\n", args)
},
}
provideCmd = &cobra.Command{
Use: "provide",
Run: nil,
}
appCmd = &cobra.Command{
Use: "apps",
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
if name != "myapp" {
err := errors.New("app name doesnt exist")
return err
}
return nil
},
SilenceUsage: true,
}
)
func init() {
// Add the application command to app command
provideCmd.AddCommand(appCmd)
// add provide command to root command
RootCmd.AddCommand(provideCmd)
}
func main() {
if err := RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(-1)
}
}
Now if I compile the binary and run exec.Command against the binary everything is working as expected. but if I want to test the error scenario like mycli provide apps apps1
I want to see that returned in std.err and not at std.out
When I execute mycli provide apps myapp everything should be OK
but if I run mycli provide apps myapp2 I want to get std.err and not std.out , which is not the case here ...what am I missing here ?
https://play.golang.org/p/B00z4eZ7Sj-
Your sample already prints the error both to stdout and stderr.
By default the cobra package prints any errors it encounters to stderr, unless you specifically change that.
So running
./main provide apps something 2> ./stderr.txt creates a text file with the following content (this is what cobra writes to stderr without your intervention):
Error: app name doesnt exist
And running ./main provide apps something > ./stdout.txt - creates a text file with the following content (you printed that yourself with fmt.Println(err), the second line from the bottom in your code):
app name doesnt exist
Which means default behaviour prints errors both to stdout and stderr.
As Devin has advised you, changing the last line to os.Stderr.WriteString(err) or
fmt.Fprintln(os.Stderr, err) (the one I would use) will make your project to print everything to stderr only, which means printing errors twice:
Error: app name doesnt exist
app name doesnt exist
It might be useful to know that cobra allows you some control of error printing behaviour. For example, you can tell a cobra command which stream to print to:
command.SetOutput(os.Stdout) // Defaults to os.Stderr
you could also prevent printing of errors:
command.SilenceErrors = true
or prevent printing of usage text:
command.SilenceUsage = true

Issue with golang flags

So recently I ran into a problem with Golang flags and was curious to know if this is as intended, if its a problem or if I'm being a blithering idiot and there is a very simple answer to this.
So using the following code:
func main() {
test := flag.String("-test", "", "test var")
flag.Parse()
if *test != "" {
fmt.Println(*test)
}
}
And then run it using the following command ./main -test 1
You get the following autogenerated error:
flag provided but not defined: -test
Usage of ./main:
--test string
test var
The same happens if you then use ./main --test 1, you get the same error. The only way I have found around this is to change the flag definition to be test := flag.String("test", "", "test var") and then run it with ./main -test 1.
So my question is why can you not use double hyphens with flags? If you can, where did I go wrong when doing this?
You can use double hyphens and this is documented in the package here:
One or two minus signs may be used; they are equivalent.
Having declared your flags, flag.Parse() will try to remove one or two hyphens from the flags passed and use the remaining characters of each flag to detect whether one has been declared or not. This is done internally by the flag package in parseOne().
Declaring a flag with name -test will literally map it as is, resulting in flag.Parse() internally looking for the name test instead of -test which is the one you actually declared, resulting in the error you see.
Instead, use only the name of the flag when declaring one:
test := flag.String("test", "", "test var")
and you can use this flag with both one (./main -test 1) or two hyphens (./main --test 2).
Try to define the flag without -:
func main() {
test := flag.String("test", "", "test var")
flag.Parse()
if *test != "" {
fmt.Println(*test)
}
}
For us the flag package is probably not the nicest thing to use here.
Instead of that, you might want to try getopt package owned by Google, which provides the more standard command line parsing (vs the flag package).
package main
import (
"fmt"
"os"
"github.com/pborman/getopt"
)
func main() {
optName := getopt.StringLong("name", 'n', "", "Your name")
optHelp := getopt.BoolLong("test", 0, "test var")
getopt.Parse()
if *optHelp {
getopt.Usage()
os.Exit(0)
}
fmt.Println("Hello " + *optName + "!")
}
$ ./hello --help
Usage: hello [--test] [-n value] [parameters ...]
--test test var
-n, --name=value Your name
$ ./hello --name Bob
Hello Bob!

Resources