luci: go.chromium.org/luci/common/flag/multiflag Index | Examples | Files

package multiflag

import "go.chromium.org/luci/common/flag/multiflag"

Package multiflag is a package providing a flag.Value implementation capable of switching between multiple registered sub-flags, each of which have their own set of parameter flags.

Example

This can be used to construct complex option flags. For example:

-backend mysql,address="192.168.1.1",port="12345"
-backend sqlite3,path="/path/to/database"

In this example, a MultiFlag is defined and bound to the option name, "backend". This MultiFlag has (at least) two registered Options:

1) mysql, whose FlagSet binds "address" and "port" options.
2) sqlite3, whose FlagSet binds "path".

The MultiFlag Option that is selected (e.g., "mysql") has the remainder of its option string parsed into its FlagSet, populating its "address" and "port" parameters. If "sqlite3" is selected, the remainder of the option string would be parsed into its FlagSet, which hosts the "path" parameter.

Index

Examples

Package Files

multiflag.go

type FlagOption Uses

type FlagOption struct {
    Name        string
    Description string
    Pinned      bool
    // contains filtered or unexported fields
}

FlagOption is an implementation of Option that is describes a single nestedflagset option. This option has sub-properties that

func (*FlagOption) Descriptor Uses

func (o *FlagOption) Descriptor() *OptionDescriptor

Descriptor implements Option.

func (*FlagOption) Flags Uses

func (o *FlagOption) Flags() *flag.FlagSet

Flags returns this Option's nested FlagSet for configuration.

func (*FlagOption) IsPinned Uses

func (o *FlagOption) IsPinned() bool

IsPinned implements Option.

func (*FlagOption) PrintHelp Uses

func (o *FlagOption) PrintHelp(output io.Writer)

PrintHelp implements Option.

func (*FlagOption) Run Uses

func (o *FlagOption) Run(value string) error

Run implements Option.

type MultiFlag Uses

type MultiFlag struct {
    Description string
    Options     []Option
    Output      io.Writer // Output writer, or nil to use STDERR.

    // The selected Option, populated after Parsing.
    Selected Option
}

MultiFlag is a flag.Value-like object that contains multiple sub-options. Each sub-option presents itself as a flag.FlagSet. The sub-option that gets selected will have its FlagSet be evaluated against the accompanying options.

For example, one can construct flag that, depending on its options, chooses one of two sets of sub-properties:

-myflag foo,foovalue=123
-myflag bar,barvalue=456,barothervalue="hello"

"myflag" is the name of the MultiFlag's top-level flag, as registered with a flag.FlagSet. The first token in the flag's value selects which Option should be configured. If "foo" is configured, the remaining configuration is parsed by the "foo" Option's FlagSet, and the equivalent is true for "bar".

Code:

package main

import (
    "bytes"
    "errors"
    "flag"
    "fmt"
    "os"
    "sort"
    "testing"

    . "github.com/smartystreets/goconvey/convey"
)

type testMultiFlag struct {
    MultiFlag

    S   string
    I   int
    B   bool
}

func (of *testMultiFlag) newOption(name, description string) Option {
    o := &FlagOption{Name: name, Description: description}

    flags := o.Flags()
    flags.StringVar(&of.S, "string-var", "", "A string variable.")
    flags.IntVar(&of.I, "int-var", 123, "An integer variable.")
    flags.BoolVar(&of.B, "bool-var", false, "A boolean variable.")

    // Set our option name.
    return o
}

// A Writer implementation that controllably fails.
type failWriter struct {
    Max   int   // When failing, never "write" more than this many bytes.
    Error error // The error to return when used to write.
}

// Implements io.Writer.
func (w *failWriter) Write(data []byte) (int, error) {
    size := len(data)
    if w.Error != nil && size > w.Max {
        size = w.Max
    }
    return size, w.Error
}

func TestParsing(t *testing.T) {
    Convey("Given empty MultiFlag", t, func() {
        of := &testMultiFlag{}

        Convey("Its option list should be empty", func() {
            So(of.OptionNames(), ShouldBeEmpty)
        })

        Convey(`Parsing an option spec with an empty option value should fail.`, func() {
            So(of.Parse(`,params`), ShouldNotBeNil)
        })
    })

    Convey("Given Flags with two option values", t, func() {
        of := &testMultiFlag{}
        of.Options = []Option{
            of.newOption("foo", "Test option 'foo'."),
            of.newOption("bar", "Test option 'bar'."),
        }

        Convey("Its option list should be: ['foo', 'bar'].", func() {
            So(of.OptionNames(), ShouldResemble, []string{"foo", "bar"})
        })

        Convey(`When parsing 'foo,string-var="hello world"'`, func() {
            err := of.Parse(`foo,string-var="hello world",bool-var=true`)
            So(err, ShouldBeNil)

            Convey("The option, 'foo', should be selected.", func() {
                So(of.Selected, ShouldNotBeNil)
                So(of.Selected.Descriptor().Name, ShouldEqual, "foo")
            })

            Convey(`The value of 'string-var' should be, "hello world".`, func() {
                So(of.S, ShouldEqual, "hello world")
            })

            Convey(`The value of 'int-var' should be default (123)".`, func() {
                So(of.I, ShouldEqual, 123)
            })

            Convey(`The value of 'bool-var' should be "true".`, func() {
                So(of.B, ShouldEqual, true)
            })
        })
    })
}

