github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/cmds/exp/rush/parse.go (about)

     1  // Copyright 2014-2017 the u-root 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  // The u-root shell is intended to be very simple, since builtins and extensions
     6  // are written in Go. It should not need YACC. As in the JSON parser, we hope this
     7  // simple state machine will do the job.
     8  package main
     9  
    10  import (
    11  	"bufio"
    12  	"errors"
    13  	"fmt"
    14  	"io"
    15  	"os/exec"
    16  	"strings"
    17  )
    18  
    19  type arg struct {
    20  	val string
    21  	mod string
    22  }
    23  
    24  // The Command struct is initially filled in by the parser. The shell itself
    25  // adds to it as processing continues, and then uses it to creates os.Commands
    26  type Command struct {
    27  	*exec.Cmd
    28  	// These are filled in by the parser.
    29  	Args  []arg
    30  	fdmap map[int]string
    31  	files map[int]io.Closer
    32  	Link  string
    33  	BG    bool
    34  
    35  	// These are set up by the shell as it evaluates the Commands
    36  	// provided by the parser.
    37  	// we separate the command so people don't have to put checks for the length
    38  	// of argv in their builtins. We do that for them.
    39  	cmd  string
    40  	argv []string
    41  }
    42  
    43  var (
    44  	cmds  []Command
    45  	punct = "<>|&$ \t\n"
    46  )
    47  
    48  func pushback(b *bufio.Reader) {
    49  	// If we can't UnreadByte it will get us an obscure error, but
    50  	// it is hard to tell what else to do.
    51  	_ = b.UnreadByte()
    52  }
    53  
    54  func one(b *bufio.Reader) byte {
    55  	c, err := b.ReadByte()
    56  	// On any kind of error, just return 0.
    57  	// This is not a serious kind of shell any more, it
    58  	// is more for diagnostics when things are really broken,
    59  	// so we need not be too picky about errors.
    60  	if err != nil {
    61  		return 0
    62  	}
    63  	return c
    64  }
    65  
    66  func next(b *bufio.Reader) byte {
    67  	c := one(b)
    68  	if c == '\\' {
    69  		return one(b)
    70  	}
    71  	return byte(c)
    72  }
    73  
    74  // Tokenize stuff coming in from the stream. For everything but an arg, the
    75  // type is just the thing itself, since we can switch on strings.
    76  func tok(b *bufio.Reader) (string, string) {
    77  	var tokType, arg string
    78  	c := next(b)
    79  
    80  	switch c {
    81  	case 0, 4:
    82  		return "EOF", ""
    83  	case '>':
    84  		return "FD", "1"
    85  	case '<':
    86  		return "FD", "0"
    87  	// yes, I realize $ handling is still pretty hokey.
    88  	// And, again, this is a diagnostic tool now, not a general
    89  	// purpose shell, so the hell with it.
    90  	case '$':
    91  		arg = ""
    92  		return "ENV", arg
    93  	case '\'':
    94  		for {
    95  			nc := next(b)
    96  			if nc == '\'' {
    97  				return "ARG", arg
    98  			}
    99  			arg = arg + string(nc)
   100  		}
   101  	case ' ', '\t':
   102  		return "white", string(c)
   103  	case '\n', '\r':
   104  		return "EOL", ""
   105  	case '|', '&':
   106  		// peek ahead. We need the literal, so don't use next()
   107  		nc := one(b)
   108  		if nc == c {
   109  			return "LINK", string(c) + string(c)
   110  		}
   111  		if nc != 0 {
   112  			pushback(b)
   113  		}
   114  		if c == '&' {
   115  			tokType = "BG"
   116  			if nc == 0 {
   117  				tokType = "EOL"
   118  			}
   119  			return "BG", tokType
   120  		}
   121  		return "LINK", string(c)
   122  	default:
   123  		for {
   124  			if c == 0 {
   125  				return "ARG", arg
   126  			}
   127  			if strings.Contains(punct, string(c)) {
   128  				pushback(b)
   129  				return "ARG", arg
   130  			}
   131  			arg = arg + string(c)
   132  			c = next(b)
   133  		}
   134  
   135  	}
   136  }
   137  
   138  // get an ARG. It has to work.
   139  func getArg(b *bufio.Reader, what string) string {
   140  	for {
   141  		nt, s := tok(b)
   142  		if nt == "EOF" || nt == "EOL" {
   143  			// We used to panic here, but what's the sense in that?
   144  			return ""
   145  		}
   146  		if nt == "white" {
   147  			continue
   148  		}
   149  		// It has to work, but if not ... too bad.
   150  		if nt != "ARG" {
   151  			return ""
   152  		}
   153  		return s
   154  	}
   155  }
   156  
   157  func parsestring(b *bufio.Reader, c *Command) (*Command, string) {
   158  	t, s := tok(b)
   159  	if s == "\n" || t == "EOF" || t == "EOL" {
   160  		return nil, t
   161  	}
   162  	for {
   163  		switch t {
   164  		// In old rush, env strings were substituted wholesale, and
   165  		// parsed in line. This was very useful, but nobody uses it so...
   166  		// for now, screw it. Do environment later.
   167  		case "ENV":
   168  		case "ARG":
   169  			c.Args = append(c.Args, arg{s, t})
   170  		case "white":
   171  		case "FD":
   172  			x := 0
   173  			_, err := fmt.Sscanf(s, "%v", &x)
   174  			if err != nil {
   175  				panic(fmt.Errorf("bad FD on redirect: %v, %v", s, err))
   176  			}
   177  			// whitespace is allowed
   178  			c.fdmap[x] = getArg(b, t)
   179  		// LINK and BG are similar save that LINK requires another command. If we don't get one, well.
   180  		case "LINK":
   181  			c.Link = s
   182  			return c, t
   183  		case "BG":
   184  			c.BG = true
   185  			return c, t
   186  		case "EOF":
   187  			return c, t
   188  		case "EOL":
   189  			return c, t
   190  		default:
   191  			panic(fmt.Errorf("unknown token type %v", t))
   192  		}
   193  		t, s = tok(b)
   194  	}
   195  }
   196  
   197  func parse(b *bufio.Reader) (*Command, string) {
   198  	c := newCommand()
   199  	return parsestring(b, c)
   200  }
   201  
   202  func newCommand() *Command {
   203  	return &Command{Cmd: exec.Command(""), fdmap: make(map[int]string), files: make(map[int]io.Closer)}
   204  }
   205  
   206  // Just eat it up until you have all the commands you need.
   207  func parsecommands(b *bufio.Reader) ([]*Command, string) {
   208  	cmds := make([]*Command, 0)
   209  	for {
   210  		c, t := parse(b)
   211  		if c == nil {
   212  			return cmds, t
   213  		}
   214  		cmds = append(cmds, c)
   215  		if t == "EOF" || t == "EOL" {
   216  			return cmds, t
   217  		}
   218  	}
   219  }
   220  
   221  func getCommand(b *bufio.Reader) (c []*Command, t string, err error) {
   222  	defer func() {
   223  		if e := recover(); e != nil {
   224  			err = e.(error)
   225  		}
   226  	}()
   227  
   228  	// TODO: put a recover here that just returns an error.
   229  	c, t = parsecommands(b)
   230  	// the rules.
   231  	// For now, no empty commands.
   232  	// Can't have a redir and a redirect for fd1.
   233  	for i, v := range c {
   234  		if len(v.Args) == 0 {
   235  			return nil, "", errors.New("empty commands not allowed (yet)")
   236  		}
   237  		if v.Link == "|" && v.fdmap[1] != "" {
   238  			return nil, "", errors.New("Can't have a pipe and > on one command")
   239  		}
   240  		if v.Link == "|" && i == len(c)-1 {
   241  			return nil, "", errors.New("Can't have a pipe to nowhere")
   242  		}
   243  		if i < len(c)-1 && v.Link == "|" && c[i+1].fdmap[0] != "" {
   244  			return nil, "", errors.New("Can't have a pipe to command with redirect on stdin")
   245  		}
   246  	}
   247  	return c, t, err
   248  }
   249  
   250  /*
   251  func main() {
   252  	b := bufio.NewReader(os.Stdin)
   253  	for {
   254  	    c, t, err := getCommand(b)
   255  		fmt.Printf("%v %v %v\n", c, t, err)
   256  	    if t == "EOF" {
   257  	       break
   258  	       }
   259  	       }
   260  }
   261  */