github.com/webwurst/docker@v1.7.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 if rest == "" { 34 return nil, nil, nil 35 } 36 37 _, child, err := parseLine(rest) 38 if err != nil { 39 return nil, nil, err 40 } 41 42 return &Node{Children: []*Node{child}}, nil, nil 43 } 44 45 // parse environment like statements. Note that this does *not* handle 46 // variable interpolation, which will be handled in the evaluator. 47 func parseNameVal(rest string, key string) (*Node, map[string]bool, error) { 48 // This is kind of tricky because we need to support the old 49 // variant: KEY name value 50 // as well as the new one: KEY name=value ... 51 // The trigger to know which one is being used will be whether we hit 52 // a space or = first. space ==> old, "=" ==> new 53 54 const ( 55 inSpaces = iota // looking for start of a word 56 inWord 57 inQuote 58 ) 59 60 words := []string{} 61 phase := inSpaces 62 word := "" 63 quote := '\000' 64 blankOK := false 65 var ch rune 66 67 for pos := 0; pos <= len(rest); pos++ { 68 if pos != len(rest) { 69 ch = rune(rest[pos]) 70 } 71 72 if phase == inSpaces { // Looking for start of word 73 if pos == len(rest) { // end of input 74 break 75 } 76 if unicode.IsSpace(ch) { // skip spaces 77 continue 78 } 79 phase = inWord // found it, fall thru 80 } 81 if (phase == inWord || phase == inQuote) && (pos == len(rest)) { 82 if blankOK || len(word) > 0 { 83 words = append(words, word) 84 } 85 break 86 } 87 if phase == inWord { 88 if unicode.IsSpace(ch) { 89 phase = inSpaces 90 if blankOK || len(word) > 0 { 91 words = append(words, word) 92 93 // Look for = and if not there assume 94 // we're doing the old stuff and 95 // just read the rest of the line 96 if !strings.Contains(word, "=") { 97 word = strings.TrimSpace(rest[pos:]) 98 words = append(words, word) 99 break 100 } 101 } 102 word = "" 103 blankOK = false 104 continue 105 } 106 if ch == '\'' || ch == '"' { 107 quote = ch 108 blankOK = true 109 phase = inQuote 110 } 111 if ch == '\\' { 112 if pos+1 == len(rest) { 113 continue // just skip \ at end 114 } 115 // If we're not quoted and we see a \, then always just 116 // add \ plus the char to the word, even if the char 117 // is a quote. 118 word += string(ch) 119 pos++ 120 ch = rune(rest[pos]) 121 } 122 word += string(ch) 123 continue 124 } 125 if phase == inQuote { 126 if ch == quote { 127 phase = inWord 128 } 129 // \ is special except for ' quotes - can't escape anything for ' 130 if ch == '\\' && quote != '\'' { 131 if pos+1 == len(rest) { 132 phase = inWord 133 continue // just skip \ at end 134 } 135 pos++ 136 nextCh := rune(rest[pos]) 137 word += string(ch) 138 ch = nextCh 139 } 140 word += string(ch) 141 } 142 } 143 144 if len(words) == 0 { 145 return nil, nil, nil 146 } 147 148 // Old format (KEY name value) 149 var rootnode *Node 150 151 if !strings.Contains(words[0], "=") { 152 node := &Node{} 153 rootnode = node 154 strs := TOKEN_WHITESPACE.Split(rest, 2) 155 156 if len(strs) < 2 { 157 return nil, nil, fmt.Errorf(key + " must have two arguments") 158 } 159 160 node.Value = strs[0] 161 node.Next = &Node{} 162 node.Next.Value = strs[1] 163 } else { 164 var prevNode *Node 165 for i, word := range words { 166 if !strings.Contains(word, "=") { 167 return nil, nil, fmt.Errorf("Syntax error - can't find = in %q. Must be of the form: name=value", word) 168 } 169 parts := strings.SplitN(word, "=", 2) 170 171 name := &Node{} 172 value := &Node{} 173 174 name.Next = value 175 name.Value = parts[0] 176 value.Value = parts[1] 177 178 if i == 0 { 179 rootnode = name 180 } else { 181 prevNode.Next = name 182 } 183 prevNode = value 184 } 185 } 186 187 return rootnode, nil, nil 188 } 189 190 func parseEnv(rest string) (*Node, map[string]bool, error) { 191 return parseNameVal(rest, "ENV") 192 } 193 194 func parseLabel(rest string) (*Node, map[string]bool, error) { 195 return parseNameVal(rest, "LABEL") 196 } 197 198 // parses a whitespace-delimited set of arguments. The result is effectively a 199 // linked list of string arguments. 200 func parseStringsWhitespaceDelimited(rest string) (*Node, map[string]bool, error) { 201 if rest == "" { 202 return nil, nil, nil 203 } 204 205 node := &Node{} 206 rootnode := node 207 prevnode := node 208 for _, str := range TOKEN_WHITESPACE.Split(rest, -1) { // use regexp 209 prevnode = node 210 node.Value = str 211 node.Next = &Node{} 212 node = node.Next 213 } 214 215 // XXX to get around regexp.Split *always* providing an empty string at the 216 // end due to how our loop is constructed, nil out the last node in the 217 // chain. 218 prevnode.Next = nil 219 220 return rootnode, nil, nil 221 } 222 223 // parsestring just wraps the string in quotes and returns a working node. 224 func parseString(rest string) (*Node, map[string]bool, error) { 225 if rest == "" { 226 return nil, nil, nil 227 } 228 n := &Node{} 229 n.Value = rest 230 return n, nil, nil 231 } 232 233 // parseJSON converts JSON arrays to an AST. 234 func parseJSON(rest string) (*Node, map[string]bool, error) { 235 var myJson []interface{} 236 if err := json.NewDecoder(strings.NewReader(rest)).Decode(&myJson); err != nil { 237 return nil, nil, err 238 } 239 240 var top, prev *Node 241 for _, str := range myJson { 242 s, ok := str.(string) 243 if !ok { 244 return nil, nil, errDockerfileNotStringArray 245 } 246 247 node := &Node{Value: s} 248 if prev == nil { 249 top = node 250 } else { 251 prev.Next = node 252 } 253 prev = node 254 } 255 256 return top, map[string]bool{"json": true}, nil 257 } 258 259 // parseMaybeJSON determines if the argument appears to be a JSON array. If 260 // so, passes to parseJSON; if not, quotes the result and returns a single 261 // node. 262 func parseMaybeJSON(rest string) (*Node, map[string]bool, error) { 263 if rest == "" { 264 return nil, nil, nil 265 } 266 267 node, attrs, err := parseJSON(rest) 268 269 if err == nil { 270 return node, attrs, nil 271 } 272 if err == errDockerfileNotStringArray { 273 return nil, nil, err 274 } 275 276 node = &Node{} 277 node.Value = rest 278 return node, nil, nil 279 } 280 281 // parseMaybeJSONToList determines if the argument appears to be a JSON array. If 282 // so, passes to parseJSON; if not, attempts to parse it as a whitespace 283 // delimited string. 284 func parseMaybeJSONToList(rest string) (*Node, map[string]bool, error) { 285 node, attrs, err := parseJSON(rest) 286 287 if err == nil { 288 return node, attrs, nil 289 } 290 if err == errDockerfileNotStringArray { 291 return nil, nil, err 292 } 293 294 return parseStringsWhitespaceDelimited(rest) 295 }