Skip to content

Commit ba6b4ce

Browse files
committed
update parser for docker 1.9 commands and add more tests
1 parent d38386c commit ba6b4ce

File tree

2 files changed

+115
-23
lines changed

2 files changed

+115
-23
lines changed

parser.js

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,10 @@ function parseSubCommand(cmd) {
4141
return false;
4242
}
4343

44-
// Parse environment like statements. Note that this does *not* handle
45-
// variable interpolation, which will be handled in the evaluator.
46-
function parseNameVal(cmd) {
47-
// This is kind of tricky because we need to support the old
48-
// variant: KEY name value
49-
// as well as the new one: KEY name=value ...
50-
// The trigger to know which one is being used will be whether we hit
51-
// a space or = first. space ==> old, "=" ==> new
52-
44+
// Helper to parse words (i.e space delimited or quoted strings) in a statement.
45+
// The quotes are preserved as part of this function and they are stripped later
46+
// as part of processWords().
47+
function parseWords(rest) {
5348
var S_inSpaces = 1;
5449
var S_inWord = 2;
5550
var S_inQuote = 3;
@@ -61,7 +56,6 @@ function parseNameVal(cmd) {
6156
var blankOK = false;
6257
var ch;
6358
var pos;
64-
var rest = cmd.rest;
6559

6660
for (pos = 0; pos <= rest.length; pos++) {
6761
if (pos != rest.length) {
@@ -88,15 +82,6 @@ function parseNameVal(cmd) {
8882
phase = S_inSpaces;
8983
if (blankOK || word.length > 0) {
9084
words.push(word);
91-
92-
// Look for = and if not there assume
93-
// we're doing the old stuff and
94-
// just read the rest of the line
95-
if (!word.indexOf('=') >= 0) {
96-
word = rest.substr(pos).trim();
97-
words.push(word);
98-
break;
99-
}
10085
}
10186
word = '';
10287
blankOK = false;
@@ -139,6 +124,20 @@ function parseNameVal(cmd) {
139124
}
140125
}
141126

127+
return words;
128+
}
129+
130+
// Parse environment like statements. Note that this does *not* handle
131+
// variable interpolation, which will be handled in the evaluator.
132+
function parseNameVal(cmd) {
133+
// This is kind of tricky because we need to support the old
134+
// variant: KEY name value
135+
// as well as the new one: KEY name=value ...
136+
// The trigger to know which one is being used will be whether we hit
137+
// a space or = first. space ==> old, "=" ==> new
138+
var word;
139+
var words = parseWords(cmd.rest);
140+
142141
cmd.args = {};
143142

144143
if (words.length === 0) {
@@ -150,7 +149,7 @@ function parseNameVal(cmd) {
150149
// Old format (KEY name value)
151150
var strs = cmd.rest.split(TOKEN_WHITESPACE);
152151
if (strs.length < 2) {
153-
cmd.error = cmd.name + ' must have two arguments, got ' + rest;
152+
cmd.error = cmd.name + ' must have two arguments, got ' + cmd.rest;
154153
return false;
155154
}
156155

@@ -183,6 +182,19 @@ function parseLabel(cmd) {
183182
return parseNameVal(cmd);
184183
}
185184

185+
// Parses a statement containing one or more keyword definition(s) and/or
186+
// value assignments, like `name1 name2= name3="" name4=value`.
187+
// Note that this is a stricter format than the old format of assignment,
188+
// allowed by parseNameVal(), in a way that this only allows assignment of the
189+
// form `keyword=[<value>]` like `name2=`, `name3=""`, and `name4=value` above.
190+
// In addition, a keyword definition alone is of the form `keyword` like `name1`
191+
// above. And the assignments `name2=` and `name3=""` are equivalent and
192+
// assign an empty value to the respective keywords.
193+
function parseNameOrNameVal(cmd) {
194+
cmd.args = parseWords(cmd.rest);
195+
return true;
196+
}
197+
186198
// Parses a whitespace-delimited set of arguments. The result is a
187199
// list of string arguments.
188200
function parseStringsWhitespaceDelimited(cmd) {
@@ -246,6 +258,7 @@ function parseJsonOrList(cmd) {
246258
// be incorporated directly into the existing AST as a next.
247259
var commandParsers = {
248260
'ADD': parseJsonOrList,
261+
'ARG': parseNameOrNameVal,
249262
'CMD': parseJsonOrString,
250263
'COPY': parseJsonOrList,
251264
'ENTRYPOINT': parseJsonOrString,
@@ -256,6 +269,7 @@ var commandParsers = {
256269
'MAINTAINER': parseString,
257270
'ONBUILD': parseSubCommand,
258271
'RUN': parseJsonOrString,
272+
'STOPSIGNAL': parseString,
259273
'USER': parseString,
260274
'VOLUME': parseJsonOrList,
261275
'WORKDIR': parseString
@@ -307,13 +321,14 @@ function parseLine(line, lineno) {
307321

308322
var commandParserFn = commandParsers[command.name];
309323
if (!commandParserFn) {
310-
// Ignore invalid Dockerfile instructions
324+
// Invalid Dockerfile instruction, but allow it and move on.
311325
// log.debug('Invalid Dockerfile command:', command.name);
312-
return { command: null, remainder: '' };
326+
commandParserFn = parseString;
313327
}
314328

315329
if (commandParserFn(command)) {
316330
// Successfully converted the arguments.
331+
command.raw = line;
317332
delete command.rest;
318333
}
319334

@@ -353,7 +368,7 @@ function parse(contents, options) {
353368
for (i = 0; i < lines.length; i++) {
354369
lineno = i + 1;
355370
if (remainder) {
356-
line = remainder + ' ' + lines[i];
371+
line = remainder + lines[i];
357372
} else {
358373
line = lines[i];
359374
}

test/test_parser.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,66 @@ var fs = require('fs')
22
var tape = require('tape')
33
var dockerfileParser = require('../parser')
44

5+
6+
tape('every cmd', function (t) {
7+
var lines = [
8+
'FROM busybox:latest',
9+
10+
'ADD foo.txt /',
11+
12+
'ARG arg1',
13+
'ARG arg2=',
14+
'ARG arg3=',
15+
'ARG arg4=four',
16+
'ARG arg5="oh yeah"',
17+
18+
'CMD shell string format',
19+
'CMD ["array", "format"]',
20+
21+
'COPY foo.bar foo.baz /doo/',
22+
'COPY ["foo bar", "foo baz", "/doo/"]',
23+
24+
'ENTRYPOINT shell string format',
25+
'ENTRYPOINT ["array", "format"]',
26+
27+
'ENV key value',
28+
'ENV key2=value2',
29+
'ENV key3=value\ three= key4="value four"',
30+
31+
'EXPOSE 80',
32+
'EXPOSE 8080 9080 1',
33+
34+
'LABEL key=value',
35+
'LABEL key3=value\ three= key4="value four"',
36+
37+
'MAINTAINER I\'m the maintainer',
38+
39+
'ONBUILD ADD . /',
40+
'ONBUILD RUN /usr/local/bin/python-build --dir /app/src',
41+
42+
'RUN /usr/local/bin/python-build --dir /app/src',
43+
'RUN apt-get update && apt-get install -y x11vnc xvfb firefox',
44+
45+
'STOPSIGNAL -1',
46+
'STOPSIGNAL 18',
47+
48+
'USER daemon',
49+
50+
'VOLUME ["/data"]',
51+
'VOLUME /var/log /var/db',
52+
53+
'WORKDIR /',
54+
'WORKDIR /path\ to\ workdir/other\ path/here'
55+
];
56+
var dockerFile = lines.join('\n');
57+
var commands = dockerfileParser.parse(dockerFile);
58+
59+
t.equal(commands.length, lines.length);
60+
61+
t.end();
62+
});
63+
64+
565
tape('should parse a Dockerfile', function (t) {
666

767
var dname = __dirname;
@@ -59,3 +119,20 @@ tape('case insensitive', function (t) {
59119

60120
t.end();
61121
});
122+
123+
124+
tape('multiline', function (t) {
125+
var lines = [
126+
'FROM busybox',
127+
'MAINTAINER "Docker \\',
128+
'IO <io@\\',
129+
'docker.com>"'
130+
];
131+
var dockerFile = lines.join('\n');
132+
var commands = dockerfileParser.parse(dockerFile);
133+
134+
t.equal(commands.length, 2);
135+
t.equal(commands[1].args, '"Docker IO <[email protected]>"');
136+
137+
t.end();
138+
});

0 commit comments

Comments
 (0)