github.com/creachadair/vocab@v0.0.4-0.20190826174139-2654f99cba48/vocab.go (about) 1 // Copyright (C) 2019 Michael J. Fromberger. All Rights Reserved. 2 3 // Package vocab handles flag parsing and dispatch for a nested language of 4 // commands and subcommands. In this model, a command-line is treated as a 5 // phrase in a simple grammar: 6 // 7 // command = name [flags] [command] 8 // 9 // Each name may be either a command in itself, or a group of subcommands with 10 // a shared set of flags, or both. 11 // 12 // You describe command vocabulary with nested struct values, whose fields 13 // define flags and subcommands to be executed. The implementation of a 14 // command is provided by by implementing the vocab.Runner interface. Commands 15 // may pass shared state to their subcommands by attaching it to a context 16 // value that is propagated down the vocabulary tree. 17 // 18 // Basic usage outline: 19 // 20 // itm, err := vocab.New("toolname", v) 21 // ... 22 // if err := itm.Dispatch(ctx, args); err != nil { 23 // log.Fatalf("Dispatch failed: %v, err) 24 // } 25 // 26 package vocab 27 28 import ( 29 "context" 30 "flag" 31 "fmt" 32 "io" 33 "os" 34 "reflect" 35 "sort" 36 "strings" 37 "text/tabwriter" 38 "time" 39 40 "bitbucket.org/creachadair/stringset" 41 "golang.org/x/xerrors" 42 ) 43 44 // A Runner executes the behaviour of a command. If a command implements the 45 // Run method, it will be used to invoke the command after flag parsing. 46 type Runner interface { 47 // Run executes the command with the specified arguments. 48 // 49 // The context passed to run contains any values attached by the Init 50 // methods of enclosing commands. 51 Run(ctx context.Context, args []string) error 52 } 53 54 // RunFunc implements the vocab.Runner interface by calling a function with the 55 // matching signature. This can be used to embed command implementations into 56 // the fields of a struct type with corresponding signatures. 57 type RunFunc func(context.Context, []string) error 58 59 // Run satisfies the vocab.Runner interface. 60 func (rf RunFunc) Run(ctx context.Context, args []string) error { return rf(ctx, args) } 61 62 // An Initializer sets up the environment for a subcommand. If a command 63 // implements the Init method, it will be called before dispatching control to 64 // a subcommand. 65 type Initializer interface { 66 // Init prepares a command for execution of the named subcommand with the 67 // given arguments, prior to parsing the subcommand's flags. The name is the 68 // resolved canonical name of the subcommand, and the first element of args 69 // is the name as written (which may be an alias). 70 // 71 // If the returned context is not nil, it replaces ctx in the subcommand; 72 // otherwise ctx is used. If init reports an error, the command execution 73 // will fail. 74 Init(ctx context.Context, name string, args []string) (context.Context, error) 75 } 76 77 // New constructs a vocabulary item from the given value. The root value must 78 // either itself implement the vocab.Runner interface, or be a (pointer to a) 79 // struct value whose field annotations describe subcommand vocabulary. 80 // 81 // To define a field as implementing a subcommand, use the "vocab:" tag to 82 // define its name: 83 // 84 // type Cmd struct{ 85 // A Type1 `vocab:"first"` 86 // B *Type2 `vocab:"second"` 87 // } 88 // 89 // The field types in this example must similarly implement vocab.Runner, or be 90 // structs with their own corresponding annotations. During dispatch, an 91 // argument list beginning with "first" will dispatch through A, and an 92 // argument list beginning with "second" will dispatch through B. The nesting 93 // may occur to arbitrary depth, but note that New does not handle cycles. 94 // 95 // A subcommand may also have aliases, specified as: 96 // 97 // vocab:"name,alias1,alias2,..." 98 // 99 // The names and aliases must be unique within a given value. 100 // 101 // You can also attach flag to struct fields using the "flag:" tag: 102 // 103 // flag:"name,description" 104 // 105 // The name becomes the flag string, and the description its help text. The 106 // field must either be one of the standard types understood by the flag 107 // package, or its pointer must implement the flag.Value interface. In each 108 // case the default value for the flag is the current value of the field. 109 // 110 // Documentation 111 // 112 // In addition to its name, each command has "summary" and "help" strings. The 113 // summary is a short (typically one-line) synopsis, and help is a longer and 114 // more explanatory (possibly multi-line) description. There are three ways to 115 // associate these strings with a command: 116 // 117 // If the command implements vocab.Summarizer, its Summary method is used to 118 // generate the summary string. Otherwise, if the command has a blank field 119 // ("_") whose tag begins with "help-summary:", the rest of that tag is used as 120 // the summary string. Otherwise, if the command's type is used as a field of 121 // an enclosing command type with a "help-summary:" comment tag, that text is 122 // used as the summary string for the command. 123 // 124 // If the type implements vocab.Helper, its Help method is used to generate the 125 // full help string. Otherwise, if the command has a blank field ("_") whose 126 // tag begins with "help-long:", the rest of that tag is used as the long help 127 // string. Otherwise, if the command's type is used as a field of an enclosing 128 // command type with a "help-log:" comment tag, that text is used as the long 129 // help string for the command. 130 // 131 // Caveat: Although the Go grammar allows arbitrary string literals as struct 132 // field tags, there is a strong convention supported by the reflect package 133 // and "go vet" for single-line tags with key:"value" structure. This package 134 // will accept multi-line unquoted tags, but be aware that some lint tools may 135 // complain if you use them. You can use standard string escapes (e.g., "\n") 136 // in the quoted values of tags to avoid this, at the cost of a long line. 137 func New(name string, root interface{}) (*Item, error) { 138 itm, err := newItem(name, root) 139 if err != nil { 140 return nil, err 141 } else if itm.init == nil && itm.run == nil && len(itm.items) == 0 { 142 return nil, xerrors.New("value does not implement any vocabulary") 143 } 144 return itm, nil 145 } 146 147 // An Item represents a parsed command tree. 148 type Item struct { 149 cmd interface{} 150 run func(context.Context, []string) error 151 init func(context.Context, string, []string) (context.Context, error) 152 153 name string // the canonical name of this command 154 summary func() string // the summary text, if defined 155 helpText func() string // the long help text, if defined 156 fs *flag.FlagSet // the flags for the command (if nil, none are defined) 157 hasFlags bool // whether any flags were explicitly defined 158 items map[string]*Item // :: name → subcommand 159 alias map[string]string // :: subcommand alias → name 160 out io.Writer // output writer 161 } 162 163 // SetOutput sets the output writer for m and all its nested subcommands to w. 164 // It will panic if w == nil. 165 func (m *Item) SetOutput(w io.Writer) { 166 if w == nil { 167 panic("vocab: output writer is nil") 168 } 169 m.out = w 170 m.fs.SetOutput(w) 171 for _, itm := range m.items { 172 itm.SetOutput(w) 173 } 174 } 175 176 // shortHelp prints a brief summary of m to its output writer. 177 func (m *Item) shortHelp() { m.printHelp(false) } 178 179 // longHelp prints complete help for m to its output writer. 180 func (m *Item) longHelp() { m.printHelp(true) } 181 182 // printHelp prints a summary of m to w, including help text if full == true. 183 func (m *Item) printHelp(full bool) { 184 w := m.out 185 186 // Always print a summary line giving the command name. 187 summary := "(undocumented)" 188 if m.summary != nil { 189 summary = m.summary() 190 } 191 fmt.Fprint(w, m.name, ": ", summary, "\n") 192 193 // If full help is requested, include help text and flags. 194 if full { 195 if m.helpText != nil { 196 fmt.Fprint(w, "\n", m.helpText(), "\n") 197 } 198 if m.hasFlags { 199 fmt.Fprintln(w, "\nFlags:") 200 m.fs.SetOutput(w) 201 m.fs.PrintDefaults() 202 } 203 } 204 205 // If any subcommands are defined, summarize them. 206 if len(m.items) != 0 { 207 amap := make(map[string][]string) 208 for alias, name := range m.alias { 209 amap[name] = append(amap[name], alias) 210 } 211 212 fmt.Fprintln(w, "\nSubcommands:") 213 tw := tabwriter.NewWriter(w, 0, 8, 3, ' ', 0) 214 for _, name := range stringset.FromKeys(m.items).Elements() { 215 summary := "(undocumented)" 216 if s := m.items[name].summary; s != nil { 217 summary = s() 218 } 219 fmt.Fprint(tw, " ", name, "\t", summary) 220 221 // Include aliases in the summary, if any exist. 222 if as := amap[name]; len(as) != 0 { 223 sort.Strings(as) 224 fmt.Fprintf(tw, " (alias: %s)", strings.Join(as, ", ")) 225 } 226 fmt.Fprintln(tw) 227 } 228 tw.Flush() 229 } 230 } 231 232 // findCommand reports whether key is the name or registered alias of any 233 // subcommand of m, and if so returns its item. 234 func (m *Item) findCommand(key string) (*Item, bool) { 235 itm, ok := m.items[key] 236 if !ok { 237 itm, ok = m.items[m.alias[key]] 238 } 239 return itm, ok 240 } 241 242 // Resolve traverses the vocabulary of m to find the command described by path. 243 // The path should contain only names; Resolve does not parse flags. 244 // It returns the last item successfully reached, along with the unresolved 245 // tail of the path (which is empty if the path was fully resolved). 246 func (m *Item) Resolve(path []string) (*Item, []string) { 247 cur := m 248 for i, arg := range path { 249 next, ok := cur.findCommand(arg) 250 if !ok { 251 return cur, path[i:] 252 } 253 cur = next 254 } 255 return cur, nil 256 } 257 258 // Dispatch traverses the vocabulary of m parsing and executing the described 259 // commands. 260 func (m *Item) Dispatch(ctx context.Context, args []string) error { 261 if err := m.fs.Parse(args); err == flag.ErrHelp { 262 return nil // the usage message already contains the short help 263 } else if err != nil { 264 return xerrors.Errorf("parsing flags for %q: %w", m.name, err) 265 } else { 266 args = m.fs.Args() 267 } 268 269 // Check whether there is a subcommand that can follow from m. 270 if len(args) != 0 { 271 if sub, ok := m.findCommand(args[0]); ok { 272 // Having found a subcommand, give m a chance to initialize itself and 273 // update the context before invoking the subcommand. 274 if m.init != nil { 275 pctx, err := m.init(ctx, sub.name, args) 276 if err != nil { 277 return xerrors.Errorf("subcommand %q: %w", m.name, err) 278 } else if pctx != nil { 279 ctx = pctx 280 } 281 } 282 283 // Dispatch to the subcommand with the remaining arguments. 284 return sub.Dispatch(withParent(ctx, m), args[1:]) 285 } 286 287 // No matching subcommand; fall through and let this command handle it. 288 } 289 290 if m.run != nil { 291 return m.run(ctx, args) 292 } else if len(args) != 0 { 293 return xerrors.Errorf("no command found matching %q", args) 294 } 295 m.shortHelp() 296 return nil // TODO: Return something like flag.ErrHelp 297 } 298 299 // parentItemKey identifies the parent of a subcommand in the context. This is 300 // used by the help system to locate the item for which help is requested, and 301 // is not exposed outside the package. 302 type parentItemKey struct{} 303 304 func withParent(ctx context.Context, m *Item) context.Context { 305 return context.WithValue(ctx, parentItemKey{}, m) 306 } 307 308 func parentItem(ctx context.Context) *Item { 309 if v := ctx.Value(parentItemKey{}); v != nil { 310 return v.(*Item) 311 } 312 return nil 313 } 314 315 func newItem(name string, x interface{}) (*Item, error) { 316 // Requirements: x must either be a struct or a non-nil pointer to a struct, 317 // or must implement the vocab.Runnier interface. 318 r, isRunner := x.(Runner) 319 v := reflect.Indirect(reflect.ValueOf(x)) 320 isStruct := v.Kind() == reflect.Struct 321 if !isRunner && !isStruct { 322 return nil, xerrors.Errorf("value must be a struct (have %v)", v.Kind()) 323 } 324 325 item := &Item{ 326 cmd: x, 327 name: name, 328 fs: flag.NewFlagSet(name, flag.ContinueOnError), 329 items: make(map[string]*Item), 330 alias: make(map[string]string), 331 out: os.Stderr, 332 } 333 item.fs.Usage = func() { item.shortHelp() } 334 335 // Cache the callbacks, if they are defined. 336 if isRunner { 337 item.run = r.Run 338 } 339 if in, ok := x.(Initializer); ok { 340 item.init = in.Init 341 } 342 if sum, ok := x.(Summarizer); ok { 343 item.summary = sum.Summary 344 } 345 if help, ok := x.(Helper); ok { 346 item.helpText = help.Help 347 } 348 349 if !isStruct { 350 return item, nil // nothing more to do here 351 } 352 353 // At this point we know x is a struct. Scan its field tags for 354 // annotations. 355 // 356 // To indicate that the field is a subcommand: 357 // vocab:"name" or vocab:"name,alias,..." 358 // 359 // To attach a flag to a field: 360 // flag:"name,description" 361 // 362 t := v.Type() 363 for i := 0; i < t.NumField(); i++ { 364 ft := t.Field(i) // field type metadata (name, tags) 365 fv := v.Field(i) // field value 366 367 // Annotation: flag:"name,description". 368 // This requires x is a pointer. 369 if tag := ft.Tag.Get("flag"); tag != "" && ft.PkgPath == "" { 370 if fv.Kind() != reflect.Ptr { 371 if !fv.CanAddr() { 372 return nil, xerrors.Errorf("cannot flag field %q of type %T", ft.Name, x) 373 } 374 fv = fv.Addr() 375 } else if !fv.Elem().IsValid() { 376 return nil, xerrors.Errorf("cannot flag pointer field %q with nil value", ft.Name) 377 } 378 fname, help := tag, tag 379 if i := strings.Index(tag, ","); i >= 0 { 380 fname, help = tag[:i], tag[i+1:] 381 } 382 if err := registerFlag(item.fs, fv.Interface(), fname, help); err != nil { 383 return nil, xerrors.Errorf("flagged field %q: %w", ft.Name, err) 384 } 385 item.hasFlags = true 386 continue 387 } 388 389 // Annotation: vocab:"name" or vocab:"name,alias1,alias2,..." 390 if tag := ft.Tag.Get("vocab"); tag != "" { 391 names := strings.Split(tag, ",") 392 if fv.Kind() != reflect.Ptr && fv.CanAddr() { 393 fv = fv.Addr() 394 } 395 if !fv.CanInterface() { 396 return nil, xerrors.Errorf("vocab field %q: cannot capture unexported value", ft.Name) 397 } 398 sub, err := newItem(names[0], fv.Interface()) 399 if err != nil { 400 return nil, xerrors.Errorf("vocab field %q: %w", ft.Name, err) 401 } else if _, ok := item.items[sub.name]; ok { 402 return nil, xerrors.Errorf("duplicate subcommand %q", name) 403 } 404 405 // If the field returned a command but lacks documentation, check for 406 // help tags on the field. 407 if sub.summary == nil { 408 if hs := ft.Tag.Get("help-summary"); hs != "" { 409 sub.summary = func() string { return hs } 410 } 411 } 412 if sub.helpText == nil { 413 if hs := ft.Tag.Get("help-long"); hs != "" { 414 sub.helpText = func() string { return hs } 415 } 416 } 417 418 item.items[sub.name] = sub 419 for _, a := range names[1:] { 420 if old, ok := item.alias[a]; ok && old != sub.name { 421 return nil, xerrors.Errorf("duplicate alias %q (%q, %q)", a, old, sub.name) 422 } 423 item.alias[a] = sub.name 424 } 425 continue 426 } 427 428 // Check for help annotations embedded in blank fields. Note that we do 429 // not require these tags to have the canonical format: In particular, 430 // the quotes may be omitted, and internal whitespace is preserved. 431 if ft.Name == "_" { 432 if t := fieldTag("help-summary", ft); t != "" && item.summary == nil { 433 item.summary = func() string { return t } 434 } 435 if t := fieldTag("help-long", ft); t != "" && item.helpText == nil { 436 item.helpText = func() string { return t } 437 } 438 } 439 } 440 return item, nil 441 } 442 443 func registerFlag(fs *flag.FlagSet, fv interface{}, name, help string) error { 444 switch t := fv.(type) { 445 case flag.Value: 446 fs.Var(t, name, help) 447 case *bool: 448 fs.BoolVar(t, name, *t, help) 449 case *time.Duration: 450 fs.DurationVar(t, name, *t, help) 451 case *float64: 452 fs.Float64Var(t, name, *t, help) 453 case *int64: 454 fs.Int64Var(t, name, *t, help) 455 case *int: 456 fs.IntVar(t, name, *t, help) 457 case *string: 458 fs.StringVar(t, name, *t, help) 459 case *uint64: 460 fs.Uint64Var(t, name, *t, help) 461 case *uint: 462 fs.UintVar(t, name, *t, help) 463 default: 464 return xerrors.Errorf("type %T does not implement flag.Value", fv) 465 } 466 return nil 467 } 468 469 func fieldTag(name string, ft reflect.StructField) string { 470 if t := ft.Tag.Get(name); t != "" { 471 return t 472 } 473 s := string(ft.Tag) 474 if t := strings.TrimPrefix(s, name+":"); t != s { 475 return cleanString(t) 476 } 477 return "" 478 } 479 480 // cleanString removes surrounding whitespace and quotation marks from s. 481 func cleanString(s string) string { 482 return strings.TrimSpace(strings.Trim(strings.TrimSpace(s), `"`)) 483 }