github.com/raychaser/docker@v1.5.0/builder/parser/line_parsers.go (about) 1 package parser 2 3 // line parsers are dispatch calls that parse a single unit of text into a 4 // Node object which contains the whole statement. Dockerfiles have varied 5 // (but not usually unique, see ONBUILD for a unique example) parsing rules 6 // per-command, and these unify the processing in a way that makes it 7 // manageable. 8 9 import ( 10 "encoding/json" 11 "errors" 12 "fmt" 13 "strings" 14 "unicode" 15 ) 16 17 var ( 18 errDockerfileNotStringArray = errors.New("When using JSON array syntax, arrays must be comprised of strings only.") 19 ) 20 21 // ignore the current argument. This will still leave a command parsed, but 22 // will not incorporate the arguments into the ast. 23 func parseIgnore(rest string) (*Node, map[string]bool, error) { 24 return &Node{}, nil, nil 25 } 26 27 // used for onbuild. Could potentially be used for anything that represents a 28 // statement with sub-statements. 29 // 30 // ONBUILD RUN foo bar -> (onbuild (run foo bar)) 31 // 32 func parseSubCommand(rest string) (*Node, map[string]bool, error) { 33 _, child, err := parseLine(rest) 34 if err != nil { 35 return nil, nil, err 36 } 37 38 return &Node{Children: []*Node{child}}, nil, nil 39 } 40 41 // parse environment like statements. Note that this does *not* handle 42 // variable interpolation, which will be handled in the evaluator. 43 func parseEnv(rest string) (*Node, map[string]bool, error) { 44 // This is kind of tricky because we need to support the old 45 // variant: ENV name value 46 // as well as the new one: ENV name=value ... 47 // The trigger to know which one is being used will be whether we hit 48 // a space or = first. space ==> old, "=" ==> new 49 50 const ( 51 inSpaces = iota // looking for start of a word 52 inWord 53 inQuote 54 ) 55 56 words := []string{} 57 phase := inSpaces 58 word := "" 59 quote := '\000' 60 blankOK := false 61 var ch rune 62 63 for pos := 0; pos <= len(rest); pos++ { 64 if pos != len(rest) { 65 ch = rune(rest[pos]) 66 } 67 68 if phase == inSpaces { // Looking for start of word 69 if pos == len(rest) { // end of input 70 break 71 } 72 if unicode.IsSpace(ch) { // skip spaces 73 continue 74 } 75 phase = inWord // found it, fall thru 76 } 77 if (phase == inWord || phase == inQuote) && (pos == len(rest)) { 78 if blankOK || len(word) > 0 { 79 words = append(words, word) 80 } 81 break 82 } 83 if phase == inWord { 84 if unicode.IsSpace(ch) { 85 phase = inSpaces 86 if blankOK || len(word) > 0 { 87 words = append(words, word) 88 89 // Look for = and if no there assume 90 // we're doing the old stuff and 91 // just read the rest of the line 92 if !strings.Contains(word, "=") { 93 word = strings.TrimSpace(rest[pos:]) 94 words = append(words, word) 95 break 96 } 97 } 98 word = "" 99 blankOK = false 100 continue 101 } 102 if ch == '\'' || ch == '"' { 103 quote = ch 104 blankOK = true 105 phase = inQuote 106 continue 107 } 108 if ch == '\\' { 109 if pos+1 == len(rest) { 110 continue // just skip \ at end 111 } 112 pos++ 113 ch = rune(rest[pos]) 114 } 115 word += string(ch) 116 continue 117 } 118 if phase == inQuote { 119 if ch == quote { 120 phase = inWord 121 continue 122 } 123 if ch == '\\' { 124 if pos+1 == len(rest) { 125 phase = inWord 126 continue // just skip \ at end 127 } 128 pos++ 129 ch = rune(rest[pos]) 130 } 131 word += string(ch) 132 } 133 } 134 135 if len(words) == 0 { 136 return nil, nil, fmt.Errorf("ENV must have some arguments") 137 } 138 139 // Old format (ENV name value) 140 var rootnode *Node 141 142 if !strings.Contains(words[0], "=") { 143 node := &Node{} 144 rootnode = node 145 strs := TOKEN_WHITESPACE.Split(rest, 2) 146 147 if len(strs) < 2 { 148 return nil, nil, fmt.Errorf("ENV must have two arguments") 149 } 150 151 node.Value = strs[0] 152 node.Next = &Node{} 153 node.Next.Value = strs[1] 154 } else { 155 var prevNode *Node 156 for i, word := range words { 157 if !strings.Contains(word, "=") { 158 return nil, nil, fmt.Errorf("Syntax error - can't find = in %q. Must be of the form: name=value", word) 159 } 160 parts := strings.SplitN(word, "=", 2) 161 162 name := &Node{} 163 value := &Node{} 164 165 name.Next = value 166 name.Value = parts[0] 167 value.Value = parts[1] 168 169 if i == 0 { 170 rootnode = name 171 } else { 172 prevNode.Next = name 173 } 174 prevNode = value 175 } 176 } 177 178 return rootnode, nil, nil 179 } 180 181 // parses a whitespace-delimited set of arguments. The result is effectively a 182 // linked list of string arguments. 183 func parseStringsWhitespaceDelimited(rest string) (*Node, map[string]bool, error) { 184 node := &Node{} 185 rootnode := node 186 prevnode := node 187 for _, str := range TOKEN_WHITESPACE.Split(rest, -1) { // use regexp 188 prevnode = node 189 node.Value = str 190 node.Next = &Node{} 191 node = node.Next 192 } 193 194 // XXX to get around regexp.Split *always* providing an empty string at the 195 // end due to how our loop is constructed, nil out the last node in the 196 // chain. 197 prevnode.Next = nil 198 199 return rootnode, nil, nil 200 } 201 202 // parsestring just wraps the string in quotes and returns a working node. 203 func parseString(rest string) (*Node, map[string]bool, error) { 204 n := &Node{} 205 n.Value = rest 206 return n, nil, nil 207 } 208 209 // parseJSON converts JSON arrays to an AST. 210 func parseJSON(rest string) (*Node, map[string]bool, error) { 211 var myJson []interface{} 212 if err := json.Unmarshal([]byte(rest), &myJson); err != nil { 213 return nil, nil, err 214 } 215 216 var top, prev *Node 217 for _, str := range myJson { 218 if s, ok := str.(string); !ok { 219 return nil, nil, errDockerfileNotStringArray 220 } else { 221 node := &Node{Value: s} 222 if prev == nil { 223 top = node 224 } else { 225 prev.Next = node 226 } 227 prev = node 228 } 229 } 230 231 return top, map[string]bool{"json": true}, nil 232 } 233 234 // parseMaybeJSON determines if the argument appears to be a JSON array. If 235 // so, passes to parseJSON; if not, quotes the result and returns a single 236 // node. 237 func parseMaybeJSON(rest string) (*Node, map[string]bool, error) { 238 rest = strings.TrimSpace(rest) 239 240 node, attrs, err := parseJSON(rest) 241 242 if err == nil { 243 return node, attrs, nil 244 } 245 if err == errDockerfileNotStringArray { 246 return nil, nil, err 247 } 248 249 node = &Node{} 250 node.Value = rest 251 return node, nil, nil 252 } 253 254 // parseMaybeJSONToList determines if the argument appears to be a JSON array. If 255 // so, passes to parseJSON; if not, attmpts to parse it as a whitespace 256 // delimited string. 257 func parseMaybeJSONToList(rest string) (*Node, map[string]bool, error) { 258 rest = strings.TrimSpace(rest) 259 260 node, attrs, err := parseJSON(rest) 261 262 if err == nil { 263 return node, attrs, nil 264 } 265 if err == errDockerfileNotStringArray { 266 return nil, nil, err 267 } 268 269 return parseStringsWhitespaceDelimited(rest) 270 }