shell_parser.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. package dockerfile
  2. // This will take a single word and an array of env variables and
  3. // process all quotes (" and ') as well as $xxx and ${xxx} env variable
  4. // tokens. Tries to mimic bash shell process.
  5. // It doesn't support all flavors of ${xx:...} formats but new ones can
  6. // be added by adding code to the "special ${} format processing" section
  7. import (
  8. "bytes"
  9. "strings"
  10. "text/scanner"
  11. "unicode"
  12. "github.com/pkg/errors"
  13. )
  14. type shellWord struct {
  15. word string
  16. scanner scanner.Scanner
  17. envs []string
  18. pos int
  19. escapeToken rune
  20. }
  21. // ProcessWord will use the 'env' list of environment variables,
  22. // and replace any env var references in 'word'.
  23. func ProcessWord(word string, env []string, escapeToken rune) (string, error) {
  24. word, _, err := process(word, env, escapeToken)
  25. return word, err
  26. }
  27. // ProcessWords will use the 'env' list of environment variables,
  28. // and replace any env var references in 'word' then it will also
  29. // return a slice of strings which represents the 'word'
  30. // split up based on spaces - taking into account quotes. Note that
  31. // this splitting is done **after** the env var substitutions are done.
  32. // Note, each one is trimmed to remove leading and trailing spaces (unless
  33. // they are quoted", but ProcessWord retains spaces between words.
  34. func ProcessWords(word string, env []string, escapeToken rune) ([]string, error) {
  35. _, words, err := process(word, env, escapeToken)
  36. return words, err
  37. }
  38. func process(word string, env []string, escapeToken rune) (string, []string, error) {
  39. sw := &shellWord{
  40. word: word,
  41. envs: env,
  42. pos: 0,
  43. escapeToken: escapeToken,
  44. }
  45. sw.scanner.Init(strings.NewReader(word))
  46. return sw.process()
  47. }
  48. func (sw *shellWord) process() (string, []string, error) {
  49. return sw.processStopOn(scanner.EOF)
  50. }
  51. type wordsStruct struct {
  52. word string
  53. words []string
  54. inWord bool
  55. }
  56. func (w *wordsStruct) addChar(ch rune) {
  57. if unicode.IsSpace(ch) && w.inWord {
  58. if len(w.word) != 0 {
  59. w.words = append(w.words, w.word)
  60. w.word = ""
  61. w.inWord = false
  62. }
  63. } else if !unicode.IsSpace(ch) {
  64. w.addRawChar(ch)
  65. }
  66. }
  67. func (w *wordsStruct) addRawChar(ch rune) {
  68. w.word += string(ch)
  69. w.inWord = true
  70. }
  71. func (w *wordsStruct) addString(str string) {
  72. var scan scanner.Scanner
  73. scan.Init(strings.NewReader(str))
  74. for scan.Peek() != scanner.EOF {
  75. w.addChar(scan.Next())
  76. }
  77. }
  78. func (w *wordsStruct) addRawString(str string) {
  79. w.word += str
  80. w.inWord = true
  81. }
  82. func (w *wordsStruct) getWords() []string {
  83. if len(w.word) > 0 {
  84. w.words = append(w.words, w.word)
  85. // Just in case we're called again by mistake
  86. w.word = ""
  87. w.inWord = false
  88. }
  89. return w.words
  90. }
  91. // Process the word, starting at 'pos', and stop when we get to the
  92. // end of the word or the 'stopChar' character
  93. func (sw *shellWord) processStopOn(stopChar rune) (string, []string, error) {
  94. var result bytes.Buffer
  95. var words wordsStruct
  96. var charFuncMapping = map[rune]func() (string, error){
  97. '\'': sw.processSingleQuote,
  98. '"': sw.processDoubleQuote,
  99. '$': sw.processDollar,
  100. }
  101. for sw.scanner.Peek() != scanner.EOF {
  102. ch := sw.scanner.Peek()
  103. if stopChar != scanner.EOF && ch == stopChar {
  104. sw.scanner.Next()
  105. break
  106. }
  107. if fn, ok := charFuncMapping[ch]; ok {
  108. // Call special processing func for certain chars
  109. tmp, err := fn()
  110. if err != nil {
  111. return "", []string{}, err
  112. }
  113. result.WriteString(tmp)
  114. if ch == rune('$') {
  115. words.addString(tmp)
  116. } else {
  117. words.addRawString(tmp)
  118. }
  119. } else {
  120. // Not special, just add it to the result
  121. ch = sw.scanner.Next()
  122. if ch == sw.escapeToken {
  123. // '\' (default escape token, but ` allowed) escapes, except end of line
  124. ch = sw.scanner.Next()
  125. if ch == scanner.EOF {
  126. break
  127. }
  128. words.addRawChar(ch)
  129. } else {
  130. words.addChar(ch)
  131. }
  132. result.WriteRune(ch)
  133. }
  134. }
  135. return result.String(), words.getWords(), nil
  136. }
  137. func (sw *shellWord) processSingleQuote() (string, error) {
  138. // All chars between single quotes are taken as-is
  139. // Note, you can't escape '
  140. //
  141. // From the "sh" man page:
  142. // Single Quotes
  143. // Enclosing characters in single quotes preserves the literal meaning of
  144. // all the characters (except single quotes, making it impossible to put
  145. // single-quotes in a single-quoted string).
  146. var result bytes.Buffer
  147. sw.scanner.Next()
  148. for {
  149. ch := sw.scanner.Next()
  150. switch ch {
  151. case scanner.EOF:
  152. return "", errors.New("unexpected end of statement while looking for matching single-quote")
  153. case '\'':
  154. return result.String(), nil
  155. }
  156. result.WriteRune(ch)
  157. }
  158. }
  159. func (sw *shellWord) processDoubleQuote() (string, error) {
  160. // All chars up to the next " are taken as-is, even ', except any $ chars
  161. // But you can escape " with a \ (or ` if escape token set accordingly)
  162. //
  163. // From the "sh" man page:
  164. // Double Quotes
  165. // Enclosing characters within double quotes preserves the literal meaning
  166. // of all characters except dollarsign ($), backquote (`), and backslash
  167. // (\). The backslash inside double quotes is historically weird, and
  168. // serves to quote only the following characters:
  169. // $ ` " \ <newline>.
  170. // Otherwise it remains literal.
  171. var result bytes.Buffer
  172. sw.scanner.Next()
  173. for {
  174. switch sw.scanner.Peek() {
  175. case scanner.EOF:
  176. return "", errors.New("unexpected end of statement while looking for matching double-quote")
  177. case '"':
  178. sw.scanner.Next()
  179. return result.String(), nil
  180. case '$':
  181. value, err := sw.processDollar()
  182. if err != nil {
  183. return "", err
  184. }
  185. result.WriteString(value)
  186. default:
  187. ch := sw.scanner.Next()
  188. if ch == sw.escapeToken {
  189. switch sw.scanner.Peek() {
  190. case scanner.EOF:
  191. // Ignore \ at end of word
  192. continue
  193. case '"', '$', sw.escapeToken:
  194. // These chars can be escaped, all other \'s are left as-is
  195. // Note: for now don't do anything special with ` chars.
  196. // Not sure what to do with them anyway since we're not going
  197. // to execute the text in there (not now anyway).
  198. ch = sw.scanner.Next()
  199. }
  200. }
  201. result.WriteRune(ch)
  202. }
  203. }
  204. }
  205. func (sw *shellWord) processDollar() (string, error) {
  206. sw.scanner.Next()
  207. // $xxx case
  208. if sw.scanner.Peek() != '{' {
  209. name := sw.processName()
  210. if name == "" {
  211. return "$", nil
  212. }
  213. return sw.getEnv(name), nil
  214. }
  215. sw.scanner.Next()
  216. name := sw.processName()
  217. ch := sw.scanner.Peek()
  218. if ch == '}' {
  219. // Normal ${xx} case
  220. sw.scanner.Next()
  221. return sw.getEnv(name), nil
  222. }
  223. if ch == ':' {
  224. // Special ${xx:...} format processing
  225. // Yes it allows for recursive $'s in the ... spot
  226. sw.scanner.Next() // skip over :
  227. modifier := sw.scanner.Next()
  228. word, _, err := sw.processStopOn('}')
  229. if err != nil {
  230. return "", err
  231. }
  232. // Grab the current value of the variable in question so we
  233. // can use to to determine what to do based on the modifier
  234. newValue := sw.getEnv(name)
  235. switch modifier {
  236. case '+':
  237. if newValue != "" {
  238. newValue = word
  239. }
  240. return newValue, nil
  241. case '-':
  242. if newValue == "" {
  243. newValue = word
  244. }
  245. return newValue, nil
  246. default:
  247. return "", errors.Errorf("unsupported modifier (%c) in substitution: %s", modifier, sw.word)
  248. }
  249. }
  250. return "", errors.Errorf("missing ':' in substitution: %s", sw.word)
  251. }
  252. func (sw *shellWord) processName() string {
  253. // Read in a name (alphanumeric or _)
  254. // If it starts with a numeric then just return $#
  255. var name bytes.Buffer
  256. for sw.scanner.Peek() != scanner.EOF {
  257. ch := sw.scanner.Peek()
  258. if name.Len() == 0 && unicode.IsDigit(ch) {
  259. ch = sw.scanner.Next()
  260. return string(ch)
  261. }
  262. if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' {
  263. break
  264. }
  265. ch = sw.scanner.Next()
  266. name.WriteRune(ch)
  267. }
  268. return name.String()
  269. }
  270. func (sw *shellWord) getEnv(name string) string {
  271. for _, env := range sw.envs {
  272. i := strings.Index(env, "=")
  273. if i < 0 {
  274. if equalEnvKeys(name, env) {
  275. // Should probably never get here, but just in case treat
  276. // it like "var" and "var=" are the same
  277. return ""
  278. }
  279. continue
  280. }
  281. compareName := env[:i]
  282. if !equalEnvKeys(name, compareName) {
  283. continue
  284. }
  285. return env[i+1:]
  286. }
  287. return ""
  288. }