Skip to content

Commit f202b5d

Browse files
committed
Fix heredoc and add naive shell handling
1 parent c6381d1 commit f202b5d

10 files changed

+133
-36
lines changed

cmd/version.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ var versionCmd = &cobra.Command{
1414
Use: "version",
1515
Short: "Print the version number of dockerfmt",
1616
Run: func(cmd *cobra.Command, args []string) {
17-
fmt.Println("dockerfmt 0.3.2")
17+
fmt.Println("dockerfmt 0.3.3")
1818
},
1919
}

js/format.wasm

4.71 KB
Binary file not shown.

js/package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@reteps/dockerfmt",
3-
"version": "0.3.2",
3+
"version": "0.3.3",
44
"type": "module",
55
"description": "",
66
"repository": "git+https://github.com/reteps/dockerfmt/tree/main/js",

lib/format.go

+94-23
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func FormatNode(ast *ExtendedNode, c *Config) (string, bool) {
4444
command.Arg: formatBasic,
4545
command.Cmd: formatCmd,
4646
command.Copy: formatSpaceSeparated,
47-
command.Entrypoint: formatCmd,
47+
command.Entrypoint: formatEntrypoint,
4848
command.Env: formatEnv,
4949
command.Expose: formatSpaceSeparated,
5050
command.From: formatSpaceSeparated,
@@ -341,10 +341,7 @@ func formatRun(n *ExtendedNode, c *Config) string {
341341
flags := n.Node.Flags
342342

343343
var content string
344-
if len(n.Node.Heredocs) > 1 {
345-
// Not implemented yet
346-
panic("Multiple Heredocs not implemented yet")
347-
} else if len(n.Node.Heredocs) == 1 {
344+
if len(n.Node.Heredocs) >= 1 {
348345
content = n.Node.Heredocs[0].Content
349346
hereDoc = true
350347
// TODO: check if doc.FileDescriptor == 0?
@@ -373,7 +370,8 @@ func formatRun(n *ExtendedNode, c *Config) string {
373370
} else {
374371
content = formatShell(content, hereDoc, c)
375372
if hereDoc {
376-
content = "<<" + n.Node.Heredocs[0].Name + "\n" + content + n.Node.Heredocs[0].Name + "\n"
373+
n.Node.Heredocs[0].Content = content
374+
content, _ = GetHeredoc(n)
377375
}
378376
}
379377

@@ -384,12 +382,33 @@ func formatRun(n *ExtendedNode, c *Config) string {
384382
return strings.ToUpper(n.Value) + " " + content
385383
}
386384

385+
func GetHeredoc(n *ExtendedNode) (string, bool) {
386+
if len(n.Node.Heredocs) == 0 {
387+
return "", false
388+
}
389+
390+
printAST(n, 0)
391+
args := []string{}
392+
cur := n.Next
393+
for cur != nil {
394+
if cur.Node.Value != "" {
395+
args = append(args, cur.Node.Value)
396+
}
397+
cur = cur.Next
398+
}
399+
400+
content := strings.Join(args, " ") + "\n" + n.Node.Heredocs[0].Content + n.Node.Heredocs[0].Name + "\n"
401+
return content, true
402+
}
387403
func formatBasic(n *ExtendedNode, c *Config) string {
388404
// Uppercases the command, and indent the following lines
389405
originalTrimmed := strings.TrimLeft(n.OriginalMultiline, " \t")
390406

391-
parts := regexp.MustCompile(" ").Split(originalTrimmed, 2)
392-
return IndentFollowingLines(strings.ToUpper(n.Value)+" "+parts[1], c.IndentSize)
407+
value, success := GetHeredoc(n)
408+
if !success {
409+
value = regexp.MustCompile(" ").Split(originalTrimmed, 2)[1]
410+
}
411+
return IndentFollowingLines(strings.ToUpper(n.Value)+" "+value, c.IndentSize)
393412
}
394413

395414
// Marshal is a UTF-8 friendly marshaler. Go's json.Marshal is not UTF-8
@@ -407,47 +426,99 @@ func Marshal(i interface{}) ([]byte, error) {
407426
return bytes.TrimRight(buffer.Bytes(), "\n"), err
408427
}
409428

410-
func getCmd(n *ExtendedNode) []string {
429+
func getCmd(n *ExtendedNode, shouldSplitNode bool) []string {
411430
cmd := []string{}
412431
for node := n; node != nil; node = node.Next {
413432
// Split value by whitespace
414-
rawValue := strings.Trim(node.Value, " \t")
415-
if len(node.Flags) > 0 {
416-
rawValue += " " + strings.Join(node.Flags, " ")
433+
rawValue := strings.Trim(node.Node.Value, " \t")
434+
if len(node.Node.Flags) > 0 {
435+
cmd = append(cmd, node.Node.Flags...)
417436
}
418-
parts, err := shlex.Split(rawValue)
419-
if err != nil {
420-
log.Fatalf("Error splitting: %s\n", node.Value)
437+
// log.Printf("ShouldSplitNode: %v\n", shouldSplitNode)
438+
if shouldSplitNode {
439+
parts, err := shlex.Split(rawValue)
440+
if err != nil {
441+
log.Fatalf("Error splitting: %s\n", node.Node.Value)
442+
}
443+
cmd = append(cmd, parts...)
444+
} else {
445+
cmd = append(cmd, rawValue)
421446
}
422-
cmd = append(cmd, parts...)
423447
}
424448
// log.Printf("getCmd: %v\n", cmd)
425449
return cmd
426450
}
427451

452+
func formatEntrypoint(n *ExtendedNode, c *Config) string {
453+
// this can technically change behavior. https://docs.docker.com/reference/dockerfile/#understand-how-cmd-and-entrypoint-interact
454+
isJSON, ok := n.Node.Attributes["json"]
455+
if !ok {
456+
isJSON = false
457+
}
458+
if !isJSON {
459+
// https://docs.docker.com/reference/dockerfile/#entrypoint
460+
node := n.Next.Node.Value
461+
parts, err := shlex.Split(node)
462+
if err != nil {
463+
log.Fatalf("Error splitting: %s\n", node)
464+
}
465+
466+
doNotSplit := false
467+
// This is a simplistic check to determine if we need to run in a full shell.
468+
for _, part := range parts {
469+
if part == "&&" || part == ";" || part == "||" {
470+
doNotSplit = true
471+
break
472+
}
473+
}
474+
475+
if doNotSplit {
476+
n.Next.Node.Flags = append(n.Next.Node.Flags, []string{"/bin/sh", "-c"}...)
477+
// Hacky workaround to tell getCmd to not split the command
478+
if n.Node.Attributes == nil {
479+
n.Node.Attributes = make(map[string]bool)
480+
}
481+
n.Node.Attributes["json"] = true
482+
}
483+
}
484+
// printAST(n, 0)
485+
return formatCmd(n, c)
486+
}
428487
func formatCmd(n *ExtendedNode, c *Config) string {
429-
cmd := getCmd(n.Next)
488+
// printAST(n, 0)
489+
isJSON, ok := n.Node.Attributes["json"]
490+
if !ok {
491+
isJSON = false
492+
}
493+
cmd := getCmd(n.Next, !isJSON)
430494
b, err := Marshal(cmd)
431495
if err != nil {
432496
return ""
433497
}
434498
bWithSpace := strings.ReplaceAll(string(b), "\",\"", "\", \"")
435-
return strings.ToUpper(n.Value) + " " + string(bWithSpace) + "\n"
499+
return strings.ToUpper(n.Node.Value) + " " + string(bWithSpace) + "\n"
436500
}
437501

438502
func formatSpaceSeparated(n *ExtendedNode, c *Config) string {
439-
cmd := strings.Join(getCmd(n.Next), " ")
440-
if len(n.Node.Flags) > 0 {
441-
cmd = strings.Join(n.Node.Flags, " ") + " " + cmd
503+
isJSON, ok := n.Node.Attributes["json"]
504+
if !ok {
505+
isJSON = false
506+
}
507+
cmd, success := GetHeredoc(n)
508+
if !success {
509+
cmd = strings.Join(getCmd(n.Next, isJSON), " ")
510+
if len(n.Node.Flags) > 0 {
511+
cmd = strings.Join(n.Node.Flags, " ") + " " + cmd
512+
}
442513
}
443514

444-
return strings.ToUpper(n.Value) + " " + cmd + "\n"
515+
return strings.ToUpper(n.Node.Value) + " " + cmd + "\n"
445516
}
446517

447518
func formatMaintainer(n *ExtendedNode, c *Config) string {
448519

449520
// Get text between quotes
450-
maintainer := strings.Trim(n.Next.Value, "\"")
521+
maintainer := strings.Trim(n.Next.Node.Value, "\"")
451522
return "LABEL org.opencontainers.image.authors=\"" + maintainer + "\"\n"
452523
}
453524

tests/in/heredoc.dockerfile

+7-4
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ echo "World!">>/hello
44
EOF
55
RUN ls
66

7-
RUN <<EOF
7+
RUN <<-EOF
88
echo "Hello" >> /hello
99
echo "World!">>/hello
1010
EOF
1111

12-
RUN ls
13-
14-
12+
COPY <<-EOF /x
13+
x
14+
EOF
1515

16+
COPY <<-EOT /script.sh
17+
echo "hello ${FOO}"
18+
EOT

tests/in/quoting.dockerfile

+9-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,12 @@ FROM hackernews
22
# https://news.ycombinator.com/item?id=43630653
33
ENTRYPOINT service ssh restart && bash
44

5-
ENTRYPOINT sh -c 'service ssh restart && bash'
5+
ENTRYPOINT sh -c 'service ssh restart && bash'
6+
7+
# https://github.com/reteps/dockerfmt/issues/20
8+
FROM nginx
9+
ENTRYPOINT ["nginx", "-g", "daemon off;"]
10+
11+
# https://github.com/reteps/dockerfmt/issues/20
12+
FROM nginx
13+
ENTRYPOINT nginx -g 'daemon off;'

tests/out/heredoc.dockerfile

+9-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@ echo "World!" >>/hello
44
EOF
55
RUN ls
66

7-
RUN <<EOF
7+
RUN <<-EOF
88
echo "Hello" >>/hello
99
echo "World!" >>/hello
1010
EOF
1111

12-
RUN ls
12+
COPY <<-EOF /x
13+
x
14+
EOF
15+
16+
17+
COPY <<-EOT /script.sh
18+
echo "hello ${FOO}"
19+
EOT

tests/out/label.dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,4 @@ ADD https://www.qlcplus.org/downloads/${QLC_VERSION}/qlcplus_${QLC_VERSION}_amd6
5252
RUN dpkg -i /opt/qlcplus.deb
5353

5454
# https://www.qlcplus.org/docs/html_en_EN/commandlineparameters.html
55-
CMD ["/usr/bin/qlcplus", "--operate", "--web", "--open", "/QLC/default_workspace.qxw"]
55+
CMD ["/usr/bin/qlcplus", "--operate", "--web", "--open /QLC/default_workspace.qxw"]

tests/out/quoting.dockerfile

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
FROM hackernews
22
# https://news.ycombinator.com/item?id=43630653
3-
ENTRYPOINT ["service", "ssh", "restart", "&&", "bash"]
3+
ENTRYPOINT ["/bin/sh", "-c", "service ssh restart && bash"]
44

55
ENTRYPOINT ["sh", "-c", "service ssh restart && bash"]
6+
7+
# https://github.com/reteps/dockerfmt/issues/20
8+
FROM nginx
9+
ENTRYPOINT ["nginx", "-g", "daemon off;"]
10+
11+
# https://github.com/reteps/dockerfmt/issues/20
12+
FROM nginx
13+
ENTRYPOINT ["nginx", "-g", "daemon off;"]

0 commit comments

Comments
 (0)