Subcommand flags (options) - go

I'm trying to create a CLI application that will accept few arguments based on the subcommand by using the standard flag package. This is the code I'm trying to use:
package main
import (
"flag"
"fmt"
"os"
)
func main() {
fmt.Printf("start application\n")
fooCmd := flag.NewFlagSet("foo", flag.ExitOnError)
fooName := fooCmd.String("name", "", "name")
barCmd := flag.NewFlagSet("bar", flag.ExitOnError)
barLevel := barCmd.Int("level", 0, "level")
if len(os.Args) < 2 {
fmt.Println("expected 'foo' or 'bar' subcommands")
os.Exit(1)
}
switch os.Args[1] {
case "foo":
fooCmd.Parse(os.Args[2:])
fmt.Println(" name:", *fooName)
case "bar":
barCmd.Parse(os.Args[2:])
fmt.Println(" level:", *barLevel)
default:
fmt.Println("expected 'foo' or 'bar' subcommands")
os.Exit(1)
}
}
and by calling it with:
$ ./main foo -name=test
It's working as expected.
The issue is that I want to have another flag (let's say --loglevel=Debug) which should be called for any of those subcommands (foo/bar), something like:
$ ./main foo -name=test -loglevel=debug
One option would be to create the same flag (loglevel) for any of those subcommands, but I just wonder is there any other way to achieve this without duplicating the code?
In my case, I have about 6 subcommands and 4 "general" flags.

That is why I prefer using a third-party library, rather than the default flags package.
For, instance, with alecthomas/kong:
func TestPropagatedFlags(t *testing.T) {
var cli struct {
Flag1 string
Command1 struct {
Flag2 bool
Command2 struct{} `kong:"cmd"`
} `kong:"cmd"`
}
parser := mustNew(t, &cli)
_, err := parser.Parse([]string{"command-1", "command-2", "--flag-2", "--flag-1=moo"})
require.NoError(t, err)
require.Equal(t, "moo", cli.Flag1)
require.Equal(t, true, cli.Command1.Flag2)
}
Flag1 is global, and would apply to any subcommand.

Related

Global flags and subcommands

I'm implementing a little CLI with multiple subcommands. I'd like to support global flags, that is flags that apply to all subcommands to avoid repeating them.
For example, in the example below I'm trying to have -required flag that is required for all subcommands.
package main
import (
"flag"
"fmt"
"log"
"os"
)
var (
required = flag.String(
"required",
"",
"required for all commands",
)
fooCmd = flag.NewFlagSet("foo", flag.ExitOnError)
barCmd = flag.NewFlagSet("bar", flag.ExitOnError)
)
func main() {
flag.Parse()
if *required == "" {
fmt.Println("-required is required for all commands")
}
switch os.Args[1] {
case "foo":
fooCmd.Parse(os.Args[2:])
fmt.Println("foo")
case "bar":
barCmd.Parse(os.Args[2:])
fmt.Println("bar")
default:
log.Fatalf("[ERROR] unknown subcommand '%s', see help for more details.", os.Args[1])
}
}
I would expect usage to be like:
$ go run main.go foo -required helloworld
but if I ran that with the above code I get:
$ go run main.go foo -required hello
-required is required for all commands
flag provided but not defined: -required
Usage of foo:
exit status 2
It looks like flag.Parse() is not capturing -required from the CLI, and then the fooCmd is complaining that I've given it a flag it doesn't recognize.
What's the easiest way to have subcommands with global flags in Golang?
If you intend to implement subcommands, you shouldn't call flag.Parse().
Instead decide which subcommand to use (as you did with os.Args[1]), and call only its FlagSet.Parse() method.
Yes, for this to work, all flag sets should contain the common flags. But it's easy to register them once (in one place). Create a package level variable:
var (
required string
fooCmd = flag.NewFlagSet("foo", flag.ExitOnError)
barCmd = flag.NewFlagSet("bar", flag.ExitOnError)
)
And use a loop to iterate over all flagsets, and register the common flags, pointing to your variable using FlagSet.StringVar():
func setupCommonFlags() {
for _, fs := range []*flag.FlagSet{fooCmd, barCmd} {
fs.StringVar(
&required,
"required",
"",
"required for all commands",
)
}
}
And in main() call Parse() of the appropriate flag set, and test required afterwards:
func main() {
setupCommonFlags()
switch os.Args[1] {
case "foo":
fooCmd.Parse(os.Args[2:])
fmt.Println("foo")
case "bar":
barCmd.Parse(os.Args[2:])
fmt.Println("bar")
default:
log.Fatalf("[ERROR] unknown subcommand '%s', see help for more details.", os.Args[1])
}
if required == "" {
fmt.Println("-required is required for all commands")
}
}
You can improve the above solution by creating a map of flag sets, so you can use that map to register common flags, and also to do the parsing.
Full app:
var (
required string
fooCmd = flag.NewFlagSet("foo", flag.ExitOnError)
barCmd = flag.NewFlagSet("bar", flag.ExitOnError)
)
var subcommands = map[string]*flag.FlagSet{
fooCmd.Name(): fooCmd,
barCmd.Name(): barCmd,
}
func setupCommonFlags() {
for _, fs := range subcommands {
fs.StringVar(
&required,
"required",
"",
"required for all commands",
)
}
}
func main() {
setupCommonFlags()
cmd := subcommands[os.Args[1]]
if cmd == nil {
log.Fatalf("[ERROR] unknown subcommand '%s', see help for more details.", os.Args[1])
}
cmd.Parse(os.Args[2:])
fmt.Println(cmd.Name())
if required == "" {
fmt.Println("-required is required for all commands")
}
}
Put the global flags before the subcommand:
go run . -required=x foo.
Use flag.Args() instead of os.Args:
package main
import (
"flag"
"fmt"
"log"
"os"
)
var (
required = flag.String(
"required",
"",
"required for all commands",
)
fooCmd = flag.NewFlagSet("foo", flag.ExitOnError)
barCmd = flag.NewFlagSet("bar", flag.ExitOnError)
)
func main() {
flag.Parse()
if *required == "" {
fmt.Println("-required is required for all commands")
}
args := flag.Args() // everything after the -required flag, e.g. [foo, -foo-flag-1, -foo-flag-2, ...]
switch args[0] {
case "foo":
fooCmd.Parse(args[1:])
fmt.Println("foo")
case "bar":
barCmd.Parse(args[1:])
fmt.Println("bar")
default:
log.Fatalf("[ERROR] unknown subcommand '%s', see help for more details.", args[0])
}
}
If you want to keep all flags together, after the subcommand, write a helper function that adds common flags to each FlagSet:
var (
fooCmd = flag.NewFlagSet("foo", flag.ExitOnError)
barCmd = flag.NewFlagSet("bar", flag.ExitOnError)
)
type globalOpts struct {
required string
}
func main() {
var opts globalOpts
addGlobalFlags(fooCmd, &opts)
addGlobalFlags(barCmd, &opts)
if opts.required == "" {
fmt.Println("-required is required for all commands")
}
// ...
}
func addGlobalFlags(fs *flag.FlagSet, opts *globalOpts) {
fs.StringVar(
&opts.required,
"required",
"",
"required for all commands",
)
}
Perhaps you can also combine the two approaches to make the global flags work in any position.
Maybe you would be interested in using https://github.com/spf13/cobra - it supports exactly this usecase and many others.

Testing urfave/cli based applications with go

I'm writing a small CLI application in Golang using urfave/cli framework and I'd like to write tests for it, but I can't find any useful information on how to test CLI applications, specifically written with the urfave/cli library. I have a lot of flags in the application and some of them are mutually exclusive and I'd like a proper test to stay on top of them - does anyone have an idea how to do it the right way?
EDIT:
Consider the following minimal example of application with several flags and restrictions around them. How would you test these flags usage (requirements, exclusivity, etc.) and how they influence the functions when they're set or not?
package main
import (
"errors"
"fmt"
"os"
"github.com/urfave/cli"
)
func doSomething(flag1 string, flag2 string, flag3 bool, flag4 bool) error {
err := errors.New("something")
return err
}
func main() {
app := cli.NewApp()
app.Name = "greet"
app.Usage = "fight the loneliness!"
var flag1, flag2 string
var flag3, flag4 bool
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "flag1",
Value: "",
Usage: "flag1",
Destination: &flag1,
},
cli.StringFlag{
Name: "flag2",
Value: "",
Usage: "flag2",
Destination: &flag2,
},
cli.BoolFlag{
Name: "flag3",
Usage: "flag3",
Destination: &flag3,
},
cli.BoolFlag{
Name: "flag4",
Usage: "flag4",
Destination: &flag4,
},
}
app.Action = func(c *cli.Context) error {
if flag1 != "" && c.NumFlags() > 1 {
fmt.Println("--flag1 flag cannot be used with any other flags")
cli.ShowAppHelp(c)
os.Exit(1)
}
if flag1 == "" && flag2 == "" || c.NumFlags() < 1 {
fmt.Println("--flag2 is required")
cli.ShowAppHelp(c)
os.Exit(1)
}
if flag3 && flag4 {
fmt.Println("--flag3 and --flag4 flags are mutually exclusive")
cli.ShowAppHelp(c)
os.Exit(1)
}
err := doSomething(flag1, flag2, flag3, flag4)
return err
}
}
As Adrian correctly wrote
the same way you test anything else
Given a slightly modified example of the sample code of the project
package main
import (
"fmt"
"log"
"os"
"github.com/urfave/cli"
)
func Friend(c *cli.Context) error {
fmt.Println("Hello friend!")
return nil
}
func main() {
app := cli.NewApp()
app.Name = "greet"
app.Usage = "fight the loneliness!"
app.Action = Friend
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}
Since this code actually prints something instead of returning a value you can evaluate, you could use a testable example
func ExampleFriend(){
// Yeah, technically, we can save the error check with the code above
// but this illustrates how you can make sure the output
// is not what the testable example expects.
if err := Friend(nil){
fmt.Printf("Friend: %s",err)
}
// Output:
// Hello friend!
}
Note that Action expects an ActionFunc. Where you define that ActionFunc is pretty much your thing. It could even come from a different package. So it is your design on how good your application will be testable.
Edit The signature of the value Action expects will change in the future, at least according to the docs. I already find it questionable to use interface{} to be able to pass nil to Action, and then check and type assert for ActionFunc, where a no-op ActionFunc would actually serve the same purpose, but removing an error return value really makes me scratch my head. I strongly recommend to have a look at alecthomas/kingpin for smaller to medium size applications or spf13/cobra, which is suitable even for the most complex of cli applications.

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)
}
}