func TestHelp(t *testing.T) {
    Convey(`A 'MultiFlag' instance`, t, func() {
        of := &MultiFlag{}

        Convey(`Uses os.Stderr for output.`, func() {
            So(of.GetOutput(), ShouldEqual, os.Stderr)
        })

        Convey(`Configured with a simple FlagOption with flags`, func() {
            opt := &FlagOption{
                Name:        "foo",
                Description: "An option, 'foo'.",
            }
            opt.Flags().String("bar", "", "An option, 'bar'.")
            of.Options = []Option{opt}

            Convey(`Should successfully parse "foo" with no flags.`, func() {
                So(of.Parse(`foo`), ShouldBeNil)
            })

            Convey(`Should successfully parse "foo" with a "bar" flag.`, func() {
                So(of.Parse(`foo,bar="Hello!"`), ShouldBeNil)
            })

            Convey(`Should fail to parse a non-existent flag.`, func() {
                So(of.Parse(`foo,baz`), ShouldNotBeNil)
            })
        })
    })

    Convey(`Given a testMultiFlag configured with 'nil' output.`, t, func() {
        of := &testMultiFlag{}

        Convey(`Should default to os.Stderr`, func() {
            So(of.GetOutput(), ShouldEqual, os.Stderr)
        })
    })

    Convey("Given Flags with two options, one of which is 'help'", t, func() {
        var buf bytes.Buffer
        of := &testMultiFlag{}
        of.Output = &buf
        of.Options = []Option{
            HelpOption(&of.MultiFlag),
            of.newOption("foo", "Test option 'foo'."),
        }

        correctHelpString := `
help  Displays this help message. Can be run as "help,<option>" to display help for an option.
foo   Test option 'foo'.
`

        Convey("Should print a correct help string", func() {
            err := of.PrintHelp()
            So(err, ShouldBeNil)

            So(buf.String(), ShouldEqual, correctHelpString)
        })

        Convey(`Should fail to print a help string when the writer fails.`, func() {
            w := &failWriter{}
            w.Error = errors.New("fail")
            of.Output = w
            So(of.PrintHelp(), ShouldNotBeNil)
        })

        Convey(`Should parse a request for a specific option's help string.`, func() {
            err := of.Parse(`help,foo`)
            So(err, ShouldBeNil)

            Convey(`And should print help for that option.`, func() {
                correctOptionHelpString := `Help for 'foo': Test option 'foo'.
  -bool-var
    	A boolean variable.
  -int-var int
    	An integer variable. (default 123)
  -string-var string
    	A string variable.
`
                So(buf.String(), ShouldEqual, correctOptionHelpString)
            })
        })

        Convey("Should parse the 'help' option", func() {
            err := of.Parse(`help`)
            So(err, ShouldBeNil)

            Convey("Should print the correct help string in response.", func() {
                So(buf.String(), ShouldEqual, correctHelpString)
            })
        })

        Convey("Should parse 'help,junk=1'", func() {
            err := of.Parse(`help,junk=1`)
            So(err, ShouldBeNil)

            Convey(`Should notify the user that "junk=1" is not an option.`, func() {
                So(buf.String(), ShouldEqual, "Unknown option 'junk=1'\n")
            })
        })
    })
}

func TestHelpItemSlice(t *testing.T) {
    Convey(`Given a slice of testOption instances`, t, func() {
        options := optionDescriptorSlice{
            &OptionDescriptor{
                Name:        "a",
                Description: "An unpinned help item",
                Pinned:      false,
            },
            &OptionDescriptor{
                Name:        "b",
                Description: "Another unpinned help item",
                Pinned:      false,
            },
            &OptionDescriptor{
                Name:        "c",
                Description: "A pinned help item",
                Pinned:      true,
            },
            &OptionDescriptor{
                Name:        "d",
                Description: "Another pinned help item",
                Pinned:      true,
            },
        }
        sort.Sort(options)

        Convey(`The options should be sorted: c, b, a, b`, func() {
            var names []string
            for _, opt := range options {
                names = append(names, opt.Name)
            }
            So(names, ShouldResemble, []string{"c", "d", "a", "b"})
        })
    })
}

