shell_parser.go 6.9 KB

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