shell_parser.go 7.2 KB

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