func TestFlagParse(t *testing.T) {
    Convey("Given a MultiFlag with one option, 'foo'", t, func() {
        of := &testMultiFlag{}
        of.Options = []Option{
            of.newOption("foo", "Test option 'foo'."),
        }

        Convey("When configured as an output to a Go flag.MultiFlag", func() {
            gof := &flag.FlagSet{}
            gof.Var(of, "option", "Single line option")

            Convey(`Should parse '-option foo,string-var="hello world",int-var=1337'`, func() {
                err := gof.Parse([]string{"-option", `foo,string-var="hello world",int-var=1337"`})
                So(err, ShouldBeNil)

                Convey("Should parse out 'foo' as the option", func() {
                    So(of.Selected.Descriptor().Name, ShouldEqual, "foo")
                })

                Convey(`Should parse out 'string-var' as "hello world".`, func() {
                    So(of.S, ShouldEqual, "hello world")
                })

                Convey(`Should parse out 'int-var' as '1337'.`, func() {
                    So(of.I, ShouldEqual, 1337)
                })
            })

            Convey(`When parsing 'bar'`, func() {
                err := gof.Parse([]string{"-option", "bar"})
                So(err, ShouldNotBeNil)
            })
        })
    })
}

func main() {
    // Setup multiflag.
    param := ""
    deprecated := FlagOption{
        Name:        "deprecated",
        Description: "The deprecated option.",
    }
    deprecated.Flags().StringVar(&param, "param", "", "String parameter.")

    beta := FlagOption{Name: "beta", Description: "The new option, which is still beta."}
    beta.Flags().StringVar(&param, "param", "", "Beta string parameter.")

    mf := MultiFlag{
        Description: "My test MultiFlag.",
        Output:      os.Stdout,
    }
    mf.Options = []Option{
        HelpOption(&mf),
        &deprecated,
        &beta,
    }

    // Install the multiflag as a flag in "flags".
    fs := flag.NewFlagSet("test", flag.ContinueOnError)
    fs.Var(&mf, "multiflag", "Multiflag option.")

    // Parse flags (help).
    cmd := []string{"-multiflag", "help"}
    fmt.Println("Selecting help option:", cmd)
    if err := fs.Parse(cmd); err != nil {
        panic(err)
    }

    // Parse flags (param).
    cmd = []string{"-multiflag", "beta,param=Sup"}
    fmt.Println("Selecting beta option:", cmd)
    if err := fs.Parse(cmd); err != nil {
        panic(err)
    }
    fmt.Printf("Option [%s], parameter: [%s].\n", mf.Selected.Descriptor().Name, param)

}

func (*MultiFlag) GetOptionFor Uses

func (mf *MultiFlag) GetOptionFor(name string) Option

GetOptionFor returns the Option associated with the specified name, or nil if one isn't defined.

func (*MultiFlag) GetOutput Uses

func (mf *MultiFlag) GetOutput() io.Writer

GetOutput returns the output Writer used for help output.

func (MultiFlag) OptionNames Uses

func (mf MultiFlag) OptionNames() []string

OptionNames returns a list of the option names registered with a MultiFlag.

func (*MultiFlag) Parse Uses

func (mf *MultiFlag) Parse(value string) error

Parse applies a value string to a MultiFlag.

For example, if the value string is: foo,option1=test

Parse will identify the MultiFlag option named "foo" and have it parse the string, "option1=test".

func (*MultiFlag) PrintHelp Uses

func (mf *MultiFlag) PrintHelp() error

PrintHelp prints a formatted help string for a MultiFlag. This help string details the Options registered with the MultiFlag.

func (*MultiFlag) Set Uses

func (mf *MultiFlag) Set(value string) error

Set implements flag.Value

func (*MultiFlag) String Uses

func (mf *MultiFlag) String() string

String implements flag.Value

type Option Uses

type Option interface {
    Descriptor() *OptionDescriptor

    PrintHelp(io.Writer)
    Run(string) error // Parses the Option from a configuration string.
}

Option is a single option entry in a MultiFlag. An Option is responsible for parsing a FlagSet from an option string.

func HelpOption Uses

func HelpOption(mf *MultiFlag) Option

HelpOption instantiates a new Option instance that prints a help string when parsed.

type OptionDescriptor Uses

type OptionDescriptor struct {
    Name        string
    Description string

    Pinned bool
}

OptionDescriptor is a collection of common Option properties.

Package multiflag imports 9 packages (graph) and is imported by 1 packages. Updated 2020-04-07. Refresh now. Tools for package owners.