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 }