Signed-off-by: Daniel Nephin <dnephin@docker.com>
Daniel Nephin authored on 2017/04/13 04:40:16... | ... |
@@ -12,6 +12,7 @@ import ( |
12 | 12 |
"unicode" |
13 | 13 |
|
14 | 14 |
"github.com/docker/docker/builder/dockerfile/command" |
15 |
+ "github.com/pkg/errors" |
|
15 | 16 |
) |
16 | 17 |
|
17 | 18 |
// Node is a structure used to represent a parse tree. |
... | ... |
@@ -77,7 +78,7 @@ const DefaultEscapeToken = '\\' |
77 | 77 |
type Directive struct { |
78 | 78 |
escapeToken rune // Current escape token |
79 | 79 |
lineContinuationRegex *regexp.Regexp // Current line continuation regex |
80 |
- lookingForDirectives bool // Whether we are currently looking for directives |
|
80 |
+ processingComplete bool // Whether we are done looking for directives |
|
81 | 81 |
escapeSeen bool // Whether the escape directive has been seen |
82 | 82 |
} |
83 | 83 |
|
... | ... |
@@ -91,12 +92,35 @@ func (d *Directive) setEscapeToken(s string) error { |
91 | 91 |
return nil |
92 | 92 |
} |
93 | 93 |
|
94 |
+// processLine looks for a parser directive '# escapeToken=<char>. Parser |
|
95 |
+// directives must precede any builder instruction or other comments, and cannot |
|
96 |
+// be repeated. |
|
97 |
+func (d *Directive) processLine(line string) error { |
|
98 |
+ if d.processingComplete { |
|
99 |
+ return nil |
|
100 |
+ } |
|
101 |
+ // Processing is finished after the first call |
|
102 |
+ defer func() { d.processingComplete = true }() |
|
103 |
+ |
|
104 |
+ tecMatch := tokenEscapeCommand.FindStringSubmatch(strings.ToLower(line)) |
|
105 |
+ if len(tecMatch) == 0 { |
|
106 |
+ return nil |
|
107 |
+ } |
|
108 |
+ if d.escapeSeen == true { |
|
109 |
+ return errors.New("only one escape parser directive can be used") |
|
110 |
+ } |
|
111 |
+ for i, n := range tokenEscapeCommand.SubexpNames() { |
|
112 |
+ if n == "escapechar" { |
|
113 |
+ d.escapeSeen = true |
|
114 |
+ return d.setEscapeToken(tecMatch[i]) |
|
115 |
+ } |
|
116 |
+ } |
|
117 |
+ return nil |
|
118 |
+} |
|
119 |
+ |
|
94 | 120 |
// NewDefaultDirective returns a new Directive with the default escapeToken token |
95 | 121 |
func NewDefaultDirective() *Directive { |
96 |
- directive := Directive{ |
|
97 |
- escapeSeen: false, |
|
98 |
- lookingForDirectives: true, |
|
99 |
- } |
|
122 |
+ directive := Directive{} |
|
100 | 123 |
directive.setEscapeToken(string(DefaultEscapeToken)) |
101 | 124 |
return &directive |
102 | 125 |
} |
... | ... |
@@ -132,13 +156,10 @@ func init() { |
132 | 132 |
|
133 | 133 |
// ParseLine parses a line and returns the remainder. |
134 | 134 |
func ParseLine(line string, d *Directive, ignoreCont bool) (string, *Node, error) { |
135 |
- if escapeFound, err := handleParserDirective(line, d); err != nil || escapeFound { |
|
136 |
- d.escapeSeen = escapeFound |
|
135 |
+ if err := d.processLine(line); err != nil { |
|
137 | 136 |
return "", nil, err |
138 | 137 |
} |
139 | 138 |
|
140 |
- d.lookingForDirectives = false |
|
141 |
- |
|
142 | 139 |
if line = stripComments(line); line == "" { |
143 | 140 |
return "", nil, nil |
144 | 141 |
} |
... | ... |
@@ -180,44 +201,27 @@ func newNodeFromLine(line string, directive *Directive) (*Node, error) { |
180 | 180 |
}, nil |
181 | 181 |
} |
182 | 182 |
|
183 |
-// Handle the parser directive '# escapeToken=<char>. Parser directives must precede |
|
184 |
-// any builder instruction or other comments, and cannot be repeated. |
|
185 |
-func handleParserDirective(line string, d *Directive) (bool, error) { |
|
186 |
- if !d.lookingForDirectives { |
|
187 |
- return false, nil |
|
188 |
- } |
|
189 |
- tecMatch := tokenEscapeCommand.FindStringSubmatch(strings.ToLower(line)) |
|
190 |
- if len(tecMatch) == 0 { |
|
191 |
- return false, nil |
|
192 |
- } |
|
193 |
- if d.escapeSeen == true { |
|
194 |
- return false, fmt.Errorf("only one escape parser directive can be used") |
|
195 |
- } |
|
196 |
- for i, n := range tokenEscapeCommand.SubexpNames() { |
|
197 |
- if n == "escapechar" { |
|
198 |
- if err := d.setEscapeToken(tecMatch[i]); err != nil { |
|
199 |
- return false, err |
|
200 |
- } |
|
201 |
- return true, nil |
|
202 |
- } |
|
203 |
- } |
|
204 |
- return false, nil |
|
205 |
-} |
|
206 |
- |
|
207 | 183 |
// Result is the result of parsing a Dockerfile |
208 | 184 |
type Result struct { |
209 | 185 |
AST *Node |
210 | 186 |
EscapeToken rune |
211 | 187 |
} |
212 | 188 |
|
189 |
+// scanLines is a split function for bufio.Scanner. that augments the default |
|
190 |
+// line scanner by supporting newline escapes. |
|
191 |
+func scanLines(data []byte, atEOF bool) (int, []byte, error) { |
|
192 |
+ advance, token, err := bufio.ScanLines(data, atEOF) |
|
193 |
+ return advance, token, err |
|
194 |
+} |
|
195 |
+ |
|
213 | 196 |
// Parse reads lines from a Reader, parses the lines into an AST and returns |
214 | 197 |
// the AST and escape token |
215 | 198 |
func Parse(rwc io.Reader) (*Result, error) { |
216 | 199 |
d := NewDefaultDirective() |
217 | 200 |
currentLine := 0 |
218 |
- root := &Node{} |
|
219 |
- root.StartLine = -1 |
|
201 |
+ root := &Node{StartLine: -1} |
|
220 | 202 |
scanner := bufio.NewScanner(rwc) |
203 |
+ scanner.Split(scanLines) |
|
221 | 204 |
|
222 | 205 |
utf8bom := []byte{0xEF, 0xBB, 0xBF} |
223 | 206 |
for scanner.Scan() { |
... | ... |
@@ -226,6 +230,7 @@ func Parse(rwc io.Reader) (*Result, error) { |
226 | 226 |
if currentLine == 0 { |
227 | 227 |
scannedBytes = bytes.TrimPrefix(scannedBytes, utf8bom) |
228 | 228 |
} |
229 |
+ |
|
229 | 230 |
scannedLine := strings.TrimLeftFunc(string(scannedBytes), unicode.IsSpace) |
230 | 231 |
currentLine++ |
231 | 232 |
line, child, err := ParseLine(scannedLine, d, false) |
... | ... |
@@ -59,7 +59,7 @@ func TestTestData(t *testing.T) { |
59 | 59 |
content = bytes.Replace(content, []byte{'\x0d', '\x0a'}, []byte{'\x0a'}, -1) |
60 | 60 |
} |
61 | 61 |
|
62 |
- assert.Equal(t, result.AST.Dump()+"\n", string(content)) |
|
62 |
+ assert.Equal(t, result.AST.Dump()+"\n", string(content), "In "+dockerfile) |
|
63 | 63 |
} |
64 | 64 |
} |
65 | 65 |
|