go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/flag/multiflag/multiflag.go (about)

     1  // Copyright 2016 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package multiflag is a package providing a flag.Value implementation capable
    16  // of switching between multiple registered sub-flags, each of which have their
    17  // own set of parameter flags.
    18  //
    19  // # Example
    20  //
    21  // This can be used to construct complex option flags. For example:
    22  //
    23  //	-backend mysql,address="192.168.1.1",port="12345"
    24  //	-backend sqlite3,path="/path/to/database"
    25  //
    26  // In this example, a MultiFlag is defined and bound to the option name,
    27  // "backend". This MultiFlag has (at least) two registered Options:
    28  //  1. mysql, whose FlagSet binds "address" and "port" options.
    29  //  2. sqlite3, whose FlagSet binds "path".
    30  //
    31  // The MultiFlag Option that is selected (e.g., "mysql") has the remainder of
    32  // its option string parsed into its FlagSet, populating its "address" and
    33  // "port" parameters. If "sqlite3" is selected, the remainder of the option
    34  // string would be parsed into its FlagSet, which hosts the "path" parameter.
    35  package multiflag
    36  
    37  import (
    38  	"errors"
    39  	"flag"
    40  	"fmt"
    41  	"io"
    42  	"os"
    43  	"sort"
    44  	"strings"
    45  	"text/tabwriter"
    46  
    47  	"go.chromium.org/luci/common/flag/nestedflagset"
    48  )
    49  
    50  // OptionDescriptor is a collection of common Option properties.
    51  type OptionDescriptor struct {
    52  	Name        string
    53  	Description string
    54  
    55  	Pinned bool
    56  }
    57  
    58  // Option is a single option entry in a MultiFlag. An Option is responsible
    59  // for parsing a FlagSet from an option string.
    60  type Option interface {
    61  	Descriptor() *OptionDescriptor
    62  
    63  	PrintHelp(io.Writer)
    64  	Run(string) error // Parses the Option from a configuration string.
    65  }
    66  
    67  // MultiFlag is a flag.Value-like object that contains multiple sub-options.
    68  // Each sub-option presents itself as a flag.FlagSet. The sub-option that gets
    69  // selected will have its FlagSet be evaluated against the accompanying options.
    70  //
    71  // For example, one can construct flag that, depending on its options, chooses
    72  // one of two sets of sub-properties:
    73  //
    74  //	-myflag foo,foovalue=123
    75  //	-myflag bar,barvalue=456,barothervalue="hello"
    76  //
    77  // "myflag" is the name of the MultiFlag's top-level flag, as registered with a
    78  // flag.FlagSet. The first token in the flag's value selects which Option should
    79  // be configured. If "foo" is configured, the remaining configuration is parsed
    80  // by the "foo" Option's FlagSet, and the equivalent is true for "bar".
    81  type MultiFlag struct {
    82  	Description string
    83  	Options     []Option
    84  	Output      io.Writer // Output writer, or nil to use STDERR.
    85  
    86  	// The selected Option, populated after Parsing.
    87  	Selected Option
    88  }
    89  
    90  var _ flag.Value = (*MultiFlag)(nil)
    91  
    92  // GetOutput returns the output Writer used for help output.
    93  func (mf *MultiFlag) GetOutput() io.Writer {
    94  	if w := mf.Output; w != nil {
    95  		return w
    96  	}
    97  	return os.Stderr
    98  }
    99  
   100  // Parse applies a value string to a MultiFlag.
   101  //
   102  // For example, if the value string is:
   103  // foo,option1=test
   104  //
   105  // Parse will identify the MultiFlag option named "foo" and have it parse the
   106  // string, "option1=test".
   107  func (mf *MultiFlag) Parse(value string) error {
   108  	option, params := parseOptionParams(value)
   109  	if len(option) == 0 {
   110  		return errors.New("option cannot be empty")
   111  	}
   112  
   113  	mf.Selected = mf.GetOptionFor(option)
   114  	if mf.Selected == nil {
   115  		return fmt.Errorf("invalid option: %v", option)
   116  	}
   117  	return mf.Selected.Run(params)
   118  }
   119  
   120  // Set implements flag.Value
   121  func (mf *MultiFlag) Set(value string) error {
   122  	return mf.Parse(value)
   123  }
   124  
   125  // String implements flag.Value
   126  func (mf *MultiFlag) String() string {
   127  	return strings.Join(mf.OptionNames(), ",")
   128  }
   129  
   130  // GetOptionFor returns the Option associated with the specified name, or nil
   131  // if one isn't defined.
   132  func (mf *MultiFlag) GetOptionFor(name string) Option {
   133  	for _, option := range mf.Options {
   134  		if option.Descriptor().Name == name {
   135  			return option
   136  		}
   137  	}
   138  	return nil
   139  }
   140  
   141  // OptionNames returns a list of the option names registered with a MultiFlag.
   142  func (mf MultiFlag) OptionNames() []string {
   143  	optionNames := make([]string, 0, len(mf.Options))
   144  	for _, opt := range mf.Options {
   145  		optionNames = append(optionNames, opt.Descriptor().Name)
   146  	}
   147  	return optionNames
   148  }
   149  
   150  // PrintHelp prints a formatted help string for a MultiFlag. This help string
   151  // details the Options registered with the MultiFlag.
   152  func (mf *MultiFlag) PrintHelp() error {
   153  	descriptors := make(optionDescriptorSlice, len(mf.Options))
   154  	for idx, opt := range mf.Options {
   155  		descriptors[idx] = opt.Descriptor()
   156  	}
   157  	sort.Sort(descriptors)
   158  
   159  	fmt.Fprintln(mf.Output, mf.Description)
   160  	return writeAlignedOptionDescriptors(mf.Output, []*OptionDescriptor(descriptors))
   161  }
   162  
   163  // FlagOption is an implementation of Option that is describes a single
   164  // nestedflagset option. This option has sub-properties that
   165  type FlagOption struct {
   166  	Name        string
   167  	Description string
   168  	Pinned      bool
   169  
   170  	flags nestedflagset.FlagSet
   171  }
   172  
   173  var _ Option = (*FlagOption)(nil)
   174  
   175  // IsPinned implements Option.
   176  func (o *FlagOption) IsPinned() bool {
   177  	return o.Pinned
   178  }
   179  
   180  // Descriptor implements Option.
   181  func (o *FlagOption) Descriptor() *OptionDescriptor {
   182  	return &OptionDescriptor{
   183  		Name:        o.Name,
   184  		Description: o.Description,
   185  		Pinned:      o.Pinned,
   186  	}
   187  }
   188  
   189  // PrintHelp implements Option.
   190  func (o *FlagOption) PrintHelp(output io.Writer) {
   191  	flags := o.Flags()
   192  	flags.SetOutput(output)
   193  	flags.PrintDefaults()
   194  }
   195  
   196  // Flags returns this Option's nested FlagSet for configuration.
   197  func (o *FlagOption) Flags() *flag.FlagSet {
   198  	return &o.flags.F
   199  }
   200  
   201  // Run implements Option.
   202  func (o *FlagOption) Run(value string) error {
   203  	if err := o.flags.Parse(value); err != nil {
   204  		return err
   205  	}
   206  	return nil
   207  }
   208  
   209  // optionDescriptorSlice is a slice of Option interfaces.
   210  type optionDescriptorSlice []*OptionDescriptor
   211  
   212  var _ sort.Interface = optionDescriptorSlice(nil)
   213  
   214  // Implement sort.Interface
   215  func (s optionDescriptorSlice) Len() int {
   216  	return len(s)
   217  }
   218  
   219  // Implement sort.Interface
   220  func (s optionDescriptorSlice) Less(i, j int) bool {
   221  	// Pinned items are always less than unpinned items.
   222  	if s[i].Pinned {
   223  		if !s[j].Pinned {
   224  			return true
   225  		}
   226  	} else if s[j].Pinned {
   227  		return false
   228  	}
   229  
   230  	return s[i].Name < s[j].Name
   231  }
   232  
   233  // Implement sort.Interface
   234  func (s optionDescriptorSlice) Swap(i, j int) {
   235  	s[i], s[j] = s[j], s[i]
   236  }
   237  
   238  // Option implementation that displays help for a configured MultiFlag.
   239  type helpOption struct {
   240  	mf *MultiFlag
   241  }
   242  
   243  var helpOptionDescriptor = OptionDescriptor{
   244  	Name:        "help",
   245  	Description: `Displays this help message. Can be run as "help,<option>" to display help for an option.`,
   246  	Pinned:      true,
   247  }
   248  
   249  // HelpOption instantiates a new Option instance that prints a help string when
   250  // parsed.
   251  func HelpOption(mf *MultiFlag) Option {
   252  	return &helpOption{mf}
   253  }
   254  
   255  func (o *helpOption) Descriptor() *OptionDescriptor {
   256  	return &helpOptionDescriptor
   257  }
   258  
   259  func (o *helpOption) PrintHelp(io.Writer) {}
   260  
   261  func (o *helpOption) Run(value string) error {
   262  	if value == "" {
   263  		return o.mf.PrintHelp()
   264  	}
   265  
   266  	output := o.mf.GetOutput()
   267  	opt := o.mf.GetOptionFor(value)
   268  	if opt != nil {
   269  		desc := opt.Descriptor()
   270  		fmt.Fprintf(output, "Help for '%s': %s\n", desc.Name, desc.Description)
   271  		opt.PrintHelp(output)
   272  		return nil
   273  	}
   274  
   275  	fmt.Fprintf(output, "Unknown option '%s'\n", value)
   276  	return nil
   277  }
   278  
   279  // parseOptionParams parses an input parameter into its option name (first
   280  // component) and optional parameter data.
   281  //
   282  // For example:
   283  // "option" => option="option", params=""
   284  // "option,params,foo,bar" => option="option", params="params,foo,bar"
   285  func parseOptionParams(value string) (option, params string) {
   286  	// Strip off the first component; use this as the option name.
   287  	idx := strings.IndexRune(value, ',')
   288  
   289  	if idx == -1 {
   290  		option, params = value, ""
   291  	} else {
   292  		option, params = value[:idx], value[(idx+1):]
   293  	}
   294  	return
   295  }
   296  
   297  // writeAlignedOptionDescriptors writes help entries for a series of Options.
   298  func writeAlignedOptionDescriptors(w io.Writer, descriptors []*OptionDescriptor) error {
   299  	tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
   300  
   301  	for _, desc := range descriptors {
   302  		fmt.Fprintf(tw, "%s\t%s\n", desc.Name, desc.Description)
   303  	}
   304  
   305  	if err := tw.Flush(); err != nil {
   306  		return err
   307  	}
   308  	return nil
   309  }