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

View as plain text