github.com/theclapp/sh@v2.6.4+incompatible/interp/builtin.go (about)

     1  // Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
     2  // See LICENSE for licensing information
     3  
     4  package interp
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"mvdan.cc/sh/expand"
    17  	"mvdan.cc/sh/syntax"
    18  )
    19  
    20  func isBuiltin(name string) bool {
    21  	switch name {
    22  	case "true", ":", "false", "exit", "set", "shift", "unset",
    23  		"echo", "printf", "break", "continue", "pwd", "cd",
    24  		"wait", "builtin", "trap", "type", "source", ".", "command",
    25  		"dirs", "pushd", "popd", "umask", "alias", "unalias",
    26  		"fg", "bg", "getopts", "eval", "test", "[", "exec",
    27  		"return", "read", "shopt":
    28  		return true
    29  	}
    30  	return false
    31  }
    32  
    33  func oneIf(b bool) int {
    34  	if b {
    35  		return 1
    36  	}
    37  	return 0
    38  }
    39  
    40  // atoi is just a shorthand for strconv.Atoi that ignores the error,
    41  // just like shells do.
    42  func atoi(s string) int {
    43  	n, _ := strconv.Atoi(s)
    44  	return n
    45  }
    46  
    47  func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, args []string) int {
    48  	switch name {
    49  	case "true", ":":
    50  	case "false":
    51  		return 1
    52  	case "exit":
    53  		switch len(args) {
    54  		case 0:
    55  		case 1:
    56  			if n, err := strconv.Atoi(args[0]); err != nil {
    57  				r.errf("invalid exit status code: %q\n", args[0])
    58  				r.exit = 2
    59  			} else {
    60  				r.exit = n
    61  			}
    62  		default:
    63  			r.errf("exit cannot take multiple arguments\n")
    64  			r.exit = 1
    65  		}
    66  		r.setErr(ShellExitStatus(r.exit))
    67  		return 0 // the command's exit status does not matter
    68  	case "set":
    69  		if err := Params(args...)(r); err != nil {
    70  			r.errf("set: %v\n", err)
    71  			return 2
    72  		}
    73  		r.updateExpandOpts()
    74  	case "shift":
    75  		n := 1
    76  		switch len(args) {
    77  		case 0:
    78  		case 1:
    79  			if n2, err := strconv.Atoi(args[0]); err == nil {
    80  				n = n2
    81  				break
    82  			}
    83  			fallthrough
    84  		default:
    85  			r.errf("usage: shift [n]\n")
    86  			return 2
    87  		}
    88  		if n >= len(r.Params) {
    89  			r.Params = nil
    90  		} else {
    91  			r.Params = r.Params[n:]
    92  		}
    93  	case "unset":
    94  		vars := true
    95  		funcs := true
    96  	unsetOpts:
    97  		for i, arg := range args {
    98  			switch arg {
    99  			case "-v":
   100  				funcs = false
   101  			case "-f":
   102  				vars = false
   103  			default:
   104  				args = args[i:]
   105  				break unsetOpts
   106  			}
   107  		}
   108  
   109  		for _, arg := range args {
   110  			if vr := r.lookupVar(arg); vr.IsSet() && vars {
   111  				r.delVar(arg)
   112  				continue
   113  			}
   114  			if _, ok := r.Funcs[arg]; ok && funcs {
   115  				delete(r.Funcs, arg)
   116  			}
   117  		}
   118  	case "echo":
   119  		newline, doExpand := true, false
   120  	echoOpts:
   121  		for len(args) > 0 {
   122  			switch args[0] {
   123  			case "-n":
   124  				newline = false
   125  			case "-e":
   126  				doExpand = true
   127  			case "-E": // default
   128  			default:
   129  				break echoOpts
   130  			}
   131  			args = args[1:]
   132  		}
   133  		for i, arg := range args {
   134  			if i > 0 {
   135  				r.out(" ")
   136  			}
   137  			if doExpand {
   138  				arg, _, _ = expand.Format(r.ecfg, arg, nil)
   139  			}
   140  			r.out(arg)
   141  		}
   142  		if newline {
   143  			r.out("\n")
   144  		}
   145  	case "printf":
   146  		if len(args) == 0 {
   147  			r.errf("usage: printf format [arguments]\n")
   148  			return 2
   149  		}
   150  		format, args := args[0], args[1:]
   151  		for {
   152  			s, n, err := expand.Format(r.ecfg, format, args)
   153  			if err != nil {
   154  				r.errf("%v\n", err)
   155  				return 1
   156  			}
   157  			r.out(s)
   158  			args = args[n:]
   159  			if n == 0 || len(args) == 0 {
   160  				break
   161  			}
   162  		}
   163  	case "break", "continue":
   164  		if !r.inLoop {
   165  			r.errf("%s is only useful in a loop", name)
   166  			break
   167  		}
   168  		enclosing := &r.breakEnclosing
   169  		if name == "continue" {
   170  			enclosing = &r.contnEnclosing
   171  		}
   172  		switch len(args) {
   173  		case 0:
   174  			*enclosing = 1
   175  		case 1:
   176  			if n, err := strconv.Atoi(args[0]); err == nil {
   177  				*enclosing = n
   178  				break
   179  			}
   180  			fallthrough
   181  		default:
   182  			r.errf("usage: %s [n]\n", name)
   183  			return 2
   184  		}
   185  	case "pwd":
   186  		r.outf("%s\n", r.envGet("PWD"))
   187  	case "cd":
   188  		var path string
   189  		switch len(args) {
   190  		case 0:
   191  			path = r.envGet("HOME")
   192  		case 1:
   193  			path = args[0]
   194  		default:
   195  			r.errf("usage: cd [dir]\n")
   196  			return 2
   197  		}
   198  		return r.changeDir(path)
   199  	case "wait":
   200  		if len(args) > 0 {
   201  			panic("wait with args not handled yet")
   202  		}
   203  		switch err := r.bgShells.Wait().(type) {
   204  		case nil:
   205  		case ExitStatus:
   206  		case ShellExitStatus:
   207  		default:
   208  			r.setErr(err)
   209  		}
   210  	case "builtin":
   211  		if len(args) < 1 {
   212  			break
   213  		}
   214  		if !isBuiltin(args[0]) {
   215  			return 1
   216  		}
   217  		return r.builtinCode(ctx, pos, args[0], args[1:])
   218  	case "type":
   219  		anyNotFound := false
   220  		for _, arg := range args {
   221  			if _, ok := r.Funcs[arg]; ok {
   222  				r.outf("%s is a function\n", arg)
   223  				continue
   224  			}
   225  			if isBuiltin(arg) {
   226  				r.outf("%s is a shell builtin\n", arg)
   227  				continue
   228  			}
   229  			if path, err := exec.LookPath(arg); err == nil {
   230  				r.outf("%s is %s\n", arg, path)
   231  				continue
   232  			}
   233  			r.errf("type: %s: not found\n", arg)
   234  			anyNotFound = true
   235  		}
   236  		if anyNotFound {
   237  			return 1
   238  		}
   239  	case "eval":
   240  		src := strings.Join(args, " ")
   241  		p := syntax.NewParser()
   242  		file, err := p.Parse(strings.NewReader(src), "")
   243  		if err != nil {
   244  			r.errf("eval: %v\n", err)
   245  			return 1
   246  		}
   247  		r.stmts(ctx, file.StmtList)
   248  		return r.exit
   249  	case "source", ".":
   250  		if len(args) < 1 {
   251  			r.errf("%v: source: need filename\n", pos)
   252  			return 2
   253  		}
   254  		f, err := r.open(ctx, r.relPath(args[0]), os.O_RDONLY, 0, false)
   255  		if err != nil {
   256  			r.errf("source: %v\n", err)
   257  			return 1
   258  		}
   259  		defer f.Close()
   260  		p := syntax.NewParser()
   261  		file, err := p.Parse(f, args[0])
   262  		if err != nil {
   263  			r.errf("source: %v\n", err)
   264  			return 1
   265  		}
   266  		oldParams := r.Params
   267  		r.Params = args[1:]
   268  		oldInSource := r.inSource
   269  		r.inSource = true
   270  		r.stmts(ctx, file.StmtList)
   271  
   272  		r.Params = oldParams
   273  		r.inSource = oldInSource
   274  		if code, ok := r.err.(returnStatus); ok {
   275  			r.err = nil
   276  			r.exit = int(code)
   277  		}
   278  		return r.exit
   279  	case "[":
   280  		if len(args) == 0 || args[len(args)-1] != "]" {
   281  			r.errf("%v: [: missing matching ]\n", pos)
   282  			return 2
   283  		}
   284  		args = args[:len(args)-1]
   285  		fallthrough
   286  	case "test":
   287  		parseErr := false
   288  		p := testParser{
   289  			rem: args,
   290  			err: func(err error) {
   291  				r.errf("%v: %v\n", pos, err)
   292  				parseErr = true
   293  			},
   294  		}
   295  		p.next()
   296  		expr := p.classicTest("[", false)
   297  		if parseErr {
   298  			return 2
   299  		}
   300  		return oneIf(r.bashTest(ctx, expr, true) == "")
   301  	case "exec":
   302  		// TODO: Consider syscall.Exec, i.e. actually replacing
   303  		// the process. It's in theory what a shell should do,
   304  		// but in practice it would kill the entire Go process
   305  		// and it's not available on Windows.
   306  		if len(args) == 0 {
   307  			r.keepRedirs = true
   308  			break
   309  		}
   310  		r.exec(ctx, args)
   311  		r.setErr(ShellExitStatus(r.exit))
   312  		return 0
   313  	case "command":
   314  		show := false
   315  		for len(args) > 0 && strings.HasPrefix(args[0], "-") {
   316  			switch args[0] {
   317  			case "-v":
   318  				show = true
   319  			default:
   320  				r.errf("command: invalid option %s\n", args[0])
   321  				return 2
   322  			}
   323  			args = args[1:]
   324  		}
   325  		if len(args) == 0 {
   326  			break
   327  		}
   328  		if !show {
   329  			if isBuiltin(args[0]) {
   330  				return r.builtinCode(ctx, pos, args[0], args[1:])
   331  			}
   332  			r.exec(ctx, args)
   333  			return r.exit
   334  		}
   335  		last := 0
   336  		for _, arg := range args {
   337  			last = 0
   338  			if r.Funcs[arg] != nil || isBuiltin(arg) {
   339  				r.outf("%s\n", arg)
   340  			} else if path, err := exec.LookPath(arg); err == nil {
   341  				r.outf("%s\n", path)
   342  			} else {
   343  				last = 1
   344  			}
   345  		}
   346  		return last
   347  	case "dirs":
   348  		for i := len(r.dirStack) - 1; i >= 0; i-- {
   349  			r.outf("%s", r.dirStack[i])
   350  			if i > 0 {
   351  				r.out(" ")
   352  			}
   353  		}
   354  		r.out("\n")
   355  	case "pushd":
   356  		change := true
   357  		if len(args) > 0 && args[0] == "-n" {
   358  			change = false
   359  			args = args[1:]
   360  		}
   361  		swap := func() string {
   362  			oldtop := r.dirStack[len(r.dirStack)-1]
   363  			top := r.dirStack[len(r.dirStack)-2]
   364  			r.dirStack[len(r.dirStack)-1] = top
   365  			r.dirStack[len(r.dirStack)-2] = oldtop
   366  			return top
   367  		}
   368  		switch len(args) {
   369  		case 0:
   370  			if !change {
   371  				break
   372  			}
   373  			if len(r.dirStack) < 2 {
   374  				r.errf("pushd: no other directory\n")
   375  				return 1
   376  			}
   377  			newtop := swap()
   378  			if code := r.changeDir(newtop); code != 0 {
   379  				return code
   380  			}
   381  			r.builtinCode(ctx, syntax.Pos{}, "dirs", nil)
   382  		case 1:
   383  			if change {
   384  				if code := r.changeDir(args[0]); code != 0 {
   385  					return code
   386  				}
   387  				r.dirStack = append(r.dirStack, r.Dir)
   388  			} else {
   389  				r.dirStack = append(r.dirStack, args[0])
   390  				swap()
   391  			}
   392  			r.builtinCode(ctx, syntax.Pos{}, "dirs", nil)
   393  		default:
   394  			r.errf("pushd: too many arguments\n")
   395  			return 2
   396  		}
   397  	case "popd":
   398  		change := true
   399  		if len(args) > 0 && args[0] == "-n" {
   400  			change = false
   401  			args = args[1:]
   402  		}
   403  		switch len(args) {
   404  		case 0:
   405  			if len(r.dirStack) < 2 {
   406  				r.errf("popd: directory stack empty\n")
   407  				return 1
   408  			}
   409  			oldtop := r.dirStack[len(r.dirStack)-1]
   410  			r.dirStack = r.dirStack[:len(r.dirStack)-1]
   411  			if change {
   412  				newtop := r.dirStack[len(r.dirStack)-1]
   413  				if code := r.changeDir(newtop); code != 0 {
   414  					return code
   415  				}
   416  			} else {
   417  				r.dirStack[len(r.dirStack)-1] = oldtop
   418  			}
   419  			r.builtinCode(ctx, syntax.Pos{}, "dirs", nil)
   420  		default:
   421  			r.errf("popd: invalid argument\n")
   422  			return 2
   423  		}
   424  	case "return":
   425  		if !r.inFunc && !r.inSource {
   426  			r.errf("return: can only be done from a func or sourced script\n")
   427  			return 1
   428  		}
   429  		code := 0
   430  		switch len(args) {
   431  		case 0:
   432  		case 1:
   433  			code = atoi(args[0])
   434  		default:
   435  			r.errf("return: too many arguments\n")
   436  			return 2
   437  		}
   438  		r.setErr(returnStatus(code))
   439  	case "read":
   440  		raw := false
   441  		for len(args) > 0 && strings.HasPrefix(args[0], "-") {
   442  			switch args[0] {
   443  			case "-r":
   444  				raw = true
   445  			default:
   446  				r.errf("read: invalid option %q\n", args[0])
   447  				return 2
   448  			}
   449  			args = args[1:]
   450  		}
   451  
   452  		for _, name := range args {
   453  			if !syntax.ValidName(name) {
   454  				r.errf("read: invalid identifier %q\n", name)
   455  				return 2
   456  			}
   457  		}
   458  
   459  		line, err := r.readLine(raw)
   460  		if err != nil {
   461  			return 1
   462  		}
   463  		if len(args) == 0 {
   464  			args = append(args, "REPLY")
   465  		}
   466  
   467  		values := expand.ReadFields(r.ecfg, string(line), len(args), raw)
   468  		for i, name := range args {
   469  			val := ""
   470  			if i < len(values) {
   471  				val = values[i]
   472  			}
   473  			r.setVar(name, nil, expand.Variable{Value: val})
   474  		}
   475  
   476  		return 0
   477  
   478  	case "getopts":
   479  		if len(args) < 2 {
   480  			r.errf("getopts: usage: getopts optstring name [arg]\n")
   481  			return 2
   482  		}
   483  		optind, _ := strconv.Atoi(r.envGet("OPTIND"))
   484  		if optind-1 != r.optState.argidx {
   485  			if optind < 1 {
   486  				optind = 1
   487  			}
   488  			r.optState = getopts{argidx: optind - 1}
   489  		}
   490  		optstr := args[0]
   491  		name := args[1]
   492  		if !syntax.ValidName(name) {
   493  			r.errf("getopts: invalid identifier: %q\n", name)
   494  			return 2
   495  		}
   496  		args = args[2:]
   497  		if len(args) == 0 {
   498  			args = r.Params
   499  		}
   500  		diagnostics := !strings.HasPrefix(optstr, ":")
   501  
   502  		opt, optarg, done := r.optState.Next(optstr, args)
   503  
   504  		r.setVarString(name, string(opt))
   505  		r.delVar("OPTARG")
   506  		switch {
   507  		case opt == '?' && diagnostics && !done:
   508  			r.errf("getopts: illegal option -- %q\n", optarg)
   509  		case opt == ':' && diagnostics:
   510  			r.errf("getopts: option requires an argument -- %q\n", optarg)
   511  		default:
   512  			if optarg != "" {
   513  				r.setVarString("OPTARG", optarg)
   514  			}
   515  		}
   516  		if optind-1 != r.optState.argidx {
   517  			r.setVarString("OPTIND", strconv.FormatInt(int64(r.optState.argidx+1), 10))
   518  		}
   519  
   520  		return oneIf(done)
   521  
   522  	case "shopt":
   523  		mode := ""
   524  		posixOpts := false
   525  		for len(args) > 0 && strings.HasPrefix(args[0], "-") {
   526  			switch args[0] {
   527  			case "-s", "-u":
   528  				mode = args[0]
   529  			case "-o":
   530  				posixOpts = true
   531  			case "-p", "-q":
   532  				panic(fmt.Sprintf("unhandled shopt flag: %s", args[0]))
   533  			default:
   534  				r.errf("shopt: invalid option %q\n", args[0])
   535  				return 2
   536  			}
   537  			args = args[1:]
   538  		}
   539  		if len(args) == 0 {
   540  			if !posixOpts {
   541  				for i, name := range bashOptsTable {
   542  					r.printOptLine(name, r.opts[len(shellOptsTable)+i])
   543  				}
   544  				break
   545  			}
   546  			for i, opt := range &shellOptsTable {
   547  				r.printOptLine(opt.name, r.opts[i])
   548  			}
   549  			break
   550  		}
   551  		for _, arg := range args {
   552  			opt := r.optByName(arg, !posixOpts)
   553  			if opt == nil {
   554  				r.errf("shopt: invalid option name %q\n", arg)
   555  				return 1
   556  			}
   557  			switch mode {
   558  			case "-s", "-u":
   559  				*opt = mode == "-s"
   560  			default: // ""
   561  				r.printOptLine(arg, *opt)
   562  			}
   563  		}
   564  		r.updateExpandOpts()
   565  
   566  	default:
   567  		// "trap", "umask", "alias", "unalias", "fg", "bg",
   568  		panic(fmt.Sprintf("unhandled builtin: %s", name))
   569  	}
   570  	return 0
   571  }
   572  
   573  func (r *Runner) printOptLine(name string, enabled bool) {
   574  	status := "off"
   575  	if enabled {
   576  		status = "on"
   577  	}
   578  	r.outf("%s\t%s\n", name, status)
   579  }
   580  
   581  func (r *Runner) readLine(raw bool) ([]byte, error) {
   582  	var line []byte
   583  	esc := false
   584  
   585  	for {
   586  		var buf [1]byte
   587  		n, err := r.Stdin.Read(buf[:])
   588  		if n > 0 {
   589  			b := buf[0]
   590  			switch {
   591  			case !raw && b == '\\':
   592  				line = append(line, b)
   593  				esc = !esc
   594  			case !raw && b == '\n' && esc:
   595  				// line continuation
   596  				line = line[len(line)-1:]
   597  				esc = false
   598  			case b == '\n':
   599  				return line, nil
   600  			default:
   601  				line = append(line, b)
   602  				esc = false
   603  			}
   604  		}
   605  		if err == io.EOF && len(line) > 0 {
   606  			return line, nil
   607  		}
   608  		if err != nil {
   609  			return nil, err
   610  		}
   611  	}
   612  }
   613  
   614  func (r *Runner) changeDir(path string) int {
   615  	path = r.relPath(path)
   616  	info, err := r.stat(path)
   617  	if err != nil || !info.IsDir() {
   618  		return 1
   619  	}
   620  	if !hasPermissionToDir(info) {
   621  		return 1
   622  	}
   623  	r.Dir = path
   624  	r.Vars["OLDPWD"] = r.Vars["PWD"]
   625  	r.Vars["PWD"] = expand.Variable{Value: path}
   626  	return 0
   627  }
   628  
   629  func (r *Runner) relPath(path string) string {
   630  	if !filepath.IsAbs(path) {
   631  		path = filepath.Join(r.Dir, path)
   632  	}
   633  	return filepath.Clean(path)
   634  }
   635  
   636  type getopts struct {
   637  	argidx  int
   638  	runeidx int
   639  }
   640  
   641  func (g *getopts) Next(optstr string, args []string) (opt rune, optarg string, done bool) {
   642  	if len(args) == 0 || g.argidx >= len(args) {
   643  		return '?', "", true
   644  	}
   645  	arg := []rune(args[g.argidx])
   646  	if len(arg) < 2 || arg[0] != '-' || arg[1] == '-' {
   647  		return '?', "", true
   648  	}
   649  
   650  	opts := arg[1:]
   651  	opt = opts[g.runeidx]
   652  	if g.runeidx+1 < len(opts) {
   653  		g.runeidx++
   654  	} else {
   655  		g.argidx++
   656  		g.runeidx = 0
   657  	}
   658  
   659  	i := strings.IndexRune(optstr, opt)
   660  	if i < 0 {
   661  		// invalid option
   662  		return '?', string(opt), false
   663  	}
   664  
   665  	if i+1 < len(optstr) && optstr[i+1] == ':' {
   666  		if g.argidx >= len(args) {
   667  			// missing argument
   668  			return ':', string(opt), false
   669  		}
   670  		optarg = args[g.argidx]
   671  		g.argidx++
   672  		g.runeidx = 0
   673  	}
   674  
   675  	return opt, optarg, false
   676  }