Validating flags using Cobra - go

The sketch below is a command line application written using Cobra and Go. I'd like to throw an error if the value of flag1 doesn't match the regex ^\s+\/\s+. How do I do that?
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
)
var flag1 string
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "cobra-sketch",
Short: "Sketch for Cobra flags",
Long: "Sketch for Cobra flags",
Run: func(cmd *cobra.Command, args []string) { fmt.Printf("Flag1 is %s\n", flag1)},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
cobra.CheckErr(rootCmd.Execute())
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra-sketch.yaml)")
rootCmd.PersistentFlags().StringVar(&flag1, "flag1", "", "Value of Flag 1")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
cobra.CheckErr(err)
// Search config in home directory with name ".cobra-sketch" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".cobra-sketch")
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}

Let's say a user runs the command like this: cobra-sketch --flag1 "hello". "hello" will be stored in the var flag1 string variable you have assigned to the flag, to check if the input matches any regexp, you can do:
var rootCmd = &cobra.Command{
Use: "cobra-sketch",
...
RunE: func(cmd *cobra.Command, args []string) error {
// You can also use MustCompile if you are sure the regular expression
// is valid, it panics instead of returning an error
re, err := regexp.Compile(`^\s+\/\s+`)
if err != nil {
return err // Handle error
}
if !regexp.MatchString(flag1) {
return fmt.Errorf("invalid value: %q", flag1)
}
fmt.Printf("Flag1 is %s\n", flag1)
return nil
},
}

Related

Test command line arguments

Ho do you test command line arguments?
I can do this:
func TestMainFunc(t *testing.T) {
_, filename, _, _ := runtime.Caller(0)
dir := filepath.Dir(filename)
os.Args[1] = dir
main()
}
But then I override "test.v" in an argument list of:
os.Args[0]={string} "/private/var/folder/.../___appname.go"
os.Args[1]={string} "test.v"
os.Args[2]={string} "-test.panicontext0"
os.Args[3]={string} "^QTestMainFunc\E$"
I have no idea what these arguments are doing, but I suspect it is not good to override any of them - so how can you test arguments?
To expand upon Adrian's comment a bit, I would separate flag parsing from configuration entirely. Here's an example:
type Config struct {
verbose bool
greeting string
level int
}
func parseArgs(progname string, args []string) (config *Config, output string, err error) {
flags := flag.NewFlagSet(progname, flag.ContinueOnError)
var buf bytes.Buffer
flags.SetOutput(&buf)
var conf Config
flags.BoolVar(&conf.verbose, "verbose", false, "set verbosity")
flags.StringVar(&conf.greeting, "greeting", "", "set greeting")
flags.IntVar(&conf.level, "level", 0, "set level")
err = flags.Parse(args)
if err != nil {
return nil, buf.String(), err
}
return &conf, "", nil
}
The parseArgs function parses your program's configuration from command-line arguments, and is easily testable in isolation from the rest of the program.
So the flow in main would be:
func main() {
conf, output, err := parseArgs(os.Args[0], os.Args[1:])
if err != nil {
// ...
}
realMain(conf)
}
And now you can test parseArgs and realMain completely separately in unit tests.
This also opens up the possibility to have multiple sources for configuration: e.g. you could also read it from env vars or config files, or command-line. Multiple readers could populate a Config struct, and all would be testable in isolation.

How can I invoke a default subcommand with cobra?

