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

View as plain text