github.com/bingoohuang/gg@v0.0.0-20240325092523-45da7dee9335/pkg/shellwords/shellwords.go (about)

     1  package shellwords
     2  
     3  import (
     4  	"errors"
     5  	"os"
     6  	"regexp"
     7  	"strings"
     8  )
     9  
    10  var (
    11  	ParseEnv      = false
    12  	ParseBacktick = false
    13  )
    14  
    15  var envRe = regexp.MustCompile(`\$({[a-zA-Z0-9_]+}|[a-zA-Z0-9_]+)`)
    16  
    17  func isSpace(r rune) bool {
    18  	switch r {
    19  	case ' ', '\t', '\r', '\n':
    20  		return true
    21  	}
    22  
    23  	return false
    24  }
    25  
    26  func replaceEnv(getenv func(string) string, s string) string {
    27  	if getenv == nil {
    28  		getenv = os.Getenv
    29  	}
    30  
    31  	return envRe.ReplaceAllStringFunc(s, func(s string) string {
    32  		s = s[1:]
    33  		if s[0] == '{' {
    34  			s = s[1 : len(s)-1]
    35  		}
    36  		return getenv(s)
    37  	})
    38  }
    39  
    40  // Parser keeps state of parsing.
    41  type Parser struct {
    42  	ParseEnv      bool
    43  	ParseBacktick bool
    44  	Position      int
    45  	Dir           string
    46  
    47  	// If ParseEnv is true, use this for getenv.
    48  	// If nil, use os.Getenv.
    49  	Getenv func(string) string
    50  }
    51  
    52  // NewParser creates a Parser
    53  func NewParser() *Parser {
    54  	return &Parser{
    55  		ParseEnv:      ParseEnv,
    56  		ParseBacktick: ParseBacktick,
    57  	}
    58  }
    59  
    60  type state struct {
    61  	escaped, doubleQuoted, singleQuoted, backQuote, dollarQuote, got bool
    62  
    63  	args     []string
    64  	buf      string
    65  	backtick string
    66  	pos      int
    67  }
    68  
    69  // Parse parses a lines.
    70  func (p *Parser) Parse(line string) ([]string, error) {
    71  	s := state{
    72  		args: make([]string, 0),
    73  		pos:  -1,
    74  	}
    75  
    76  loop:
    77  	for i, r := range line {
    78  		if s.escaped {
    79  			s.buf += string(r)
    80  			s.escaped = false
    81  			continue
    82  		}
    83  
    84  		if r == '\\' {
    85  			if s.singleQuoted {
    86  				s.buf += string(r)
    87  			} else {
    88  				s.escaped = true
    89  			}
    90  			continue
    91  		}
    92  
    93  		if isSpace(r) {
    94  			if s.singleQuoted || s.doubleQuoted || s.backQuote || s.dollarQuote {
    95  				s.buf += string(r)
    96  				s.backtick += string(r)
    97  			} else if s.got {
    98  				if p.ParseEnv {
    99  					va := replaceEnv(p.Getenv, s.buf)
   100  					if va != "" {
   101  						s.args = append(s.args, va)
   102  					}
   103  				} else {
   104  					s.args = append(s.args, s.buf)
   105  				}
   106  				s.buf = ""
   107  				s.got = false
   108  			}
   109  			continue
   110  		}
   111  
   112  		switch r {
   113  		case '`':
   114  			if !s.singleQuoted && !s.doubleQuoted && !s.dollarQuote {
   115  				if p.ParseBacktick {
   116  					if s.backQuote {
   117  						out, err := shellRun(s.backtick, p.Dir)
   118  						if err != nil {
   119  							return nil, err
   120  						}
   121  						s.buf = s.buf[:len(s.buf)-len(s.backtick)] + out
   122  					}
   123  					s.backtick = ""
   124  					s.backQuote = !s.backQuote
   125  					continue
   126  				}
   127  				s.backtick = ""
   128  				s.backQuote = !s.backQuote
   129  			}
   130  		case ')':
   131  			if !s.singleQuoted && !s.doubleQuoted && !s.backQuote {
   132  				if p.ParseBacktick {
   133  					if s.dollarQuote {
   134  						out, err := shellRun(s.backtick, p.Dir)
   135  						if err != nil {
   136  							return nil, err
   137  						}
   138  						s.buf = s.buf[:len(s.buf)-len(s.backtick)-2] + out
   139  					}
   140  					s.backtick = ""
   141  					s.dollarQuote = !s.dollarQuote
   142  					continue
   143  				}
   144  				s.backtick = ""
   145  				s.dollarQuote = !s.dollarQuote
   146  			}
   147  		case '(':
   148  			if !s.singleQuoted && !s.doubleQuoted && !s.backQuote {
   149  				if !s.dollarQuote && strings.HasSuffix(s.buf, "$") {
   150  					s.dollarQuote = true
   151  					s.buf += "("
   152  					continue
   153  				} else {
   154  					return nil, errors.New("invalid command line string")
   155  				}
   156  			}
   157  		case '"':
   158  			if !s.singleQuoted && !s.dollarQuote {
   159  				if s.doubleQuoted {
   160  					s.got = true
   161  				}
   162  				s.doubleQuoted = !s.doubleQuoted
   163  				continue
   164  			}
   165  		case '\'':
   166  			if !s.doubleQuoted && !s.dollarQuote {
   167  				if s.singleQuoted {
   168  					s.got = true
   169  				}
   170  				s.singleQuoted = !s.singleQuoted
   171  				continue
   172  			}
   173  		case ';', '&', '|', '<', '>':
   174  			if !(s.escaped || s.singleQuoted || s.doubleQuoted || s.backQuote || s.dollarQuote) {
   175  				if r == '>' && len(s.buf) > 0 {
   176  					if c := s.buf[0]; '0' <= c && c <= '9' {
   177  						i--
   178  						s.got = false
   179  					}
   180  				}
   181  				s.pos = i
   182  				break loop
   183  			}
   184  		}
   185  
   186  		s.got = true
   187  		s.buf += string(r)
   188  		if s.backQuote || s.dollarQuote {
   189  			s.backtick += string(r)
   190  		}
   191  	}
   192  
   193  	if s.got {
   194  		if p.ParseEnv {
   195  			va := replaceEnv(p.Getenv, s.buf)
   196  			if va != "" {
   197  				s.args = append(s.args, va)
   198  			}
   199  		} else {
   200  			s.args = append(s.args, s.buf)
   201  		}
   202  	}
   203  
   204  	if s.escaped || s.singleQuoted || s.doubleQuoted || s.backQuote || s.dollarQuote {
   205  		return nil, errors.New("invalid command line string")
   206  	}
   207  
   208  	p.Position = s.pos
   209  
   210  	return s.args, nil
   211  }
   212  
   213  // Parse parses a lines.
   214  func Parse(line string) ([]string, error) {
   215  	return NewParser().Parse(line)
   216  }