Skip to content

Commit 873ba2c

Browse files
committed
recipe: FixAlternateEnvSyntax
1 parent 8176120 commit 873ba2c

File tree

4 files changed

+204
-1
lines changed

4 files changed

+204
-1
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (c) 2025 Jim Schubert
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
package com.github.jimschubert.rewrite.docker.format;
16+
17+
import com.github.jimschubert.rewrite.docker.DockerIsoVisitor;
18+
import com.github.jimschubert.rewrite.docker.tree.Docker;
19+
import com.github.jimschubert.rewrite.docker.tree.DockerRightPadded;
20+
import com.github.jimschubert.rewrite.docker.tree.Quoting;
21+
import lombok.EqualsAndHashCode;
22+
import lombok.RequiredArgsConstructor;
23+
import lombok.Value;
24+
import org.openrewrite.ExecutionContext;
25+
import org.openrewrite.NlsRewrite;
26+
import org.openrewrite.Recipe;
27+
import org.openrewrite.TreeVisitor;
28+
import org.openrewrite.internal.ListUtils;
29+
30+
import java.util.List;
31+
32+
@Value
33+
@EqualsAndHashCode(callSuper = false)
34+
@RequiredArgsConstructor
35+
public class FixAlternateEnvSyntax extends Recipe {
36+
@Override
37+
public @NlsRewrite.DisplayName String getDisplayName() {
38+
return "Fix alternate ENV syntax";
39+
}
40+
41+
@Override
42+
public @NlsRewrite.Description String getDescription() {
43+
return "Fix alternate ENV syntax by ensuring 'key=value' syntax is used instead of 'key value' syntax.";
44+
}
45+
46+
@Override
47+
public TreeVisitor<?, ExecutionContext> getVisitor() {
48+
return new DockerIsoVisitor<>() {
49+
@Override
50+
public Docker.Env visitEnv(Docker.Env env, ExecutionContext executionContext) {
51+
// note that we will not "undo" Docker's documented caveat.
52+
// ENV ONE TWO= THREE=world would be parsed by Docker as ONE="TWO= THREE=world".
53+
// This recipe will not change that behavior and will only add the equals and ensure
54+
// the resulting string is correct.
55+
List<DockerRightPadded<Docker.KeyArgs>> args = ListUtils.flatMap(
56+
env.getArgs(),
57+
keyArgs -> keyArgs.map(a -> {
58+
a = a.withHasEquals(true);
59+
if (a.getValue().getText().contains(" ") && a.getQuoting() != Quoting.DOUBLE_QUOTED) {
60+
a = a.withQuoting(Quoting.DOUBLE_QUOTED);
61+
}
62+
63+
return a;
64+
})
65+
);
66+
env = env.withArgs(args);
67+
return super.visitEnv(env, executionContext);
68+
}
69+
};
70+
}
71+
}

src/main/java/com/github/jimschubert/rewrite/docker/internal/parsers/EnvInstructionParser.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
import com.github.jimschubert.rewrite.docker.tree.Docker;
2020
import com.github.jimschubert.rewrite.docker.tree.DockerRightPadded;
2121
import com.github.jimschubert.rewrite.docker.tree.Space;
22+
import org.jetbrains.annotations.NotNull;
2223
import org.openrewrite.Tree;
2324
import org.openrewrite.marker.Markers;
2425

26+
import java.util.ArrayList;
2527
import java.util.List;
2628