Hide golang messages

Is it possible to hide the golang messages? I show you an example:
package main
import (
"flag"
"fmt"
"os"
)
var signal = flag.String("z", "", "")
func main() {
flag.Usage = func() {
fmt.Printf("Usage: kata -z <command>\n\n")
fmt.Printf(" test\tTesting\n")
fmt.Printf(" version\tVersion\n")
fmt.Println("")
}
flag.Parse()
if len(os.Args) != 3 {
flag.Usage()
os.Exit(1)
}
switch *signal {
case "test":
fmt.Println("testing...")
case "version":
fmt.Println("0.0.1")
default:
fmt.Println("incorrect...")
}
}
This app show to user the next information:
https://play.golang.org/p/oYwADdmlAJ
But if I write in the command-line kata -flag, the system returns: flag needs an argument: or flag provided but not defined: and the information that I show you before.
I would like to know if it's possible to hide the golang messages?
P.S.: If you don't understand my question, I can rephrase.
Using the global functions in flag actually passes through to a global flag.FlagSet called flag.CommandLine. Internally, this prints errors to an output, which is stderr by default. You can suppress the messages by setting this explicitly to, for example, ioutil.Discard:
flag.CommandLine.SetOutput(ioutil.Discard)
flag.Parse()
This will discard all messages output internally by flag.Parse(). You could also log it to anywhere else you choose by passing in an appropriate io.Writer.
I have found a solution:
Before:
flag.Parse()
if len(os.Args) != 3 {
flag.Usage()
os.Exit(1)
}
Now:
if len(os.Args) != 3 || os.Args[1] != "-z" {
flag.Usage()
os.Exit(1)
} else {
flag.Parse()
}

Resources