diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b00b77..02d697f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v1.6.0 with: - dotnet-version: "5.0.100-preview.8.20417.9" + dotnet-version: "5.0.100" env: NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce0ed9a..7466e36 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v1.6.0 with: - dotnet-version: "5.0.100-preview.8.20417.9" + dotnet-version: "5.0.100" - name: Install dependencies run: dotnet restore diff --git a/src/DockerfileModel/DockerfileModel.Tests/AddInstructionTests.cs b/src/DockerfileModel/DockerfileModel.Tests/AddInstructionTests.cs new file mode 100644 index 0000000..5a67b6b --- /dev/null +++ b/src/DockerfileModel/DockerfileModel.Tests/AddInstructionTests.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Xunit; + +namespace DockerfileModel.Tests +{ + public class AddInstructionTests : FileTransferInstructionTests + { + public AddInstructionTests() + : base(AddInstruction.Parse, AddInstruction.Create) + { + } + + [Theory] + [MemberData(nameof(ParseTestInput))] + public void Parse(FileTransferInstructionParseTestScenario scenario) => RunParseTest(scenario); + + [Theory] + [MemberData(nameof(CreateTestInput))] + public void Create(CreateTestScenario scenario) => RunCreateTest(scenario); + + public static IEnumerable ParseTestInput() => ParseTestInput("ADD"); + + + public static IEnumerable CreateTestInput() => CreateTestInput("ADD"); + } +} diff --git a/src/DockerfileModel/DockerfileModel.Tests/ChangeOwnerFlagTests.cs b/src/DockerfileModel/DockerfileModel.Tests/ChangeOwnerFlagTests.cs new file mode 100644 index 0000000..812c69b --- /dev/null +++ b/src/DockerfileModel/DockerfileModel.Tests/ChangeOwnerFlagTests.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DockerfileModel.Tokens; +using Sprache; +using Xunit; + +using static DockerfileModel.Tests.TokenValidator; + +namespace DockerfileModel.Tests +{ + public class ChangeOwnerFlagTests + { + [Theory] + [MemberData(nameof(ParseTestInput))] + public void Parse(ChangeOwnerFlagParseTestScenario scenario) + { + if (scenario.ParseExceptionPosition is null) + { + ChangeOwnerFlag result = ChangeOwnerFlag.Parse(scenario.Text, scenario.EscapeChar); + Assert.Equal(scenario.Text, result.ToString()); + Assert.Collection(result.Tokens, scenario.TokenValidators); + scenario.Validate?.Invoke(result); + } + else + { + ParseException exception = Assert.Throws( + () => ArgInstruction.Parse(scenario.Text, scenario.EscapeChar)); + Assert.Equal(scenario.ParseExceptionPosition.Line, exception.Position.Line); + Assert.Equal(scenario.ParseExceptionPosition.Column, exception.Position.Column); + } + } + + [Theory] + [MemberData(nameof(CreateTestInput))] + public void Create(CreateTestScenario scenario) + { + ChangeOwnerFlag result = ChangeOwnerFlag.Create(scenario.User, scenario.Group); + Assert.Collection(result.Tokens, scenario.TokenValidators); + scenario.Validate?.Invoke(result); + } + + [Fact] + public void User() + { + ChangeOwnerFlag flag = ChangeOwnerFlag.Create("foo"); + Assert.Equal("foo", flag.User); + Assert.Equal("--chown=foo", flag.ToString()); + + flag.User = "test"; + Assert.Equal("test", flag.User); + Assert.Equal("--chown=test", flag.ToString()); + + Assert.Throws(() => flag.User = ""); + Assert.Throws(() => flag.User = null); + } + + [Fact] + public void Group() + { + ChangeOwnerFlag flag = ChangeOwnerFlag.Create("user", "foo"); + Assert.Equal("foo", flag.Group); + Assert.Equal("--chown=user:foo", flag.ToString()); + + flag.Group = "test"; + Assert.Equal("test", flag.Group); + Assert.Equal("--chown=user:test", flag.ToString()); + + flag.Group = null; + Assert.Null(flag.Group); + Assert.Equal("--chown=user", flag.ToString()); + } + + [Fact] + public void ChangeOwnerToken() + { + ChangeOwnerFlag flag = ChangeOwnerFlag.Create("user"); + Assert.Equal("--chown=user", flag.ToString()); + + flag.ChangeOwnerToken.ValueToken.Group = "foo"; + Assert.Equal("foo", flag.Group); + Assert.Equal("--chown=user:foo", flag.ToString()); + + flag.ChangeOwnerToken = KeyValueToken.Create("chown", ChangeOwner.Create("test1:test2")); + Assert.Equal("test2", flag.Group); + Assert.Equal("--chown=test1:test2", flag.ToString()); + } + + public static IEnumerable ParseTestInput() + { + ChangeOwnerFlagParseTestScenario[] testInputs = new ChangeOwnerFlagParseTestScenario[] + { + new ChangeOwnerFlagParseTestScenario + { + Text = "--chown=foo:bar", + TokenValidators = new Action[] + { + token => ValidateSymbol(token, '-'), + token => ValidateSymbol(token, '-'), + token => ValidateAggregate>(token, "chown=foo:bar", + token => ValidateKeyword(token, "chown"), + token => ValidateSymbol(token, '='), + token => ValidateAggregate(token, "foo:bar", + token => ValidateLiteral(token, "foo"), + token => ValidateSymbol(token, ':'), + token => ValidateLiteral(token, "bar"))) + } + }, + new ChangeOwnerFlagParseTestScenario + { + Text = "--chown=foo", + TokenValidators = new Action[] + { + token => ValidateSymbol(token, '-'), + token => ValidateSymbol(token, '-'), + token => ValidateAggregate>(token, "chown=foo", + token => ValidateKeyword(token, "chown"), + token => ValidateSymbol(token, '='), + token => ValidateAggregate(token, "foo", + token => ValidateLiteral(token, "foo"))) + } + }, + new ChangeOwnerFlagParseTestScenario + { + Text = "--chown`\n=`\nfoo", + EscapeChar = '`', + TokenValidators = new Action[] + { + token => ValidateSymbol(token, '-'), + token => ValidateSymbol(token, '-'), + token => ValidateAggregate>(token, "chown`\n=`\nfoo", + token => ValidateKeyword(token, "chown"), + token => ValidateLineContinuation(token, '`', "\n"), + token => ValidateSymbol(token, '='), + token => ValidateLineContinuation(token, '`', "\n"), + token => ValidateAggregate(token, "foo", + token => ValidateLiteral(token, "foo"))) + } + }, + new ChangeOwnerFlagParseTestScenario + { + Text = "changeOwner=foo", + ParseExceptionPosition = new Position(1, 1, 1) + } + }; + + return testInputs.Select(input => new object[] { input }); + } + + public static IEnumerable CreateTestInput() + { + CreateTestScenario[] testInputs = new CreateTestScenario[] + { + new CreateTestScenario + { + User = "user", + Group = "group", + TokenValidators = new Action[] + { + token => ValidateSymbol(token, '-'), + token => ValidateSymbol(token, '-'), + token => ValidateAggregate>(token, "chown=user:group", + token => ValidateKeyword(token, "chown"), + token => ValidateSymbol(token, '='), + token => ValidateAggregate(token, "user:group", + token => ValidateLiteral(token, "user"), + token => ValidateSymbol(token, ':'), + token => ValidateLiteral(token, "group"))) + } + }, + new CreateTestScenario + { + User = "user", + TokenValidators = new Action[] + { + token => ValidateSymbol(token, '-'), + token => ValidateSymbol(token, '-'), + token => ValidateAggregate>(token, "chown=user", + token => ValidateKeyword(token, "chown"), + token => ValidateSymbol(token, '='), + token => ValidateAggregate(token, "user", + token => ValidateLiteral(token, "user"))) + } + } + }; + + return testInputs.Select(input => new object[] { input }); + } + + public class ChangeOwnerFlagParseTestScenario : ParseTestScenario + { + public char EscapeChar { get; set; } + } + + public class CreateTestScenario : TestScenario + { + public string User { get; set; } + public string Group { get; set; } + } + } +} diff --git a/src/DockerfileModel/DockerfileModel.Tests/ChangeOwnerTests.cs b/src/DockerfileModel/DockerfileModel.Tests/ChangeOwnerTests.cs new file mode 100644 index 0000000..5a872ff --- /dev/null +++ b/src/DockerfileModel/DockerfileModel.Tests/ChangeOwnerTests.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DockerfileModel.Tokens; +using Sprache; +using Xunit; + +using static DockerfileModel.Tests.TokenValidator; + +namespace DockerfileModel.Tests +{ + public class ChangeOwnerTests + { + [Theory] + [MemberData(nameof(ParseTestInput))] + public void Parse(ChangeOwnerParseTestScenario scenario) + { + if (scenario.ParseExceptionPosition is null) + { + ChangeOwner result = ChangeOwner.Parse(scenario.Text, scenario.EscapeChar); + Assert.Equal(scenario.Text, result.ToString()); + Assert.Collection(result.Tokens, scenario.TokenValidators); + scenario.Validate?.Invoke(result); + } + else + { + ParseException exception = Assert.Throws( + () => ArgInstruction.Parse(scenario.Text, scenario.EscapeChar)); + Assert.Equal(scenario.ParseExceptionPosition.Line, exception.Position.Line); + Assert.Equal(scenario.ParseExceptionPosition.Column, exception.Position.Column); + } + } + + [Theory] + [MemberData(nameof(CreateTestInput))] + public void Create(CreateTestScenario scenario) + { + ChangeOwner result = ChangeOwner.Create(scenario.User, scenario.Group); + Assert.Collection(result.Tokens, scenario.TokenValidators); + scenario.Validate?.Invoke(result); + } + + [Fact] + public void User() + { + ChangeOwner changeOwner = ChangeOwner.Create("test", "group"); + Assert.Equal("test", changeOwner.User); + Assert.Equal("test", changeOwner.UserToken.Value); + + changeOwner.User = "test2"; + Assert.Equal("test2", changeOwner.User); + Assert.Equal("test2", changeOwner.UserToken.Value); + + changeOwner.UserToken.Value = "test3"; + Assert.Equal("test3", changeOwner.User); + Assert.Equal("test3", changeOwner.UserToken.Value); + + changeOwner.UserToken = new LiteralToken("test4"); + Assert.Equal("test4", changeOwner.User); + Assert.Equal("test4", changeOwner.UserToken.Value); + Assert.Equal("test4:group", changeOwner.ToString()); + + Assert.Throws(() => changeOwner.User = ""); + Assert.Throws(() => changeOwner.User = null); + Assert.Throws(() => changeOwner.UserToken = null); + } + + [Fact] + public void Group() + { + ChangeOwner changeOwner = ChangeOwner.Create("user", "test"); + Assert.Equal("test", changeOwner.Group); + Assert.Equal("test", changeOwner.GroupToken.Value); + + changeOwner.Group = "test2"; + Assert.Equal("test2", changeOwner.Group); + Assert.Equal("test2", changeOwner.GroupToken.Value); + + changeOwner.GroupToken.Value = "test3"; + Assert.Equal("test3", changeOwner.Group); + Assert.Equal("test3", changeOwner.GroupToken.Value); + + changeOwner.Group = null; + Assert.Null(changeOwner.Group); + Assert.Null(changeOwner.GroupToken); + Assert.Equal("user", changeOwner.ToString()); + + changeOwner.GroupToken = new LiteralToken("test4"); + Assert.Equal("test4", changeOwner.Group); + Assert.Equal("test4", changeOwner.GroupToken.Value); + Assert.Equal("user:test4", changeOwner.ToString()); + + changeOwner.GroupToken = null; + Assert.Null(changeOwner.Group); + Assert.Null(changeOwner.GroupToken); + Assert.Equal("user", changeOwner.ToString()); + + changeOwner.Group = ""; + Assert.Null(changeOwner.Group); + Assert.Null(changeOwner.GroupToken); + Assert.Equal("user", changeOwner.ToString()); + } + + public static IEnumerable ParseTestInput() + { + ChangeOwnerParseTestScenario[] testInputs = new ChangeOwnerParseTestScenario[] + { + new ChangeOwnerParseTestScenario + { + Text = "55:mygroup", + TokenValidators = new Action[] + { + token => ValidateLiteral(token, "55"), + token => ValidateSymbol(token, ':'), + token => ValidateLiteral(token, "mygroup") + }, + Validate = result => + { + Assert.Equal("55", result.User); + Assert.Equal("mygroup", result.Group); + } + }, + new ChangeOwnerParseTestScenario + { + Text = "bin", + TokenValidators = new Action[] + { + token => ValidateLiteral(token, "bin") + }, + Validate = result => + { + Assert.Equal("bin", result.User); + Assert.Null(result.Group); + } + }, + new ChangeOwnerParseTestScenario + { + EscapeChar = '`', + Text = "us`\ner`\n:`\ngr`\noup", + TokenValidators = new Action[] + { + token => ValidateAggregate(token, "us`\ner", + token => ValidateString(token, "us"), + token => ValidateLineContinuation(token, '`', "\n"), + token => ValidateString(token, "er")), + token => ValidateLineContinuation(token, '`', "\n"), + token => ValidateSymbol(token, ':'), + token => ValidateLineContinuation(token, '`', "\n"), + token => ValidateAggregate(token, "gr`\noup", + token => ValidateString(token, "gr"), + token => ValidateLineContinuation(token, '`', "\n"), + token => ValidateString(token, "oup")) + }, + Validate = result => + { + Assert.Equal("user", result.User); + Assert.Equal("group", result.Group); + + result.Group = null; + Assert.Equal("us`\ner`\n", result.ToString()); + } + }, + new ChangeOwnerParseTestScenario + { + Text = "$user:group$var", + TokenValidators = new Action[] + { + token => ValidateAggregate(token, "$user", + token => ValidateAggregate(token, "$user", + token => ValidateString(token, "user"))), + token => ValidateSymbol(token, ':'), + token => ValidateAggregate(token, "group$var", + token => ValidateString(token, "group"), + token => ValidateAggregate(token, "$var", + token => ValidateString(token, "var"))) + } + }, + new ChangeOwnerParseTestScenario + { + Text = "user:", + ParseExceptionPosition = new Position(1, 1, 1) + }, + new ChangeOwnerParseTestScenario + { + Text = ":group", + ParseExceptionPosition = new Position(1, 1, 1) + } + }; + + return testInputs.Select(input => new object[] { input }); + } + + public static IEnumerable CreateTestInput() + { + CreateTestScenario[] testInputs = new CreateTestScenario[] + { + new CreateTestScenario + { + User = "user", + Group = "group", + TokenValidators = new Action[] + { + token => ValidateLiteral(token, "user"), + token => ValidateSymbol(token, ':'), + token => ValidateLiteral(token, "group") + }, + Validate = result => + { + Assert.Equal("user", result.User); + Assert.Equal("group", result.Group); + } + }, + new CreateTestScenario + { + User = "user", + Group = null, + TokenValidators = new Action[] + { + token => ValidateLiteral(token, "user") + }, + Validate = result => + { + Assert.Equal("user", result.User); + Assert.Null(result.Group); + } + } + }; + + return testInputs.Select(input => new object[] { input }); + } + + public class ChangeOwnerParseTestScenario : ParseTestScenario + { + public char EscapeChar { get; set; } + } + + public class CreateTestScenario : TestScenario + { + public string User { get; set; } + public string Group { get; set; } + } + } +} diff --git a/src/DockerfileModel/DockerfileModel.Tests/CommandInstructionTests.cs b/src/DockerfileModel/DockerfileModel.Tests/CommandInstructionTests.cs index d4b2c69..543cfbd 100644 --- a/src/DockerfileModel/DockerfileModel.Tests/CommandInstructionTests.cs +++ b/src/DockerfileModel/DockerfileModel.Tests/CommandInstructionTests.cs @@ -147,13 +147,15 @@ public static IEnumerable ParseTestInput() token => ValidateKeyword(token, "CMD"), token => ValidateWhitespace(token, " "), token => ValidateAggregate(token, "[\"/bin/bash\", \"-c\", \"echo hello\"]", + token => ValidateSymbol(token, '['), token => ValidateLiteral(token, "/bin/bash", ParseHelper.DoubleQuote), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), token => ValidateLiteral(token, "-c", ParseHelper.DoubleQuote), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), - token => ValidateLiteral(token, "echo hello", ParseHelper.DoubleQuote)) + token => ValidateLiteral(token, "echo hello", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ']')) }, Validate = result => { @@ -206,13 +208,15 @@ public static IEnumerable CreateTestInput() token => ValidateKeyword(token, "CMD"), token => ValidateWhitespace(token, " "), token => ValidateAggregate(token, "[\"/bin/bash\", \"-c\", \"echo hello\"]", + token => ValidateSymbol(token, '['), token => ValidateLiteral(token, "/bin/bash", ParseHelper.DoubleQuote), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), token => ValidateLiteral(token, "-c", ParseHelper.DoubleQuote), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), - token => ValidateLiteral(token, "echo hello", ParseHelper.DoubleQuote)) + token => ValidateLiteral(token, "echo hello", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ']')) } } }; diff --git a/src/DockerfileModel/DockerfileModel.Tests/CopyInstructionTests.cs b/src/DockerfileModel/DockerfileModel.Tests/CopyInstructionTests.cs new file mode 100644 index 0000000..30b99f8 --- /dev/null +++ b/src/DockerfileModel/DockerfileModel.Tests/CopyInstructionTests.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Xunit; + +namespace DockerfileModel.Tests +{ + public class CopyInstructionTests : FileTransferInstructionTests + { + public CopyInstructionTests() + : base(CopyInstruction.Parse, CopyInstruction.Create) + { + } + + [Theory] + [MemberData(nameof(ParseTestInput))] + public void Parse(FileTransferInstructionParseTestScenario scenario) => RunParseTest(scenario); + + [Theory] + [MemberData(nameof(CreateTestInput))] + public void Create(CreateTestScenario scenario) => RunCreateTest(scenario); + + public static IEnumerable ParseTestInput() => ParseTestInput("COPY"); + + + public static IEnumerable CreateTestInput() => CreateTestInput("COPY"); + } +} diff --git a/src/DockerfileModel/DockerfileModel.Tests/DockerfileBuilderTests.cs b/src/DockerfileModel/DockerfileModel.Tests/DockerfileBuilderTests.cs index 300127d..ba3ef4b 100644 --- a/src/DockerfileModel/DockerfileModel.Tests/DockerfileBuilderTests.cs +++ b/src/DockerfileModel/DockerfileModel.Tests/DockerfileBuilderTests.cs @@ -18,6 +18,7 @@ public void BuildAllConstructs() { DockerfileBuilder builder = new DockerfileBuilder(); builder + .AddInstruction(new string[] { "src" }, "dst") .ArgInstruction("ARG", "value") .CommandInstruction("echo hello") .Comment("my comment") @@ -27,6 +28,7 @@ public void BuildAllConstructs() .RunInstruction("echo hi"); string expectedOutput = + "ADD src dst" + Environment.NewLine + "ARG ARG=value" + Environment.NewLine + "CMD echo hello" + Environment.NewLine + "# my comment" + Environment.NewLine + diff --git a/src/DockerfileModel/DockerfileModel.Tests/ExecFormCommandTests.cs b/src/DockerfileModel/DockerfileModel.Tests/ExecFormCommandTests.cs index ba543cd..3de5b4b 100644 --- a/src/DockerfileModel/DockerfileModel.Tests/ExecFormCommandTests.cs +++ b/src/DockerfileModel/DockerfileModel.Tests/ExecFormCommandTests.cs @@ -111,13 +111,15 @@ public static IEnumerable ParseTestInput() Text = "[\"/bin/bash\", \"-c\", \"echo hello\"]", TokenValidators = new Action[] { + token => ValidateSymbol(token, '['), token => ValidateLiteral(token, "/bin/bash", ParseHelper.DoubleQuote), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), token => ValidateLiteral(token, "-c", ParseHelper.DoubleQuote), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), - token => ValidateLiteral(token, "echo hello", ParseHelper.DoubleQuote) + token => ValidateLiteral(token, "echo hello", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ']') }, Validate = result => { @@ -139,6 +141,7 @@ public static IEnumerable ParseTestInput() EscapeChar = '`', TokenValidators = new Action[] { + token => ValidateSymbol(token, '['), token => ValidateWhitespace(token, " "), token => ValidateQuotableAggregate(token, "\"/bi`\nn/bash\"", ParseHelper.DoubleQuote, token => ValidateString(token, "/bi"), @@ -156,7 +159,8 @@ public static IEnumerable ParseTestInput() token => ValidateWhitespace(token, " "), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), - token => ValidateLiteral(token, "echo he`\"llo", ParseHelper.DoubleQuote) + token => ValidateLiteral(token, "echo he`\"llo", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ']') }, Validate = result => { @@ -196,13 +200,15 @@ public static IEnumerable CreateTestInput() }, TokenValidators = new Action[] { + token => ValidateSymbol(token, '['), token => ValidateLiteral(token, "/bin/bash", ParseHelper.DoubleQuote), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), token => ValidateLiteral(token, "-c", ParseHelper.DoubleQuote), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), - token => ValidateLiteral(token, "echo hello", ParseHelper.DoubleQuote) + token => ValidateLiteral(token, "echo hello", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ']') } } }; diff --git a/src/DockerfileModel/DockerfileModel.Tests/FileTransferInstructionTests.cs b/src/DockerfileModel/DockerfileModel.Tests/FileTransferInstructionTests.cs new file mode 100644 index 0000000..9c6de41 --- /dev/null +++ b/src/DockerfileModel/DockerfileModel.Tests/FileTransferInstructionTests.cs @@ -0,0 +1,365 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DockerfileModel.Tokens; +using Sprache; +using Xunit; + +using static DockerfileModel.Tests.TokenValidator; + +namespace DockerfileModel.Tests +{ + public abstract class FileTransferInstructionTests + where TInstruction : FileTransferInstruction + { + private readonly Func parse; + private readonly Func, string, ChangeOwnerFlag, char, TInstruction> create; + + public FileTransferInstructionTests( + Func parse, + Func, string, ChangeOwnerFlag, char, TInstruction> create) + { + this.parse = parse; + this.create = create; + } + + [Fact] + public void Sources() + { + TInstruction instruction = this.create(new string[] { "src1", "src2" }, "dst", null, Dockerfile.DefaultEscapeChar); + Assert.Equal(new string[] { "src1", "src2" }, instruction.Sources); + Assert.Equal(new string[] { "src1", "src2" }, instruction.SourceTokens.Select(token => token.Value).ToArray()); + + instruction.Sources[1] = "test2"; + Assert.Equal(new string[] { "src1", "test2" }, instruction.Sources); + Assert.Equal(new string[] { "src1", "test2" }, instruction.SourceTokens.Select(token => token.Value).ToArray()); + + instruction.SourceTokens[0] = new LiteralToken("test1"); + Assert.Equal(new string[] { "test1", "test2" }, instruction.Sources); + Assert.Equal(new string[] { "test1", "test2" }, instruction.SourceTokens.Select(token => token.Value).ToArray()); + + instruction.SourceTokens[1].Value = "foo"; + Assert.Equal(new string[] { "test1", "foo" }, instruction.Sources); + Assert.Equal(new string[] { "test1", "foo" }, instruction.SourceTokens.Select(token => token.Value).ToArray()); + } + + [Fact] + public void Destination() + { + TInstruction instruction = this.create(new string[] { "src1", "src2" }, "dst", null, Dockerfile.DefaultEscapeChar); + Assert.Equal("dst", instruction.Destination); + Assert.Equal("dst", instruction.DestinationToken.Value); + + instruction.Destination = "test"; + Assert.Equal("test", instruction.Destination); + Assert.Equal("test", instruction.DestinationToken.Value); + + instruction.DestinationToken.Value = "foo"; + Assert.Equal("foo", instruction.Destination); + Assert.Equal("foo", instruction.DestinationToken.Value); + + instruction.DestinationToken = new LiteralToken("bar"); + Assert.Equal("bar", instruction.Destination); + Assert.Equal("bar", instruction.DestinationToken.Value); + + Assert.Throws(() => instruction.Destination = null); + Assert.Throws(() => instruction.Destination = ""); + Assert.Throws(() => instruction.DestinationToken = null); + } + + protected void RunParseTest(FileTransferInstructionParseTestScenario scenario) + { + if (scenario.ParseExceptionPosition is null) + { + TInstruction result = this.parse(scenario.Text, scenario.EscapeChar); + Assert.Equal(scenario.Text, result.ToString()); + Assert.Collection(result.Tokens, scenario.TokenValidators); + scenario.Validate?.Invoke(result); + } + else + { + ParseException exception = Assert.Throws( + () => AddInstruction.Parse(scenario.Text, scenario.EscapeChar)); + Assert.Equal(scenario.ParseExceptionPosition.Line, exception.Position.Line); + Assert.Equal(scenario.ParseExceptionPosition.Column, exception.Position.Column); + } + } + + protected void RunCreateTest(CreateTestScenario scenario) + { + TInstruction result = this.create(scenario.Sources, scenario.Destination, scenario.ChangeOwnerFlag, scenario.EscapeChar); + Assert.Collection(result.Tokens, scenario.TokenValidators); + scenario.Validate?.Invoke(result); + } + + public static IEnumerable ParseTestInput(string instructionName) + { + FileTransferInstructionParseTestScenario[] testInputs = new FileTransferInstructionParseTestScenario[] + { + new FileTransferInstructionParseTestScenario + { + Text = $"{instructionName} src dst", + TokenValidators = new Action[] + { + token => ValidateKeyword(token, instructionName), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "src"), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "dst") + }, + Validate = result => + { + Assert.Empty(result.Comments); + Assert.Equal(instructionName, result.InstructionName); + Assert.Equal(new string[] { "src" }, result.Sources.ToArray()); + Assert.Equal("dst", result.Destination); + } + }, + new FileTransferInstructionParseTestScenario + { + Text = $"{instructionName} --chown=1:2 src dst", + TokenValidators = new Action[] + { + token => ValidateKeyword(token, instructionName), + token => ValidateWhitespace(token, " "), + token => ValidateAggregate(token, "--chown=1:2", + token => ValidateSymbol(token, '-'), + token => ValidateSymbol(token, '-'), + token => ValidateAggregate>(token, "chown=1:2", + token => ValidateKeyword(token, "chown"), + token => ValidateSymbol(token, '='), + token => ValidateAggregate(token, "1:2", + token => ValidateLiteral(token, "1"), + token => ValidateSymbol(token, ':'), + token => ValidateLiteral(token, "2")))), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "src"), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "dst") + }, + Validate = result => + { + Assert.Empty(result.Comments); + Assert.Equal(instructionName, result.InstructionName); + Assert.Equal(new string[] { "src" }, result.Sources.ToArray()); + Assert.Equal("dst", result.Destination); + } + }, + new FileTransferInstructionParseTestScenario + { + Text = $"{instructionName} path/to/src1.txt src2 my/dst/", + TokenValidators = new Action[] + { + token => ValidateKeyword(token, instructionName), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "path/to/src1.txt"), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "src2"), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "my/dst/") + }, + Validate = result => + { + Assert.Empty(result.Comments); + Assert.Equal(instructionName, result.InstructionName); + Assert.Equal(new string[] { "path/to/src1.txt", "src2" }, result.Sources.ToArray()); + Assert.Equal("my/dst/", result.Destination); + } + }, + new FileTransferInstructionParseTestScenario + { + Text = $"{instructionName} $src dst", + TokenValidators = new Action[] + { + token => ValidateKeyword(token, instructionName), + token => ValidateWhitespace(token, " "), + token => ValidateAggregate(token, "$src", + token => ValidateAggregate(token, "$src", + token => ValidateString(token, "src"))), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "dst") + } + }, + new FileTransferInstructionParseTestScenario + { + Text = $"{instructionName} [\"$src\", \"dst\"]", + TokenValidators = new Action[] + { + token => ValidateKeyword(token, instructionName), + token => ValidateWhitespace(token, " "), + token => ValidateSymbol(token, '['), + token => ValidateQuotableAggregate(token, "\"$src\"", ParseHelper.DoubleQuote, + token => ValidateAggregate(token, "$src", + token => ValidateString(token, "src"))), + token => ValidateSymbol(token, ','), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "dst", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ']') + } + }, + new FileTransferInstructionParseTestScenario + { + Text = $"{instructionName} s\\$rc dst", + EscapeChar = '\\', + TokenValidators = new Action[] + { + token => ValidateKeyword(token, instructionName), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "s\\$rc"), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "dst") + } + }, + new FileTransferInstructionParseTestScenario + { + Text = $"{instructionName} src `\n#test comment\ndst", + EscapeChar = '`', + TokenValidators = new Action[] + { + token => ValidateKeyword(token, instructionName), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "src"), + token => ValidateWhitespace(token, " "), + token => ValidateLineContinuation(token, '`', "\n"), + token => ValidateAggregate(token, "#test comment\n", + token => ValidateSymbol(token, '#'), + token => ValidateString(token, "test comment"), + token => ValidateNewLine(token, "\n")), + token => ValidateLiteral(token, "dst") + }, + Validate = result => + { + Assert.Single(result.Comments); + Assert.Equal("test comment", result.Comments.First()); + Assert.Equal(instructionName, result.InstructionName); + Assert.Equal(new string[] { "src" }, result.Sources.ToArray()); + Assert.Equal("dst", result.Destination); + } + }, + new FileTransferInstructionParseTestScenario + { + Text = $"{instructionName} [\"source 1.txt\", \"path/to/source 2.txt\", \"/my dst/\"]", + TokenValidators = new Action[] + { + token => ValidateKeyword(token, instructionName), + token => ValidateWhitespace(token, " "), + token => ValidateSymbol(token, '['), + token => ValidateLiteral(token, "source 1.txt", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ','), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "path/to/source 2.txt", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ','), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "/my dst/", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ']') + }, + Validate = result => + { + Assert.Empty(result.Comments); + Assert.Equal(instructionName, result.InstructionName); + Assert.Equal(new string[] { "source 1.txt", "path/to/source 2.txt" }, result.Sources.ToArray()); + Assert.Equal("/my dst/", result.Destination); + } + } + }; + + return testInputs.Select(input => new object[] { input }); + } + + public static IEnumerable CreateTestInput(string instructionName) + { + CreateTestScenario[] testInputs = new CreateTestScenario[] + { + new CreateTestScenario + { + Sources = new string[] + { + "src1", + "src2" + }, + Destination = "dst", + TokenValidators = new Action[] + { + token => ValidateKeyword(token, instructionName), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "src1"), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "src2"), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "dst") + } + }, + new CreateTestScenario + { + Sources = new string[] + { + "src 1.txt", + "my path/to/src2" + }, + Destination = "dst", + TokenValidators = new Action[] + { + token => ValidateKeyword(token, instructionName), + token => ValidateWhitespace(token, " "), + token => ValidateSymbol(token, '['), + token => ValidateLiteral(token, "src 1.txt", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ','), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "my path/to/src2", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ','), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "dst", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ']') + } + }, + + new CreateTestScenario + { + Sources = new string[] + { + "src1", + "src2" + }, + Destination = "dst", + ChangeOwnerFlag = ChangeOwnerFlag.Create("user", "group"), + TokenValidators = new Action[] + { + token => ValidateKeyword(token, instructionName), + token => ValidateWhitespace(token, " "), + token => ValidateAggregate(token, "--chown=user:group", + token => ValidateSymbol(token, '-'), + token => ValidateSymbol(token, '-'), + token => ValidateAggregate>(token, "chown=user:group", + token => ValidateKeyword(token, "chown"), + token => ValidateSymbol(token, '='), + token => ValidateAggregate(token, "user:group", + token => ValidateLiteral(token, "user"), + token => ValidateSymbol(token, ':'), + token => ValidateLiteral(token, "group")))), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "src1"), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "src2"), + token => ValidateWhitespace(token, " "), + token => ValidateLiteral(token, "dst") + } + }, + }; + + return testInputs.Select(input => new object[] { input }); + } + + public class FileTransferInstructionParseTestScenario : ParseTestScenario + { + public char EscapeChar { get; set; } + } + + public class CreateTestScenario : TestScenario + { + public string Destination { get; set; } + public IEnumerable Sources { get; set; } + public ChangeOwnerFlag ChangeOwnerFlag { get; set; } + public char EscapeChar { get; set; } = Dockerfile.DefaultEscapeChar; + } + } +} diff --git a/src/DockerfileModel/DockerfileModel.Tests/KeyValueTokenTests.cs b/src/DockerfileModel/DockerfileModel.Tests/KeyValueTokenTests.cs index 4b16b58..3465b36 100644 --- a/src/DockerfileModel/DockerfileModel.Tests/KeyValueTokenTests.cs +++ b/src/DockerfileModel/DockerfileModel.Tests/KeyValueTokenTests.cs @@ -36,7 +36,7 @@ public void Parse(KeyValueTokenParseTestScenario scenario) [MemberData(nameof(CreateTestInput))] public void Create(CreateTestScenario scenario) { - KeyValueToken result = KeyValueToken.Create(scenario.Key, scenario.Value); + KeyValueToken result = KeyValueToken.Create(scenario.Key, new LiteralToken(scenario.Value)); Assert.Collection(result.Tokens, scenario.TokenValidators); scenario.Validate?.Invoke(result); } @@ -44,7 +44,7 @@ public void Create(CreateTestScenario scenario) [Fact] public void Key() { - KeyValueToken token = KeyValueToken.Create("foo", "test"); + KeyValueToken token = KeyValueToken.Create("foo", new LiteralToken("test")); Assert.Equal("foo", token.Key); Assert.Equal("foo", token.KeyToken.Value); diff --git a/src/DockerfileModel/DockerfileModel.Tests/PlatformFlagTests.cs b/src/DockerfileModel/DockerfileModel.Tests/PlatformFlagTests.cs index 6094369..8721b61 100644 --- a/src/DockerfileModel/DockerfileModel.Tests/PlatformFlagTests.cs +++ b/src/DockerfileModel/DockerfileModel.Tests/PlatformFlagTests.cs @@ -55,7 +55,7 @@ public void Platform() Assert.Equal("test3", platformFlag.Platform); Assert.Equal("test3", platformFlag.PlatformToken.Value); - platformFlag.PlatformToken = KeyValueToken.Create("platform", "test4"); + platformFlag.PlatformToken = KeyValueToken.Create("platform", new LiteralToken("test4")); Assert.Equal("test4", platformFlag.Platform); Assert.Equal("test4", platformFlag.PlatformToken.Value); diff --git a/src/DockerfileModel/DockerfileModel.Tests/RunInstructionTests.cs b/src/DockerfileModel/DockerfileModel.Tests/RunInstructionTests.cs index 43e187d..7b1b879 100644 --- a/src/DockerfileModel/DockerfileModel.Tests/RunInstructionTests.cs +++ b/src/DockerfileModel/DockerfileModel.Tests/RunInstructionTests.cs @@ -162,13 +162,15 @@ public static IEnumerable ParseTestInput() token => ValidateKeyword(token, "RUN"), token => ValidateWhitespace(token, " "), token => ValidateAggregate(token, "[\"/bin/bash\", \"-c\", \"echo hello\"]", + token => ValidateSymbol(token, '['), token => ValidateLiteral(token, "/bin/bash", ParseHelper.DoubleQuote), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), token => ValidateLiteral(token, "-c", ParseHelper.DoubleQuote), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), - token => ValidateLiteral(token, "echo hello", ParseHelper.DoubleQuote)) + token => ValidateLiteral(token, "echo hello", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ']')) }, Validate = result => { @@ -200,6 +202,7 @@ public static IEnumerable ParseTestInput() token => ValidateSymbol(token, '`'), token => ValidateNewLine(token, "\n")), token => ValidateAggregate(token, "[ \"/bi`\nn/bash\", `\n \"-c\" , \"echo he`\"llo\"]", + token => ValidateSymbol(token, '['), token => ValidateWhitespace(token, " "), token => ValidateQuotableAggregate(token, "\"/bi`\nn/bash\"", ParseHelper.DoubleQuote, token => ValidateString(token, "/bi"), @@ -217,7 +220,8 @@ public static IEnumerable ParseTestInput() token => ValidateWhitespace(token, " "), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), - token => ValidateLiteral(token, "echo he`\"llo", ParseHelper.DoubleQuote)) + token => ValidateLiteral(token, "echo he`\"llo", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ']')) }, Validate = result => { @@ -381,13 +385,15 @@ public static IEnumerable CreateTestInput() token => ValidateKeyword(token, "RUN"), token => ValidateWhitespace(token, " "), token => ValidateAggregate(token, "[\"/bin/bash\", \"-c\", \"echo hello\"]", + token => ValidateSymbol(token, '['), token => ValidateLiteral(token, "/bin/bash", ParseHelper.DoubleQuote), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), token => ValidateLiteral(token, "-c", ParseHelper.DoubleQuote), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), - token => ValidateLiteral(token, "echo hello", ParseHelper.DoubleQuote)) + token => ValidateLiteral(token, "echo hello", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ']')) } }, new CreateTestScenario diff --git a/src/DockerfileModel/DockerfileModel.Tests/SecretMountTests.cs b/src/DockerfileModel/DockerfileModel.Tests/SecretMountTests.cs index a27c52d..ebc47fe 100644 --- a/src/DockerfileModel/DockerfileModel.Tests/SecretMountTests.cs +++ b/src/DockerfileModel/DockerfileModel.Tests/SecretMountTests.cs @@ -55,7 +55,7 @@ public void Id() Assert.Equal("test3", secretMount.Id); Assert.Equal("test3", secretMount.IdToken.Value); - secretMount.IdToken = KeyValueToken.Create("id", "test4"); + secretMount.IdToken = KeyValueToken.Create("id", new LiteralToken("test4")); Assert.Equal("test4", secretMount.Id); Assert.Equal("test4", secretMount.IdToken.Value); @@ -78,7 +78,7 @@ public void DestinationPath() Assert.Equal("test3", secretMount.DestinationPath); Assert.Equal("test3", secretMount.DestinationPathToken.Value); - secretMount.DestinationPathToken = KeyValueToken.Create("dst", "test4"); + secretMount.DestinationPathToken = KeyValueToken.Create("dst", new LiteralToken("test4")); Assert.Equal("test4", secretMount.DestinationPath); Assert.Equal("test4", secretMount.DestinationPathToken.Value); Assert.Equal("type=secret,id=foo,dst=test4", secretMount.ToString()); diff --git a/src/DockerfileModel/DockerfileModel.Tests/TokenBuilderTests.cs b/src/DockerfileModel/DockerfileModel.Tests/TokenBuilderTests.cs index 55b93fd..630d549 100644 --- a/src/DockerfileModel/DockerfileModel.Tests/TokenBuilderTests.cs +++ b/src/DockerfileModel/DockerfileModel.Tests/TokenBuilderTests.cs @@ -13,12 +13,14 @@ public void BuildAllTokens() { TokenBuilder builder = new TokenBuilder(); builder + .ChangeOwner("user") + .ChangeOwnerFlag("user", "group") .Comment("comment") .Digest("digest") .ExecFormCommand("cmd1", "cmd2") .Identifier("id") .ImageName("repo") - .KeyValue("key", "value") + .KeyValue("key", new LiteralToken("value")) .Keyword("key") .LineContinuation() .Literal("literal") @@ -37,16 +39,30 @@ public void BuildAllTokens() Assert.Collection(builder.Tokens, new Action[] { + token => ValidateAggregate(token, "user", + token => ValidateLiteral(token, "user")), + token => ValidateAggregate(token, "--chown=user:group", + token => ValidateSymbol(token, '-'), + token => ValidateSymbol(token, '-'), + token => ValidateAggregate>(token, "chown=user:group", + token => ValidateKeyword(token, "chown"), + token => ValidateSymbol(token, '='), + token => ValidateAggregate(token, "user:group", + token => ValidateLiteral(token, "user"), + token => ValidateSymbol(token, ':'), + token => ValidateLiteral(token, "group")))), token => ValidateAggregate(token, "#comment", token => ValidateSymbol(token, '#'), token => ValidateString(token, "comment")), token => ValidateAggregate(token, "digest", token => ValidateString(token, "digest")), token => ValidateAggregate(token, "[\"cmd1\", \"cmd2\"]", + token => ValidateSymbol(token, '['), token => ValidateLiteral(token, "cmd1", ParseHelper.DoubleQuote), token => ValidateSymbol(token, ','), token => ValidateWhitespace(token, " "), - token => ValidateLiteral(token, "cmd2", ParseHelper.DoubleQuote)), + token => ValidateLiteral(token, "cmd2", ParseHelper.DoubleQuote), + token => ValidateSymbol(token, ']')), token => ValidateIdentifier(token, "id"), token => ValidateAggregate(token, "repo", token => ValidateAggregate(token, "repo", @@ -99,6 +115,8 @@ public void BuildAllTokens() }); string expectedResult = + "user" + + "--chown=user:group" + "#comment" + "digest" + "[\"cmd1\", \"cmd2\"]" + diff --git a/src/DockerfileModel/DockerfileModel/AddInstruction.cs b/src/DockerfileModel/DockerfileModel/AddInstruction.cs new file mode 100644 index 0000000..b067d59 --- /dev/null +++ b/src/DockerfileModel/DockerfileModel/AddInstruction.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; +using DockerfileModel.Tokens; +using Sprache; + +namespace DockerfileModel +{ + public class AddInstruction : FileTransferInstruction + { + private const string Name = "ADD"; + + private AddInstruction(IEnumerable tokens) : base(tokens) + { + } + + public static AddInstruction Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => + new AddInstruction(GetTokens(text, GetInnerParser(escapeChar, Name))); + + public static Parser GetParser(char escapeChar = Dockerfile.DefaultEscapeChar) => + from tokens in GetInnerParser(escapeChar, Name) + select new AddInstruction(tokens); + + public static AddInstruction Create(IEnumerable sources, string destination, + ChangeOwnerFlag? changeOwnerFlag = null, char escapeChar = Dockerfile.DefaultEscapeChar) => + Create(sources, destination, changeOwnerFlag, escapeChar, Name, Parse); + } +} diff --git a/src/DockerfileModel/DockerfileModel/ArgInstruction.cs b/src/DockerfileModel/DockerfileModel/ArgInstruction.cs index fc33c43..4792f2c 100644 --- a/src/DockerfileModel/DockerfileModel/ArgInstruction.cs +++ b/src/DockerfileModel/DockerfileModel/ArgInstruction.cs @@ -16,11 +16,6 @@ private ArgInstruction(IEnumerable tokens) : base(tokens) { } - private ArgInstruction(string text, char escapeChar) - : base(text, GetInnerParser(escapeChar)) - { - } - public string ArgName { get => this.ArgNameToken.Value; @@ -96,7 +91,7 @@ public LiteralToken? ArgValueToken } public static ArgInstruction Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => - new ArgInstruction(text, escapeChar); + new ArgInstruction(GetTokens(text, GetInnerParser(escapeChar))); public static ArgInstruction Create(string argName, string? argValue = null, char escapeChar = Dockerfile.DefaultEscapeChar) diff --git a/src/DockerfileModel/DockerfileModel/ChangeOwner.cs b/src/DockerfileModel/DockerfileModel/ChangeOwner.cs new file mode 100644 index 0000000..5f8986a --- /dev/null +++ b/src/DockerfileModel/DockerfileModel/ChangeOwner.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using DockerfileModel.Tokens; +using Sprache; +using Validation; +using static DockerfileModel.ParseHelper; + +namespace DockerfileModel +{ + public class ChangeOwner : AggregateToken + { + internal ChangeOwner(IEnumerable tokens) + : base(tokens) + { + } + + public string User + { + get => UserToken.Value; + set + { + Requires.NotNullOrEmpty(value, nameof(value)); + UserToken.Value = value; + } + } + + public LiteralToken UserToken + { + get => Tokens.OfType().First(); + set + { + Requires.NotNull(value, nameof(value)); + SetToken(UserToken, value); + } + } + + public string? Group + { + get => GroupToken?.Value; + set + { + LiteralToken? groupToken = GroupToken; + if (groupToken is not null && value is not null) + { + groupToken.Value = value; + } + else + { + GroupToken = String.IsNullOrEmpty(value) ? null : new LiteralToken(value!); + } + } + } + + public LiteralToken? GroupToken + { + get => Tokens.OfType().Skip(1).FirstOrDefault(); + set + { + SetToken(GroupToken, value, + addToken: token => + { + TokenList.Add(new SymbolToken(':')); + TokenList.Add(token); + }, + removeToken: token => + { + int colonIndex = TokenList.IndexOf(Tokens.OfType().First()); + TokenList.RemoveRange( + colonIndex, + TokenList.Count - colonIndex); + }); + } + } + + public static ChangeOwner Create(string user, string? group = null, char escapeChar = Dockerfile.DefaultEscapeChar) => + Parse($"{user}{(String.IsNullOrEmpty(group) ? "" : $":{group}")}", escapeChar); + + public static ChangeOwner Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => + new ChangeOwner(GetTokens(text, GetInnerParser(escapeChar))); + + public static Parser GetParser(char escapeChar = Dockerfile.DefaultEscapeChar) => + from tokens in GetInnerParser(escapeChar) + select new ChangeOwner(tokens); + + private static Parser> GetInnerParser(char escapeChar) => + from user in LiteralAggregate(escapeChar, new char[] { ':' }).AsEnumerable() + from groupSegment in ( + from colon in CharWrappedInOptionalLineContinuations(escapeChar, Sprache.Parse.Char(':'), ch => new SymbolToken(ch)) + from @group in LiteralAggregate(escapeChar, Enumerable.Empty()).AsEnumerable() + select ConcatTokens(colon, @group)).Optional() + select ConcatTokens(user, groupSegment.GetOrDefault()); + } +} diff --git a/src/DockerfileModel/DockerfileModel/ChangeOwnerFlag.cs b/src/DockerfileModel/DockerfileModel/ChangeOwnerFlag.cs new file mode 100644 index 0000000..3798340 --- /dev/null +++ b/src/DockerfileModel/DockerfileModel/ChangeOwnerFlag.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using DockerfileModel.Tokens; +using Sprache; +using Validation; +using static DockerfileModel.ParseHelper; + +namespace DockerfileModel +{ + public class ChangeOwnerFlag : AggregateToken + { + internal ChangeOwnerFlag(IEnumerable tokens) + : base(tokens) + { + } + + public string User + { + get => ChangeOwnerToken.ValueToken.User; + set + { + Requires.NotNull(value, nameof(value)); + ChangeOwnerToken.ValueToken.User = value; + } + } + + public string? Group + { + get => ChangeOwnerToken.ValueToken.Group; + set => ChangeOwnerToken.ValueToken.Group = value; + } + + public KeyValueToken ChangeOwnerToken + { + get => Tokens.OfType>().First(); + set + { + Requires.NotNull(value, nameof(value)); + SetToken(ChangeOwnerToken, value); + } + } + + public static ChangeOwnerFlag Create(string user, string? group = null, char escapeChar = Dockerfile.DefaultEscapeChar) + { + Requires.NotNull(user, nameof(user)); + return Parse($"--chown={ChangeOwner.Create(user, group, escapeChar)}", escapeChar); + } + + public static ChangeOwnerFlag Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => + new ChangeOwnerFlag(GetTokens(text, GetInnerParser(escapeChar))); + + public static Parser GetParser(char escapeChar = Dockerfile.DefaultEscapeChar) => + from tokens in GetInnerParser(escapeChar) + select new ChangeOwnerFlag(tokens); + + private static Parser> GetInnerParser(char escapeChar) => + Flag(escapeChar, KeyValueToken.GetParser("chown", escapeChar, ChangeOwner.GetParser(escapeChar)).AsEnumerable()); + } +} diff --git a/src/DockerfileModel/DockerfileModel/Command.cs b/src/DockerfileModel/DockerfileModel/Command.cs index 0117db3..57df3c8 100644 --- a/src/DockerfileModel/DockerfileModel/Command.cs +++ b/src/DockerfileModel/DockerfileModel/Command.cs @@ -1,9 +1,6 @@ using System.Collections.Generic; -using System.Linq; using DockerfileModel.Tokens; using Sprache; -using Validation; -using static DockerfileModel.ParseHelper; namespace DockerfileModel { @@ -13,58 +10,6 @@ protected Command(IEnumerable tokens) : base(tokens) { } - protected Command(string text, Parser> parser) - : base(text, parser) - { - } - public abstract CommandType CommandType { get; } - - /// - /// Parses a literal token. - /// - /// Escape character. - /// Characters to exclude from the parsed value. - protected static Parser LiteralToken(char escapeChar, IEnumerable excludedChars) - { - Requires.NotNull(excludedChars, nameof(excludedChars)); - return - from literal in LiteralString(escapeChar, excludedChars, excludeVariableRefChars: false).Many().Flatten() - where literal.Any() - select new LiteralToken(TokenHelper.CollapseStringTokens(literal)); - } - - protected static IEnumerable CollapseCommandTokens(IEnumerable tokens, char? quoteChar = null) - { - Requires.NotNullEmptyOrNullElements(tokens, nameof(tokens)); - return new Token[] - { - new LiteralToken( - TokenHelper.CollapseTokens(ExtractLiteralTokenContents(tokens), - token => token is StringToken || token.GetType() == typeof(WhitespaceToken), - val => new StringToken(val))) - { - QuoteChar = quoteChar - } - }; - } - - private static IEnumerable ExtractLiteralTokenContents(IEnumerable tokens) - { - foreach (Token token in tokens) - { - if (token is LiteralToken literal) - { - foreach (Token literalItem in literal.Tokens) - { - yield return literalItem; - } - } - else - { - yield return token; - } - } - } } } diff --git a/src/DockerfileModel/DockerfileModel/CommandInstruction.cs b/src/DockerfileModel/DockerfileModel/CommandInstruction.cs index 33d28ee..9707830 100644 --- a/src/DockerfileModel/DockerfileModel/CommandInstruction.cs +++ b/src/DockerfileModel/DockerfileModel/CommandInstruction.cs @@ -13,11 +13,6 @@ private CommandInstruction(IEnumerable tokens) : base(tokens) { } - private CommandInstruction(string text, char escapeChar) - : base(text, GetInnerParser(escapeChar)) - { - } - public Command Command { get => this.Tokens.OfType().First(); @@ -29,7 +24,7 @@ public Command Command } public static CommandInstruction Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => - new CommandInstruction(text, escapeChar); + new CommandInstruction(GetTokens(text, GetInnerParser(escapeChar))); public static Parser GetParser(char escapeChar = Dockerfile.DefaultEscapeChar) => from tokens in GetInnerParser(escapeChar) @@ -44,7 +39,7 @@ public static CommandInstruction Create(string command, char escapeChar = Docker public static CommandInstruction Create(IEnumerable commands, char escapeChar = Dockerfile.DefaultEscapeChar) { Requires.NotNullEmptyOrNullElements(commands, nameof(commands)); - return Parse($"CMD {ExecFormCommand.FormatCommands(commands)}", escapeChar); + return Parse($"CMD {StringHelper.FormatAsJson(commands)}", escapeChar); } public override string? ResolveVariables(char escapeChar, IDictionary? variables = null, ResolutionOptions? options = null) diff --git a/src/DockerfileModel/DockerfileModel/Comment.cs b/src/DockerfileModel/DockerfileModel/Comment.cs index 6320b18..dd3a21a 100644 --- a/src/DockerfileModel/DockerfileModel/Comment.cs +++ b/src/DockerfileModel/DockerfileModel/Comment.cs @@ -8,7 +8,7 @@ namespace DockerfileModel public class Comment : DockerfileConstruct { private Comment(string text) - : base(text, ParseHelper.CommentText()) + : base(GetTokens(text, ParseHelper.CommentText())) { } diff --git a/src/DockerfileModel/DockerfileModel/CopyInstruction.cs b/src/DockerfileModel/DockerfileModel/CopyInstruction.cs new file mode 100644 index 0000000..d377051 --- /dev/null +++ b/src/DockerfileModel/DockerfileModel/CopyInstruction.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; +using DockerfileModel.Tokens; +using Sprache; + +namespace DockerfileModel +{ + public class CopyInstruction : FileTransferInstruction + { + private const string Name = "COPY"; + + private CopyInstruction(IEnumerable tokens) : base(tokens) + { + } + + public static CopyInstruction Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => + new CopyInstruction(GetTokens(text, GetInnerParser(escapeChar, Name))); + + public static Parser GetParser(char escapeChar = Dockerfile.DefaultEscapeChar) => + from tokens in GetInnerParser(escapeChar, Name) + select new CopyInstruction(tokens); + + public static CopyInstruction Create(IEnumerable sources, string destination, + ChangeOwnerFlag? changeOwnerFlag = null, char escapeChar = Dockerfile.DefaultEscapeChar) => + Create(sources, destination, changeOwnerFlag, escapeChar, Name, Parse); + } +} diff --git a/src/DockerfileModel/DockerfileModel/Dockerfile.cs b/src/DockerfileModel/DockerfileModel/Dockerfile.cs index b1dc2a8..9ec64f1 100644 --- a/src/DockerfileModel/DockerfileModel/Dockerfile.cs +++ b/src/DockerfileModel/DockerfileModel/Dockerfile.cs @@ -50,7 +50,7 @@ public string ResolveVariables( argValues, stagesView => { - Stage stage = stagesView.Stages + Stage? stage = stagesView.Stages .FirstOrDefault(stage => stage.FromInstruction == instruction || stage.Items.Contains(instruction)); if (stage is null) diff --git a/src/DockerfileModel/DockerfileModel/DockerfileBuilder.cs b/src/DockerfileModel/DockerfileModel/DockerfileBuilder.cs index 65c91f4..a1c946c 100644 --- a/src/DockerfileModel/DockerfileModel/DockerfileBuilder.cs +++ b/src/DockerfileModel/DockerfileModel/DockerfileBuilder.cs @@ -40,6 +40,9 @@ public override string ToString() public DockerfileBuilder NewLine() => AddConstruct(Whitespace.Create(DefaultNewLine)); + public DockerfileBuilder AddInstruction(IEnumerable sources, string destination, ChangeOwnerFlag? changeOwnerFlag = null) => + AddConstruct(DockerfileModel.AddInstruction.Create(sources, destination, changeOwnerFlag, EscapeChar)); + public DockerfileBuilder ArgInstruction(string argName, string? argValue = null) => AddConstruct(DockerfileModel.ArgInstruction.Create(argName, argValue, EscapeChar)); @@ -62,6 +65,9 @@ public DockerfileBuilder Comment(string comment) => public DockerfileBuilder Comment(Action configureBuilder) => ParseTokens(configureBuilder, DockerfileModel.Comment.Parse); + public DockerfileBuilder CopyInstruction(IEnumerable sources, string destination, ChangeOwnerFlag? changeOwnerFlag = null) => + AddConstruct(DockerfileModel.CopyInstruction.Create(sources, destination, changeOwnerFlag, EscapeChar)); + public DockerfileBuilder FromInstruction(string imageName, string? stageName = null, string? platform = null) => AddConstruct( DockerfileModel.FromInstruction.Create(imageName, stageName, platform, EscapeChar)); diff --git a/src/DockerfileModel/DockerfileModel/DockerfileConstruct.cs b/src/DockerfileModel/DockerfileModel/DockerfileConstruct.cs index d6f1ace..d21f485 100644 --- a/src/DockerfileModel/DockerfileModel/DockerfileConstruct.cs +++ b/src/DockerfileModel/DockerfileModel/DockerfileConstruct.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using DockerfileModel.Tokens; -using Sprache; namespace DockerfileModel { @@ -10,11 +9,6 @@ protected DockerfileConstruct(IEnumerable tokens) : base(tokens) { } - protected DockerfileConstruct(string text, Parser> parser) - : base(text, parser) - { - } - public abstract ConstructType Type { get; } } } diff --git a/src/DockerfileModel/DockerfileModel/DockerfileModel.csproj b/src/DockerfileModel/DockerfileModel/DockerfileModel.csproj index 8ddb336..50c9aac 100644 --- a/src/DockerfileModel/DockerfileModel/DockerfileModel.csproj +++ b/src/DockerfileModel/DockerfileModel/DockerfileModel.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + netstandard2.0;net5.0 preview enable diff --git a/src/DockerfileModel/DockerfileModel/DockerfileParser.cs b/src/DockerfileModel/DockerfileModel/DockerfileParser.cs index 666c98b..e1f4818 100644 --- a/src/DockerfileModel/DockerfileModel/DockerfileParser.cs +++ b/src/DockerfileModel/DockerfileModel/DockerfileParser.cs @@ -14,10 +14,10 @@ internal static class DockerfileParser private static readonly Dictionary> instructionParsers = new Dictionary> { - { "ADD", GenericInstruction.Parse }, + { "ADD", AddInstruction.Parse }, { "ARG", ArgInstruction.Parse }, { "CMD", CommandInstruction.Parse }, - { "COPY", GenericInstruction.Parse }, + { "COPY", CopyInstruction.Parse }, { "ENTRYPOINT", GenericInstruction.Parse }, { "EXPOSE", GenericInstruction.Parse }, { "ENV", GenericInstruction.Parse }, diff --git a/src/DockerfileModel/DockerfileModel/ExecFormCommand.cs b/src/DockerfileModel/DockerfileModel/ExecFormCommand.cs index 92b3292..e3012a5 100644 --- a/src/DockerfileModel/DockerfileModel/ExecFormCommand.cs +++ b/src/DockerfileModel/DockerfileModel/ExecFormCommand.cs @@ -1,5 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Dynamic; using System.Linq; using DockerfileModel.Tokens; using Sprache; @@ -10,11 +10,6 @@ namespace DockerfileModel { public class ExecFormCommand : Command { - private ExecFormCommand(string text, char escapeChar) - : base(text, GetInnerParser(escapeChar)) - { - } - internal ExecFormCommand(IEnumerable tokens) : base(tokens) { } @@ -22,17 +17,11 @@ internal ExecFormCommand(IEnumerable tokens) : base(tokens) public static ExecFormCommand Create(IEnumerable commands, char escapeChar = Dockerfile.DefaultEscapeChar) { Requires.NotNullEmptyOrNullElements(commands, nameof(commands)); - return Parse(FormatCommands(commands), escapeChar); + return Parse(StringHelper.FormatAsJson(commands), escapeChar); } public static ExecFormCommand Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => - new ExecFormCommand(text, escapeChar); - - public static string FormatCommands(IEnumerable commands) - { - Requires.NotNullEmptyOrNullElements(commands, nameof(commands)); - return $"[{String.Join(", ", commands.Select(command => $"\"{command}\"").ToArray())}]"; - } + new ExecFormCommand(GetTokens(text, GetInnerParser(escapeChar))); public static Parser GetParser(char escapeChar = Dockerfile.DefaultEscapeChar) => from tokens in GetInnerParser(escapeChar) @@ -48,39 +37,7 @@ from tokens in GetInnerParser(escapeChar) public override CommandType CommandType => CommandType.ExecForm; - protected override string GetUnderlyingValue(TokenStringOptions options) => - $"[{base.GetUnderlyingValue(options)}]"; - private static Parser> GetInnerParser(char escapeChar) => - from openingBracket in Sprache.Parse.Char('[') - from execFormArgs in - from arg in ExecFormArg(escapeChar).Once().Flatten() - from tail in ( - from delimiter in ExecFormArgDelimiter(escapeChar) - from nextArg in ExecFormArg(escapeChar) - select ConcatTokens(delimiter, nextArg)).Many() - select ConcatTokens(arg, tail.Flatten()) - from closingBracket in Sprache.Parse.Char(']') - select execFormArgs; - - private static Parser> ExecFormArgDelimiter(char escapeChar) => - from leading in OptionalWhitespaceOrLineContinuation(escapeChar) - from comma in Symbol(',').AsEnumerable() - from trailing in OptionalWhitespaceOrLineContinuation(escapeChar) - select ConcatTokens( - leading, - comma, - trailing); - - private static Parser> ExecFormArg(char escapeChar) => - from leading in OptionalWhitespaceOrLineContinuation(escapeChar) - from openingQuote in Symbol(DoubleQuote) - from argValue in ArgTokens(LiteralToken(escapeChar, new char[] { DoubleQuote }).AsEnumerable(), escapeChar).Many() - from closingQuote in Symbol(DoubleQuote) - from trailing in OptionalWhitespaceOrLineContinuation(escapeChar) - select ConcatTokens( - leading, - CollapseCommandTokens(argValue.Flatten(), DoubleQuote), - trailing); + JsonArray(escapeChar, canContainVariables: false); } } diff --git a/src/DockerfileModel/DockerfileModel/FileTransferInstruction.cs b/src/DockerfileModel/DockerfileModel/FileTransferInstruction.cs new file mode 100644 index 0000000..c720d2a --- /dev/null +++ b/src/DockerfileModel/DockerfileModel/FileTransferInstruction.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DockerfileModel.Tokens; +using Sprache; +using Validation; + +using static DockerfileModel.ParseHelper; + +namespace DockerfileModel +{ + public abstract class FileTransferInstruction : Instruction + { + private readonly TokenList sourceTokens; + + protected FileTransferInstruction(IEnumerable tokens) : base(tokens) + { + this.sourceTokens = new TokenList(TokenList, + literals => literals.Take(literals.Count() - 1)); + } + + public IList Sources => + new ProjectedItemList( + SourceTokens, + token => token.Value, + (token, value) => token.Value = value); + + public IList SourceTokens => sourceTokens; + + public string Destination + { + get => DestinationToken.Value; + set + { + Requires.NotNullOrEmpty(value, nameof(value)); + DestinationToken.Value = value; + } + } + + public LiteralToken DestinationToken + { + get => Tokens.OfType().Last(); + set + { + Requires.NotNull(value, nameof(value)); + SetToken(DestinationToken, value); + } + } + + protected static TInstruction Create(IEnumerable sources, string destination, ChangeOwnerFlag? changeOwnerFlag, char escapeChar, + string instructionName, Func parse) + where TInstruction : FileTransferInstruction + { + Requires.NotNullEmptyOrNullElements(sources, nameof(sources)); + Requires.NotNullOrEmpty(destination, nameof(destination)); + + IEnumerable locations = sources.Append(destination); + + string changeOwnerFlagStr = changeOwnerFlag is null ? string.Empty : $"{changeOwnerFlag} "; + + bool useJsonForm = locations.Any(loc => loc.Contains(" ")); + if (useJsonForm) + { + return parse($"{instructionName} {changeOwnerFlagStr}{StringHelper.FormatAsJson(locations)}", escapeChar); + } + else + { + return parse($"{instructionName} {changeOwnerFlagStr}{String.Join(" ", locations.ToArray())}", escapeChar); + } + } + + protected static Parser> GetInnerParser(char escapeChar, string instructionName) => + Instruction(instructionName, escapeChar, + GetArgsParser(escapeChar)); + + private static Parser> GetArgsParser(char escapeChar) => + from changeOwner in ArgTokens(ChangeOwnerFlag.GetParser(escapeChar).AsEnumerable(), escapeChar).Optional() + from whitespace in Whitespace() + from files in JsonArray(escapeChar, canContainVariables: true).Or( + from literals in ArgTokens( + LiteralAggregate(escapeChar).AsEnumerable(), + escapeChar).Many() + select literals.Flatten()) + select ConcatTokens(changeOwner.GetOrDefault(), whitespace, files); + } +} diff --git a/src/DockerfileModel/DockerfileModel/FromInstruction.cs b/src/DockerfileModel/DockerfileModel/FromInstruction.cs index ed72c62..58f85fa 100644 --- a/src/DockerfileModel/DockerfileModel/FromInstruction.cs +++ b/src/DockerfileModel/DockerfileModel/FromInstruction.cs @@ -13,19 +13,19 @@ public class FromInstruction : Instruction { private LiteralToken imageName; -#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. private FromInstruction(IEnumerable tokens) : base(tokens) -#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. { - Initialize(); - } + PlatformFlag? platform = this.PlatformFlag; + int startIndex = 0; + if (platform != null) + { + startIndex = this.TokenList.IndexOf(platform) + 1; + } -#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - private FromInstruction(string text, char escapeChar) -#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - : base(text, GetInnerParser(escapeChar)) - { - Initialize(); + this.imageName = this.TokenList + .Skip(startIndex) + .OfType() + .First(); } public string ImageName @@ -122,7 +122,7 @@ public StageName? StageNameToken } public static FromInstruction Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => - new FromInstruction(text, escapeChar); + new FromInstruction(GetTokens(text, GetInnerParser(escapeChar))); public static FromInstruction Create(string imageName, string? stageName = null, string? platform = null, char escapeChar = Dockerfile.DefaultEscapeChar) @@ -170,20 +170,5 @@ private static Parser> GetStageNameParser(char escapeChar) => private static Parser> GetImageNameParser(char escapeChar) => ArgTokens( LiteralAggregate(escapeChar).AsEnumerable(), escapeChar); - - private void Initialize() - { - PlatformFlag? platform = this.PlatformFlag; - int startIndex = 0; - if (platform != null) - { - startIndex = this.TokenList.IndexOf(platform) + 1; - } - - this.imageName = this.TokenList - .Skip(startIndex) - .OfType() - .First(); - } } } diff --git a/src/DockerfileModel/DockerfileModel/GenericInstruction.cs b/src/DockerfileModel/DockerfileModel/GenericInstruction.cs index 218ae41..4a8d3fd 100644 --- a/src/DockerfileModel/DockerfileModel/GenericInstruction.cs +++ b/src/DockerfileModel/DockerfileModel/GenericInstruction.cs @@ -10,7 +10,7 @@ namespace DockerfileModel public class GenericInstruction : Instruction { private GenericInstruction(string text, char escapeChar) - : base(text, InstructionParser(escapeChar)) + : base(GetTokens(text, InstructionParser(escapeChar))) { } diff --git a/src/DockerfileModel/DockerfileModel/ImageName.cs b/src/DockerfileModel/DockerfileModel/ImageName.cs index aa10cd6..e2b209e 100644 --- a/src/DockerfileModel/DockerfileModel/ImageName.cs +++ b/src/DockerfileModel/DockerfileModel/ImageName.cs @@ -17,15 +17,6 @@ public class ImageName : AggregateToken private TagToken? tagToken; private DigestToken? digestToken; - private ImageName(string text) - : base(text, ImageNameParser.GetParser()) - { - registryToken = Tokens.OfType().FirstOrDefault(); - repositoryToken = Tokens.OfType().First(); - tagToken = Tokens.OfType().FirstOrDefault(); - digestToken = Tokens.OfType().FirstOrDefault(); - } - internal ImageName(IEnumerable tokens) : base(tokens) { registryToken = Tokens.OfType().FirstOrDefault(); @@ -65,7 +56,7 @@ public static ImageName Create(string repository, string? registry = null, strin } public static ImageName Parse(string imageName) => - new ImageName(imageName); + new ImageName(GetTokens(imageName, ImageNameParser.GetParser())); public static Parser> GetParser() => ImageNameParser.GetParser(); diff --git a/src/DockerfileModel/DockerfileModel/Instruction.cs b/src/DockerfileModel/DockerfileModel/Instruction.cs index d31c797..ef66ad7 100644 --- a/src/DockerfileModel/DockerfileModel/Instruction.cs +++ b/src/DockerfileModel/DockerfileModel/Instruction.cs @@ -13,11 +13,6 @@ protected Instruction(IEnumerable tokens) : base(tokens) { } - protected Instruction(string text, Parser> parser) - : base(text, parser) - { - } - public string InstructionName { get => this.InstructionNameToken.Value; diff --git a/src/DockerfileModel/DockerfileModel/Mount.cs b/src/DockerfileModel/DockerfileModel/Mount.cs index cbccf4a..cb7f931 100644 --- a/src/DockerfileModel/DockerfileModel/Mount.cs +++ b/src/DockerfileModel/DockerfileModel/Mount.cs @@ -12,11 +12,6 @@ protected Mount(IEnumerable tokens) : base(tokens) { } - protected Mount(string text, Parser> parser) - : base(text, parser) - { - } - public string Type { get => TypeToken.Value; diff --git a/src/DockerfileModel/DockerfileModel/MountFlag.cs b/src/DockerfileModel/DockerfileModel/MountFlag.cs index ed247a7..299e149 100644 --- a/src/DockerfileModel/DockerfileModel/MountFlag.cs +++ b/src/DockerfileModel/DockerfileModel/MountFlag.cs @@ -9,11 +9,6 @@ namespace DockerfileModel { public class MountFlag : AggregateToken { - private MountFlag(string text, char escapeChar) - : base(text, GetInnerParser(escapeChar)) - { - } - internal MountFlag(IEnumerable tokens) : base(tokens) { @@ -38,14 +33,14 @@ public static MountFlag Create(Mount mount, char escapeChar = Dockerfile.Default } public static MountFlag Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => - new MountFlag(text, escapeChar); + new MountFlag(GetTokens(text, GetInnerParser(escapeChar))); public static Parser GetParser(char escapeChar = Dockerfile.DefaultEscapeChar) => from tokens in GetInnerParser(escapeChar) select new MountFlag(tokens); private static Parser> GetInnerParser(char escapeChar) => - Flag(escapeChar, KeyValueToken.GetParser("mount", escapeChar, GetMount(escapeChar))); + Flag(escapeChar, KeyValueToken.GetParser("mount", escapeChar, GetMount(escapeChar)).AsEnumerable()); private static Parser GetMount(char escapeChar) => SecretMount.GetParser(escapeChar); diff --git a/src/DockerfileModel/DockerfileModel/ParseHelper.cs b/src/DockerfileModel/DockerfileModel/ParseHelper.cs index 48278da..766213d 100644 --- a/src/DockerfileModel/DockerfileModel/ParseHelper.cs +++ b/src/DockerfileModel/DockerfileModel/ParseHelper.cs @@ -3,6 +3,7 @@ using System.Linq; using DockerfileModel.Tokens; using Sprache; +using Validation; namespace DockerfileModel { @@ -214,6 +215,18 @@ from lineContinuation in LineContinuationToken.GetParser(escapeChar).Many() from ch in charParser select ConcatTokens(lineContinuation, new Token[] { createToken(ch) }); + /// + /// Parses a single character preceded by an optional line continuation. + /// + /// Escape character. + /// Character parser. + /// Parsed tokens. + public static Parser> CharWrappedInOptionalLineContinuations(char escapeChar, Parser charParser, Func createToken) => + from leadingLineContinuations in LineContinuationToken.GetParser(escapeChar).Many() + from ch in charParser + from trailingLineContinuations in LineContinuationToken.GetParser(escapeChar).Many() + select ConcatTokens(leadingLineContinuations, new Token[] { createToken(ch) }, trailingLineContinuations); + /// /// Parses a single character preceded by an optional line continuation. /// @@ -318,11 +331,100 @@ public static Parser> LiteralString(char escapeChar, IEnumera LiteralStringWithoutSpaces(escapeChar, excludedChars, excludeVariableRefChars), EscapedChar(escapeChar)); - public static Parser> Flag(char escapeChar, Parser flagParser) => - from flag in SymbolTokenCharWithOptionalLineContinuation(escapeChar, Sprache.Parse.Char('-')).Repeat(2) + public static Parser> Flag(char escapeChar, Parser> flagParser) => + from flag in SymbolTokenCharWithOptionalLineContinuation(escapeChar, Parse.Char('-')).Repeat(2) from lineCont in LineContinuationToken.GetParser(escapeChar).AsEnumerable().Optional() from token in flagParser - select ConcatTokens(flag.Flatten(), lineCont.GetOrDefault(), new Token[] { token }); + select ConcatTokens(flag.Flatten(), lineCont.GetOrDefault(), token); + + public static Parser> ArgumentListAsLiteral(char escapeChar) => + from literals in ArgTokens( + LiteralToken(escapeChar, Enumerable.Empty()).AsEnumerable(), + escapeChar).Many() + select CollapseLiteralTokens(literals.Flatten()); + + /// + /// Parses a literal token. + /// + /// Escape character. + /// Characters to exclude from the parsed value. + public static Parser LiteralToken(char escapeChar, IEnumerable excludedChars) => + from literal in LiteralString(escapeChar, excludedChars, excludeVariableRefChars: false).Many().Flatten() + where literal.Any() + select new LiteralToken(TokenHelper.CollapseStringTokens(literal)); + + public static Parser> JsonArray(char escapeChar, bool canContainVariables) => + from openingBracket in Symbol('[').AsEnumerable() + from execFormArgs in + from arg in JsonArrayElement(escapeChar, canContainVariables).Once().Flatten() + from tail in ( + from delimiter in JsonArrayElementDelimiter(escapeChar) + from nextArg in JsonArrayElement(escapeChar, canContainVariables) + select ConcatTokens(delimiter, nextArg)).Many() + select ConcatTokens(arg, tail.Flatten()) + from closingBracket in Symbol(']').AsEnumerable() + select ConcatTokens(openingBracket, execFormArgs, closingBracket); + + private static IEnumerable CollapseLiteralTokens(IEnumerable tokens, char? quoteChar = null) + { + Requires.NotNullEmptyOrNullElements(tokens, nameof(tokens)); + return new Token[] + { + new LiteralToken( + TokenHelper.CollapseTokens(ExtractLiteralTokenContents(tokens), + token => token is StringToken || token.GetType() == typeof(WhitespaceToken), + val => new StringToken(val))) + { + QuoteChar = quoteChar + } + }; + } + + private static Parser> JsonArrayElementDelimiter(char escapeChar) => + from leading in OptionalWhitespaceOrLineContinuation(escapeChar) + from comma in Symbol(',').AsEnumerable() + from trailing in OptionalWhitespaceOrLineContinuation(escapeChar) + select ConcatTokens( + leading, + comma, + trailing); + + private static Parser> JsonArrayElement(char escapeChar, bool canContainVariables) + { + Parser literalParser = canContainVariables ? + LiteralAggregate(escapeChar, new char[] { DoubleQuote }) : + LiteralToken(escapeChar, new char[] { DoubleQuote }); + + return + from leading in OptionalWhitespaceOrLineContinuation(escapeChar) + from openingQuote in Symbol(DoubleQuote) + from argValue in ArgTokens(literalParser.AsEnumerable(), escapeChar).Many() + from closingQuote in Symbol(DoubleQuote) + from trailing in OptionalWhitespaceOrLineContinuation(escapeChar) + select ConcatTokens( + leading, + CollapseLiteralTokens(argValue.Flatten(), DoubleQuote), + trailing); + } + + + private static IEnumerable ExtractLiteralTokenContents(IEnumerable tokens) + { + foreach (Token token in tokens) + { + if (token is LiteralToken literal) + { + foreach (Token literalItem in literal.Tokens) + { + yield return literalItem; + } + } + else + { + yield return token; + } + } + } /// /// Parses an identifier string wrapped in quotes. diff --git a/src/DockerfileModel/DockerfileModel/ParserDirective.cs b/src/DockerfileModel/DockerfileModel/ParserDirective.cs index 3a69212..d802f5e 100644 --- a/src/DockerfileModel/DockerfileModel/ParserDirective.cs +++ b/src/DockerfileModel/DockerfileModel/ParserDirective.cs @@ -13,7 +13,7 @@ public class ParserDirective : DockerfileConstruct public const string SyntaxDirective = "syntax"; private ParserDirective(string text) - : base(text, GetParser()) + : base(GetTokens(text, GetParser())) { } diff --git a/src/DockerfileModel/DockerfileModel/PlatformFlag.cs b/src/DockerfileModel/DockerfileModel/PlatformFlag.cs index fea682f..3a28fc7 100644 --- a/src/DockerfileModel/DockerfileModel/PlatformFlag.cs +++ b/src/DockerfileModel/DockerfileModel/PlatformFlag.cs @@ -9,11 +9,6 @@ namespace DockerfileModel { public class PlatformFlag : AggregateToken { - private PlatformFlag(string text, char escapeChar) - : base(text, GetInnerParser(escapeChar)) - { - } - internal PlatformFlag(IEnumerable tokens) : base(tokens) { @@ -46,13 +41,13 @@ public static PlatformFlag Create(string platform, char escapeChar = Dockerfile. } public static PlatformFlag Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => - new PlatformFlag(text, escapeChar); + new PlatformFlag(GetTokens(text, GetInnerParser(escapeChar))); public static Parser GetParser(char escapeChar = Dockerfile.DefaultEscapeChar) => from tokens in GetInnerParser(escapeChar) select new PlatformFlag(tokens); private static Parser> GetInnerParser(char escapeChar) => - Flag(escapeChar, KeyValueToken.GetParser("platform", escapeChar)); + Flag(escapeChar, KeyValueToken.GetParser("platform", escapeChar).AsEnumerable()); } } diff --git a/src/DockerfileModel/DockerfileModel/RunInstruction.cs b/src/DockerfileModel/DockerfileModel/RunInstruction.cs index f4ff0fb..0f72f6c 100644 --- a/src/DockerfileModel/DockerfileModel/RunInstruction.cs +++ b/src/DockerfileModel/DockerfileModel/RunInstruction.cs @@ -14,11 +14,6 @@ private RunInstruction(IEnumerable tokens) : base(tokens) { } - private RunInstruction(string text, char escapeChar) - : base(text, GetInnerParser(escapeChar)) - { - } - public Command Command { get => this.Tokens.OfType().First(); @@ -32,7 +27,7 @@ public Command Command public IEnumerable MountFlags => Tokens.OfType(); public static RunInstruction Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => - new RunInstruction(text, escapeChar); + new RunInstruction(GetTokens(text, GetInnerParser(escapeChar))); public static Parser GetParser(char escapeChar = Dockerfile.DefaultEscapeChar) => from tokens in GetInnerParser(escapeChar) @@ -57,7 +52,7 @@ public static RunInstruction Create(IEnumerable commands, IEnumerable? variables = null, ResolutionOptions? options = null) diff --git a/src/DockerfileModel/DockerfileModel/SecretMount.cs b/src/DockerfileModel/DockerfileModel/SecretMount.cs index cee4cb9..639ac34 100644 --- a/src/DockerfileModel/DockerfileModel/SecretMount.cs +++ b/src/DockerfileModel/DockerfileModel/SecretMount.cs @@ -10,11 +10,6 @@ namespace DockerfileModel { public class SecretMount : Mount { - private SecretMount(string text, char escapeChar) - : base(text, GetInnerParser(escapeChar)) - { - } - internal SecretMount(IEnumerable tokens) : base(tokens) { @@ -52,7 +47,9 @@ public string? DestinationPath } else { - DestinationPathToken = String.IsNullOrEmpty(value) ? null : KeyValueToken.Create("dst", value!); + DestinationPathToken = String.IsNullOrEmpty(value) ? + null : + KeyValueToken.Create("dst", new LiteralToken(value!)); } } } @@ -89,7 +86,7 @@ public static SecretMount Create(string id, string? destinationPath = null, } public static SecretMount Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => - new SecretMount(text, escapeChar); + new SecretMount(GetTokens(text, GetInnerParser(escapeChar))); public static Parser GetParser(char escapeChar = Dockerfile.DefaultEscapeChar) => from tokens in GetInnerParser(escapeChar) diff --git a/src/DockerfileModel/DockerfileModel/ShellFormCommand.cs b/src/DockerfileModel/DockerfileModel/ShellFormCommand.cs index c04b620..ecfb007 100644 --- a/src/DockerfileModel/DockerfileModel/ShellFormCommand.cs +++ b/src/DockerfileModel/DockerfileModel/ShellFormCommand.cs @@ -10,11 +10,6 @@ namespace DockerfileModel { public class ShellFormCommand : Command { - private ShellFormCommand(string text, char escapeChar) - : base(text, GetInnerParser(escapeChar)) - { - } - internal ShellFormCommand(IEnumerable tokens) : base(tokens) { } @@ -23,10 +18,10 @@ public static ShellFormCommand Create(string command, char escapeChar = Dockerfi Parse(command, escapeChar); public static ShellFormCommand Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => - new ShellFormCommand(text, escapeChar); + new ShellFormCommand(GetTokens(text, ArgumentListAsLiteral(escapeChar))); public static Parser GetParser(char escapeChar = Dockerfile.DefaultEscapeChar) => - from tokens in GetInnerParser(escapeChar) + from tokens in ArgumentListAsLiteral(escapeChar) select new ShellFormCommand(tokens); public override CommandType CommandType => CommandType.ShellForm; @@ -50,11 +45,5 @@ public LiteralToken ValueToken SetToken(ValueToken, value); } } - - private static Parser> GetInnerParser(char escapeChar) => - from literals in ArgTokens( - LiteralToken(escapeChar, new char[0]).AsEnumerable(), - escapeChar).Many() - select CollapseCommandTokens(literals.Flatten()); } } diff --git a/src/DockerfileModel/DockerfileModel/StageName.cs b/src/DockerfileModel/DockerfileModel/StageName.cs index a2d8468..b9b951f 100644 --- a/src/DockerfileModel/DockerfileModel/StageName.cs +++ b/src/DockerfileModel/DockerfileModel/StageName.cs @@ -11,20 +11,10 @@ public class StageName : AggregateToken, ICommentable { private IdentifierToken stage; -#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - private StageName(string text, char escapeChar) -#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - : base(text, GetInnerParser(escapeChar)) - { - Initialize(); - } - -#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. internal StageName(IEnumerable tokens) -#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. : base(tokens) { - Initialize(); + this.stage = this.TokenList.OfType().First(); } public string Stage @@ -59,7 +49,7 @@ public static StageName Create(string stageName, char escapeChar = Dockerfile.De } public static StageName Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => - new StageName(text, escapeChar); + new StageName(GetTokens(text, GetInnerParser(escapeChar))); public static Parser GetParser(char escapeChar = Dockerfile.DefaultEscapeChar) => from tokens in GetInnerParser(escapeChar) @@ -81,10 +71,5 @@ from stageName in Sprache.Parse.Identifier( Sprache.Parse.Letter, Sprache.Parse.LetterOrDigit.Or(Sprache.Parse.Char('_')).Or(Sprache.Parse.Char('-')).Or(Sprache.Parse.Char('.'))) select new IdentifierToken(stageName); - - private void Initialize() - { - this.stage = this.TokenList.OfType().First(); - } } } diff --git a/src/DockerfileModel/DockerfileModel/StringHelper.cs b/src/DockerfileModel/DockerfileModel/StringHelper.cs new file mode 100644 index 0000000..1ea432c --- /dev/null +++ b/src/DockerfileModel/DockerfileModel/StringHelper.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Validation; + +namespace DockerfileModel +{ + internal static class StringHelper + { + public static string FormatAsJson(IEnumerable values) + { + Requires.NotNullEmptyOrNullElements(values, nameof(values)); + return $"[{String.Join(", ", values.Select(val => $"\"{val}\"").ToArray())}]"; + } + } +} diff --git a/src/DockerfileModel/DockerfileModel/Tokens/AggregateToken.cs b/src/DockerfileModel/DockerfileModel/Tokens/AggregateToken.cs index 169b09a..1b78b75 100644 --- a/src/DockerfileModel/DockerfileModel/Tokens/AggregateToken.cs +++ b/src/DockerfileModel/DockerfileModel/Tokens/AggregateToken.cs @@ -10,31 +10,20 @@ namespace DockerfileModel.Tokens { public abstract class AggregateToken : Token { - protected AggregateToken(string text, Parser> parser) + protected AggregateToken(IEnumerable tokens) { - Requires.NotNull(text, nameof(text)); - Requires.NotNull(parser, nameof(parser)); + Requires.NotNull(tokens, nameof(tokens)); - this.TokenList = FilterNulls(parser.Parse(text)) - .ToList(); + this.TokenList = tokens.ToList(); } - protected AggregateToken(string text, Parser parser) + protected static IEnumerable GetTokens(string text, Parser> parser) { Requires.NotNull(text, nameof(text)); Requires.NotNull(parser, nameof(parser)); - this.TokenList = new List - { - parser.Parse(text) - }; - } - - protected AggregateToken(IEnumerable tokens) - { - Requires.NotNull(tokens, nameof(tokens)); - - this.TokenList = tokens.ToList(); + return FilterNulls(parser.Parse(text)) + .ToList(); } protected internal List TokenList { get; } diff --git a/src/DockerfileModel/DockerfileModel/Tokens/CommentToken.cs b/src/DockerfileModel/DockerfileModel/Tokens/CommentToken.cs index c6e3786..8b5ae9b 100644 --- a/src/DockerfileModel/DockerfileModel/Tokens/CommentToken.cs +++ b/src/DockerfileModel/DockerfileModel/Tokens/CommentToken.cs @@ -8,11 +8,6 @@ namespace DockerfileModel.Tokens { public class CommentToken : AggregateToken { - private CommentToken(string text) - : base(text, GetParser()) - { - } - internal CommentToken(IEnumerable tokens) : base(tokens) { @@ -45,7 +40,7 @@ public static CommentToken Create(string comment) => Parse($"#{comment}"); public static CommentToken Parse(string text) => - new CommentToken(text); + new CommentToken(GetTokens(text, GetParser())); public static Parser> GetParser() => from commentChar in CommentCharParser() diff --git a/src/DockerfileModel/DockerfileModel/Tokens/KeyValueToken.cs b/src/DockerfileModel/DockerfileModel/Tokens/KeyValueToken.cs index 6c49a13..060c095 100644 --- a/src/DockerfileModel/DockerfileModel/Tokens/KeyValueToken.cs +++ b/src/DockerfileModel/DockerfileModel/Tokens/KeyValueToken.cs @@ -9,11 +9,6 @@ namespace DockerfileModel.Tokens public class KeyValueToken : AggregateToken where TValue : Token { - private KeyValueToken(string text, string key, char escapeChar) - : base(text, GetInnerParser(key, escapeChar)) - { - } - internal KeyValueToken(IEnumerable tokens) : base(tokens) { @@ -46,11 +41,15 @@ public TValue ValueToken } } - public static KeyValueToken Create(string key, string value, char escapeChar = Dockerfile.DefaultEscapeChar) => - Parse($"{key}={value}", key, escapeChar); + public static KeyValueToken Create(string key, TValue value, char escapeChar = Dockerfile.DefaultEscapeChar) => + new KeyValueToken( + ConcatTokens( + new KeywordToken(key), + new SymbolToken('='), + value)); public static KeyValueToken Parse(string text, string key, char escapeChar = Dockerfile.DefaultEscapeChar) => - new KeyValueToken(text, key, escapeChar); + new KeyValueToken(GetTokens(text, GetInnerParser(key, escapeChar))); public static Parser> GetParser(string key, char escapeChar = Dockerfile.DefaultEscapeChar, Parser? valueTokenParser = null) => from tokens in GetInnerParser(key, escapeChar, valueTokenParser) @@ -66,7 +65,7 @@ private static Parser> GetInnerParser(string key, char escape } return from keyword in Keyword(key, escapeChar).AsEnumerable() - from equalOperator in CharWithOptionalLineContinuation(escapeChar, Sprache.Parse.Char('='), ch => new SymbolToken(ch)) + from equalOperator in CharWrappedInOptionalLineContinuations(escapeChar, Sprache.Parse.Char('='), ch => new SymbolToken(ch)) from lineCont in LineContinuationToken.GetParser(escapeChar).AsEnumerable().Optional() from value in valueTokenParser.AsEnumerable() select ConcatTokens(keyword, equalOperator, lineCont.GetOrDefault(), value); diff --git a/src/DockerfileModel/DockerfileModel/Tokens/LineContinuationToken.cs b/src/DockerfileModel/DockerfileModel/Tokens/LineContinuationToken.cs index ec190e9..7a6a34b 100644 --- a/src/DockerfileModel/DockerfileModel/Tokens/LineContinuationToken.cs +++ b/src/DockerfileModel/DockerfileModel/Tokens/LineContinuationToken.cs @@ -8,17 +8,12 @@ namespace DockerfileModel.Tokens { public class LineContinuationToken : AggregateToken { - private LineContinuationToken(string text, char escapeChar) - : base(text, GetInnerParser(escapeChar)) - { - } - internal LineContinuationToken(IEnumerable tokens) : base(tokens) { } public static LineContinuationToken Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => - new LineContinuationToken(text, escapeChar); + new LineContinuationToken(GetTokens(text, GetInnerParser(escapeChar))); public static LineContinuationToken Create(char escapeChar = Dockerfile.DefaultEscapeChar) => Create(Environment.NewLine, escapeChar); diff --git a/src/DockerfileModel/DockerfileModel/Tokens/TokenBuilder.cs b/src/DockerfileModel/DockerfileModel/Tokens/TokenBuilder.cs index 1ca423c..be3b472 100644 --- a/src/DockerfileModel/DockerfileModel/Tokens/TokenBuilder.cs +++ b/src/DockerfileModel/DockerfileModel/Tokens/TokenBuilder.cs @@ -12,6 +12,12 @@ public class TokenBuilder public IList Tokens { get; } = new List(); + public TokenBuilder ChangeOwner(string user, string? group = null) => + AddToken(DockerfileModel.ChangeOwner.Create(user, group, EscapeChar)); + + public TokenBuilder ChangeOwnerFlag(string user, string? group = null) => + AddToken(DockerfileModel.ChangeOwnerFlag.Create(user, group, EscapeChar)); + public TokenBuilder Comment(string comment) => AddToken(CommentToken.Create(comment)); @@ -42,7 +48,7 @@ public TokenBuilder ImageName(string repository, string? registry = null, string public TokenBuilder ImageName(Action configureBuilder) => AddToken(new ImageName(GetTokens(configureBuilder))); - public TokenBuilder KeyValue(string key, string value) + public TokenBuilder KeyValue(string key, TValue value) where TValue : Token => AddToken(KeyValueToken.Create(key, value, EscapeChar)); diff --git a/src/DockerfileModel/DockerfileModel/Tokens/TokenList.cs b/src/DockerfileModel/DockerfileModel/Tokens/TokenList.cs new file mode 100644 index 0000000..5c5d0a6 --- /dev/null +++ b/src/DockerfileModel/DockerfileModel/Tokens/TokenList.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace DockerfileModel.Tokens +{ + public class TokenList : IList + where TToken : Token + { + private readonly IList innerTokens; + private readonly Func, IEnumerable> filterTokens; + + internal TokenList(IList innerTokens, Func, IEnumerable> filterTokens) + { + this.innerTokens = innerTokens; + this.filterTokens = filterTokens; + } + + private IEnumerable GetFilteredTokens() => + filterTokens(innerTokens.OfType()); + + public TToken this[int index] + { + get => GetFilteredTokens().ElementAt(index); + set + { + index = innerTokens.IndexOf(this[index]); + innerTokens[index] = value; + } + } + + public int Count => GetFilteredTokens().Count(); + + public bool IsReadOnly => false; + + public void Add(TToken item) => ThrowAddRemoveNotSupported(); + + public void Clear() => ThrowAddRemoveNotSupported(); + + public bool Contains(TToken item) => + GetFilteredTokens().Contains(item); + + public void CopyTo(TToken[] array, int arrayIndex) => ThrowAddRemoveNotSupported(); + + public IEnumerator GetEnumerator() => GetFilteredTokens().GetEnumerator(); + + public int IndexOf(TToken item) => GetFilteredTokens().ToList().IndexOf(item); + + public void Insert(int index, TToken item) => ThrowAddRemoveNotSupported(); + + public bool Remove(TToken item) + { + ThrowAddRemoveNotSupported(); + return false; + } + + public void RemoveAt(int index) => ThrowAddRemoveNotSupported(); + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private void ThrowAddRemoveNotSupported() + { + throw new NotSupportedException("Items may not be added or removed from the list."); + } + } +} diff --git a/src/DockerfileModel/DockerfileModel/Tokens/VariableRefToken.cs b/src/DockerfileModel/DockerfileModel/Tokens/VariableRefToken.cs index ecdbb99..f3a7022 100644 --- a/src/DockerfileModel/DockerfileModel/Tokens/VariableRefToken.cs +++ b/src/DockerfileModel/DockerfileModel/Tokens/VariableRefToken.cs @@ -20,11 +20,6 @@ public class VariableRefToken : AggregateToken .Select(modifier => Sprache.Parse.String(modifier).Text()) .ToArray(); - private VariableRefToken(string text, char escapeChar, CreateTokenParserDelegate createModifierValueToken) - : base(text, GetInnerParser(escapeChar, createModifierValueToken)) - { - } - internal VariableRefToken(IEnumerable tokens) : base(tokens) { } @@ -235,8 +230,8 @@ public static VariableRefToken Create(string variableName, string modifier, stri public static VariableRefToken Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) => - new VariableRefToken(text, escapeChar, (char escapeChar, IEnumerable excludedChars) => - LiteralString(escapeChar, excludedChars)); + new VariableRefToken(GetTokens(text, GetInnerParser(escapeChar, (char escapeChar, IEnumerable excludedChars) => + LiteralString(escapeChar, excludedChars)))); /// /// Parses a variable reference. diff --git a/src/DockerfileModel/DockerfileModel/Whitespace.cs b/src/DockerfileModel/DockerfileModel/Whitespace.cs index 10fcea3..07f5f23 100644 --- a/src/DockerfileModel/DockerfileModel/Whitespace.cs +++ b/src/DockerfileModel/DockerfileModel/Whitespace.cs @@ -9,7 +9,7 @@ namespace DockerfileModel public class Whitespace : DockerfileConstruct { private Whitespace(string value) - : base(value, GetParser()) + : base(GetTokens(value, GetParser())) { }