2729
public class EnvInstructionParser implements InstructionParser {
@@ -33,6 +35,55 @@ public String instructionName() {
3335
@Override
3436
public Docker.Instruction parse(String line, ParserState state) {
3537
List<DockerRightPadded<Docker.KeyArgs>> args = ParserUtils.parseArgs(line, state);
36-
return new Docker.Env(Tree.randomId(), state.prefix(), args, Markers.EMPTY, Space.EMPTY);
38+
// HACK: generally we have ENV key=value syntax. But we are going to post-process here to work out the alternate syntax.
39+
//
40+
// For ENV instruction in Docker, if the first key has no value (no equals sign),
41+
// then everything that follows is considered the value for that key
42+
//
43+
// From Docker's documentation:
44+
// This syntax does not allow for multiple environment-variables to be set in a single ENV instruction, and
45+
// can be confusing. For example, the following sets a single environment variable (ONE) with value "TWO= THREE=world":
46+
//
47+
// ENV ONE TWO= THREE=world
48+
List<DockerRightPadded<Docker.KeyArgs>> processedArgs = new ArrayList<>();
49+
50+
if (!args.isEmpty() && args.get(0).getElement().getValue().getText() == null) {
51+
Docker.KeyArgs firstKey = args.get(0).getElement();
52+
53+
// The rest of the args (if any) should be combined as the value for the first key
54+
if (args.size() > 1) {
55+
StringBuilder combinedValue = getCombinedValue(args);
56+
Docker.KeyArgs updatedFirstKey = Docker.KeyArgs.build(firstKey.key(),combinedValue.toString()).withHasEquals(false);
57+
processedArgs.add(args.get(0).withElement(updatedFirstKey));
58+
} else {
59+
// If there are no other args, keep the first one as is
60+
processedArgs.add(args.get(0));
61+
}
62+
} else {
63+
// Standard key=value syntax, keep the args as they are
64+
processedArgs.addAll(args);
65+
}
66+
67+
return new Docker.Env(Tree.randomId(), state.prefix(), processedArgs, Markers.EMPTY, Space.EMPTY);
68+
}
69+
70+
private static @NotNull StringBuilder getCombinedValue(List<DockerRightPadded<Docker.KeyArgs>> args) {
71+
StringBuilder combinedValue = new StringBuilder();
72+
for (int i = 1; i < args.size(); i++) {
73+
Docker.KeyArgs keyArg = args.get(i).getElement();
74+
if (i > 1) {
75+
String space = keyArg.getKey().getPrefix().getWhitespace();
76+
combinedValue.append(!space.isEmpty() ? space : " ");
77+
}
78+
combinedValue.append(keyArg.getKey().getText());
79+
if (keyArg.getValue().getText() != null) {
80+
if (keyArg.isHasEquals()) {
81+
combinedValue.append("=");
82+
}
83+
combinedValue.append(keyArg.getValue().getPrefix().getWhitespace());
84+
combinedValue.append(keyArg.getValue().getText());
85+
}
86+
}
87+
return combinedValue;
3788
}
3889
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright (c) 2025 Jim Schubert
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
package com.github.jimschubert.rewrite.docker.format;
16+
17+
import org.junit.jupiter.api.Test;
18+
import org.openrewrite.test.RewriteTest;
19+
20+
import static com.github.jimschubert.rewrite.docker.Assertions.dockerfile;
21+
22+
class FixAlternateEnvSyntaxTest implements RewriteTest {
23+
@Override
24+
public void defaults(org.openrewrite.test.RecipeSpec spec) {
25+
spec.recipe(new FixAlternateEnvSyntax());
26+
}
27+
28+
@Test
29+
void fixEnvSyntax() {
30+
rewriteRun(
31+
//language=dockerfile
32+
dockerfile(
33+
"ENV key value",
34+
"ENV key=value"
35+
)
36+
);
37+
}
38+
39+
@Test
40+
void fixEnvAlternateSyntaxExample() {
41+
rewriteRun(
42+
//language=dockerfile
43+
dockerfile(
44+
"ENV ONE TWO= THREE=world",
45+
"ENV ONE=\"TWO= THREE=world\""
46+
)
47+
);
48+
}
49+
}

src/test/java/com/github/jimschubert/rewrite/docker/internal/DockerfileParserTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,38 @@ void testEntrypointShellFormWithQuotes() {
307307
assertLiteral(args.get(0), Quoting.DOUBLE_QUOTED, " ", "echo Hello World", " ");
308308
}
309309

310+
@Test
311+
void testEnvAlternateSyntax() {
312+
DockerfileParser parser = new DockerfileParser();
313+
Docker.Document doc = parser.parse(new ByteArrayInputStream("ENV MY_NAME Jim".getBytes(StandardCharsets.UTF_8)));
314+
315+
Docker.Stage stage = assertSingleStageWithChildCount(doc, 1);
316+
317+
Docker.Env env = (Docker.Env) stage.getChildren().get(0);
318+
assertEquals(Space.EMPTY, env.getPrefix());
319+
320+
List<DockerRightPadded<Docker.KeyArgs>> args = env.getArgs();
321+
assertEquals(1, args.size());
322+
323+
assertRightPaddedArg(args.get(0), Quoting.UNQUOTED, " ", "MY_NAME", false, "Jim", "");
324+
}
325+
326+
@Test
327+
void testEnvAlternateSyntaxDocumentedCaveat() {
328+
DockerfileParser parser = new DockerfileParser();
329+
Docker.Document doc = parser.parse(new ByteArrayInputStream("ENV ONE TWO= THREE=world".getBytes(StandardCharsets.UTF_8)));
330+
331+
Docker.Stage stage = assertSingleStageWithChildCount(doc, 1);
332+
333+
Docker.Env env = (Docker.Env) stage.getChildren().get(0);
334+
assertEquals(Space.EMPTY, env.getPrefix());
335+
336+
List<DockerRightPadded<Docker.KeyArgs>> args = env.getArgs();
337+
assertEquals(1, args.size());
338+
339+
assertRightPaddedArg(args.get(0), Quoting.UNQUOTED, " ", "ONE", false, "TWO= THREE=world", "");
340+
}
341+
310342
@Test
311343
void testEnvComplex() {
312344
DockerfileParser parser = new DockerfileParser();

0 commit comments

Comments
 (0)