github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/script/engine.go (about) 1 // Copyright 2022 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package script implements a small, customizable, platform-agnostic scripting 6 // language. 7 // 8 // Scripts are run by an [Engine] configured with a set of available commands 9 // and conditions that guard those commands. Each script has an associated 10 // working directory and environment, along with a buffer containing the stdout 11 // and stderr output of a prior command, tracked in a [State] that commands can 12 // inspect and modify. 13 // 14 // The default commands configured by [NewEngine] resemble a simplified Unix 15 // shell. 16 // 17 // # Script Language 18 // 19 // Each line of a script is parsed into a sequence of space-separated command 20 // words, with environment variable expansion within each word and # marking an 21 // end-of-line comment. Additional variables named ':' and '/' are expanded 22 // within script arguments (expanding to the value of os.PathListSeparator and 23 // os.PathSeparator respectively) but are not inherited in subprocess 24 // environments. 25 // 26 // Adding single quotes around text keeps spaces in that text from being treated 27 // as word separators and also disables environment variable expansion. 28 // Inside a single-quoted block of text, a repeated single quote indicates 29 // a literal single quote, as in: 30 // 31 // 'Don''t communicate by sharing memory.' 32 // 33 // A line beginning with # is a comment and conventionally explains what is 34 // being done or tested at the start of a new section of the script. 35 // 36 // Commands are executed one at a time, and errors are checked for each command; 37 // if any command fails unexpectedly, no subsequent commands in the script are 38 // executed. The command prefix ! indicates that the command on the rest of the 39 // line (typically go or a matching predicate) must fail instead of succeeding. 40 // The command prefix ? indicates that the command may or may not succeed, but 41 // the script should continue regardless. 42 // 43 // The command prefix [cond] indicates that the command on the rest of the line 44 // should only run when the condition is satisfied. 45 // 46 // A condition can be negated: [!root] means to run the rest of the line only if 47 // the user is not root. Multiple conditions may be given for a single command, 48 // for example, '[linux] [amd64] skip'. The command will run if all conditions 49 // are satisfied. 50 package script 51 52 import ( 53 "bufio" 54 "context" 55 "errors" 56 "fmt" 57 "io" 58 "sort" 59 "strings" 60 "time" 61 ) 62 63 // An Engine stores the configuration for executing a set of scripts. 64 // 65 // The same Engine may execute multiple scripts concurrently. 66 type Engine struct { 67 Cmds map[string]Cmd 68 Conds map[string]Cond 69 70 // If Quiet is true, Execute deletes log prints from the previous 71 // section when starting a new section. 72 Quiet bool 73 } 74 75 // NewEngine returns an Engine configured with a basic set of commands and conditions. 76 func NewEngine() *Engine { 77 return &Engine{ 78 Cmds: DefaultCmds(), 79 Conds: DefaultConds(), 80 } 81 } 82 83 // A Cmd is a command that is available to a script. 84 type Cmd interface { 85 // Run begins running the command. 86 // 87 // If the command produces output or can be run in the background, run returns 88 // a WaitFunc that will be called to obtain the result of the command and 89 // update the engine's stdout and stderr buffers. 90 // 91 // Run itself and the returned WaitFunc may inspect and/or modify the State, 92 // but the State's methods must not be called concurrently after Run has 93 // returned. 94 // 95 // Run may retain and access the args slice until the WaitFunc has returned. 96 Run(s *State, args ...string) (WaitFunc, error) 97 98 // Usage returns the usage for the command, which the caller must not modify. 99 Usage() *CmdUsage 100 } 101 102 // A WaitFunc is a function called to retrieve the results of a Cmd. 103 type WaitFunc func(*State) (stdout, stderr string, err error) 104 105 // A CmdUsage describes the usage of a Cmd, independent of its name 106 // (which can change based on its registration). 107 type CmdUsage struct { 108 Summary string // in the style of the Name section of a Unix 'man' page, omitting the name 109 Args string // a brief synopsis of the command's arguments (only) 110 Detail []string // zero or more sentences in the style of the Description section of a Unix 'man' page 111 112 // If Async is true, the Cmd is meaningful to run in the background, and its 113 // Run method must return either a non-nil WaitFunc or a non-nil error. 114 Async bool 115 116 // RegexpArgs reports which arguments, if any, should be treated as regular 117 // expressions. It takes as input the raw, unexpanded arguments and returns 118 // the list of argument indices that will be interpreted as regular 119 // expressions. 120 // 121 // If RegexpArgs is nil, all arguments are assumed not to be regular 122 // expressions. 123 RegexpArgs func(rawArgs ...string) []int 124 } 125 126 // A Cond is a condition deciding whether a command should be run. 127 type Cond interface { 128 // Eval reports whether the condition applies to the given State. 129 // 130 // If the condition's usage reports that it is a prefix, 131 // the condition must be used with a suffix. 132 // Otherwise, the passed-in suffix argument is always the empty string. 133 Eval(s *State, suffix string) (bool, error) 134 135 // Usage returns the usage for the condition, which the caller must not modify. 136 Usage() *CondUsage 137 } 138 139 // A CondUsage describes the usage of a Cond, independent of its name 140 // (which can change based on its registration). 141 type CondUsage struct { 142 Summary string // a single-line summary of when the condition is true 143 144 // If Prefix is true, the condition is a prefix and requires a 145 // colon-separated suffix (like "[GOOS:linux]" for the "GOOS" condition). 146 // The suffix may be the empty string (like "[prefix:]"). 147 Prefix bool 148 } 149 150 // Execute reads and executes script, writing the output to log. 151 // 152 // Execute stops and returns an error at the first command that does not succeed. 153 // The returned error's text begins with "file:line: ". 154 // 155 // If the script runs to completion or ends by a 'stop' command, 156 // Execute returns nil. 157 // 158 // Execute does not stop background commands started by the script 159 // before returning. To stop those, use [State.CloseAndWait] or the 160 // [Wait] command. 161 func (e *Engine) Execute(s *State, file string, script *bufio.Reader, log io.Writer) (err error) { 162 defer func(prev *Engine) { s.engine = prev }(s.engine) 163 s.engine = e 164 165 var sectionStart time.Time 166 // endSection flushes the logs for the current section from s.log to log. 167 // ok indicates whether all commands in the section succeeded. 168 endSection := func(ok bool) error { 169 var err error 170 if sectionStart.IsZero() { 171 // We didn't write a section header or record a timestamp, so just dump the 172 // whole log without those. 173 if s.log.Len() > 0 { 174 err = s.flushLog(log) 175 } 176 } else if s.log.Len() == 0 { 177 // Adding elapsed time for doing nothing is meaningless, so don't. 178 _, err = io.WriteString(log, "\n") 179 } else { 180 // Insert elapsed time for section at the end of the section's comment. 181 _, err = fmt.Fprintf(log, " (%.3fs)\n", time.Since(sectionStart).Seconds()) 182 183 if err == nil && (!ok || !e.Quiet) { 184 err = s.flushLog(log) 185 } else { 186 s.log.Reset() 187 } 188 } 189 190 sectionStart = time.Time{} 191 return err 192 } 193 194 var lineno int 195 lineErr := func(err error) error { 196 if errors.As(err, new(*CommandError)) { 197 return err 198 } 199 return fmt.Errorf("%s:%d: %w", file, lineno, err) 200 } 201 202 // In case of failure or panic, flush any pending logs for the section. 203 defer func() { 204 if sErr := endSection(false); sErr != nil && err == nil { 205 err = lineErr(sErr) 206 } 207 }() 208 209 for { 210 if err := s.ctx.Err(); err != nil { 211 // This error wasn't produced by any particular command, 212 // so don't wrap it in a CommandError. 213 return lineErr(err) 214 } 215 216 line, err := script.ReadString('\n') 217 if err == io.EOF { 218 if line == "" { 219 break // Reached the end of the script. 220 } 221 // If the script doesn't end in a newline, interpret the final line. 222 } else if err != nil { 223 return lineErr(err) 224 } 225 line = strings.TrimSuffix(line, "\n") 226 lineno++ 227 228 // The comment character "#" at the start of the line delimits a section of 229 // the script. 230 if strings.HasPrefix(line, "#") { 231 // If there was a previous section, the fact that we are starting a new 232 // one implies the success of the previous one. 233 // 234 // At the start of the script, the state may also contain accumulated logs 235 // from commands executed on the State outside of the engine in order to 236 // set it up; flush those logs too. 237 if err := endSection(true); err != nil { 238 return lineErr(err) 239 } 240 241 // Log the section start without a newline so that we can add 242 // a timestamp for the section when it ends. 243 _, err = fmt.Fprintf(log, "%s", line) 244 sectionStart = time.Now() 245 if err != nil { 246 return lineErr(err) 247 } 248 continue 249 } 250 251 cmd, err := parse(file, lineno, line) 252 if cmd == nil && err == nil { 253 continue // Ignore blank lines. 254 } 255 s.Logf("> %s\n", line) 256 if err != nil { 257 return lineErr(err) 258 } 259 260 // Evaluate condition guards. 261 ok, err := e.conditionsActive(s, cmd.conds) 262 if err != nil { 263 return lineErr(err) 264 } 265 if !ok { 266 s.Logf("[condition not met]\n") 267 continue 268 } 269 270 impl := e.Cmds[cmd.name] 271 272 // Expand variables in arguments. 273 var regexpArgs []int 274 if impl != nil { 275 usage := impl.Usage() 276 if usage.RegexpArgs != nil { 277 // First join rawArgs without expansion to pass to RegexpArgs. 278 rawArgs := make([]string, 0, len(cmd.rawArgs)) 279 for _, frags := range cmd.rawArgs { 280 var b strings.Builder 281 for _, frag := range frags { 282 b.WriteString(frag.s) 283 } 284 rawArgs = append(rawArgs, b.String()) 285 } 286 regexpArgs = usage.RegexpArgs(rawArgs...) 287 } 288 } 289 cmd.args = expandArgs(s, cmd.rawArgs, regexpArgs) 290 291 // Run the command. 292 err = e.runCommand(s, cmd, impl) 293 if err != nil { 294 if stop := (stopError{}); errors.As(err, &stop) { 295 // Since the 'stop' command halts execution of the entire script, 296 // log its message separately from the section in which it appears. 297 err = endSection(true) 298 s.Logf("%v\n", stop) 299 if err == nil { 300 return nil 301 } 302 } 303 return lineErr(err) 304 } 305 } 306 307 if err := endSection(true); err != nil { 308 return lineErr(err) 309 } 310 return nil 311 } 312 313 // A command is a complete command parsed from a script. 314 type command struct { 315 file string 316 line int 317 want expectedStatus 318 conds []condition // all must be satisfied 319 name string // the name of the command; must be non-empty 320 rawArgs [][]argFragment 321 args []string // shell-expanded arguments following name 322 background bool // command should run in background (ends with a trailing &) 323 } 324 325 // An expectedStatus describes the expected outcome of a command. 326 // Script execution halts when a command does not match its expected status. 327 type expectedStatus string 328 329 const ( 330 success expectedStatus = "" 331 failure expectedStatus = "!" 332 successOrFailure expectedStatus = "?" 333 ) 334 335 type argFragment struct { 336 s string 337 quoted bool // if true, disable variable expansion for this fragment 338 } 339 340 type condition struct { 341 want bool 342 tag string 343 } 344 345 const argSepChars = " \t\r\n#" 346 347 // parse parses a single line as a list of space-separated arguments. 348 // subject to environment variable expansion (but not resplitting). 349 // Single quotes around text disable splitting and expansion. 350 // To embed a single quote, double it: 351 // 352 // 'Don''t communicate by sharing memory.' 353 func parse(filename string, lineno int, line string) (cmd *command, err error) { 354 cmd = &command{file: filename, line: lineno} 355 var ( 356 rawArg []argFragment // text fragments of current arg so far (need to add line[start:i]) 357 start = -1 // if >= 0, position where current arg text chunk starts 358 quoted = false // currently processing quoted text 359 ) 360 361 flushArg := func() error { 362 if len(rawArg) == 0 { 363 return nil // Nothing to flush. 364 } 365 defer func() { rawArg = nil }() 366 367 if cmd.name == "" && len(rawArg) == 1 && !rawArg[0].quoted { 368 arg := rawArg[0].s 369 370 // Command prefix ! means negate the expectations about this command: 371 // go command should fail, match should not be found, etc. 372 // Prefix ? means allow either success or failure. 373 switch want := expectedStatus(arg); want { 374 case failure, successOrFailure: 375 if cmd.want != "" { 376 return errors.New("duplicated '!' or '?' token") 377 } 378 cmd.want = want 379 return nil 380 } 381 382 // Command prefix [cond] means only run this command if cond is satisfied. 383 if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") { 384 want := true 385 arg = strings.TrimSpace(arg[1 : len(arg)-1]) 386 if strings.HasPrefix(arg, "!") { 387 want = false 388 arg = strings.TrimSpace(arg[1:]) 389 } 390 if arg == "" { 391 return errors.New("empty condition") 392 } 393 cmd.conds = append(cmd.conds, condition{want: want, tag: arg}) 394 return nil 395 } 396 397 if arg == "" { 398 return errors.New("empty command") 399 } 400 cmd.name = arg 401 return nil 402 } 403 404 cmd.rawArgs = append(cmd.rawArgs, rawArg) 405 return nil 406 } 407 408 for i := 0; ; i++ { 409 if !quoted && (i >= len(line) || strings.ContainsRune(argSepChars, rune(line[i]))) { 410 // Found arg-separating space. 411 if start >= 0 { 412 rawArg = append(rawArg, argFragment{s: line[start:i], quoted: false}) 413 start = -1 414 } 415 if err := flushArg(); err != nil { 416 return nil, err 417 } 418 if i >= len(line) || line[i] == '#' { 419 break 420 } 421 continue 422 } 423 if i >= len(line) { 424 return nil, errors.New("unterminated quoted argument") 425 } 426 if line[i] == '\'' { 427 if !quoted { 428 // starting a quoted chunk 429 if start >= 0 { 430 rawArg = append(rawArg, argFragment{s: line[start:i], quoted: false}) 431 } 432 start = i + 1 433 quoted = true 434 continue 435 } 436 // 'foo''bar' means foo'bar, like in rc shell and Pascal. 437 if i+1 < len(line) && line[i+1] == '\'' { 438 rawArg = append(rawArg, argFragment{s: line[start:i], quoted: true}) 439 start = i + 1 440 i++ // skip over second ' before next iteration 441 continue 442 } 443 // ending a quoted chunk 444 rawArg = append(rawArg, argFragment{s: line[start:i], quoted: true}) 445 start = i + 1 446 quoted = false 447 continue 448 } 449 // found character worth saving; make sure we're saving 450 if start < 0 { 451 start = i 452 } 453 } 454 455 if cmd.name == "" { 456 if cmd.want != "" || len(cmd.conds) > 0 || len(cmd.rawArgs) > 0 || cmd.background { 457 // The line contains a command prefix or suffix, but no actual command. 458 return nil, errors.New("missing command") 459 } 460 461 // The line is blank, or contains only a comment. 462 return nil, nil 463 } 464 465 if n := len(cmd.rawArgs); n > 0 { 466 last := cmd.rawArgs[n-1] 467 if len(last) == 1 && !last[0].quoted && last[0].s == "&" { 468 cmd.background = true 469 cmd.rawArgs = cmd.rawArgs[:n-1] 470 } 471 } 472 return cmd, nil 473 } 474 475 // expandArgs expands the shell variables in rawArgs and joins them to form the 476 // final arguments to pass to a command. 477 func expandArgs(s *State, rawArgs [][]argFragment, regexpArgs []int) []string { 478 args := make([]string, 0, len(rawArgs)) 479 for i, frags := range rawArgs { 480 isRegexp := false 481 for _, j := range regexpArgs { 482 if i == j { 483 isRegexp = true 484 break 485 } 486 } 487 488 var b strings.Builder 489 for _, frag := range frags { 490 if frag.quoted { 491 b.WriteString(frag.s) 492 } else { 493 b.WriteString(s.ExpandEnv(frag.s, isRegexp)) 494 } 495 } 496 args = append(args, b.String()) 497 } 498 return args 499 } 500 501 // quoteArgs returns a string that parse would parse as args when passed to a command. 502 // 503 // TODO(bcmills): This function should have a fuzz test. 504 func quoteArgs(args []string) string { 505 var b strings.Builder 506 for i, arg := range args { 507 if i > 0 { 508 b.WriteString(" ") 509 } 510 if strings.ContainsAny(arg, "'"+argSepChars) { 511 // Quote the argument to a form that would be parsed as a single argument. 512 b.WriteString("'") 513 b.WriteString(strings.ReplaceAll(arg, "'", "''")) 514 b.WriteString("'") 515 } else { 516 b.WriteString(arg) 517 } 518 } 519 return b.String() 520 } 521 522 func (e *Engine) conditionsActive(s *State, conds []condition) (bool, error) { 523 for _, cond := range conds { 524 var impl Cond 525 prefix, suffix, ok := strings.Cut(cond.tag, ":") 526 if ok { 527 impl = e.Conds[prefix] 528 if impl == nil { 529 return false, fmt.Errorf("unknown condition prefix %q", prefix) 530 } 531 if !impl.Usage().Prefix { 532 return false, fmt.Errorf("condition %q cannot be used with a suffix", prefix) 533 } 534 } else { 535 impl = e.Conds[cond.tag] 536 if impl == nil { 537 return false, fmt.Errorf("unknown condition %q", cond.tag) 538 } 539 if impl.Usage().Prefix { 540 return false, fmt.Errorf("condition %q requires a suffix", cond.tag) 541 } 542 } 543 active, err := impl.Eval(s, suffix) 544 545 if err != nil { 546 return false, fmt.Errorf("evaluating condition %q: %w", cond.tag, err) 547 } 548 if active != cond.want { 549 return false, nil 550 } 551 } 552 553 return true, nil 554 } 555 556 func (e *Engine) runCommand(s *State, cmd *command, impl Cmd) error { 557 if impl == nil { 558 return cmdError(cmd, errors.New("unknown command")) 559 } 560 561 async := impl.Usage().Async 562 if cmd.background && !async { 563 return cmdError(cmd, errors.New("command cannot be run in background")) 564 } 565 566 wait, runErr := impl.Run(s, cmd.args...) 567 if wait == nil { 568 if async && runErr == nil { 569 return cmdError(cmd, errors.New("internal error: async command returned a nil WaitFunc")) 570 } 571 return checkStatus(cmd, runErr) 572 } 573 if runErr != nil { 574 return cmdError(cmd, errors.New("internal error: command returned both an error and a WaitFunc")) 575 } 576 577 if cmd.background { 578 s.background = append(s.background, backgroundCmd{ 579 command: cmd, 580 wait: wait, 581 }) 582 // Clear stdout and stderr, since they no longer correspond to the last 583 // command executed. 584 s.stdout = "" 585 s.stderr = "" 586 return nil 587 } 588 589 if wait != nil { 590 stdout, stderr, waitErr := wait(s) 591 s.stdout = stdout 592 s.stderr = stderr 593 if stdout != "" { 594 s.Logf("[stdout]\n%s", stdout) 595 } 596 if stderr != "" { 597 s.Logf("[stderr]\n%s", stderr) 598 } 599 if cmdErr := checkStatus(cmd, waitErr); cmdErr != nil { 600 return cmdErr 601 } 602 if waitErr != nil { 603 // waitErr was expected (by cmd.want), so log it instead of returning it. 604 s.Logf("[%v]\n", waitErr) 605 } 606 } 607 return nil 608 } 609 610 func checkStatus(cmd *command, err error) error { 611 if err == nil { 612 if cmd.want == failure { 613 return cmdError(cmd, ErrUnexpectedSuccess) 614 } 615 return nil 616 } 617 618 if s := (stopError{}); errors.As(err, &s) { 619 // This error originated in the Stop command. 620 // Propagate it as-is. 621 return cmdError(cmd, err) 622 } 623 624 if w := (waitError{}); errors.As(err, &w) { 625 // This error was surfaced from a background process by a call to Wait. 626 // Add a call frame for Wait itself, but ignore its "want" field. 627 // (Wait itself cannot fail to wait on commands or else it would leak 628 // processes and/or goroutines — so a negative assertion for it would be at 629 // best ambiguous.) 630 return cmdError(cmd, err) 631 } 632 633 if cmd.want == success { 634 return cmdError(cmd, err) 635 } 636 637 if cmd.want == failure && (errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled)) { 638 // The command was terminated because the script is no longer interested in 639 // its output, so we don't know what it would have done had it run to 640 // completion — for all we know, it could have exited without error if it 641 // ran just a smidge faster. 642 return cmdError(cmd, err) 643 } 644 645 return nil 646 } 647 648 // ListCmds prints to w a list of the named commands, 649 // annotating each with its arguments and a short usage summary. 650 // If verbose is true, ListCmds prints full details for each command. 651 // 652 // Each of the name arguments should be a command name. 653 // If no names are passed as arguments, ListCmds lists all the 654 // commands registered in e. 655 func (e *Engine) ListCmds(w io.Writer, verbose bool, names ...string) error { 656 if names == nil { 657 names = make([]string, 0, len(e.Cmds)) 658 for name := range e.Cmds { 659 names = append(names, name) 660 } 661 sort.Strings(names) 662 } 663 664 for _, name := range names { 665 cmd := e.Cmds[name] 666 usage := cmd.Usage() 667 668 suffix := "" 669 if usage.Async { 670 suffix = " [&]" 671 } 672 673 _, err := fmt.Fprintf(w, "%s %s%s\n\t%s\n", name, usage.Args, suffix, usage.Summary) 674 if err != nil { 675 return err 676 } 677 678 if verbose { 679 if _, err := io.WriteString(w, "\n"); err != nil { 680 return err 681 } 682 for _, line := range usage.Detail { 683 if err := wrapLine(w, line, 60, "\t"); err != nil { 684 return err 685 } 686 } 687 if _, err := io.WriteString(w, "\n"); err != nil { 688 return err 689 } 690 } 691 } 692 693 return nil 694 } 695 696 func wrapLine(w io.Writer, line string, cols int, indent string) error { 697 line = strings.TrimLeft(line, " ") 698 for len(line) > cols { 699 bestSpace := -1 700 for i, r := range line { 701 if r == ' ' { 702 if i <= cols || bestSpace < 0 { 703 bestSpace = i 704 } 705 if i > cols { 706 break 707 } 708 } 709 } 710 if bestSpace < 0 { 711 break 712 } 713 714 if _, err := fmt.Fprintf(w, "%s%s\n", indent, line[:bestSpace]); err != nil { 715 return err 716 } 717 line = line[bestSpace+1:] 718 } 719 720 _, err := fmt.Fprintf(w, "%s%s\n", indent, line) 721 return err 722 } 723 724 // ListConds prints to w a list of conditions, one per line, 725 // annotating each with a description and whether the condition 726 // is true in the state s (if s is non-nil). 727 // 728 // Each of the tag arguments should be a condition string of 729 // the form "name" or "name:suffix". If no tags are passed as 730 // arguments, ListConds lists all conditions registered in 731 // the engine e. 732 func (e *Engine) ListConds(w io.Writer, s *State, tags ...string) error { 733 if tags == nil { 734 tags = make([]string, 0, len(e.Conds)) 735 for name := range e.Conds { 736 tags = append(tags, name) 737 } 738 sort.Strings(tags) 739 } 740 741 for _, tag := range tags { 742 if prefix, suffix, ok := strings.Cut(tag, ":"); ok { 743 cond := e.Conds[prefix] 744 if cond == nil { 745 return fmt.Errorf("unknown condition prefix %q", prefix) 746 } 747 usage := cond.Usage() 748 if !usage.Prefix { 749 return fmt.Errorf("condition %q cannot be used with a suffix", prefix) 750 } 751 752 activeStr := "" 753 if s != nil { 754 if active, _ := cond.Eval(s, suffix); active { 755 activeStr = " (active)" 756 } 757 } 758 _, err := fmt.Fprintf(w, "[%s]%s\n\t%s\n", tag, activeStr, usage.Summary) 759 if err != nil { 760 return err 761 } 762 continue 763 } 764 765 cond := e.Conds[tag] 766 if cond == nil { 767 return fmt.Errorf("unknown condition %q", tag) 768 } 769 var err error 770 usage := cond.Usage() 771 if usage.Prefix { 772 _, err = fmt.Fprintf(w, "[%s:*]\n\t%s\n", tag, usage.Summary) 773 } else { 774 activeStr := "" 775 if s != nil { 776 if ok, _ := cond.Eval(s, ""); ok { 777 activeStr = " (active)" 778 } 779 } 780 _, err = fmt.Fprintf(w, "[%s]%s\n\t%s\n", tag, activeStr, usage.Summary) 781 } 782 if err != nil { 783 return err 784 } 785 } 786 787 return nil 788 }