Browse code

Cleanup processing of directives.

Signed-off-by: Daniel Nephin <dnephin@docker.com>

Daniel Nephin authored on 2017/04/13 04:40:16
Showing 3 changed files
... ...
@@ -5,7 +5,6 @@ import (
5 5
 	"os"
6 6
 
7 7
 	"github.com/docker/docker/builder/dockerfile/parser"
8
-	"go/ast"
9 8
 )
10 9
 
11 10
 func main() {
... ...
@@ -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