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 }