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

View as plain text