Source file src/cmd/go/internal/work/shell.go

     1  // Copyright 2023 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 work
     6  
     7  import (
     8  	"bytes"
     9  	"cmd/go/internal/base"
    10  	"cmd/go/internal/cache"
    11  	"cmd/go/internal/cfg"
    12  	"cmd/go/internal/load"
    13  	"cmd/go/internal/str"
    14  	"cmd/internal/par"
    15  	"cmd/internal/pathcache"
    16  	"errors"
    17  	"fmt"
    18  	"internal/lazyregexp"
    19  	"io"
    20  	"io/fs"
    21  	"os"
    22  	"os/exec"
    23  	"path/filepath"
    24  	"runtime"
    25  	"strconv"
    26  	"strings"
    27  	"sync"
    28  	"time"
    29  )
    30  
    31  // A Shell runs shell commands and performs shell-like file system operations.
    32  //
    33  // Shell tracks context related to running commands, and form a tree much like
    34  // context.Context.
    35  type Shell struct {
    36  	action       *Action // nil for the root shell
    37  	*shellShared         // per-Builder state shared across Shells
    38  }
    39  
    40  // shellShared is Shell state shared across all Shells derived from a single
    41  // root shell (generally a single Builder).
    42  type shellShared struct {
    43  	workDir string // $WORK, immutable
    44  
    45  	printLock sync.Mutex
    46  	printer   load.Printer
    47  	scriptDir string // current directory in printed script
    48  
    49  	mkdirCache par.Cache[string, error] // a cache of created directories
    50  }
    51  
    52  // NewShell returns a new Shell.
    53  //
    54  // Shell will internally serialize calls to the printer.
    55  // If printer is nil, it uses load.DefaultPrinter.
    56  func NewShell(workDir string, printer load.Printer) *Shell {
    57  	if printer == nil {
    58  		printer = load.DefaultPrinter()
    59  	}
    60  	shared := &shellShared{
    61  		workDir: workDir,
    62  		printer: printer,
    63  	}
    64  	return &Shell{shellShared: shared}
    65  }
    66  
    67  func (sh *Shell) pkg() *load.Package {
    68  	if sh.action == nil {
    69  		return nil
    70  	}
    71  	return sh.action.Package
    72  }
    73  
    74  // Printf emits a to this Shell's output stream, formatting it like fmt.Printf.
    75  // It is safe to call concurrently.
    76  func (sh *Shell) Printf(format string, a ...any) {
    77  	sh.printLock.Lock()
    78  	defer sh.printLock.Unlock()
    79  	sh.printer.Printf(sh.pkg(), format, a...)
    80  }
    81  
    82  func (sh *Shell) printfLocked(format string, a ...any) {
    83  	sh.printer.Printf(sh.pkg(), format, a...)
    84  }
    85  
    86  // Errorf reports an error on sh's package and sets the process exit status to 1.
    87  func (sh *Shell) Errorf(format string, a ...any) {
    88  	sh.printLock.Lock()
    89  	defer sh.printLock.Unlock()
    90  	sh.printer.Errorf(sh.pkg(), format, a...)
    91  }
    92  
    93  // WithAction returns a Shell identical to sh, but bound to Action a.
    94  func (sh *Shell) WithAction(a *Action) *Shell {
    95  	sh2 := *sh
    96  	sh2.action = a
    97  	return &sh2
    98  }
    99  
   100  // Shell returns a shell for running commands on behalf of Action a.
   101  func (b *Builder) Shell(a *Action) *Shell {
   102  	if a == nil {
   103  		// The root shell has a nil Action. The point of this method is to
   104  		// create a Shell bound to an Action, so disallow nil Actions here.
   105  		panic("nil Action")
   106  	}
   107  	if a.sh == nil {
   108  		a.sh = b.backgroundSh.WithAction(a)
   109  	}
   110  	return a.sh
   111  }
   112  
   113  // BackgroundShell returns a Builder-wide Shell that's not bound to any Action.
   114  // Try not to use this unless there's really no sensible Action available.
   115  func (b *Builder) BackgroundShell() *Shell {
   116  	return b.backgroundSh
   117  }
   118  
   119  // moveOrCopyFile is like 'mv src dst' or 'cp src dst'.
   120  func (sh *Shell) moveOrCopyFile(dst, src string, perm fs.FileMode, force bool) error {
   121  	if cfg.BuildN {
   122  		sh.ShowCmd("", "mv %s %s", src, dst)
   123  		return nil
   124  	}
   125  
   126  	// If we can update the mode and rename to the dst, do it.
   127  	// Otherwise fall back to standard copy.
   128  
   129  	// If the source is in the build cache, we need to copy it.
   130  	dir, _, _ := cache.DefaultDir()
   131  	if strings.HasPrefix(src, dir) {
   132  		return sh.CopyFile(dst, src, perm, force)
   133  	}
   134  
   135  	if err := sh.move(src, dst, perm); err == nil {
   136  		if cfg.BuildX {
   137  			sh.ShowCmd("", "mv %s %s", src, dst)
   138  		}
   139  		return nil
   140  	}
   141  
   142  	return sh.CopyFile(dst, src, perm, force)
   143  }
   144  
   145  // CopyFile is like 'cp src dst'.
   146  func (sh *Shell) CopyFile(dst, src string, perm fs.FileMode, force bool) error {
   147  	if cfg.BuildN || cfg.BuildX {
   148  		sh.ShowCmd("", "cp %s %s", src, dst)
   149  		if cfg.BuildN {
   150  			return nil
   151  		}
   152  	}
   153  
   154  	sf, err := os.Open(src)
   155  	if err != nil {
   156  		return err
   157  	}
   158  	defer sf.Close()
   159  
   160  	// Be careful about removing/overwriting dst.
   161  	// Do not remove/overwrite if dst exists and is a directory
   162  	// or a non-empty non-object file.
   163  	if fi, err := os.Stat(dst); err == nil {
   164  		if fi.IsDir() {
   165  			return fmt.Errorf("build output %q already exists and is a directory", dst)
   166  		}
   167  		if !force && fi.Mode().IsRegular() && fi.Size() != 0 && !isObject(dst) {
   168  			return fmt.Errorf("build output %q already exists and is not an object file", dst)
   169  		}
   170  	}
   171  
   172  	// On Windows, remove lingering ~ file from last attempt.
   173  	if runtime.GOOS == "windows" {
   174  		if _, err := os.Stat(dst + "~"); err == nil {
   175  			os.Remove(dst + "~")
   176  		}
   177  	}
   178  
   179  	mayberemovefile(dst)
   180  	df, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
   181  	if err != nil && runtime.GOOS == "windows" {
   182  		// Windows does not allow deletion of a binary file
   183  		// while it is executing. Try to move it out of the way.
   184  		// If the move fails, which is likely, we'll try again the
   185  		// next time we do an install of this binary.
   186  		if err := os.Rename(dst, dst+"~"); err == nil {
   187  			os.Remove(dst + "~")
   188  		}
   189  		df, err = os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
   190  	}
   191  	if err != nil {
   192  		return fmt.Errorf("copying %s: %w", src, err) // err should already refer to dst
   193  	}
   194  
   195  	_, err = io.Copy(df, sf)
   196  	df.Close()
   197  	if err != nil {
   198  		mayberemovefile(dst)
   199  		return fmt.Errorf("copying %s to %s: %v", src, dst, err)
   200  	}
   201  	return nil
   202  }
   203  
   204  // mayberemovefile removes a file only if it is a regular file
   205  // When running as a user with sufficient privileges, we may delete
   206  // even device files, for example, which is not intended.
   207  func mayberemovefile(s string) {
   208  	if fi, err := os.Lstat(s); err == nil && !fi.Mode().IsRegular() {
   209  		return
   210  	}
   211  	os.Remove(s)
   212  }
   213  
   214  // writeFile writes the text to file.
   215  func (sh *Shell) writeFile(file string, text []byte) error {
   216  	if cfg.BuildN || cfg.BuildX {
   217  		switch {
   218  		case len(text) == 0:
   219  			sh.ShowCmd("", "echo -n > %s # internal", file)
   220  		case bytes.IndexByte(text, '\n') == len(text)-1:
   221  			// One line. Use a simpler "echo" command.
   222  			sh.ShowCmd("", "echo '%s' > %s # internal", bytes.TrimSuffix(text, []byte("\n")), file)
   223  		default:
   224  			// Use the most general form.
   225  			sh.ShowCmd("", "cat >%s << 'EOF' # internal\n%sEOF", file, text)
   226  		}
   227  	}
   228  	if cfg.BuildN {
   229  		return nil
   230  	}
   231  	return os.WriteFile(file, text, 0666)
   232  }
   233  
   234  // Mkdir makes the named directory.
   235  func (sh *Shell) Mkdir(dir string) error {
   236  	// Make Mkdir(a.Objdir) a no-op instead of an error when a.Objdir == "".
   237  	if dir == "" {
   238  		return nil
   239  	}
   240  
   241  	// We can be a little aggressive about being
   242  	// sure directories exist. Skip repeated calls.
   243  	return sh.mkdirCache.Do(dir, func() error {
   244  		if cfg.BuildN || cfg.BuildX {
   245  			sh.ShowCmd("", "mkdir -p %s", dir)
   246  			if cfg.BuildN {
   247  				return nil
   248  			}
   249  		}
   250  
   251  		return os.MkdirAll(dir, 0777)
   252  	})
   253  }
   254  
   255  // RemoveAll is like 'rm -rf'. It attempts to remove all paths even if there's
   256  // an error, and returns the first error.
   257  func (sh *Shell) RemoveAll(paths ...string) error {
   258  	if cfg.BuildN || cfg.BuildX {
   259  		// Don't say we are removing the directory if we never created it.
   260  		show := func() bool {
   261  			for _, path := range paths {
   262  				if _, ok := sh.mkdirCache.Get(path); ok {
   263  					return true
   264  				}
   265  				if _, err := os.Stat(path); !os.IsNotExist(err) {
   266  					return true
   267  				}
   268  			}
   269  			return false
   270  		}
   271  		if show() {
   272  			sh.ShowCmd("", "rm -rf %s", strings.Join(paths, " "))
   273  		}
   274  	}
   275  	if cfg.BuildN {
   276  		return nil
   277  	}
   278  
   279  	var err error
   280  	for _, path := range paths {
   281  		if err2 := os.RemoveAll(path); err2 != nil && err == nil {
   282  			err = err2
   283  		}
   284  	}
   285  	return err
   286  }
   287  
   288  // Symlink creates a symlink newname -> oldname.
   289  func (sh *Shell) Symlink(oldname, newname string) error {
   290  	// It's not an error to try to recreate an existing symlink.
   291  	if link, err := os.Readlink(newname); err == nil && link == oldname {
   292  		return nil
   293  	}
   294  
   295  	if cfg.BuildN || cfg.BuildX {
   296  		sh.ShowCmd("", "ln -s %s %s", oldname, newname)
   297  		if cfg.BuildN {
   298  			return nil
   299  		}
   300  	}
   301  	return os.Symlink(oldname, newname)
   302  }
   303  
   304  // fmtCmd formats a command in the manner of fmt.Sprintf but also:
   305  //
   306  //	fmtCmd replaces the value of b.WorkDir with $WORK.
   307  func (sh *Shell) fmtCmd(dir string, format string, args ...any) string {
   308  	cmd := fmt.Sprintf(format, args...)
   309  	if sh.workDir != "" && !strings.HasPrefix(cmd, "cat ") {
   310  		cmd = strings.ReplaceAll(cmd, sh.workDir, "$WORK")
   311  		escaped := strconv.Quote(sh.workDir)
   312  		escaped = escaped[1 : len(escaped)-1] // strip quote characters
   313  		if escaped != sh.workDir {
   314  			cmd = strings.ReplaceAll(cmd, escaped, "$WORK")
   315  		}
   316  	}
   317  	return cmd
   318  }
   319  
   320  // ShowCmd prints the given command to standard output
   321  // for the implementation of -n or -x.
   322  //
   323  // ShowCmd also replaces the name of the current script directory with dot (.)
   324  // but only when it is at the beginning of a space-separated token.
   325  //
   326  // If dir is not "" or "/" and not the current script directory, ShowCmd first
   327  // prints a "cd" command to switch to dir and updates the script directory.
   328  func (sh *Shell) ShowCmd(dir string, format string, args ...any) {
   329  	// Use the output lock directly so we can manage scriptDir.
   330  	sh.printLock.Lock()
   331  	defer sh.printLock.Unlock()
   332  
   333  	cmd := sh.fmtCmd(dir, format, args...)
   334  
   335  	if dir != "" && dir != "/" {
   336  		if dir != sh.scriptDir {
   337  			// Show changing to dir and update the current directory.
   338  			sh.printfLocked("%s", sh.fmtCmd("", "cd %s\n", dir))
   339  			sh.scriptDir = dir
   340  		}
   341  		// Replace scriptDir is our working directory. Replace it
   342  		// with "." in the command.
   343  		dot := " ."
   344  		if dir[len(dir)-1] == filepath.Separator {
   345  			dot += string(filepath.Separator)
   346  		}
   347  		cmd = strings.ReplaceAll(" "+cmd, " "+dir, dot)[1:]
   348  	}
   349  
   350  	sh.printfLocked("%s\n", cmd)
   351  }
   352  
   353  // reportCmd reports the output and exit status of a command. The cmdOut and
   354  // cmdErr arguments are the output and exit error of the command, respectively.
   355  //
   356  // The exact reporting behavior is as follows:
   357  //
   358  //	cmdOut  cmdErr  Result
   359  //	""      nil     print nothing, return nil
   360  //	!=""    nil     print output, return nil
   361  //	""      !=nil   print nothing, return cmdErr (later printed)
   362  //	!=""    !=nil   print nothing, ignore err, return output as error (later printed)
   363  //
   364  // reportCmd returns a non-nil error if and only if cmdErr != nil. It assumes
   365  // that the command output, if non-empty, is more detailed than the command
   366  // error (which is usually just an exit status), so prefers using the output as
   367  // the ultimate error. Typically, the caller should return this error from an
   368  // Action, which it will be printed by the Builder.
   369  //
   370  // reportCmd formats the output as "# desc" followed by the given output. The
   371  // output is expected to contain references to 'dir', usually the source
   372  // directory for the package that has failed to build. reportCmd rewrites
   373  // mentions of dir with a relative path to dir when the relative path is
   374  // shorter. This is usually more pleasant. For example, if fmt doesn't compile
   375  // and we are in src/html, the output is
   376  //
   377  //	$ go build
   378  //	# fmt
   379  //	../fmt/print.go:1090: undefined: asdf
   380  //	$
   381  //
   382  // instead of
   383  //
   384  //	$ go build
   385  //	# fmt
   386  //	/usr/gopher/go/src/fmt/print.go:1090: undefined: asdf
   387  //	$
   388  //
   389  // reportCmd also replaces references to the work directory with $WORK, replaces
   390  // cgo file paths with the original file path, and replaces cgo-mangled names
   391  // with "C.name".
   392  //
   393  // desc is optional. If "", a.Package.Desc() is used.
   394  //
   395  // dir is optional. If "", a.Package.Dir is used.
   396  func (sh *Shell) reportCmd(desc, dir string, cmdOut []byte, cmdErr error) error {
   397  	if len(cmdOut) == 0 && cmdErr == nil {
   398  		// Common case
   399  		return nil
   400  	}
   401  	if len(cmdOut) == 0 && cmdErr != nil {
   402  		// Just return the error.
   403  		//
   404  		// TODO: This is what we've done for a long time, but it may be a
   405  		// mistake because it loses all of the extra context and results in
   406  		// ultimately less descriptive output. We should probably just take the
   407  		// text of cmdErr as the output in this case and do everything we
   408  		// otherwise would. We could chain the errors if we feel like it.
   409  		return cmdErr
   410  	}
   411  
   412  	// Fetch defaults from the package.
   413  	var p *load.Package
   414  	a := sh.action
   415  	if a != nil {
   416  		p = a.Package
   417  	}
   418  	var importPath string
   419  	if p != nil {
   420  		importPath = p.ImportPath
   421  		if desc == "" {
   422  			desc = p.Desc()
   423  		}
   424  		if dir == "" {
   425  			dir = p.Dir
   426  		}
   427  	}
   428  
   429  	out := string(cmdOut)
   430  
   431  	if !strings.HasSuffix(out, "\n") {
   432  		out = out + "\n"
   433  	}
   434  
   435  	// Replace workDir with $WORK
   436  	out = replacePrefix(out, sh.workDir, "$WORK")
   437  
   438  	// Rewrite mentions of dir with a relative path to dir
   439  	// when the relative path is shorter.
   440  	for {
   441  		// Note that dir starts out long, something like
   442  		// /foo/bar/baz/root/a
   443  		// The target string to be reduced is something like
   444  		// (blah-blah-blah) /foo/bar/baz/root/sibling/whatever.go:blah:blah
   445  		// /foo/bar/baz/root/a doesn't match /foo/bar/baz/root/sibling, but the prefix
   446  		// /foo/bar/baz/root does.  And there may be other niblings sharing shorter
   447  		// prefixes, the only way to find them is to look.
   448  		// This doesn't always produce a relative path --
   449  		// /foo is shorter than ../../.., for example.
   450  		if reldir := base.ShortPath(dir); reldir != dir {
   451  			out = replacePrefix(out, dir, reldir)
   452  			if filepath.Separator == '\\' {
   453  				// Don't know why, sometimes this comes out with slashes, not backslashes.
   454  				wdir := strings.ReplaceAll(dir, "\\", "/")
   455  				out = replacePrefix(out, wdir, reldir)
   456  			}
   457  		}
   458  		dirP := filepath.Dir(dir)
   459  		if dir == dirP {
   460  			break
   461  		}
   462  		dir = dirP
   463  	}
   464  
   465  	// Fix up output referring to cgo-generated code to be more readable.
   466  	// Replace x.go:19[/tmp/.../x.cgo1.go:18] with x.go:19.
   467  	// Replace *[100]_Ctype_foo with *[100]C.foo.
   468  	// If we're using -x, assume we're debugging and want the full dump, so disable the rewrite.
   469  	if !cfg.BuildX && cgoLine.MatchString(out) {
   470  		out = cgoLine.ReplaceAllString(out, "")
   471  		out = cgoTypeSigRe.ReplaceAllString(out, "C.")
   472  	}
   473  
   474  	// Usually desc is already p.Desc(), but if not, signal cmdError.Error to
   475  	// add a line explicitly mentioning the import path.
   476  	needsPath := importPath != "" && p != nil && desc != p.Desc()
   477  
   478  	err := &cmdError{desc, out, importPath, needsPath}
   479  	if cmdErr != nil {
   480  		// The command failed. Report the output up as an error.
   481  		return err
   482  	}
   483  	// The command didn't fail, so just print the output as appropriate.
   484  	if a != nil && a.output != nil {
   485  		// The Action is capturing output.
   486  		a.output = append(a.output, err.Error()...)
   487  	} else {
   488  		// Write directly to the Builder output.
   489  		sh.Printf("%s", err)
   490  	}
   491  	return nil
   492  }
   493  
   494  // replacePrefix is like strings.ReplaceAll, but only replaces instances of old
   495  // that are preceded by ' ', '\t', or appear at the beginning of a line.
   496  func replacePrefix(s, old, new string) string {
   497  	n := strings.Count(s, old)
   498  	if n == 0 {
   499  		return s
   500  	}
   501  
   502  	s = strings.ReplaceAll(s, " "+old, " "+new)
   503  	s = strings.ReplaceAll(s, "\n"+old, "\n"+new)
   504  	s = strings.ReplaceAll(s, "\n\t"+old, "\n\t"+new)
   505  	if strings.HasPrefix(s, old) {
   506  		s = new + s[len(old):]
   507  	}
   508  	return s
   509  }
   510  
   511  type cmdError struct {
   512  	desc       string
   513  	text       string
   514  	importPath string
   515  	needsPath  bool // Set if desc does not already include the import path
   516  }
   517  
   518  func (e *cmdError) Error() string {
   519  	var msg string
   520  	if e.needsPath {
   521  		// Ensure the import path is part of the message.
   522  		// Clearly distinguish the description from the import path.
   523  		msg = fmt.Sprintf("# %s\n# [%s]\n", e.importPath, e.desc)
   524  	} else {
   525  		msg = "# " + e.desc + "\n"
   526  	}
   527  	return msg + e.text
   528  }
   529  
   530  func (e *cmdError) ImportPath() string {
   531  	return e.importPath
   532  }
   533  
   534  var cgoLine = lazyregexp.New(`\[[^\[\]]+\.(cgo1|cover)\.go:[0-9]+(:[0-9]+)?\]`)
   535  var cgoTypeSigRe = lazyregexp.New(`\b_C2?(type|func|var|macro)_\B`)
   536  
   537  // run runs the command given by cmdline in the directory dir.
   538  // If the command fails, run prints information about the failure
   539  // and returns a non-nil error.
   540  func (sh *Shell) run(dir string, desc string, env []string, cmdargs ...any) error {
   541  	out, err := sh.runOut(dir, env, cmdargs...)
   542  	if desc == "" {
   543  		desc = sh.fmtCmd(dir, "%s", strings.Join(str.StringList(cmdargs...), " "))
   544  	}
   545  	return sh.reportCmd(desc, dir, out, err)
   546  }
   547  
   548  // runOut runs the command given by cmdline in the directory dir.
   549  // It returns the command output and any errors that occurred.
   550  // It accumulates execution time in a.
   551  func (sh *Shell) runOut(dir string, env []string, cmdargs ...any) ([]byte, error) {
   552  	a := sh.action
   553  
   554  	cmdline := str.StringList(cmdargs...)
   555  
   556  	for _, arg := range cmdline {
   557  		// GNU binutils commands, including gcc and gccgo, interpret an argument
   558  		// @foo anywhere in the command line (even following --) as meaning
   559  		// "read and insert arguments from the file named foo."
   560  		// Don't say anything that might be misinterpreted that way.
   561  		if strings.HasPrefix(arg, "@") {
   562  			return nil, fmt.Errorf("invalid command-line argument %s in command: %s", arg, joinUnambiguously(cmdline))
   563  		}
   564  	}
   565  
   566  	if cfg.BuildN || cfg.BuildX {
   567  		var envcmdline string
   568  		for _, e := range env {
   569  			if j := strings.IndexByte(e, '='); j != -1 {
   570  				if strings.ContainsRune(e[j+1:], '\'') {
   571  					envcmdline += fmt.Sprintf("%s=%q", e[:j], e[j+1:])
   572  				} else {
   573  					envcmdline += fmt.Sprintf("%s='%s'", e[:j], e[j+1:])
   574  				}
   575  				envcmdline += " "
   576  			}
   577  		}
   578  		envcmdline += joinUnambiguously(cmdline)
   579  		sh.ShowCmd(dir, "%s", envcmdline)
   580  		if cfg.BuildN {
   581  			return nil, nil
   582  		}
   583  	}
   584  
   585  	var buf bytes.Buffer
   586  	path, err := pathcache.LookPath(cmdline[0])
   587  	if err != nil {
   588  		return nil, err
   589  	}
   590  	cmd := exec.Command(path, cmdline[1:]...)
   591  	if cmd.Path != "" {
   592  		cmd.Args[0] = cmd.Path
   593  	}
   594  	cmd.Stdout = &buf
   595  	cmd.Stderr = &buf
   596  	cleanup := passLongArgsInResponseFiles(cmd)
   597  	defer cleanup()
   598  	if dir != "." {
   599  		cmd.Dir = dir
   600  	}
   601  	cmd.Env = cmd.Environ() // Pre-allocate with correct PWD.
   602  
   603  	// Add the TOOLEXEC_IMPORTPATH environment variable for -toolexec tools.
   604  	// It doesn't really matter if -toolexec isn't being used.
   605  	// Note that a.Package.Desc is not really an import path,
   606  	// but this is consistent with 'go list -f {{.ImportPath}}'.
   607  	// Plus, it is useful to uniquely identify packages in 'go list -json'.
   608  	if a != nil && a.Package != nil {
   609  		cmd.Env = append(cmd.Env, "TOOLEXEC_IMPORTPATH="+a.Package.Desc())
   610  	}
   611  
   612  	cmd.Env = append(cmd.Env, env...)
   613  	start := time.Now()
   614  	err = cmd.Run()
   615  	if a != nil && a.json != nil {
   616  		aj := a.json
   617  		aj.Cmd = append(aj.Cmd, joinUnambiguously(cmdline))
   618  		aj.CmdReal += time.Since(start)
   619  		if ps := cmd.ProcessState; ps != nil {
   620  			aj.CmdUser += ps.UserTime()
   621  			aj.CmdSys += ps.SystemTime()
   622  		}
   623  	}
   624  
   625  	// err can be something like 'exit status 1'.
   626  	// Add information about what program was running.
   627  	// Note that if buf.Bytes() is non-empty, the caller usually
   628  	// shows buf.Bytes() and does not print err at all, so the
   629  	// prefix here does not make most output any more verbose.
   630  	if err != nil {
   631  		err = errors.New(cmdline[0] + ": " + err.Error())
   632  	}
   633  	return buf.Bytes(), err
   634  }
   635  
   636  // joinUnambiguously prints the slice, quoting where necessary to make the
   637  // output unambiguous.
   638  // TODO: See issue 5279. The printing of commands needs a complete redo.
   639  func joinUnambiguously(a []string) string {
   640  	var buf strings.Builder
   641  	for i, s := range a {
   642  		if i > 0 {
   643  			buf.WriteByte(' ')
   644  		}
   645  		q := strconv.Quote(s)
   646  		// A gccgo command line can contain -( and -).
   647  		// Make sure we quote them since they are special to the shell.
   648  		// The trimpath argument can also contain > (part of =>) and ;. Quote those too.
   649  		if s == "" || strings.ContainsAny(s, " ()>;") || len(q) > len(s)+2 {
   650  			buf.WriteString(q)
   651  		} else {
   652  			buf.WriteString(s)
   653  		}
   654  	}
   655  	return buf.String()
   656  }
   657  

View as plain text