Using cobra, if my app is invoked without a specific action (but arguments), I'd like to run a default command:
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "mbmd",
Short: "ModBus Measurement Daemon",
Long: "Easily read and distribute data from ModBus meters and grid inverters",
Run: func(cmd *cobra.Command, args []string) {
run(cmd, args)
},
}
However, since the root command doesn't have all arguments the child command has this fails as it's apparently now aware of the child command's arguments:
❯ go run main.go -d sma:126#localhost:5061 --api 127.1:8081 -v
Error: unknown shorthand flag: 'd' in -d
as opposed to:
❯ go run main.go run -d sma:126#localhost:5061 --api 127.1:8081 -v
2019/07/29 20:58:10 mbmd unknown version (unknown commit)
How can I programmatically instantiate/invoke a child command?
Here is another solution:
cmd, _, err := rootCmd.Find(os.Args[1:])
// default cmd if no cmd is given
if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp {
args := append([]string{defaultCmd.Use}, os.Args[1:]...)
rootCmd.SetArgs(args)
}
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
Replace defaultCmd with one you want to be default
This part cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp keeps help command working for root command if no arguments was set
March 2021: You might consider a workaround as the one presented in spf13/cobra issue 823
func subCommands() (commandNames []string) {
for _, command := range cmd.Commands() {
commandNames = append(commandNames, append(command.Aliases, command.Name())...)
}
return
}
func setDefaultCommandIfNonePresent() {
if len(os.Args) > 1 {
potentialCommand := os.Args[1]
for _, command := range subCommands() {
if command == potentialCommand {
return
}
}
os.Args = append([]string{os.Args[0], "<default subcommand>"}, os.Args[1:]...)
}
}
func main() {
setDefaultCommandIfNonePresent()
if err := cmd.Execute(); err != nil {
zap.S().Error(err)
os.Exit(1)
}
}
The difference here is that it checks if len(os.Args) > 1 before changing the default subcommand.
This means that, if ran without any arguments, it will print the default help command (with all of the subcommands).
Otherwise, if supplied any arguments, it will use the subcommand.
So, it will display the main 'help' without arguments, and the subcommand's help if supplied '-h'/'--help'.
Or (Oct. 2021), from the author of PR 823:
Latest solve for this is the following:
main.go
func main() {
// Define the default sub command 'defCmd' here. If user doesn't submit
// using a default command, we'll use what is here.
defCmd:="mydefaultcmd"
cmd.Execute(defCmd)
}
root.go
func Execute(defCmd string) {
var cmdFound bool
cmd :=rootCmd.Commands()
for _,a:=range cmd{
for _,b:=range os.Args[1:] {
if a.Name()==b {
cmdFound=true
break
}
}
}
if !cmdFound {
args:=append([]string{defCmd}, os.Args[1:]...)
rootCmd.SetArgs(args)
}
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

Inside kubectl plugin, prompt for input?

I'm writing a kubectl plugin to authenticate users, and I would like to prompt the user for a password after the plugin is invoked. From what I understand, it's fairly trivial to get input from STDIN, but I'm struggling seeing messages written to STDOUT. Currently my code looks like this:
In cmd/kubectl-myauth.go:
// This is mostly boilerplate, but it's needed for the MRE
// https://stackoverflow.com/help/minimal-reproducible-example
package myauth
import (...)
func main() {
pflag.CommandLine = pflag.NewFlagSet("kubectl-myauth", pflag.ExitOnError)
root := cmd.NewCmdAuthOp(genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr})
if err := root.Execute(); err != nil {
os.Exit(1)
}
}
In pkg/cmd/auth.go:
package cmd
...
type AuthOpOptions struct {
configFlags *genericclioptions.ConfigFlags
resultingContext *api.Context
rawConfig api.Config
args []string
...
genericclioptions.IOStreams
}
func NewAuthOpOptions(streams genericclioptions.IOStreams) *AuthOpOptions {
return &AuthOpOptions{
configFlags: genericclioptions.NewConfigFlags(true),
IOStreams: streams,
}
}
func NewCmdAuthOp(streams genericclioptions.IOStreams) *cobra.Command {
o := NewAuthOpOptions(streams)
cmd := &cobra.Command{
RunE: func(c *cobra.Command, args []string) error {
return o.Run()
},
}
return cmd
}
func (o *AuthOpOptions) Run() error {
pass, err := getPassword(o)
if err != nil {
return err
}
// Do Auth Stuff
// Eventually print an ExecCredential to STDOUT
return nil
}
func getPassword(o *AuthOpOptions) (string, error) {
var reader *bufio.Reader
reader = nil
pass := ""
for pass == "" {
// THIS IS AN IMPORTANT LINE [1]
fmt.Fprintf(o.IOStreams.Out, "Password with which to authenticate:\n")
// THE REST OF THIS IS STILL IMPORTANT, BUT LESS SO [2]
if reader == nil {
// The first time through, initialize the reader
reader = bufio.NewReader(o.IOStreams.In)
}
pass, err := reader.ReadString('\n')
if err != nil {
return nil, err
}
pass = strings.Trim(pass, "\r\n")
if pass == "" {
// ALSO THIS LINE IS IMPORTANT [3]
fmt.Fprintf(o.IOStreams.Out, `Read password was empty string.
Please input a valid password.
`)
}
}
return pass, nil
}
This works the way that I expect when running from outside of the kubectl context - namely, it prints the string, prompts for input, and continues. However, from inside the kubectl context, I believe the print between the first two all-caps comments ([1] and [2]) is being swallowed by kubectl listening on STDOUT. I can get around this by printing to STDERR, but that feels... wrong. Is there a way that I can bypass kubectl's consumption of STDOUT to communicate with the user?
TL;DR: kubectl appears to be swallowing all of STDOUT for kubectl plugins, but I want to prompt the user for input - is there a simple way to do this?
Sorry I have no better answer than "Works for me" :-) Here are the steps:
git clone https://github.com/kubernetes/kubernetes.git
duplicate sample-cli-plugin as test-cli-plugin (this involves fixing import-restrictions.yaml, rules-godeps.yaml and rules.yaml under staging/publishing - maybe not necessary, but it's safer this way)
change kubectl-ns.go to kubectl-test.go:
package main
import (
"os"
"github.com/spf13/pflag"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/test-cli-plugin/pkg/cmd"
)
func main() {
flags := pflag.NewFlagSet("kubectl-test", pflag.ExitOnError)
pflag.CommandLine = flags
root := cmd.NewCmdTest(genericclioptions.IOStreams{In: os.Stdin,
Out: os.Stdout,
ErrOut: os.Stderr})
if err := root.Execute(); err != nil {
os.Exit(1)
}
}
change ns.go to test.go:
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
type TestOptions struct {
configFlags *genericclioptions.ConfigFlags
genericclioptions.IOStreams
}
func NewTestOptions(streams genericclioptions.IOStreams) *TestOptions {
return &TestOptions{
configFlags: genericclioptions.NewConfigFlags(true),
IOStreams: streams,
}
}
func NewCmdTest(streams genericclioptions.IOStreams) *cobra.Command {
o := NewTestOptions(streams)
cmd := &cobra.Command{
Use: "test",
Short: "Test plugin",
SilenceUsage: true,
RunE: func(c *cobra.Command, args []string) error {
o.Run()
return nil
},
}
return cmd
}
func (o *TestOptions) Run() error {
fmt.Fprintf(os.Stderr, "Testing Fprintf Stderr\n")
fmt.Fprintf(os.Stdout, "Testing Fprintf Stdout\n")
fmt.Printf("Testing Printf\n")
fmt.Fprintf(o.IOStreams.Out, "Testing Fprintf o.IOStreams.Out\n")
return nil
}
fix BUILD files accordingly
build the plugin
run make
copy kubectl-test to /usr/local/bin
run the compiled kubectl binary:
~/k8s/_output/bin$ ./kubectl test
Testing Fprintf Stderr
Testing Fprintf Stdout
Testing Printf
Testing Fprintf o.IOStreams.Out

Golang patterns for stdin testing

EDIT: Adrian's suggestion makes sense, so I moved my code into a function and called the function from my cobra block:
package cmd
import (
"fmt"
"log"
"os"
"io"
"github.com/spf13/cobra"
"github.com/spf13/viper"
input "github.com/tcnksm/go-input"
)
var configureCmd = &cobra.Command{
Use: "configure",
Short: "Configure your TFE credentials",
Long: `Prompts for your TFE API credentials, then writes them to
a configuration file (defaults to ~/.tgc.yaml`,
Run: func(cmd *cobra.Command, args []string) {
CreateConfigFileFromPrompts(os.Stdin, os.Stdout)
},
}
func CreateConfigFileFromPrompts(stdin io.Reader, stdout io.Writer) {
ui := &input.UI{
Writer: stdout,
Reader: stdin,
}
tfeURL, err := ui.Ask("TFE URL:", &input.Options{
Default: "https://app.terraform.io",
Required: true,
Loop: true,
})
if err != nil {
log.Fatal(err)
}
viper.Set("tfe_url", tfeURL)
tfeAPIToken, err := ui.Ask(fmt.Sprintf("TFE API Token (Create one at %s/app/settings/tokens)", tfeURL), &input.Options{
Default: "",
Required: true,
Loop: true,
Mask: true,
MaskDefault: true,
})
if err != nil {
log.Fatal(err)
}
viper.Set("tfe_api_token", tfeAPIToken)
configPath := ConfigPath()
viper.SetConfigFile(configPath)
err = viper.WriteConfig()
if err != nil {
log.Fatal("Failed to write to: ", configPath, " Error was: ", err)
}
fmt.Println("Saved to", configPath)
}
So what can I pass to this method to test that the output is as expected?
package cmd
import (
"strings"
"testing"
)
func TestCreateConfigFileFromPrompts(t *testing.T) {
// How do I pass the stdin and out to the method?
// Then how do I test their contents?
// CreateConfigFileFromPrompts()
}
func TestCreateConfigFileFromPrompts(t *testing.T) {
var in bytes.Buffer
var gotOut, wantOut bytes.Buffer
// The reader should read to the \n each of two times.
in.Write([]byte("example-url.com\nexampletoken\n"))
// wantOut could just be []byte, but for symmetry's sake I've used another buffer
wantOut.Write([]byte("TFE URL:TFE API Token (Create one at example-url.com/app/settings/tokens)"))
// I don't know enough about Viper to manage ConfigPath()
// but it seems youll have to do it here somehow.
configFilePath := "test/file/location"
CreateConfigFileFromPrompts(&in, &gotOut)
// verify that correct prompts were sent to the writer
if !bytes.Equal(gotOut.Bytes(), wantOut.Bytes()) {
t.Errorf("Prompts = %s, want %s", gotOut.Bytes(), wantOut.Bytes())
}
// May not need/want to test viper's writing of the config file here, or at all, but if so:
var fileGot, fileWant []byte
fileWant = []byte("Correct Config file contents:\n URL:example-url.com\nTOKEN:exampletoken")
fileGot, err := ioutil.ReadFile(configFilePath)
if err != nil {
t.Errorf("Error reading config file %s", configFilePath)
}
if !bytes.Equal(fileGot, fileWant) {
t.Errorf("ConfigFile: %s not created correctly got = %s, want %s", configFilePath, fileGot, fileWant)
}
}
As highlighted by #zdebra in comments to his answer, the go-input package is panicing and giving you the error: Reader must be a file. If you are married to using that package, you can avoid the problem by disabling the masking option on the ui.Ask for your second input:
tfeAPIToken, err := ui.Ask(fmt.Sprintf("TFE API Token (Create one at %s/app/settings/tokens)", tfeURL), &input.Options{
Default: "",
Required: true,
Loop: true,
//Mask: true, // if this is set to True, the input must be a file for some reason
//MaskDefault: true,
})
The reader and the writer need to be set up before the tested function is called. After is called, the result is written into the writer where it should be verified.
package cmd
import (
"strings"
"testing"
)
func TestCreateConfigFileFromPrompts(t *testing.T) {
in := strings.NewReader("<your input>") // you can use anything that satisfies io.Reader interface here
out := new(strings.Builder) // you could use anything that satisfies io.Writer interface here like bytes.Buffer
CreateConfigFileFromPrompts(in, out)
// here you verify the output written into the out
expectedOutput := "<your expected output>"
if out.String() != expectedOutput {
t.Errorf("expected %s to be equal to %s", out.String(), expectedOutput)
}
}

How to make subcommand required in Go CLI using cobra

I am using Cobra to make some cli updated to my app. I want to make this command required, meaning the application should fail if it doesn't find the argument it is looking for.
package commands
import (
"github.com/spf13/cobra"
"errors"
"fmt"
)
var (
Env string
)
var RootCmd = &cobra.Command{
Use: "myapp",
Short: "tool",
Long: `tool`,
Run: func(cmd *cobra.Command, args []string) {
// Root command does nothing
},
}
func init() {
RootCmd.AddCommand(Environment)
}
var Environment = &cobra.Command{
Use: "env",
Short: "Specify Environment to run against",
Long: `Can be dev or prod`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("requires at least one arg")
}
if args[0] == "dev" || args[0] == "prod" {
return nil
}else {
return errors.New("input can only be dev or prod")
}
return fmt.Errorf("invalid env specified: %s", args[0])
},
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("env is required")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
Env = args[0]
},
}
and main package is
package main
import (
"fmt"
"log"
"os"
"util"
"commands"
)
func main() {
log.Println("Executing")
if err := commands.RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
log.Println("Executing")
}
Now if I run this as ./myApp without any env, it doesn't complain about it. However if I use env after myapp then it activates the function and runs all the validations.
You can just make the body of the function handle it, perhaps by printing help and exiting as non-successful:
Run: func(cmd *cobra.Command, args []string) {
// Root command does nothing
cmd.Help()
os.Exit(1)
},
Omitting the Run (and RunE) field from the cobra.Command will make it a requirement for a valid subcommand to be given:
var RootCmd = &cobra.Command{
Use: "myapp",
Short: "tool",
Long: `tool long help...`,
}
If no subcommand is given on the command line, Cobra will print out the command's Help() text , which will include the root command's Long help text and the autogenerated usage help for all subcommands.

Resources