Source file src/cmd/internal/script/engine.go

     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  }
   789  

View as plain text