diff --git a/src/Bicep.Cli.IntegrationTests/HelpTests.cs b/src/Bicep.Cli.IntegrationTests/HelpTests.cs new file mode 100644 index 00000000000..1ccffd7ba3d --- /dev/null +++ b/src/Bicep.Cli.IntegrationTests/HelpTests.cs @@ -0,0 +1,377 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.UnitTests.Assertions; +using FluentAssertions; +using FluentAssertions.Execution; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bicep.Cli.IntegrationTests +{ + [TestClass] + public class HelpTests : TestBase + { + [TestMethod] + public async Task Root_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "build", + "build-params", + "decompile", + "decompile-params", + "format", + "generate-params", + "lint", + "publish", + "restore", + "test", + "--version", + "--license", + "--third-party-notices"); + } + } + + [TestMethod] + public async Task Build_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("build", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "build", + "Builds a .bicep file.", + "--stdout", + "--no-restore", + "--outdir", + "--outfile", + "--pattern", + "--diagnostics-format"); + } + } + + [TestMethod] + public async Task Test_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("test", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "test", + "Runs tests in a .bicep file.", + "--no-restore", + "--diagnostics-format"); + } + } + + [TestMethod] + public async Task BuildParams_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("build-params", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "build-params", + "Builds a .json file from a .bicepparam file.", + "--stdout", + "--no-restore", + "--outdir", + "--outfile", + "--pattern", + "--bicep-file", + "--diagnostics-format"); + } + } + + [TestMethod] + public async Task Format_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("format", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "format", + "Formats a .bicep file.", + "--stdout", + "--outdir", + "--outfile", + "--pattern", + "--newline-kind", + "--indent-kind", + "--indent-size", + "--insert-final-newline"); + } + } + + [TestMethod] + public async Task GenerateParams_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("generate-params", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "generate-params", + "Builds parameters file", + "--stdout", + "--no-restore", + "--outdir", + "--outfile", + "--output-format", + "--include-params"); + } + } + + [TestMethod] + public async Task Decompile_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("decompile", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "decompile", + "Attempts to decompile a template .json file to .bicep.", + "--stdout", + "--force", + "--outdir", + "--outfile"); + } + } + + [TestMethod] + public async Task DecompileParams_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("decompile-params", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "decompile-params", + "Attempts to decompile a parameters .json file to .bicepparam.", + "--stdout", + "--force", + "--outdir", + "--outfile", + "--bicep-file"); + } + } + + [TestMethod] + public async Task Publish_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("publish", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "publish", + "Publishes the .bicep file to the module registry.", + "--target", + "--documentation-uri", + "--no-restore", + "--force", + "--with-source"); + } + } + + [TestMethod] + public async Task PublishExtension_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("publish-extension", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "publish-extension", + "Publishes a Bicep extension to a registry.", + "--target", + "--force", + "--bin-linux-x64", + "--bin-win-x64"); + } + } + + [TestMethod] + public async Task Restore_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("restore", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "restore", + "Restores external modules", + "--pattern", + "--force"); + } + } + + [TestMethod] + public async Task Lint_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("lint", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "lint", + "Lints a .bicep file.", + "--pattern", + "--no-restore", + "--diagnostics-format"); + } + } + + [TestMethod] + public async Task JsonRpc_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("jsonrpc", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "jsonrpc", + "JSONRPC", + "--pipe", + "--socket", + "--stdio"); + } + } + + [TestMethod] + public async Task LocalDeploy_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("local-deploy", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "local-deploy", + "local deployment", + "--no-restore", + "--format"); + } + } + + [TestMethod] + public async Task Snapshot_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("snapshot", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "snapshot", + "deployment snapshot", + "--mode", + "--tenant-id", + "--subscription-id", + "--location", + "--resource-group", + "--deployment-name"); + } + } + + [TestMethod] + public async Task Deploy_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("deploy", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "deploy", + "Deploys infrastructure", + "--no-restore", + "--format"); + } + } + + [TestMethod] + public async Task WhatIf_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("what-if", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "what-if", + "Previews the changes", + "--no-restore"); + } + } + + [TestMethod] + public async Task Teardown_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("teardown", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "teardown", + "Tears down resources", + "--no-restore"); + } + } + + [TestMethod] + public async Task Console_Help_ShouldSucceed_WithExpectedOutput() + { + var (output, error, result) = await Bicep("console", "--help"); + + using (new AssertionScope()) + { + result.Should().Be(0); + error.Should().BeEmpty(); + output.Should().ContainAll( + "console", + "Opens an interactive Bicep console."); + } + } + } +} diff --git a/src/Bicep.Cli.IntegrationTests/InvalidArgsTests.cs b/src/Bicep.Cli.IntegrationTests/InvalidArgsTests.cs new file mode 100644 index 00000000000..e66c166ec20 --- /dev/null +++ b/src/Bicep.Cli.IntegrationTests/InvalidArgsTests.cs @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using FluentAssertions; +using FluentAssertions.Execution; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bicep.Cli.IntegrationTests +{ + [TestClass] + public class InvalidArgsTests : TestBase + { + [TestMethod] + public async Task Empty_args_should_fail_with_error() + { + var (output, error, result) = await Bicep(); + + using (new AssertionScope()) + { + result.Should().NotBe(0); + error.Should().NotBeEmpty(); + } + } + + [TestMethod] + public async Task Unknown_command_should_fail_with_error() + { + var (output, error, result) = await Bicep("notacommand"); + + using (new AssertionScope()) + { + result.Should().NotBe(0); + error.Should().NotBeEmpty(); + } + } + + [DataTestMethod] + // Missing input file (CommandLineException thrown from action) + [DataRow(new[] { "test" }, "The input file path was not specified")] + [DataRow(new[] { "decompile" }, "The input file path was not specified")] + [DataRow(new[] { "generate-params" }, "The input file path was not specified")] + [DataRow(new[] { "publish" }, "The input file path was not specified")] + // Missing required option + [DataRow(new[] { "publish", "file.bicep" }, "The target module was not specified.")] + // Either input file or --pattern required (thrown from InputOutputArgumentsResolver) + [DataRow(new[] { "build" }, "Either the input file path or the --pattern parameter must be specified")] + [DataRow(new[] { "build-params" }, "Either the input file path or the --pattern parameter must be specified")] + [DataRow(new[] { "restore" }, "Either the input file path or the --pattern parameter must be specified")] + [DataRow(new[] { "lint" }, "Either the input file path or the --pattern parameter must be specified")] + [DataRow(new[] { "format" }, "Either the input file path or the --pattern parameter must be specified")] + // Unrecognized options (rejected by System.CommandLine for commands with TreatUnmatchedTokensAsErrors = true) + [DataRow(new[] { "build", "--wibble" }, "Unrecognized parameter \"--wibble\"")] + [DataRow(new[] { "test", "--wibble" }, "Unrecognized parameter \"--wibble\"")] + [DataRow(new[] { "build-params", "--wibble" }, "Unrecognized parameter \"--wibble\"")] + [DataRow(new[] { "decompile", "--wibble" }, "Unrecognized parameter \"--wibble\"")] + [DataRow(new[] { "generate-params", "--wibble" }, "Unrecognized parameter \"--wibble\"")] + [DataRow(new[] { "restore", "--fake" }, "Unrecognized parameter \"--fake\"")] + [DataRow(new[] { "lint", "--fake" }, "Unrecognized parameter \"--fake\"")] + [DataRow(new[] { "--wiggle" }, "Unrecognized command or argument '--wiggle'.")] + public async Task Invalid_args_should_fail_with_expected_error(string[] args, string expectedError) + { + var (output, error, result) = await Bicep(args); + + using (new AssertionScope()) + { + result.Should().NotBe(0); + error.Should().Contain(expectedError); + } + } + + [TestMethod] + public async Task JsonRpc_with_pipe_and_socket_should_fail() + { + var (output, error, result) = await Bicep("jsonrpc", "--pipe", "foo", "--socket", "1234"); + + using (new AssertionScope()) + { + result.Should().NotBe(0); + error.Should().Contain("Only one of --pipe, --socket, or --stdio may be specified."); + } + } + + [TestMethod] + public async Task JsonRpc_with_pipe_and_stdio_should_fail() + { + var (output, error, result) = await Bicep("jsonrpc", "--pipe", "foo", "--stdio"); + + using (new AssertionScope()) + { + result.Should().NotBe(0); + error.Should().Contain("Only one of --pipe, --socket, or --stdio may be specified."); + } + } + + [TestMethod] + public async Task JsonRpc_with_socket_and_stdio_should_fail() + { + var (output, error, result) = await Bicep("jsonrpc", "--socket", "1234", "--stdio"); + + using (new AssertionScope()) + { + result.Should().NotBe(0); + error.Should().Contain("Only one of --pipe, --socket, or --stdio may be specified."); + } + } + + [TestMethod] + public async Task Publish_with_invalid_documentation_uri_should_fail() + { + var (output, error, result) = await Bicep("publish", "file.bicep", "--target", "br:example.azurecr.io/module:v1", "--documentation-uri", "not-a-uri"); + + using (new AssertionScope()) + { + result.Should().NotBe(0); + error.Should().Contain("The --documentation-uri should be a well formed uri string."); + } + } + + [TestMethod] + public async Task Publish_with_empty_documentation_uri_should_fail() + { + // --documentation-uri present but no value supplied → Tokens.Count == 0 + var (output, error, result) = await Bicep("publish", "file.bicep", "--target", "br:example.azurecr.io/module:v1", "--documentation-uri"); + + using (new AssertionScope()) + { + result.Should().NotBe(0); + error.Should().Contain("The --documentation-uri parameter expects an argument."); + } + } + + [TestMethod] + public async Task Publish_with_documentation_uri_specified_twice_should_fail() + { + // --documentation-uri supplied twice → Tokens.Count > 1 + var (output, error, result) = await Bicep("publish", "file.bicep", "--target", "br:example.azurecr.io/module:v1", "--documentation-uri", "https://example.com", "--documentation-uri", "https://other.com"); + + using (new AssertionScope()) + { + result.Should().NotBe(0); + error.Should().Contain("The --documentation-uri parameter cannot be specified more than once."); + } + } + + [TestMethod] + public async Task Format_with_stdout_and_outdir_should_fail() + { + var (output, error, result) = await Bicep("format", "--stdout", "--outdir", ".", "file.bicep"); + + using (new AssertionScope()) + { + result.Should().NotBe(0); + error.Should().Contain("The --outdir and --stdout parameters cannot both be used"); + } + } + + [TestMethod] + public async Task Format_with_outdir_and_outfile_should_fail() + { + var (output, error, result) = await Bicep("format", "--outdir", ".", "--outfile", "out.bicep", "file.bicep"); + + using (new AssertionScope()) + { + result.Should().NotBe(0); + error.Should().Contain("The --outdir and --outfile parameters cannot both be used"); + } + } + + [DataTestMethod] + // build: --pattern conflicts + [DataRow(new[] { "build", "--stdout", "--pattern", "*.bicep" }, "The --stdout parameter cannot be used with the --pattern parameter")] + [DataRow(new[] { "build", "--outfile", "foo", "--pattern", "*.bicep" }, "The --outfile parameter cannot be used with the --pattern parameter")] + // build: output option conflicts + [DataRow(new[] { "build", "--stdout", "--outdir", ".", "file.bicep" }, "The --outdir and --stdout parameters cannot both be used")] + [DataRow(new[] { "build", "--stdout", "--outfile", "foo", "file.bicep" }, "The --outfile and --stdout parameters cannot both be used")] + [DataRow(new[] { "build", "--outdir", ".", "--outfile", "foo", "file.bicep" }, "The --outdir and --outfile parameters cannot both be used")] + // build-params: --pattern conflicts + [DataRow(new[] { "build-params", "--bicep-file", "a.bicep", "--pattern", "*.bicepparam" }, "The --bicep-file parameter cannot be used with the --pattern parameter")] + [DataRow(new[] { "build-params", "--stdout", "--pattern", "*.bicepparam" }, "The --stdout parameter cannot be used with the --pattern parameter")] + [DataRow(new[] { "build-params", "--outfile", "foo", "--pattern", "*.bicepparam" }, "The --outfile parameter cannot be used with the --pattern parameter")] + // build-params: output option conflicts + [DataRow(new[] { "build-params", "--stdout", "--outdir", ".", "file.bicepparam" }, "The --outdir and --stdout parameters cannot both be used")] + [DataRow(new[] { "build-params", "--stdout", "--outfile", "foo", "file.bicepparam" }, "The --outfile and --stdout parameters cannot both be used")] + [DataRow(new[] { "build-params", "--outdir", ".", "--outfile", "foo", "file.bicepparam" }, "The --outdir and --outfile parameters cannot both be used")] + // decompile: output option conflicts + [DataRow(new[] { "decompile", "--stdout", "--outdir", ".", "file.json" }, "The --outdir and --stdout parameters cannot both be used")] + [DataRow(new[] { "decompile", "--stdout", "--outfile", "foo", "file.json" }, "The --outfile and --stdout parameters cannot both be used")] + [DataRow(new[] { "decompile", "--outdir", ".", "--outfile", "foo", "file.json" }, "The --outdir and --outfile parameters cannot both be used")] + // decompile-params: output option conflicts + [DataRow(new[] { "decompile-params", "--stdout", "--outdir", ".", "file.json" }, "The --outdir and --stdout parameters cannot both be used")] + [DataRow(new[] { "decompile-params", "--stdout", "--outfile", "foo", "file.json" }, "The --outfile and --stdout parameters cannot both be used")] + [DataRow(new[] { "decompile-params", "--outdir", ".", "--outfile", "foo", "file.json" }, "The --outdir and --outfile parameters cannot both be used")] + // format: --pattern conflicts + [DataRow(new[] { "format", "--stdout", "--pattern", "*.bicep" }, "The --stdout parameter cannot be used with the --pattern parameter")] + [DataRow(new[] { "format", "--outdir", ".", "--pattern", "*.bicep" }, "The --outdir parameter cannot be used with the --pattern parameter")] + [DataRow(new[] { "format", "--outfile", "foo", "--pattern", "*.bicep" }, "The --outfile parameter cannot be used with the --pattern parameter")] + // format: output option conflicts (--outfile + --stdout missing from old FormatCommand.cs) + [DataRow(new[] { "format", "--stdout", "--outfile", "foo", "file.bicep" }, "The --outfile and --stdout parameters cannot both be used")] + // generate-params: output option conflicts + [DataRow(new[] { "generate-params", "--stdout", "--outdir", ".", "file.bicep" }, "The --outdir and --stdout parameters cannot both be used")] + [DataRow(new[] { "generate-params", "--stdout", "--outfile", "foo", "file.bicep" }, "The --outfile and --stdout parameters cannot both be used")] + [DataRow(new[] { "generate-params", "--outdir", ".", "--outfile", "foo", "file.bicep" }, "The --outdir and --outfile parameters cannot both be used")] + public async Task Conflicting_output_options_should_fail_with_expected_error(string[] args, string expectedError) + { + var (output, error, result) = await Bicep(args); + + using (new AssertionScope()) + { + result.Should().NotBe(0); + error.Should().Contain(expectedError); + } + } + + [TestMethod] + public async Task PublishExtension_without_input_file_or_binaries_should_fail() + { + var (output, error, result) = await Bicep("publish-extension", "--target", "br:example.azurecr.io/ext:v1"); + + using (new AssertionScope()) + { + result.Should().NotBe(0); + error.Should().Contain("The input file path was not specified."); + } + } + + [TestMethod] + public async Task PublishExtension_without_target_should_fail() + { + var (output, error, result) = await Bicep("publish-extension", "index.json"); + + using (new AssertionScope()) + { + result.Should().NotBe(0); + error.Should().Contain("The target extension was not specified."); + } + } + } +} diff --git a/src/Bicep.Cli.IntegrationTests/RootCommandTests.cs b/src/Bicep.Cli.IntegrationTests/RootCommandTests.cs index 1fc1320216b..382aa138dea 100644 --- a/src/Bicep.Cli.IntegrationTests/RootCommandTests.cs +++ b/src/Bicep.Cli.IntegrationTests/RootCommandTests.cs @@ -19,10 +19,9 @@ public async Task Build_WithWrongArgs_ShouldFail_WithExpectedErrorMessage() using (new AssertionScope()) { result.Should().Be(1); - output.Should().BeEmpty(); error.Should().NotBeEmpty(); - error.Should().Contain($"Unrecognized arguments \"wrong fake broken\" specified. Use \"bicep --help\" to view available options."); + error.Should().ContainAll("wrong", "fake", "broken"); } } @@ -41,43 +40,6 @@ public async Task BicepVersionShouldPrintVersionInformation() } } - [TestMethod] - public async Task BicepHelpShouldPrintHelp() - { - var settings = new InvocationSettings() { FeatureOverrides = new(RegistryEnabled: true) }; - - var (output, error, result) = await Bicep(settings, "--help"); - - using (new AssertionScope()) - { - result.Should().Be(0); - error.Should().BeEmpty(); - - output.Should().NotBeEmpty(); - output.Should().ContainAll( - "build", - "[options]", - "", - ".bicep", - "Arguments:", - "Options:", - "--outdir", - "--outfile", - "--stdout", - "--diagnostics-format", - "--version", - "--help", - "information", - "version", - "bicep", - "usage", - "--license", - "--third-party-notices", - "license information", - "third-party notices"); - } - } - [TestMethod] public async Task BicepLicenseShouldPrintLicense() { @@ -138,11 +100,7 @@ public async Task BicepHelpShouldAlwaysIncludePublish() output.Should().ContainAll( "publish", "Publishes", - "registry", - "reference", - "azurecr.io", - "br", - "--target"); + "registry"); } } } diff --git a/src/Bicep.Cli.UnitTests/ArgumentParserTests.cs b/src/Bicep.Cli.UnitTests/ArgumentParserTests.cs deleted file mode 100644 index 93694e9b596..00000000000 --- a/src/Bicep.Cli.UnitTests/ArgumentParserTests.cs +++ /dev/null @@ -1,420 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -using System.IO.Abstractions; -using Bicep.Cli.Arguments; -using Bicep.Cli.Services; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Bicep.Cli.UnitTests -{ - [TestClass] - public class ArgumentParserTests - { - private static readonly IFileSystem FileSystem = new FileSystem(); - - [TestMethod] - public void Empty_parameters_should_return_null() - { - var arguments = ArgumentParser.TryParse([], FileSystem); - arguments.Should().BeNull(); - } - - [TestMethod] - public void Wrong_command_should_return_null() - { - var arguments = ArgumentParser.TryParse(["wrong"], FileSystem); - arguments.Should().BeNull(); - } - - [DataTestMethod] - // build - [DataRow(new[] { "build" }, "Either the input file path or the --pattern parameter must be specified")] - [DataRow(new[] { "build", "foo.bicep", "--pattern", "*.bicep" }, "The input file path and the --pattern parameter cannot both be specified")] - [DataRow(new[] { "build", "--pattern" }, "The --pattern parameter expects an argument")] - [DataRow(new[] { "build", "--pattern", "*.bicep", "--stdout" }, "The --stdout parameter cannot be used with the --pattern parameter")] - [DataRow(new[] { "build", "--pattern", "*.bicep", "--outfile", "foo" }, "The --outfile parameter cannot be used with the --pattern parameter")] - [DataRow(new[] { "build", "--stdout" }, "Either the input file path or the --pattern parameter must be specified")] - [DataRow(new[] { "build", "file1", "file2" }, "The input file path cannot be specified multiple times")] - [DataRow(new[] { "build", "--wibble" }, "Unrecognized parameter \"--wibble\"")] - [DataRow(new[] { "build", "--outdir" }, "The --outdir parameter expects an argument")] - [DataRow(new[] { "build", "--outdir", "dir1", "--outdir", "dir2" }, "The --outdir parameter cannot be specified twice")] - [DataRow(new[] { "build", "--outfile" }, "The --outfile parameter expects an argument")] - [DataRow(new[] { "build", "--outfile", "dir1", "--outfile", "dir2" }, "The --outfile parameter cannot be specified twice")] - [DataRow(new[] { "build", "--stdout", "--outfile", "dir1", "file1" }, "The --outfile and --stdout parameters cannot both be used")] - [DataRow(new[] { "build", "--stdout", "--outdir", "dir1", "file1" }, "The --outdir and --stdout parameters cannot both be used")] - [DataRow(new[] { "build", "--outfile", "dir1", "--outdir", "dir2", "file1" }, "The --outdir and --outfile parameters cannot both be used")] - // build-params - [DataRow(new[] { "build-params" }, "Either the input file path or the --pattern parameter must be specified")] - [DataRow(new[] { "build-params", "foo.bicepparam", "--pattern", "*.bicepparam" }, "The input file path and the --pattern parameter cannot both be specified")] - [DataRow(new[] { "build-params", "--pattern" }, "The --pattern parameter expects an argument")] - [DataRow(new[] { "build-params", "--pattern", "*.bicepparam", "--stdout" }, "The --stdout parameter cannot be used with the --pattern parameter")] - [DataRow(new[] { "build-params", "--pattern", "*.bicepparam", "--outfile", "foo" }, "The --outfile parameter cannot be used with the --pattern parameter")] - [DataRow(new[] { "build-params", "--stdout" }, "Either the input file path or the --pattern parameter must be specified")] - [DataRow(new[] { "build-params", "file1", "file2" }, "The input file path cannot be specified multiple times")] - [DataRow(new[] { "build-params", "--wibble" }, "Unrecognized parameter \"--wibble\"")] - [DataRow(new[] { "build-params", "--outdir" }, "The --outdir parameter expects an argument")] - [DataRow(new[] { "build-params", "--outdir", "dir1", "--outdir", "dir2" }, "The --outdir parameter cannot be specified twice")] - [DataRow(new[] { "build-params", "--outfile" }, "The --outfile parameter expects an argument")] - [DataRow(new[] { "build-params", "--outfile", "dir1", "--outfile", "dir2" }, "The --outfile parameter cannot be specified twice")] - [DataRow(new[] { "build-params", "--stdout", "--outfile", "dir1", "file1" }, "The --outfile and --stdout parameters cannot both be used")] - [DataRow(new[] { "build-params", "--stdout", "--outdir", "dir1", "file1" }, "The --outdir and --stdout parameters cannot both be used")] - [DataRow(new[] { "build-params", "--outfile", "dir1", "--outdir", "dir2", "file1" }, "The --outdir and --outfile parameters cannot both be used")] - // decompile - [DataRow(new[] { "decompile" }, "The input file path was not specified")] - [DataRow(new[] { "decompile", "--stdout" }, "The input file path was not specified")] - [DataRow(new[] { "decompile", "file1", "file2" }, "The input file path cannot be specified multiple times")] - [DataRow(new[] { "decompile", "--wibble" }, "Unrecognized parameter \"--wibble\"")] - [DataRow(new[] { "decompile", "--outdir" }, "The --outdir parameter expects an argument")] - [DataRow(new[] { "decompile", "--outdir", "dir1", "--outdir", "dir2" }, "The --outdir parameter cannot be specified twice")] - [DataRow(new[] { "decompile", "--outfile" }, "The --outfile parameter expects an argument")] - [DataRow(new[] { "decompile", "--outfile", "dir1", "--outfile", "dir2" }, "The --outfile parameter cannot be specified twice")] - [DataRow(new[] { "decompile", "--stdout", "--outfile", "dir1", "file1" }, "The --outfile and --stdout parameters cannot both be used")] - [DataRow(new[] { "decompile", "--stdout", "--outdir", "dir1", "file1" }, "The --outdir and --stdout parameters cannot both be used")] - [DataRow(new[] { "decompile", "--outfile", "dir1", "--outdir", "dir2", "file1" }, "The --outdir and --outfile parameters cannot both be used")] - // publish - [DataRow(new[] { "publish" }, "The input file path was not specified.")] - [DataRow(new[] { "publish", "--fake" }, "Unrecognized parameter \"--fake\"")] - [DataRow(new[] { "publish", "--target" }, "The --target parameter expects an argument.")] - [DataRow(new[] { "publish", "--target", "foo", "--target" }, "The --target parameter expects an argument.")] - [DataRow(new[] { "publish", "--target", "foo", "--target", "foo2" }, "The --target parameter cannot be specified twice.")] - [DataRow(new[] { "publish", "file" }, "The target module was not specified.")] - [DataRow(new[] { "publish", "file", "file2" }, "The input file path cannot be specified multiple times.")] - // restore - [DataRow(new[] { "restore" }, "Either the input file path or the --pattern parameter must be specified")] - [DataRow(new[] { "restore", "foo.bicep", "--pattern", "*.bicep" }, "The input file path and the --pattern parameter cannot both be specified")] - [DataRow(new[] { "restore", "--pattern" }, "The --pattern parameter expects an argument")] - [DataRow(new[] { "restore", "--fake" }, "Unrecognized parameter \"--fake\"")] - [DataRow(new[] { "restore", "file1", "file2" }, "The input file path cannot be specified multiple times")] - // lint - [DataRow(new[] { "lint" }, "Either the input file path or the --pattern parameter must be specified")] - [DataRow(new[] { "lint", "foo.bicep", "--pattern", "*.bicep" }, "The input file path and the --pattern parameter cannot both be specified")] - [DataRow(new[] { "lint", "--pattern" }, "The --pattern parameter expects an argument")] - [DataRow(new[] { "lint", "--fake" }, "Unrecognized parameter \"--fake\"")] - // format - [DataRow(new[] { "format" }, "Either the input file path or the --pattern parameter must be specified")] - [DataRow(new[] { "format", "foo.bicep", "--pattern", "*.bicep" }, "The input file path and the --pattern parameter cannot both be specified")] - [DataRow(new[] { "format", "--pattern" }, "The --pattern parameter expects an argument")] - [DataRow(new[] { "format", "--fake" }, "Unrecognized parameter \"--fake\"")] - public void Invalid_args_trigger_validation_exceptions(string[] parameters, string expectedException) - { - Action parseFunc = () => ArgumentParser.TryParse(parameters, FileSystem); - - parseFunc.Should().Throw().WithMessage(expectedException); - } - - [TestMethod] - public void BuildOneFile_ShouldReturnOneFile() - { - var arguments = ArgumentParser.TryParse(["build", "file1"], FileSystem); - var buildArguments = (BuildArguments?)arguments; - - // using classic assert so R# understands the value is not null - Assert.IsNotNull(arguments); - buildArguments!.InputFile.Should().Be("file1"); - buildArguments!.OutputToStdOut.Should().BeFalse(); - buildArguments!.OutputDir.Should().BeNull(); - buildArguments!.OutputFile.Should().BeNull(); - buildArguments!.NoRestore.Should().BeFalse(); - } - - [TestMethod] - public void BuildOneFileStdOut_ShouldReturnOneFileAndStdout() - { - var arguments = ArgumentParser.TryParse(["build", "--stdout", "file1"], FileSystem); - var buildArguments = (BuildArguments?)arguments; - - // using classic assert so R# understands the value is not null - Assert.IsNotNull(arguments); - buildArguments!.InputFile.Should().Be("file1"); - buildArguments!.OutputToStdOut.Should().BeTrue(); - buildArguments!.OutputDir.Should().BeNull(); - buildArguments!.OutputFile.Should().BeNull(); - buildArguments!.NoRestore.Should().BeFalse(); - } - - [TestMethod] - public void BuildOneFileStdOut_and_no_restore_ShouldReturnOneFileAndStdout() - { - var arguments = ArgumentParser.TryParse(["build", "--stdout", "--no-restore", "file1"], FileSystem); - var buildArguments = (BuildArguments?)arguments; - - // using classic assert so R# understands the value is not null - Assert.IsNotNull(arguments); - buildArguments!.InputFile.Should().Be("file1"); - buildArguments!.OutputToStdOut.Should().BeTrue(); - buildArguments!.OutputDir.Should().BeNull(); - buildArguments!.OutputFile.Should().BeNull(); - buildArguments!.NoRestore.Should().BeTrue(); - } - - [TestMethod] - public void BuildOneFileStdOutAllCaps_ShouldReturnOneFileAndStdout() - { - var arguments = ArgumentParser.TryParse(["build", "--STDOUT", "file1"], FileSystem); - var buildArguments = (BuildArguments?)arguments; - - // using classic assert so R# understands the value is not null - Assert.IsNotNull(arguments); - buildArguments!.InputFile.Should().Be("file1"); - buildArguments!.OutputToStdOut.Should().BeTrue(); - buildArguments!.OutputDir.Should().BeNull(); - buildArguments!.OutputFile.Should().BeNull(); - buildArguments!.NoRestore.Should().BeFalse(); - } - - [TestMethod] - public void Build_with_outputdir_parameter_should_parse_correctly() - { - // Use relative . to ensure directory exists else the parser will throw. - var arguments = ArgumentParser.TryParse(["build", "--outdir", ".", "file1"], FileSystem); - var buildArguments = (BuildArguments?)arguments; - - // using classic assert so R# understands the value is not null - Assert.IsNotNull(arguments); - buildArguments!.InputFile.Should().Be("file1"); - buildArguments!.OutputToStdOut.Should().BeFalse(); - buildArguments!.OutputDir.Should().Be("."); - buildArguments!.OutputFile.Should().BeNull(); - buildArguments!.NoRestore.Should().BeFalse(); - } - - - [TestMethod] - public void Build_with_outputfile_parameter_should_parse_correctly() - { - var arguments = ArgumentParser.TryParse(["build", "--outfile", "jsonFile", "file1"], FileSystem); - var buildArguments = (BuildArguments?)arguments; - - // using classic assert so R# understands the value is not null - Assert.IsNotNull(arguments); - buildArguments!.InputFile.Should().Be("file1"); - buildArguments!.OutputToStdOut.Should().BeFalse(); - buildArguments!.OutputDir.Should().BeNull(); - buildArguments!.OutputFile.Should().Be("jsonFile"); - buildArguments!.NoRestore.Should().BeFalse(); - } - - [TestMethod] - public void Build_with_outputfile_and_no_restore_parameter_should_parse_correctly() - { - var arguments = ArgumentParser.TryParse(["build", "--outfile", "jsonFile", "file1", "--no-restore"], FileSystem); - var buildArguments = (BuildArguments?)arguments; - - // using classic assert so R# understands the value is not null - Assert.IsNotNull(arguments); - buildArguments!.InputFile.Should().Be("file1"); - buildArguments!.OutputToStdOut.Should().BeFalse(); - buildArguments!.OutputDir.Should().BeNull(); - buildArguments!.OutputFile.Should().Be("jsonFile"); - buildArguments!.NoRestore.Should().BeTrue(); - } - - [TestMethod] - public void License_argument_should_return_appropriate_RootArguments_instance() - { - var arguments = ArgumentParser.TryParse(["--license"], FileSystem); - - arguments.Should().BeOfType(); - if (arguments is RootArguments rootArguments) - { - rootArguments.PrintHelp.Should().BeFalse(); - rootArguments.PrintVersion.Should().BeFalse(); - rootArguments.PrintLicense.Should().BeTrue(); - rootArguments.PrintThirdPartyNotices.Should().BeFalse(); - } - } - - [TestMethod] - public void Third_party_notices_argument_should_return_appropriate_RootArguments_instance() - { - var arguments = ArgumentParser.TryParse(["--third-party-notices"], FileSystem); - - arguments.Should().BeOfType(); - if (arguments is RootArguments rootArguments) - { - rootArguments.PrintHelp.Should().BeFalse(); - rootArguments.PrintVersion.Should().BeFalse(); - rootArguments.PrintLicense.Should().BeFalse(); - rootArguments.PrintThirdPartyNotices.Should().BeTrue(); - } - } - - [TestMethod] - public void Version_argument_should_return_VersionArguments_instance() - { - var arguments = ArgumentParser.TryParse(["--version"], FileSystem); - - arguments.Should().BeOfType(); - if (arguments is RootArguments rootArguments) - { - rootArguments.PrintHelp.Should().BeFalse(); - rootArguments.PrintVersion.Should().BeTrue(); - rootArguments.PrintLicense.Should().BeFalse(); - rootArguments.PrintThirdPartyNotices.Should().BeFalse(); - } - } - - [TestMethod] - public void Help_argument_should_return_HelpArguments_instance() - { - var arguments = ArgumentParser.TryParse(["--help"], FileSystem); - - arguments.Should().BeOfType(); - if (arguments is RootArguments rootArguments) - { - rootArguments.PrintHelp.Should().BeTrue(); - rootArguments.PrintVersion.Should().BeFalse(); - rootArguments.PrintLicense.Should().BeFalse(); - rootArguments.PrintThirdPartyNotices.Should().BeFalse(); - } - } - - [TestMethod] - public void Version_argument_should_return_VersionShortArguments_instance() - { - var arguments = ArgumentParser.TryParse(["-v"], FileSystem); - - arguments.Should().BeOfType(); - if (arguments is RootArguments rootArguments) - { - rootArguments.PrintHelp.Should().BeFalse(); - rootArguments.PrintVersion.Should().BeTrue(); - rootArguments.PrintLicense.Should().BeFalse(); - rootArguments.PrintThirdPartyNotices.Should().BeFalse(); - } - } - - [TestMethod] - public void Help_argument_should_return_HelpShortArguments_instance() - { - var arguments = ArgumentParser.TryParse(["-h"], FileSystem); - - arguments.Should().BeOfType(); - if (arguments is RootArguments rootArguments) - { - rootArguments.PrintHelp.Should().BeTrue(); - rootArguments.PrintVersion.Should().BeFalse(); - rootArguments.PrintLicense.Should().BeFalse(); - rootArguments.PrintThirdPartyNotices.Should().BeFalse(); - } - } - - [TestMethod] - public void DecompileOneFile_ShouldReturnOneFile() - { - var arguments = ArgumentParser.TryParse(["decompile", "file1"], FileSystem); - var buildOrDecompileArguments = (DecompileArguments?)arguments; - - // using classic assert so R# understands the value is not null - Assert.IsNotNull(arguments); - buildOrDecompileArguments!.InputFile.Should().Be("file1"); - buildOrDecompileArguments!.OutputToStdOut.Should().BeFalse(); - buildOrDecompileArguments!.OutputDir.Should().BeNull(); - buildOrDecompileArguments!.OutputFile.Should().BeNull(); - } - - [TestMethod] - public void DecompileOneFileStdOut_ShouldReturnOneFileAndStdout() - { - var arguments = ArgumentParser.TryParse(["decompile", "--stdout", "file1"], FileSystem); - var buildOrDecompileArguments = (DecompileArguments?)arguments; - - // using classic assert so R# understands the value is not null - Assert.IsNotNull(arguments); - buildOrDecompileArguments!.InputFile.Should().Be("file1"); - buildOrDecompileArguments!.OutputToStdOut.Should().BeTrue(); - buildOrDecompileArguments!.OutputDir.Should().BeNull(); - buildOrDecompileArguments!.OutputFile.Should().BeNull(); - } - - [TestMethod] - public void DecompileOneFileStdOutAllCaps_ShouldReturnOneFileAndStdout() - { - var arguments = ArgumentParser.TryParse(["decompile", "--STDOUT", "file1"], FileSystem); - var buildOrDecompileArguments = (DecompileArguments?)arguments; - - // using classic assert so R# understands the value is not null - Assert.IsNotNull(arguments); - buildOrDecompileArguments!.InputFile.Should().Be("file1"); - buildOrDecompileArguments!.OutputToStdOut.Should().BeTrue(); - buildOrDecompileArguments!.OutputDir.Should().BeNull(); - buildOrDecompileArguments!.OutputFile.Should().BeNull(); - } - - [TestMethod] - public void Decompile_with_outputdir_parameter_should_parse_correctly() - { - // Use relative . to ensure directory exists else the parser will throw. - var arguments = ArgumentParser.TryParse(["decompile", "--outdir", ".", "file1"], FileSystem); - var buildOrDecompileArguments = (DecompileArguments?)arguments; - - // using classic assert so R# understands the value is not null - Assert.IsNotNull(arguments); - buildOrDecompileArguments!.InputFile.Should().Be("file1"); - buildOrDecompileArguments!.OutputToStdOut.Should().BeFalse(); - buildOrDecompileArguments!.OutputDir.Should().Be("."); - buildOrDecompileArguments!.OutputFile.Should().BeNull(); - } - - [TestMethod] - public void Decompile_with_outputfile_parameter_should_parse_correctly() - { - var arguments = ArgumentParser.TryParse(["decompile", "--outfile", "jsonFile", "file1"], FileSystem); - var buildOrDecompileArguments = (DecompileArguments?)arguments; - - // using classic assert so R# understands the value is not null - Assert.IsNotNull(arguments); - buildOrDecompileArguments!.InputFile.Should().Be("file1"); - buildOrDecompileArguments!.OutputToStdOut.Should().BeFalse(); - buildOrDecompileArguments!.OutputDir.Should().BeNull(); - buildOrDecompileArguments!.OutputFile.Should().Be("jsonFile"); - } - - [TestMethod] - public void Publish_should_parse_correctly() - { - var arguments = ArgumentParser.TryParse(["publish", "file1", "--target", "target1"], FileSystem); - arguments.Should().BeOfType(); - var typed = (PublishArguments)arguments!; - - typed.InputFile.Should().Be("file1"); - typed.TargetModuleReference.Should().Be("target1"); - typed.NoRestore.Should().BeFalse(); - } - - [TestMethod] - public void Publish_with_no_restore_should_parse_correctly() - { - var arguments = ArgumentParser.TryParse(["publish", "file1", "--target", "target1", "--no-restore"], FileSystem); - arguments.Should().BeOfType(); - var typed = (PublishArguments)arguments!; - - typed.InputFile.Should().Be("file1"); - typed.TargetModuleReference.Should().Be("target1"); - typed.NoRestore.Should().BeTrue(); - } - - [TestMethod] - public void Restore__with_no_force_should_parse_correctly() - { - var arguments = ArgumentParser.TryParse(["restore", "file1"], FileSystem); - arguments.Should().BeOfType(); - var typed = (RestoreArguments)arguments!; - - typed.ForceModulesRestore.Should().Be(false); - typed.InputFile.Should().Be("file1"); - } - - [TestMethod] - public void Restore_with_force_should_parse_correctly() - { - var arguments = ArgumentParser.TryParse(["restore", "--force", "file1"], FileSystem); - arguments.Should().BeOfType(); - var typed = (RestoreArguments)arguments!; - - typed.ForceModulesRestore.Should().Be(true); - typed.InputFile.Should().Be("file1"); - } - } -} diff --git a/src/Bicep.Cli/Arguments/ArgumentsBase.cs b/src/Bicep.Cli/Arguments/ArgumentsBase.cs deleted file mode 100644 index 17ebc58fdcc..00000000000 --- a/src/Bicep.Cli/Arguments/ArgumentsBase.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Bicep.Cli.Arguments -{ - public abstract class ArgumentsBase - { - public string CommandName { get; } - - protected ArgumentsBase(string commandName) - { - CommandName = commandName; - } - } -} diff --git a/src/Bicep.Cli/Arguments/BuildArguments.cs b/src/Bicep.Cli/Arguments/BuildArguments.cs index 2e8927e12bb..dbfafc4f064 100644 --- a/src/Bicep.Cli/Arguments/BuildArguments.cs +++ b/src/Bicep.Cli/Arguments/BuildArguments.cs @@ -1,121 +1,19 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Immutable; -using Bicep.Cli.Helpers; using Bicep.Core; using Bicep.IO.Abstraction; namespace Bicep.Cli.Arguments; -public class BuildArguments : ArgumentsBase, IFilePatternInputOutputArguments +public record BuildArguments( + string? InputFile, + bool OutputToStdOut, + bool NoRestore, + string? OutputDir, + string? OutputFile, + string? FilePattern, + DiagnosticsFormat? DiagnosticsFormat) : IFilePatternInputOutputArguments { - public BuildArguments(string[] args) : base(Constants.Command.Build) - { - for (var i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case "--stdout": - OutputToStdOut = true; - break; - - case "--no-restore": - NoRestore = true; - break; - - case ArgumentConstants.OutDir: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.OutDir, OutputDir); - OutputDir = ArgumentHelper.GetValueWithValidation(ArgumentConstants.OutDir, args, i); - i++; - break; - - case ArgumentConstants.OutFile: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.OutFile, OutputFile); - OutputFile = ArgumentHelper.GetValueWithValidation(ArgumentConstants.OutFile, args, i); - i++; - break; - - case ArgumentConstants.DiagnosticsFormat: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.DiagnosticsFormat, DiagnosticsFormat); - DiagnosticsFormat = ArgumentHelper.ToDiagnosticsFormat(ArgumentHelper.GetValueWithValidation(ArgumentConstants.DiagnosticsFormat, args, i)); - i++; - break; - - case ArgumentConstants.FilePattern: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.FilePattern, FilePattern); - FilePattern = ArgumentHelper.GetValueWithValidation(ArgumentConstants.FilePattern, args, i); - i++; - break; - - default: - if (args[i].StartsWith("--")) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - if (InputFile is not null) - { - throw new CommandLineException($"The input file path cannot be specified multiple times"); - } - InputFile = args[i]; - break; - } - } - - if (InputFile is null && FilePattern is null) - { - throw new CommandLineException($"Either the input file path or the {ArgumentConstants.FilePattern} parameter must be specified"); - } - - if (FilePattern != null) - { - if (InputFile is not null) - { - throw new CommandLineException($"The input file path and the {ArgumentConstants.FilePattern} parameter cannot both be specified"); - } - - if (OutputToStdOut) - { - throw new CommandLineException($"The --stdout parameter cannot be used with the {ArgumentConstants.FilePattern} parameter"); - } - - if (OutputFile is not null) - { - throw new CommandLineException($"The {ArgumentConstants.OutFile} parameter cannot be used with the {ArgumentConstants.FilePattern} parameter"); - } - } - - if (OutputToStdOut && OutputDir is not null) - { - throw new CommandLineException($"The {ArgumentConstants.OutDir} and --stdout parameters cannot both be used"); - } - - if (OutputToStdOut && OutputFile is not null) - { - throw new CommandLineException($"The {ArgumentConstants.OutFile} and --stdout parameters cannot both be used"); - } - - if (OutputDir is not null && OutputFile is not null) - { - throw new CommandLineException($"The {ArgumentConstants.OutDir} and {ArgumentConstants.OutFile} parameters cannot both be used"); - } - - DiagnosticsFormat ??= Arguments.DiagnosticsFormat.Default; - } - public static Func OutputFileExtensionResolver => (_, _) => LanguageConstants.JsonFileExtension; - - public bool OutputToStdOut { get; } - - public string? InputFile { get; } - - public string? OutputDir { get; } - - public string? OutputFile { get; } - - public string? FilePattern { get; } - - public DiagnosticsFormat? DiagnosticsFormat { get; } - - public bool NoRestore { get; } } diff --git a/src/Bicep.Cli/Arguments/BuildParamsArguments.cs b/src/Bicep.Cli/Arguments/BuildParamsArguments.cs index 2a5546cf97e..b87d63e9c99 100644 --- a/src/Bicep.Cli/Arguments/BuildParamsArguments.cs +++ b/src/Bicep.Cli/Arguments/BuildParamsArguments.cs @@ -1,144 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Immutable; -using Bicep.Cli.Helpers; using Bicep.Core; using Bicep.IO.Abstraction; namespace Bicep.Cli.Arguments; -public class BuildParamsArguments : ArgumentsBase, IFilePatternInputOutputArguments +public record BuildParamsArguments( + string? InputFile, + bool OutputToStdOut, + bool NoRestore, + string? OutputDir, + string? OutputFile, + string? FilePattern, + string? BicepFile, + DiagnosticsFormat? DiagnosticsFormat) : IFilePatternInputOutputArguments { - public BuildParamsArguments(string[] args) : base(Constants.Command.BuildParams) - { - for (var i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case "--bicep-file": - if (args.Length == i + 1) - { - throw new CommandLineException($"The --bicep-file parameter expects an argument"); - } - if (BicepFile is not null) - { - throw new CommandLineException($"The --bicep-file parameter cannot be specified twice"); - } - BicepFile = args[i + 1]; - i++; - break; - - case ArgumentConstants.OutDir: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.OutDir, OutputDir); - OutputDir = ArgumentHelper.GetValueWithValidation(ArgumentConstants.OutDir, args, i); - i++; - break; - - case ArgumentConstants.OutFile: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.OutFile, OutputFile); - OutputFile = ArgumentHelper.GetValueWithValidation(ArgumentConstants.OutFile, args, i); - i++; - break; - - case ArgumentConstants.DiagnosticsFormat: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.DiagnosticsFormat, DiagnosticsFormat); - DiagnosticsFormat = ArgumentHelper.ToDiagnosticsFormat(ArgumentHelper.GetValueWithValidation(ArgumentConstants.DiagnosticsFormat, args, i)); - i++; - break; - - case ArgumentConstants.FilePattern: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.FilePattern, FilePattern); - FilePattern = ArgumentHelper.GetValueWithValidation(ArgumentConstants.FilePattern, args, i); - i++; - break; - - case "--stdout": - OutputToStdOut = true; - break; - - case "--no-restore": - NoRestore = true; - break; - - default: - if (args[i].StartsWith("--")) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - if (InputFile is not null) - { - throw new CommandLineException($"The input file path cannot be specified multiple times"); - } - InputFile = args[i]; - break; - } - } - - if (InputFile is null && FilePattern is null) - { - throw new CommandLineException($"Either the input file path or the {ArgumentConstants.FilePattern} parameter must be specified"); - } - - if (FilePattern != null) - { - if (InputFile is not null) - { - throw new CommandLineException($"The input file path and the {ArgumentConstants.FilePattern} parameter cannot both be specified"); - } - - if (BicepFile is not null) - { - throw new CommandLineException($"The --bicep-file parameter cannot be used with the {ArgumentConstants.FilePattern} parameter"); - } - - if (OutputToStdOut) - { - throw new CommandLineException($"The --stdout parameter cannot be used with the {ArgumentConstants.FilePattern} parameter"); - } - - if (OutputFile is not null) - { - throw new CommandLineException($"The {ArgumentConstants.OutFile} parameter cannot be used with the {ArgumentConstants.FilePattern} parameter"); - } - } - - if (OutputToStdOut && OutputDir is not null) - { - throw new CommandLineException($"The --outdir and --stdout parameters cannot both be used"); - } - - if (OutputToStdOut && OutputFile is not null) - { - throw new CommandLineException($"The --outfile and --stdout parameters cannot both be used"); - } - - if (OutputDir is not null && OutputFile is not null) - { - throw new CommandLineException($"The --outdir and --outfile parameters cannot both be used"); - } - - if (DiagnosticsFormat is null) - { - DiagnosticsFormat = Arguments.DiagnosticsFormat.Default; - } - } - public static Func OutputFileExtensionResolver => (_, _) => LanguageConstants.JsonFileExtension; - - public bool OutputToStdOut { get; } - - public string? InputFile { get; } - - public string? BicepFile { get; } - - public string? OutputDir { get; } - - public string? OutputFile { get; } - - public string? FilePattern { get; } - - public DiagnosticsFormat? DiagnosticsFormat { get; } - - public bool NoRestore { get; } } diff --git a/src/Bicep.Cli/Arguments/ConsoleArguments.cs b/src/Bicep.Cli/Arguments/ConsoleArguments.cs index d6dfe50da2b..e24cdecb3e0 100644 --- a/src/Bicep.Cli/Arguments/ConsoleArguments.cs +++ b/src/Bicep.Cli/Arguments/ConsoleArguments.cs @@ -3,10 +3,4 @@ namespace Bicep.Cli.Arguments; -public class ConsoleArguments : ArgumentsBase -{ - public ConsoleArguments(string[] args) : base(Constants.Command.Console) - { - // Currently no options. Future flags (e.g. --subscription, --resource-group) can be added. - } -} +public record ConsoleArguments(); diff --git a/src/Bicep.Cli/Arguments/DecompileArguments.cs b/src/Bicep.Cli/Arguments/DecompileArguments.cs index 7a53feb698d..d0ec2f6de2c 100644 --- a/src/Bicep.Cli/Arguments/DecompileArguments.cs +++ b/src/Bicep.Cli/Arguments/DecompileArguments.cs @@ -1,81 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Bicep.Cli.Helpers; using Bicep.IO.Abstraction; using LanguageConstants = Bicep.Core.LanguageConstants; -namespace Bicep.Cli.Arguments -{ - public class DecompileArguments : ArgumentsBase, IInputOutputArguments - { - public DecompileArguments(string[] args) : base(Constants.Command.Decompile) - { - for (var i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case "--stdout": - OutputToStdOut = true; - break; - case "--force": - AllowOverwrite = true; - break; - case ArgumentConstants.OutDir: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.OutDir, OutputDir); - OutputDir = ArgumentHelper.GetValueWithValidation(ArgumentConstants.OutDir, args, i); - i++; - break; - - case ArgumentConstants.OutFile: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.OutFile, OutputFile); - OutputFile = ArgumentHelper.GetValueWithValidation(ArgumentConstants.OutFile, args, i); - i++; - break; - default: - if (args[i].StartsWith("--")) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - if (InputFile is not null) - { - throw new CommandLineException($"The input file path cannot be specified multiple times"); - } - InputFile = args[i]; - break; - } - } - - if (InputFile is null) - { - throw new CommandLineException($"The input file path was not specified"); - } - - if (OutputToStdOut && OutputDir is not null) - { - throw new CommandLineException($"The --outdir and --stdout parameters cannot both be used"); - } - if (OutputToStdOut && OutputFile is not null) - { - throw new CommandLineException($"The --outfile and --stdout parameters cannot both be used"); - } - - if (OutputDir is not null && OutputFile is not null) - { - throw new CommandLineException($"The --outdir and --outfile parameters cannot both be used"); - } - } - - public static Func OutputFileExtensionResolver { get; } = (_, _) => LanguageConstants.LanguageFileExtension; +namespace Bicep.Cli.Arguments; - public bool OutputToStdOut { get; } - - public string InputFile { get; } - - public string? OutputDir { get; } - - public string? OutputFile { get; } - - public bool AllowOverwrite { get; } - } +public record DecompileArguments( + string InputFile, + bool OutputToStdOut, + bool AllowOverwrite, + string? OutputDir, + string? OutputFile) : IInputOutputArguments +{ + public static Func OutputFileExtensionResolver { get; } = (_, _) => LanguageConstants.LanguageFileExtension; } diff --git a/src/Bicep.Cli/Arguments/DecompileParamsArguments.cs b/src/Bicep.Cli/Arguments/DecompileParamsArguments.cs index e2b5f461be5..25664fbba82 100644 --- a/src/Bicep.Cli/Arguments/DecompileParamsArguments.cs +++ b/src/Bicep.Cli/Arguments/DecompileParamsArguments.cs @@ -1,96 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Bicep.Cli.Helpers; using Bicep.IO.Abstraction; using LanguageConstants = Bicep.Core.LanguageConstants; -namespace Bicep.Cli.Arguments -{ - public class DecompileParamsArguments : ArgumentsBase, IInputOutputArguments - { - public DecompileParamsArguments(string[] args) : base(Constants.Command.DecompileParams) - { - for (var i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case "--stdout": - OutputToStdOut = true; - break; - case "--force": - AllowOverwrite = true; - break; - case ArgumentConstants.OutDir: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.OutDir, OutputDir); - OutputDir = ArgumentHelper.GetValueWithValidation(ArgumentConstants.OutDir, args, i); - i++; - break; - - case ArgumentConstants.OutFile: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.OutFile, OutputFile); - OutputFile = ArgumentHelper.GetValueWithValidation(ArgumentConstants.OutFile, args, i); - i++; - break; - case "--bicep-file": - if (args.Length == i + 1) - { - throw new CommandLineException($"The --bicep-file parameter expects an argument"); - } - if (BicepFilePath is not null) - { - throw new CommandLineException($"The --bicep-file parameter cannot be specified twice"); - } - BicepFilePath = args[i + 1]; - i++; - break; - - default: - if (args[i].StartsWith("--")) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - if (InputFile is not null) - { - throw new CommandLineException($"The input file path cannot be specified multiple times"); - } - InputFile = args[i]; - break; - } - } - - if (InputFile is null) - { - throw new CommandLineException($"The input file path was not specified"); - } - - if (OutputToStdOut && OutputDir is not null) - { - throw new CommandLineException($"The --outdir and --stdout parameters cannot both be used"); - } - if (OutputToStdOut && OutputFile is not null) - { - throw new CommandLineException($"The --outfile and --stdout parameters cannot both be used"); - } - - if (OutputDir is not null && OutputFile is not null) - { - throw new CommandLineException($"The --outdir and --outfile parameters cannot both be used"); - } - } - - public static Func OutputFileExtensionResolver { get; } = (_, _) => LanguageConstants.ParamsFileExtension; +namespace Bicep.Cli.Arguments; - public bool OutputToStdOut { get; } - - public string InputFile { get; } - - public string? OutputDir { get; } - - public string? OutputFile { get; } - - public bool AllowOverwrite { get; } - - public string? BicepFilePath { get; } - } +public record DecompileParamsArguments( + string InputFile, + bool OutputToStdOut, + bool AllowOverwrite, + string? OutputDir, + string? OutputFile, + string? BicepFilePath) : IInputOutputArguments +{ + public static Func OutputFileExtensionResolver { get; } = (_, _) => LanguageConstants.ParamsFileExtension; } diff --git a/src/Bicep.Cli/Arguments/DeployArguments.cs b/src/Bicep.Cli/Arguments/DeployArguments.cs index e9378582d4b..78042d9ae38 100644 --- a/src/Bicep.Cli/Arguments/DeployArguments.cs +++ b/src/Bicep.Cli/Arguments/DeployArguments.cs @@ -1,30 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Immutable; using Bicep.Cli.Helpers; namespace Bicep.Cli.Arguments; -public class DeployArguments : DeployArgumentsBase -{ - public DeployArguments(string[] args) : base(args, Constants.Command.Deploy) - { - } - - protected override void ParseAdditionalArgument(string[] args, ref int i) - { - switch (args[i].ToLowerInvariant()) - { - case ArgumentConstants.OutputFormat: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.OutputFormat, OutputFormat); - OutputFormat = ArgumentHelper.GetEnumValueWithValidation(ArgumentConstants.OutputFormat, args, i); - i++; - break; - default: - base.ParseAdditionalArgument(args, ref i); - break; - } - } - - public DeploymentOutputFormat? OutputFormat { get; private set; } -} +public record DeployArguments( + string InputFile, + bool NoRestore, + ImmutableDictionary AdditionalArguments, + DeploymentOutputFormat? OutputFormat) : DeployArgumentsBase(InputFile, NoRestore, AdditionalArguments); \ No newline at end of file diff --git a/src/Bicep.Cli/Arguments/DeployArgumentsBase.cs b/src/Bicep.Cli/Arguments/DeployArgumentsBase.cs index b199cb20486..b3f180725c0 100644 --- a/src/Bicep.Cli/Arguments/DeployArgumentsBase.cs +++ b/src/Bicep.Cli/Arguments/DeployArgumentsBase.cs @@ -1,61 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Bicep.Cli.Arguments; - -public abstract class DeployArgumentsBase : ArgumentsBase, IInputArguments -{ - protected virtual void ParseAdditionalArgument(string[] args, ref int i) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - - public DeployArgumentsBase(string[] args, string commandName) : base(commandName) - { - for (var i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case "--no-restore": - NoRestore = true; - break; - - case { } when args[i].StartsWith(ArgumentConstants.CliArgPrefix): - var key = args[i][ArgumentConstants.CliArgPrefix.Length..]; - - if (AdditionalArguments.ContainsKey(key)) - { - throw new CommandLineException($"Parameter \"{args[i]}\" cannot be specified multiple times."); - } +using System.Collections.Immutable; - AdditionalArguments[key] = args[i + 1]; - i++; - break; - - default: - if (args[i].StartsWith("--")) - { - ParseAdditionalArgument(args, ref i); - break; - } - if (InputFile is not null) - { - throw new CommandLineException($"The parameters file path cannot be specified multiple times"); - } - InputFile = args[i]; - break; - } - } - - if (InputFile is null) - { - throw new CommandLineException($"The parameters file path was not specified"); - } - } - - public string InputFile { get; } - - public bool NoRestore { get; } +namespace Bicep.Cli.Arguments; - public Dictionary AdditionalArguments { get; } = []; -} +public abstract record DeployArgumentsBase( + string InputFile, + bool NoRestore, + ImmutableDictionary AdditionalArguments) : IInputArguments; diff --git a/src/Bicep.Cli/Arguments/DiagnosticsFormat.cs b/src/Bicep.Cli/Arguments/DiagnosticsFormat.cs index 86c6b4182d7..6efc688205c 100644 --- a/src/Bicep.Cli/Arguments/DiagnosticsFormat.cs +++ b/src/Bicep.Cli/Arguments/DiagnosticsFormat.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Bicep.Cli.Arguments +namespace Bicep.Cli.Arguments; + +public enum DiagnosticsFormat { - public enum DiagnosticsFormat - { - Default, - Sarif - } + Default, + Sarif } diff --git a/src/Bicep.Cli/Arguments/FormatArguments.cs b/src/Bicep.Cli/Arguments/FormatArguments.cs index a743c403dce..d8ff54db3d9 100644 --- a/src/Bicep.Cli/Arguments/FormatArguments.cs +++ b/src/Bicep.Cli/Arguments/FormatArguments.cs @@ -1,196 +1,22 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Immutable; -using System.IO.Abstractions; -using Bicep.Cli.Extensions; -using Bicep.Cli.Helpers; using Bicep.Core.PrettyPrintV2; using Bicep.IO.Abstraction; namespace Bicep.Cli.Arguments; -public class FormatArguments : ArgumentsBase, IFilePatternInputOutputArguments +public record FormatArguments( + bool OutputToStdOut, + string? InputFile, + string? OutputDir, + string? OutputFile, + string? FilePattern, + NewlineKind? NewlineKind, + IndentKind? IndentKind, + int? IndentSize, + bool? InsertFinalNewline) : IFilePatternInputOutputArguments { - public FormatArguments(string[] args) : base(Constants.Command.Format) - { - for (var i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case "--stdout": - OutputToStdOut = true; - break; - - case ArgumentConstants.OutDir: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.OutDir, OutputDir); - OutputDir = ArgumentHelper.GetValueWithValidation(ArgumentConstants.OutDir, args, i); - i++; - break; - - case ArgumentConstants.OutFile: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.OutFile, OutputFile); - OutputFile = ArgumentHelper.GetValueWithValidation(ArgumentConstants.OutFile, args, i); - i++; - break; - - case ArgumentConstants.FilePattern: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.FilePattern, FilePattern); - FilePattern = ArgumentHelper.GetValueWithValidation(ArgumentConstants.FilePattern, args, i); - i++; - break; - - case "--newline-kind": - if (args.Length == i + 1) - { - throw new CommandLineException($"The --newline-kind parameter expects an argument"); - } - if (NewlineKind is not null) - { - throw new CommandLineException($"The --newline-kind parameter cannot be specified twice"); - } - if (!Enum.TryParse(args[i + 1], true, out var newline) || !Enum.IsDefined(newline)) - { - throw new CommandLineException($"The --newline-kind parameter only accepts these values: {string.Join(" | ", Enum.GetNames(typeof(NewlineKind)))}"); - } - NewlineKind = newline; - i++; - break; - - case "--indent-kind": - if (args.Length == i + 1) - { - throw new CommandLineException($"The --indent-kind parameter expects an argument"); - } - if (IndentKind is not null) - { - throw new CommandLineException($"The --indent-kind parameter cannot be specified twice"); - } - if (!Enum.TryParse(args[i + 1], true, out var indentKind) || !Enum.IsDefined(indentKind)) - { - throw new CommandLineException($"The --indent-kind parameter only accepts these values: {string.Join(" | ", Enum.GetNames(typeof(IndentKind)))}"); - } - IndentKind = indentKind; - i++; - break; - - case "--indent-size": - if (args.Length == i + 1) - { - throw new CommandLineException($"The --indent-size parameter expects an argument"); - } - if (IndentSize is not null) - { - throw new CommandLineException($"The --indent-size parameter cannot be specified twice"); - } - if (!int.TryParse(args[i + 1], out var indentSize)) - { - throw new CommandLineException($"The --indent-size parameter only accepts integer values"); - } - IndentSize = indentSize; - i++; - break; - - case "--insert-final-newline": - if (InsertFinalNewline is not null) - { - throw new CommandLineException($"The --insert-final-newline parameter cannot be specified twice"); - } - - if (args.Length == i + 1) - { - InsertFinalNewline = true; - break; - } - - if (bool.TryParse(args[i + 1], out var insertFinalNewline)) - { - InsertFinalNewline = insertFinalNewline; - i++; - } - else - { - // Either "true" or "false" is not supplied after "--insert-final-newline", or the value is not a valid boolean. - // Treat it as only "--insert-final-newline" is specified without a value, and default to true. - InsertFinalNewline = true; - } - break; - - default: - if (args[i].StartsWith("--")) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - if (InputFile is not null) - { - throw new CommandLineException($"The input file path cannot be specified multiple times"); - } - InputFile = args[i]; - break; - } - } - - if (InputFile is null && FilePattern is null) - { - throw new CommandLineException($"Either the input file path or the {ArgumentConstants.FilePattern} parameter must be specified"); - } - - if (FilePattern != null) - { - if (InputFile is not null) - { - throw new CommandLineException($"The input file path and the {ArgumentConstants.FilePattern} parameter cannot both be specified"); - } - - if (OutputToStdOut) - { - throw new CommandLineException($"The --stdout parameter cannot be used with the {ArgumentConstants.FilePattern} parameter"); - } - - if (OutputDir is not null) - { - throw new CommandLineException($"The {ArgumentConstants.OutDir} parameter cannot be used with the {ArgumentConstants.FilePattern} parameter"); - } - - if (OutputFile is not null) - { - throw new CommandLineException($"The {ArgumentConstants.OutFile} parameter cannot be used with the {ArgumentConstants.FilePattern} parameter"); - } - } - - if (OutputToStdOut && OutputDir is not null) - { - throw new CommandLineException($"The --outdir and --stdout parameters cannot both be used"); - } - - if (OutputToStdOut && OutputFile is not null) - { - throw new CommandLineException($"The --outfile and --stdout parameters cannot both be used"); - } - - if (OutputDir is not null && OutputFile is not null) - { - throw new CommandLineException($"The --outdir and --outfile parameters cannot both be used"); - } - } - - public static Func OutputFileExtensionResolver { get; } = (_, inputUri) => inputUri.GetExtension().ToString(); - - public bool OutputToStdOut { get; } - - public string? InputFile { get; } - - public string? OutputDir { get; } - - public string? OutputFile { get; } - - public string? FilePattern { get; } - - public NewlineKind? NewlineKind { get; } - - public IndentKind? IndentKind { get; } - - public int? IndentSize { get; } - - public bool? InsertFinalNewline { get; } + public static Func OutputFileExtensionResolver { get; } = + (_, inputUri) => inputUri.GetExtension().ToString(); } diff --git a/src/Bicep.Cli/Arguments/GenerateParametersFileArguments.cs b/src/Bicep.Cli/Arguments/GenerateParametersFileArguments.cs index 2991db5eb0f..b1b018601d0 100644 --- a/src/Bicep.Cli/Arguments/GenerateParametersFileArguments.cs +++ b/src/Bicep.Cli/Arguments/GenerateParametersFileArguments.cs @@ -1,121 +1,25 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Bicep.Cli.Helpers; using Bicep.Core; using Bicep.Core.Emit.Options; using Bicep.IO.Abstraction; -namespace Bicep.Cli.Arguments +namespace Bicep.Cli.Arguments; + +public record GenerateParametersFileArguments( + string InputFile, + bool OutputToStdOut, + bool NoRestore, + string? OutputDir, + string? OutputFile, + OutputFormatOption OutputFormat, + IncludeParamsOption IncludeParams) : IInputOutputArguments { - public class GenerateParametersFileArguments : ArgumentsBase, IInputOutputArguments + public static Func OutputFileExtensionResolver { get; } = (args, _) => args.OutputFormat switch { - public GenerateParametersFileArguments(string[] args) : base(Constants.Command.GenerateParamsFile) - { - for (var i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case "--stdout": - OutputToStdOut = true; - break; - - case "--no-restore": - NoRestore = true; - break; - - case ArgumentConstants.OutDir: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.OutDir, OutputDir); - OutputDir = ArgumentHelper.GetValueWithValidation(ArgumentConstants.OutDir, args, i); - i++; - break; - - case ArgumentConstants.OutFile: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.OutFile, OutputFile); - OutputFile = ArgumentHelper.GetValueWithValidation(ArgumentConstants.OutFile, args, i); - i++; - break; - - case "--output-format": - if (args.Length == i + 1) - { - throw new CommandLineException($"The --output-format parameter expects an argument"); - } - if (!Enum.TryParse(args[i + 1], true, out var outputFormat) || !Enum.IsDefined(outputFormat)) - { - throw new CommandLineException($"The --output-format parameter only accepts values: {string.Join(" | ", Enum.GetNames(typeof(OutputFormatOption)))}"); - } - OutputFormat = outputFormat; - i++; - break; - - case "--include-params": - if (args.Length == i + 1) - { - throw new CommandLineException($"The --include-params parameter expects an argument"); - } - if (!Enum.TryParse(args[i + 1], true, out var includeParams) || !Enum.IsDefined(includeParams)) - { - throw new CommandLineException($"The --include-params parameter only accepts values: {string.Join(" | ", Enum.GetNames(typeof(IncludeParamsOption)))}"); - } - IncludeParams = includeParams; - i++; - break; - - default: - if (args[i].StartsWith("--")) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - if (InputFile is not null) - { - throw new CommandLineException($"The input file path cannot be specified multiple times"); - } - InputFile = args[i]; - break; - } - } - - if (InputFile is null) - { - throw new CommandLineException($"The input file path was not specified"); - } - - if (OutputToStdOut && OutputDir is not null) - { - throw new CommandLineException($"The --outdir and --stdout parameters cannot both be used"); - } - - if (OutputToStdOut && OutputFile is not null) - { - throw new CommandLineException($"The --outfile and --stdout parameters cannot both be used"); - } - - if (OutputDir is not null && OutputFile is not null) - { - throw new CommandLineException($"The --outdir and --outfile parameters cannot both be used"); - } - } - - public static Func OutputFileExtensionResolver { get; } = (args, _) => args.OutputFormat switch - { - OutputFormatOption.Json => $".parameters{LanguageConstants.JsonFileExtension}", - OutputFormatOption.BicepParam => LanguageConstants.ParamsFileExtension, - _ => throw new ArgumentOutOfRangeException(nameof(args.OutputFormat), $"Unsupported output format: {args.OutputFormat}") - }; - - public bool OutputToStdOut { get; } - - public string InputFile { get; } - - public string? OutputDir { get; } - - public string? OutputFile { get; } - - public OutputFormatOption OutputFormat { get; } = OutputFormatOption.Json; - - public IncludeParamsOption IncludeParams { get; } = IncludeParamsOption.RequiredOnly; - - public bool NoRestore { get; } - } + OutputFormatOption.Json => $".parameters{LanguageConstants.JsonFileExtension}", + OutputFormatOption.BicepParam => LanguageConstants.ParamsFileExtension, + _ => throw new ArgumentOutOfRangeException(nameof(args.OutputFormat), $"Unsupported output format: {args.OutputFormat}") + }; } diff --git a/src/Bicep.Cli/Arguments/InputOutputArgumentsResolver.cs b/src/Bicep.Cli/Arguments/InputOutputArgumentsResolver.cs index 2913a7a01ec..f3cfc0cfe9e 100644 --- a/src/Bicep.Cli/Arguments/InputOutputArgumentsResolver.cs +++ b/src/Bicep.Cli/Arguments/InputOutputArgumentsResolver.cs @@ -65,7 +65,7 @@ public IReadOnlyList ResolveFilePatternInputArguments(IFilePatternInputAr return result; } - throw new InvalidOperationException("Either InputFile or FilePattern must be specified."); + throw new CommandLineException("Either the input file path or the --pattern parameter must be specified"); } public IReadOnlyList<(IOUri InputUri, IOUri OutputUri)> ResolveFilePatternInputOutputArguments(T arguments) @@ -95,7 +95,7 @@ public IReadOnlyList ResolveFilePatternInputArguments(IFilePatternInputAr return result; } - throw new InvalidOperationException("Either InputFile or FilePattern must be specified."); + throw new CommandLineException("Either the input file path or the --pattern parameter must be specified"); } private IOUri ResolveOutputUri(IOUri inputUri, string? outputDir, string? outputFile, string outputFileExtension) diff --git a/src/Bicep.Cli/Arguments/JsonRpcArguments.cs b/src/Bicep.Cli/Arguments/JsonRpcArguments.cs index 44097d00feb..9e52f3aa6cc 100644 --- a/src/Bicep.Cli/Arguments/JsonRpcArguments.cs +++ b/src/Bicep.Cli/Arguments/JsonRpcArguments.cs @@ -3,59 +3,7 @@ namespace Bicep.Cli.Arguments; -public class JsonRpcArguments : ArgumentsBase -{ - public JsonRpcArguments(string[] args) : base(Constants.Command.JsonRpc) - { - for (var i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case "--pipe": - if (args.Length == i + 1) - { - throw new CommandLineException($"The --pipe parameter expects an argument"); - } - if (Pipe is not null) - { - throw new CommandLineException($"The --pipe parameter cannot be specified twice"); - } - Pipe = args[i + 1]; - i++; - break; - case "--socket": - if (args.Length == i + 1) - { - throw new CommandLineException($"The --socket parameter expects an argument"); - } - if (Socket is not null) - { - throw new CommandLineException($"The --socket parameter cannot be specified twice"); - } - if (!int.TryParse(args[i + 1], out var socket)) - { - throw new CommandLineException($"The --socket parameter only accepts integer values"); - } - Socket = socket; - i++; - break; - - case "--stdio": - if (Stdio is not null) - { - throw new CommandLineException($"The --stdio parameter cannot be specified twice"); - } - Stdio = true; - break; - default: - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - } - } - - public string? Pipe { get; set; } - - public int? Socket { get; set; } - - public bool? Stdio { get; set; } -} +public record JsonRpcArguments( + string? Pipe, + int? Socket, + bool? Stdio); \ No newline at end of file diff --git a/src/Bicep.Cli/Arguments/LintArguments.cs b/src/Bicep.Cli/Arguments/LintArguments.cs index 18a711c69bc..4652869e745 100644 --- a/src/Bicep.Cli/Arguments/LintArguments.cs +++ b/src/Bicep.Cli/Arguments/LintArguments.cs @@ -1,71 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Immutable; -using Bicep.Cli.Helpers; - namespace Bicep.Cli.Arguments; -public class LintArguments : ArgumentsBase, IFilePatternInputArguments -{ - public LintArguments(string[] args) - : base(Constants.Command.Lint) - { - for (var i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case "--no-restore": - NoRestore = true; - break; - - case ArgumentConstants.DiagnosticsFormat: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.DiagnosticsFormat, DiagnosticsFormat); - DiagnosticsFormat = ArgumentHelper.ToDiagnosticsFormat(ArgumentHelper.GetValueWithValidation(ArgumentConstants.DiagnosticsFormat, args, i)); - i++; - break; - - case ArgumentConstants.FilePattern: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.FilePattern, FilePattern); - FilePattern = ArgumentHelper.GetValueWithValidation(ArgumentConstants.FilePattern, args, i); - i++; - break; - - default: - if (args[i].StartsWith("--")) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - if (InputFile is not null) - { - throw new CommandLineException($"The input file path cannot be specified multiple times"); - } - InputFile = args[i]; - break; - } - } - - if (InputFile is null && FilePattern is null) - { - throw new CommandLineException($"Either the input file path or the {ArgumentConstants.FilePattern} parameter must be specified"); - } - - if (FilePattern != null) - { - if (InputFile is not null) - { - throw new CommandLineException($"The input file path and the {ArgumentConstants.FilePattern} parameter cannot both be specified"); - } - } - - DiagnosticsFormat ??= Arguments.DiagnosticsFormat.Default; - } - - public string? InputFile { get; } - - public string? FilePattern { get; } - - public DiagnosticsFormat? DiagnosticsFormat { get; } - - public bool NoRestore { get; } -} +public record LintArguments( + string? InputFile, + string? FilePattern, + DiagnosticsFormat? DiagnosticsFormat, + bool NoRestore) : IFilePatternInputArguments; diff --git a/src/Bicep.Cli/Arguments/LocalDeployArguments.cs b/src/Bicep.Cli/Arguments/LocalDeployArguments.cs index 3c3ca58f90c..82f94359cfb 100644 --- a/src/Bicep.Cli/Arguments/LocalDeployArguments.cs +++ b/src/Bicep.Cli/Arguments/LocalDeployArguments.cs @@ -1,52 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Bicep.Cli.Helpers; +namespace Bicep.Cli.Arguments; -namespace Bicep.Cli.Arguments -{ - public class LocalDeployArguments : ArgumentsBase - { - public LocalDeployArguments(string[] args) : base(Constants.Command.LocalDeploy) - { - for (var i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case "--no-restore": - NoRestore = true; - break; - - case ArgumentConstants.OutputFormat: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.OutputFormat, OutputFormat); - OutputFormat = ArgumentHelper.GetEnumValueWithValidation(ArgumentConstants.OutputFormat, args, i); - i++; - break; - - default: - if (args[i].StartsWith("--")) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - if (ParamsFile is not null) - { - throw new CommandLineException($"The parameters file path cannot be specified multiple times"); - } - ParamsFile = args[i]; - break; - } - } - - if (ParamsFile is null) - { - throw new CommandLineException($"The parameters file path was not specified"); - } - } - - public string ParamsFile { get; } - - public bool NoRestore { get; } - - public DeploymentOutputFormat? OutputFormat { get; } - } -} +public record LocalDeployArguments( + string ParamsFile, + bool NoRestore, + DeploymentOutputFormat? OutputFormat); diff --git a/src/Bicep.Cli/Arguments/PublishArguments.cs b/src/Bicep.Cli/Arguments/PublishArguments.cs index ed1445af879..19a4a78217b 100644 --- a/src/Bicep.Cli/Arguments/PublishArguments.cs +++ b/src/Bicep.Cli/Arguments/PublishArguments.cs @@ -1,103 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Bicep.Cli.Extensions; -namespace Bicep.Cli.Arguments -{ - public class PublishArguments : ArgumentsBase, IInputArguments - { - public PublishArguments(string[] args) : base(Constants.Command.Publish) - { - for (int i = 0; i < args.Length; i++) - { - var isLast = args.Length == i + 1; - switch (args[i].ToLowerInvariant()) - { - case "--no-restore": - NoRestore = true; - break; +namespace Bicep.Cli.Arguments; - case "--target": - if (isLast) - { - throw new CommandLineException("The --target parameter expects an argument."); - } - - if (this.TargetModuleReference is not null) - { - throw new CommandLineException("The --target parameter cannot be specified twice."); - } - - TargetModuleReference = args[i + 1]; - i++; - break; - - case "--documentation-uri": - if (isLast) - { - throw new CommandLineException("The --documentation-uri parameter expects an argument."); - } - - if (this.DocumentationUri is not null) - { - throw new CommandLineException("The --documentation-uri parameter cannot be specified more than once."); - } - - DocumentationUri = args[i + 1]; - - if (!Uri.IsWellFormedUriString(DocumentationUri, UriKind.Absolute)) - { - throw new CommandLineException("The --documentation-uri should be a well formed uri string."); - } - - i++; - break; - - case "--with-source": - WithSource = true; - break; - - case "--force": - Force = true; - break; - - default: - if (args[i].StartsWith("--")) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - - if (InputFile is not null) - { - throw new CommandLineException($"The input file path cannot be specified multiple times."); - } - - InputFile = args[i]; - break; - } - } - - if (InputFile is null) - { - throw new CommandLineException($"The input file path was not specified."); - } - - if (TargetModuleReference is null) - { - throw new CommandLineException("The target module was not specified."); - } - } - - public string? DocumentationUri { get; } - - public string InputFile { get; } - - public string TargetModuleReference { get; } - - public bool NoRestore { get; } - - public bool Force { get; } - - public bool WithSource { get; } - } -} +public record PublishArguments( + string InputFile, + string TargetModuleReference, + string? DocumentationUri, + bool NoRestore, + bool Force, + bool WithSource) : IInputArguments; diff --git a/src/Bicep.Cli/Arguments/PublishExtensionArguments.cs b/src/Bicep.Cli/Arguments/PublishExtensionArguments.cs index c989119bbf0..f21dbd8bf73 100644 --- a/src/Bicep.Cli/Arguments/PublishExtensionArguments.cs +++ b/src/Bicep.Cli/Arguments/PublishExtensionArguments.cs @@ -1,84 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Bicep.Cli.Extensions; -using Bicep.Core.Registry.Oci; -namespace Bicep.Cli.Arguments -{ - public class PublishExtensionArguments : ArgumentsBase - { - public PublishExtensionArguments(string[] args) - : base(Constants.Command.PublishExtension) - { - for (int i = 0; i < args.Length; i++) - { - var isLast = args.Length == i + 1; - switch (args[i].ToLowerInvariant()) - { - case "--target": - if (isLast) - { - throw new CommandLineException("The --target parameter expects an argument."); - } +namespace Bicep.Cli.Arguments; - if (this.TargetExtensionReference is not null) - { - throw new CommandLineException("The --target parameter cannot be specified twice."); - } - - TargetExtensionReference = args[i + 1]; - i++; - break; - - case { } when args[i].StartsWith("--bin-"): - var architectureName = args[i].Substring("--bin-".Length); - - if (!SupportedArchitectures.All.Any(x => x.Name == architectureName)) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - - if (Binaries.ContainsKey(architectureName)) - { - throw new CommandLineException($"Parameter \"{args[i]}\" cannot be specified multiple times."); - } - - Binaries[architectureName] = args[i + 1]; - i++; - break; - - case "--force": - Force = true; - break; - - default: - if (args[i].StartsWith("--")) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - - if (IndexFile is not null) - { - throw new CommandLineException($"The input file path cannot be specified multiple times."); - } - - IndexFile = args[i]; - break; - } - } - - if (TargetExtensionReference is null) - { - throw new CommandLineException("The target extension was not specified."); - } - } - - public Dictionary Binaries { get; } = new(); - - public string? IndexFile { get; } - - public string TargetExtensionReference { get; } - - public bool Force { get; } - } -} +public record PublishExtensionArguments( + string? IndexFile, + string? TargetExtensionReference, + IReadOnlyDictionary Binaries, + bool Force); diff --git a/src/Bicep.Cli/Arguments/RestoreArguments.cs b/src/Bicep.Cli/Arguments/RestoreArguments.cs index 537f5d3ade0..d7538edaba2 100644 --- a/src/Bicep.Cli/Arguments/RestoreArguments.cs +++ b/src/Bicep.Cli/Arguments/RestoreArguments.cs @@ -1,60 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Immutable; -using Bicep.Cli.Helpers; - namespace Bicep.Cli.Arguments; -public class RestoreArguments : ArgumentsBase, IFilePatternInputArguments -{ - public RestoreArguments(string[] args) : base(Constants.Command.Restore) - { - for (var i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case "--force": - ForceModulesRestore = true; - break; - - case ArgumentConstants.FilePattern: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.FilePattern, FilePattern); - FilePattern = ArgumentHelper.GetValueWithValidation(ArgumentConstants.FilePattern, args, i); - i++; - break; - - default: - if (args[i].StartsWith("--")) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - if (InputFile is not null) - { - throw new CommandLineException($"The input file path cannot be specified multiple times"); - } - InputFile = args[i]; - break; - } - } - - if (InputFile is null && FilePattern is null) - { - throw new CommandLineException($"Either the input file path or the {ArgumentConstants.FilePattern} parameter must be specified"); - } - - if (FilePattern != null) - { - if (InputFile is not null) - { - throw new CommandLineException($"The input file path and the {ArgumentConstants.FilePattern} parameter cannot both be specified"); - } - } - } - - public string? InputFile { get; } - - public string? FilePattern { get; } - - public bool ForceModulesRestore { get; } -} +public record RestoreArguments( + string? InputFile, + string? FilePattern, + bool ForceModulesRestore) : IFilePatternInputArguments; diff --git a/src/Bicep.Cli/Arguments/RootArguments.cs b/src/Bicep.Cli/Arguments/RootArguments.cs deleted file mode 100644 index 4c545d699b9..00000000000 --- a/src/Bicep.Cli/Arguments/RootArguments.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text.RegularExpressions; - -namespace Bicep.Cli.Arguments -{ - public class RootArguments : ArgumentsBase - { - public RootArguments(string arg, string commandName) : base(commandName) - { - switch (arg) - { - case var a when new Regex(Constants.Argument.VersionRegex).IsMatch(a): - PrintVersion = true; - break; - - case var a when new Regex(Constants.Argument.HelpRegex).IsMatch(a): - PrintHelp = true; - break; - - case var a when new Regex(Constants.Argument.LicenseRegex).IsMatch(a): - PrintLicense = true; - break; - - case var a when new Regex(Constants.Argument.ThirdPartyNoticesRegex).IsMatch(a): - PrintThirdPartyNotices = true; - break; - } - ; - } - - public bool PrintHelp { get; } - public bool PrintVersion { get; } - public bool PrintLicense { get; } - public bool PrintThirdPartyNotices { get; } - } -} diff --git a/src/Bicep.Cli/Arguments/SnapshotArguments.cs b/src/Bicep.Cli/Arguments/SnapshotArguments.cs index 0cf360fb91a..312eb5bf8ab 100644 --- a/src/Bicep.Cli/Arguments/SnapshotArguments.cs +++ b/src/Bicep.Cli/Arguments/SnapshotArguments.cs @@ -1,107 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Immutable; -using Bicep.Cli.Helpers; - namespace Bicep.Cli.Arguments; -public class SnapshotArguments : ArgumentsBase, IInputArguments +public record SnapshotArguments( + string InputFile, + SnapshotArguments.SnapshotMode? Mode, + string? TenantId, + string? SubscriptionId, + string? ManagementGroupId, + string? Location, + string? ResourceGroup, + string? DeploymentName) : IInputArguments { - private const string TenantIdArgument = "--tenant-id"; - private const string SubscriptionIdArgument = "--subscription-id"; - private const string ManagementGroupIdArgument = "--management-group-id"; - private const string LocationArgument = "--location"; - private const string ResourceGroupArgument = "--resource-group"; - private const string DeploymentNameArgument = "--deployment-name"; - public enum SnapshotMode { Overwrite, Validate, } - - public SnapshotArguments(string[] args) : base(Constants.Command.Snapshot) - { - for (var i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case ArgumentConstants.Mode: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.Mode, Mode); - Mode = ArgumentHelper.GetEnumValueWithValidation(ArgumentConstants.Mode, args, i); - i++; - break; - - case TenantIdArgument: - ArgumentHelper.ValidateNotAlreadySet(TenantIdArgument, TenantId); - TenantId = ArgumentHelper.GetValueWithValidation(TenantIdArgument, args, i); - i++; - break; - - case SubscriptionIdArgument: - ArgumentHelper.ValidateNotAlreadySet(SubscriptionIdArgument, SubscriptionId); - SubscriptionId = ArgumentHelper.GetValueWithValidation(SubscriptionIdArgument, args, i); - i++; - break; - - case ManagementGroupIdArgument: - ArgumentHelper.ValidateNotAlreadySet(ManagementGroupIdArgument, ManagementGroupId); - ManagementGroupId = ArgumentHelper.GetValueWithValidation(ManagementGroupIdArgument, args, i); - i++; - break; - - case ResourceGroupArgument: - ArgumentHelper.ValidateNotAlreadySet(ResourceGroupArgument, ResourceGroup); - ResourceGroup = ArgumentHelper.GetValueWithValidation(ResourceGroupArgument, args, i); - i++; - break; - - case LocationArgument: - ArgumentHelper.ValidateNotAlreadySet(LocationArgument, Location); - Location = ArgumentHelper.GetValueWithValidation(LocationArgument, args, i); - i++; - break; - - case DeploymentNameArgument: - ArgumentHelper.ValidateNotAlreadySet(DeploymentNameArgument, DeploymentName); - DeploymentName = ArgumentHelper.GetValueWithValidation(DeploymentNameArgument, args, i); - i++; - break; - - default: - if (args[i].StartsWith("--")) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - if (InputFile is not null) - { - throw new CommandLineException($"The input file path cannot be specified multiple times"); - } - InputFile = args[i]; - break; - } - } - - if (InputFile is null) - { - throw new CommandLineException($"The input file path was not specified"); - } - } - - public string InputFile { get; } - - public SnapshotMode? Mode { get; } - - public string? TenantId { get; } - - public string? SubscriptionId { get; } - - public string? ManagementGroupId { get; } - - public string? Location { get; } - - public string? ResourceGroup { get; } - - public string? DeploymentName { get; } } diff --git a/src/Bicep.Cli/Arguments/TeardownArguments.cs b/src/Bicep.Cli/Arguments/TeardownArguments.cs index 51f23ca677b..5368c7fa269 100644 --- a/src/Bicep.Cli/Arguments/TeardownArguments.cs +++ b/src/Bicep.Cli/Arguments/TeardownArguments.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Immutable; + namespace Bicep.Cli.Arguments; -public class TeardownArguments : DeployArgumentsBase -{ - public TeardownArguments(string[] args) : base(args, Constants.Command.Teardown) - { - } -} +public record TeardownArguments( + string InputFile, + bool NoRestore, + ImmutableDictionary AdditionalArguments) : DeployArgumentsBase(InputFile, NoRestore, AdditionalArguments); \ No newline at end of file diff --git a/src/Bicep.Cli/Arguments/TestArguments.cs b/src/Bicep.Cli/Arguments/TestArguments.cs index 8e3b3ef9c23..e8007a7987d 100644 --- a/src/Bicep.Cli/Arguments/TestArguments.cs +++ b/src/Bicep.Cli/Arguments/TestArguments.cs @@ -1,57 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Bicep.Cli.Helpers; +namespace Bicep.Cli.Arguments; -namespace Bicep.Cli.Arguments -{ - public class TestArguments : ArgumentsBase, IInputArguments - { - public TestArguments(string[] args) : base(Constants.Command.Test) - { - for (var i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case "--no-restore": - NoRestore = true; - break; - - case ArgumentConstants.DiagnosticsFormat: - ArgumentHelper.ValidateNotAlreadySet(ArgumentConstants.DiagnosticsFormat, DiagnosticsFormat); - DiagnosticsFormat = ArgumentHelper.ToDiagnosticsFormat(ArgumentHelper.GetValueWithValidation(ArgumentConstants.DiagnosticsFormat, args, i)); - i++; - break; - - default: - if (args[i].StartsWith("--")) - { - throw new CommandLineException($"Unrecognized parameter \"{args[i]}\""); - } - if (InputFile is not null) - { - throw new CommandLineException($"The input file path cannot be specified multiple times"); - } - InputFile = args[i]; - break; - } - } - - if (InputFile is null) - { - throw new CommandLineException($"The input file path was not specified"); - } - - if (DiagnosticsFormat is null) - { - DiagnosticsFormat = Arguments.DiagnosticsFormat.Default; - } - } - - public string InputFile { get; } - - public DiagnosticsFormat? DiagnosticsFormat { get; } - - public bool NoRestore { get; } - } -} +public record TestArguments( + string InputFile, + bool NoRestore, + DiagnosticsFormat? DiagnosticsFormat) : IInputArguments; diff --git a/src/Bicep.Cli/Arguments/WhatIfArguments.cs b/src/Bicep.Cli/Arguments/WhatIfArguments.cs index 2a3ed520efe..40405b81d81 100644 --- a/src/Bicep.Cli/Arguments/WhatIfArguments.cs +++ b/src/Bicep.Cli/Arguments/WhatIfArguments.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Immutable; + namespace Bicep.Cli.Arguments; -public class WhatIfArguments : DeployArgumentsBase -{ - public WhatIfArguments(string[] args) : base(args, Constants.Command.WhatIf) - { - } -} +public record WhatIfArguments( + string InputFile, + bool NoRestore, + ImmutableDictionary AdditionalArguments) : DeployArgumentsBase(InputFile, NoRestore, AdditionalArguments); \ No newline at end of file diff --git a/src/Bicep.Cli/Bicep.Cli.csproj b/src/Bicep.Cli/Bicep.Cli.csproj index fb68ef30acb..61b7355cbb9 100644 --- a/src/Bicep.Cli/Bicep.Cli.csproj +++ b/src/Bicep.Cli/Bicep.Cli.csproj @@ -27,6 +27,7 @@ + diff --git a/src/Bicep.Cli/Commands/DeploymentsCommandsBase.cs b/src/Bicep.Cli/Commands/DeploymentsCommandsBase.cs index fb6e4541729..35d56dadd6a 100644 --- a/src/Bicep.Cli/Commands/DeploymentsCommandsBase.cs +++ b/src/Bicep.Cli/Commands/DeploymentsCommandsBase.cs @@ -29,12 +29,12 @@ public async Task RunAsync(TArgs args, CancellationToken cancellationToken) if (!model.Features.DeployCommandsEnabled) { - throw new CommandLineException($"The '{nameof(ExperimentalFeaturesEnabled.DeployCommands)}' experimental feature must be enabled to use the '{args.CommandName}' command."); + throw new CommandLineException($"The '{nameof(ExperimentalFeaturesEnabled.DeployCommands)}' experimental feature must be enabled."); } if (!model.HasAzureTargetScope()) { - throw new CommandLineException($"The '{args.CommandName}' command only supports Bicep files with an Azure target scope."); + throw new CommandLineException($"Only Bicep files with an Azure target scope are supported."); } CommandHelper.LogExperimentalWarning(logger, compilation); diff --git a/src/Bicep.Cli/Commands/FormatCommand.cs b/src/Bicep.Cli/Commands/FormatCommand.cs index 56c187a5e2c..2918a992839 100644 --- a/src/Bicep.Cli/Commands/FormatCommand.cs +++ b/src/Bicep.Cli/Commands/FormatCommand.cs @@ -3,6 +3,7 @@ using System.IO.Abstractions; using Bicep.Cli.Arguments; +using Bicep.Cli.Constants; using Bicep.Cli.Helpers; using Bicep.Core.Configuration; using Bicep.Core.Diagnostics; @@ -26,10 +27,15 @@ public class FormatCommand( { public int Run(FormatArguments args) { + if (args.FilePattern is not null && args.OutputDir is not null) + { + throw new CommandLineException($"The {Option.OutDir} parameter cannot be used with the {Option.Pattern} parameter"); + } + ArgumentHelper.ValidateOutputOptions(args.OutputToStdOut, args.OutputDir, args.OutputFile, args.FilePattern); + foreach (var (inputUri, outputUri) in inputOutputArgumentsResolver.ResolveFilePatternInputOutputArguments(args)) { ArgumentHelper.ValidateBicepOrBicepParamFile(inputUri); - this.Format(args, inputUri, outputUri, args.OutputToStdOut); } diff --git a/src/Bicep.Cli/Commands/PublishExtensionCommand.cs b/src/Bicep.Cli/Commands/PublishExtensionCommand.cs index d246322392a..138d3b29240 100644 --- a/src/Bicep.Cli/Commands/PublishExtensionCommand.cs +++ b/src/Bicep.Cli/Commands/PublishExtensionCommand.cs @@ -49,7 +49,11 @@ public async Task RunAsync(PublishExtensionArguments args, CancellationToke throw new CommandLineException($"The input file path was not specified."); } - logger.LogWarning($"WARNING: The '{args.CommandName}' CLI command group is an experimental feature. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking."); + logger.LogWarning($"WARNING: The '{Constants.Command.PublishExtension}' CLI command group is an experimental feature. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking."); + if (args.TargetExtensionReference is null) + { + throw new CommandLineException("The target extension was not specified."); + } var reference = ValidateReference(args.TargetExtensionReference); var overwriteIfExists = args.Force; diff --git a/src/Bicep.Cli/Commands/RootCommand.cs b/src/Bicep.Cli/Commands/RootCommand.cs deleted file mode 100644 index c91fe2fd5f6..00000000000 --- a/src/Bicep.Cli/Commands/RootCommand.cs +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.IO.Compression; -using Bicep.Cli.Arguments; -using Bicep.Core.Exceptions; -using Bicep.Core.Utils; - -namespace Bicep.Cli.Commands -{ - public class RootCommand( - IOContext io, - IEnvironment environment) : ICommand - { - public int Run(RootArguments args) - { - if (args.PrintVersion) - { - PrintVersion(); - return 0; - } - - if (args.PrintHelp) - { - PrintHelp(); - return 0; - } - - if (args.PrintLicense) - { - PrintLicense(); - return 0; - } - - if (args.PrintThirdPartyNotices) - { - PrintThirdPartyNotices(); - return 0; - } - - return 1; - } - - private void PrintHelp() - { - var exeName = ThisAssembly.AssemblyName; - var versionString = environment.GetVersionString(); - - var output = -$@"Bicep CLI version {versionString} - -Usage: - {exeName} build [options] [] - Builds a .bicep file. - - Arguments: - The input file - - Options: - --outdir Saves the output at the specified directory. - --outfile Saves the output as the specified file path. - --stdout Prints the output to stdout. - --no-restore Builds the bicep file without restoring external modules. - --diagnostics-format Sets the format with which diagnostics are displayed. Valid values are ( {string.Join(" | ", Enum.GetNames(typeof(DiagnosticsFormat)))} ). - --pattern Builds all files matching the specified glob pattern. - - Examples: - bicep build file.bicep - bicep build file.bicep --stdout - bicep build file.bicep --outdir dir1 - bicep build file.bicep --outfile file.json - bicep build file.bicep --no-restore - bicep build file.bicep --diagnostics-format sarif - bicep build --pattern './dir/**/*.bicep' - - {exeName} format [options] [] - Formats a .bicep file. - - Arguments: - The input file - - Options: - --outdir Saves the output at the specified directory. - --outfile Saves the output as the specified file path. - --stdout Prints the output to stdout. - --newline Set newline char. Valid values are ( Auto | LF | CRLF | CR ). - --indent-kind Set indentation kind. Valid values are ( Space | Tab ). - --indent-size Number of spaces to indent with (Only valid with --indentKind set to Space). - --insert-final-newline Insert a final newline. - --pattern Formats all files matching the specified glob pattern. - - Examples: - bicep format file.bicep - bicep format file.bicep --stdout - bicep format file.bicep --outdir dir1 - bicep format file.bicep --outfile file.json - bicep format file.bicep --indent-kind Tab - bicep format --pattern './dir/**/*.bicep' - - {exeName} decompile [options] - Attempts to decompile a template .json file to .bicep. - - Arguments: - The input file - - Options: - --outdir Saves the output at the specified directory. - --outfile Saves the output as the specified file path. - --stdout Prints the output to stdout. - --force Allows overwriting the output file if it exists (applies only to 'bicep decompile' or 'bicep decompile-params'). - - Examples: - bicep decompile file.json - bicep decompile file.json --stdout - bicep decompile file.json --outdir dir1 - bicep decompile file.json --force - bicep decompile file.json --outfile file.bicep - - {exeName} lint [options] [] - Lints a .bicep file. - - Arguments: - The input file - - Options: - --no-restore Skips restoring external modules. - --diagnostics-format Sets the format with which diagnostics are displayed. Valid values are ( {string.Join(" | ", Enum.GetNames(typeof(DiagnosticsFormat)))} ). - --pattern Lints all files matching the specified glob pattern. - - Examples: - bicep lint file.bicep - bicep lint file.bicep --no-restore - bicep lint file.bicep --diagnostics-format sarif - bicep lint --pattern './dir/**/*.bicep' - - {exeName} decompile-params [options] - Attempts to decompile a parameters .json file to .bicepparam. - - Arguments: - The input file - - Options: - --outdir Saves the output at the specified directory. - --outfile Saves the output as the specified file path. - --stdout Prints the output to stdout. - --force Allows overwriting the output file if it exists (applies only to 'bicep decompile' or 'bicep decompile-params'). - --bicep-file Path to the bicep template file that will be referenced in the using declaration - - Examples: - bicep decompile-params file.json - bicep decompile-params file.json --bicep-file ./dir/main.bicep - bicep decompile-params file.json --stdout - bicep decompile-params file.json --outdir dir1 - bicep decompile-params file.json --force - bicep decompile-params file.json --outfile file.bicepparam - - {exeName} generate-params [options] - Builds parameters file from the given bicep file, updates if there is an existing parameters file. - - Arguments: - The input file - - Options: - --no-restore Generates the parameters file without restoring external modules. - --outdir Saves the output at the specified directory. - --outfile Saves the output as the specified file path. - --stdout Prints the output to stdout. - --output-format Selects the output format {{json, bicepparam}} - --include-params Selects which parameters to include into output {{requiredonly, all}} - - Examples: - bicep generate-params file.bicep - bicep generate-params file.bicep --no-restore - bicep generate-params file.bicep --stdout - bicep generate-params file.bicep --outdir dir1 - bicep generate-params file.bicep --outfile file.parameters.json - bicep generate-params file.bicep --output-format bicepparam --include-params all - - {exeName} publish --target - Publishes the .bicep file to the module registry. - - Arguments: - The input file (can be a Bicep file or an ARM template file) - The module reference - - Options: - --documentation-uri Module documentation uri - --with-source [Experimental] Publish source code with the module - --force Overwrite existing published module or file - - Examples: - bicep publish file.bicep --target br:example.azurecr.io/hello/world:v1 - bicep publish file.bicep --target br:example.azurecr.io/hello/world:v1 --force - bicep publish file.bicep --target br:example.azurecr.io/hello/world:v1 --documentation-uri https://github.com/hello-world/README.md --with-source - bicep publish file.json --target br:example.azurecr.io/hello/world:v1 --documentation-uri https://github.com/hello-world/README.md - - {exeName} restore [] - Restores external modules from the specified Bicep file to the local module cache. - - Arguments: - The input file - - Options: - --pattern Restores all files matching the specified glob pattern. - - Examples: - bicep restore main.bicep - bicep restore --pattern './dir/**/*.bicep' - - {exeName} [options] - Options: - --version -v Shows bicep version information - --help -h Shows this usage information - --license Prints license information - --third-party-notices Prints third-party notices - - {exeName} build-params [] - Builds a .json file from a .bicepparam file. - - Arguments: - The input Bicepparam file - - Options: - --bicep-file Verifies if the specified bicep file path matches the one provided in the params file using declaration - --outdir Saves the output of building the parameter file only (.bicepparam) as json to the specified directory. - --outfile Saves the output of building the parameter file only (.bicepparam) as json to the specified file path. - --stdout Prints the output of building both the parameter file (.bicepparam) and the template it points to (.bicep) as json to stdout. - --no-restore Builds the bicep file (referenced in using declaration) without restoring external modules. - --diagnostics-format Sets the format with which diagnostics are displayed. Valid values are ( {string.Join(" | ", Enum.GetNames(typeof(DiagnosticsFormat)))} ). - --pattern Builds all files matching the specified glob pattern. - - Examples: - bicep build-params params.bicepparam - bicep build-params params.bicepparam --stdout - bicep build-params params.bicepparam --outdir dir1 - bicep build-params params.bicepparam --outfile otherParams.json - bicep build-params params.bicepparam --no-restore - bicep build-params params.bicepparam --diagnostics-format sarif - bicep build-params --pattern './dir/**/*.bicepparam' - - {exeName} jsonrpc [options] - Starts the Bicep CLI listening for JSONRPC messages, for programatically interacting with Bicep. See https://aka.ms/bicep/jsonrpc for more information. - - Options: - --pipe Bicep CLI will connect to the supplied named pipe as a client, and start listening for JSONRPC requests. - --socket Bicep CLI will connect to the supplied TCP port on the loopback interface as a client, and start listening for JSONRPC requests. - --stdio Bicep CLI will use stdin/stdout for JSONRPC requests. - - Examples: - bicep jsonrpc --pipe /path/to/pipe.sock - bicep jsonrpc --socket 9853 - bicep jsonrpc --stdio - - {exeName} snapshot [options] - Generates or validates a deployment snapshot from a .bicepparam file. - - Arguments: - The input .bicepparam file - - Options: - --mode Sets the snapshot mode. Valid values are ( overwrite | validate ). - Overwrite: Generates a new snapshot and saves it to .snapshot.json. - Validate: Compares the generated snapshot against an existing snapshot file. - --tenant-id The tenant ID to use for the deployment. - --subscription-id The subscription ID to use for the deployment. - --resource-group The resource group name to use for the deployment. - --location The location to use for the deployment. - --deployment-name The deployment name to use. - - Examples: - bicep snapshot params.bicepparam - bicep snapshot params.bicepparam --mode overwrite - bicep snapshot params.bicepparam --mode validate - bicep snapshot params.bicepparam --subscription-id 00000000-0000-0000-0000-000000000000 --resource-group my-rg - -"; // this newline is intentional - - io.Output.Writer.Write(output); - io.Output.Writer.Flush(); - } - - private void PrintVersion() - { - var output = $@"Bicep CLI version {environment.GetVersionString()}{System.Environment.NewLine}"; - - io.Output.Writer.Write(output); - io.Output.Writer.Flush(); - } - - private void PrintLicense() - { - WriteEmbeddedResource(io.Output.Writer, "LICENSE.deflated"); - } - - private void PrintThirdPartyNotices() - { - WriteEmbeddedResource(io.Output.Writer, "NOTICE.deflated"); - } - - private static void WriteEmbeddedResource(TextWriter writer, string streamName) - { - using var stream = typeof(RootCommand).Assembly.GetManifestResourceStream(streamName) - ?? throw new BicepException($"The resource stream '{streamName}' is missing from this executable."); - - using var decompressor = new DeflateStream(stream, CompressionMode.Decompress); - - using var reader = new StreamReader(decompressor); - string? line = null; - while ((line = reader.ReadLine()) is not null) - { - writer.WriteLine(line); - } - } - } -} diff --git a/src/Bicep.Cli/Constants/CliConstants.cs b/src/Bicep.Cli/Constants/CliConstants.cs index 7986dbc6036..549645ce37a 100644 --- a/src/Bicep.Cli/Constants/CliConstants.cs +++ b/src/Bicep.Cli/Constants/CliConstants.cs @@ -28,9 +28,69 @@ public static class Command public static class Argument { - public const string VersionRegex = @"^(--version|-v)$"; - public const string HelpRegex = @"^(--help|-h)$"; - public const string LicenseRegex = @"^--license$"; - public const string ThirdPartyNoticesRegex = @"^--third-party-notices$"; + public const string InputFile = "Input file"; + public const string IndexFile = "Index file"; + public const string ParametersFile = "Parameters file"; + } + + public static class Option + { + // Root options + public const string Version = "--version"; + public const string VersionShort = "-v"; + public const string License = "--license"; + public const string ThirdPartyNotices = "--third-party-notices"; + + // Common output options + public const string Stdout = "--stdout"; + public const string OutDir = "--outdir"; + public const string OutFile = "--outfile"; + public const string Pattern = "--pattern"; + public const string NoRestore = "--no-restore"; + public const string Force = "--force"; + public const string DiagnosticsFormat = "--diagnostics-format"; + + // Build / BuildParams + public const string BicepFile = "--bicep-file"; + + // GenerateParams + public const string OutputFormat = "--output-format"; + public const string IncludeParams = "--include-params"; + + // Format + public const string NewlineKind = "--newline-kind"; + public const string IndentKind = "--indent-kind"; + public const string IndentSize = "--indent-size"; + public const string InsertFinalNewline = "--insert-final-newline"; + + // Publish + public const string Target = "--target"; + public const string DocumentationUri = "--documentation-uri"; + public const string WithSource = "--with-source"; + + // PublishExtension binaries + public const string BinLinuxX64 = "--bin-linux-x64"; + public const string BinLinuxArm64 = "--bin-linux-arm64"; + public const string BinOsxX64 = "--bin-osx-x64"; + public const string BinOsxArm64 = "--bin-osx-arm64"; + public const string BinWinX64 = "--bin-win-x64"; + public const string BinWinArm64 = "--bin-win-arm64"; + + // JsonRpc + public const string Pipe = "--pipe"; + public const string Socket = "--socket"; + public const string Stdio = "--stdio"; + + // Deploy / local-deploy / what-if / teardown + public const string Format = "--format"; + + // Snapshot + public const string Mode = "--mode"; + public const string TenantId = "--tenant-id"; + public const string SubscriptionId = "--subscription-id"; + public const string ManagementGroupId = "--management-group-id"; + public const string Location = "--location"; + public const string ResourceGroup = "--resource-group"; + public const string DeploymentName = "--deployment-name"; } } diff --git a/src/Bicep.Cli/Helpers/ArgumentHelper.cs b/src/Bicep.Cli/Helpers/ArgumentHelper.cs index 2e7d8c0bae6..6d3703b57a5 100644 --- a/src/Bicep.Cli/Helpers/ArgumentHelper.cs +++ b/src/Bicep.Cli/Helpers/ArgumentHelper.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO.Abstractions; using Bicep.Cli.Arguments; +using Bicep.Cli.Constants; using Bicep.Cli.Logging; using Bicep.Core.Extensions; using Bicep.IO.Abstraction; @@ -13,6 +14,30 @@ namespace Bicep.Cli.Helpers; public class ArgumentHelper { + public static void ValidateOutputOptions(bool outputToStdOut, string? outputDir, string? outputFile, string? filePattern = null) + { + if (filePattern is not null && outputToStdOut) + { + throw new CommandLineException($"The {Option.Stdout} parameter cannot be used with the {Option.Pattern} parameter"); + } + if (filePattern is not null && outputFile is not null) + { + throw new CommandLineException($"The {Option.OutFile} parameter cannot be used with the {Option.Pattern} parameter"); + } + if (outputToStdOut && outputDir is not null) + { + throw new CommandLineException($"The {Option.OutDir} and {Option.Stdout} parameters cannot both be used"); + } + if (outputToStdOut && outputFile is not null) + { + throw new CommandLineException($"The {Option.OutFile} and {Option.Stdout} parameters cannot both be used"); + } + if (outputDir is not null && outputFile is not null) + { + throw new CommandLineException($"The {Option.OutDir} and {Option.OutFile} parameters cannot both be used"); + } + } + public static DiagnosticsFormat ToDiagnosticsFormat(string? format) { if (format is null || (format is not null && format.Equals("default", StringComparison.OrdinalIgnoreCase))) diff --git a/src/Bicep.Cli/Helpers/CliInfoPrinter.cs b/src/Bicep.Cli/Helpers/CliInfoPrinter.cs new file mode 100644 index 00000000000..a19bb50febb --- /dev/null +++ b/src/Bicep.Cli/Helpers/CliInfoPrinter.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO.Compression; +using Bicep.Core.Exceptions; +using Bicep.Core.Utils; + +namespace Bicep.Cli.Helpers +{ + public static class CliInfoPrinter + { + public static void PrintVersion(IOContext io, IEnvironment environment) + { + var output = $@"Bicep CLI version {environment.GetVersionString()}{System.Environment.NewLine}"; + + io.Output.Writer.Write(output); + io.Output.Writer.Flush(); + } + + public static void PrintLicense(IOContext io) + { + WriteEmbeddedResource(io.Output.Writer, "LICENSE.deflated"); + } + + public static void PrintThirdPartyNotices(IOContext io) + { + WriteEmbeddedResource(io.Output.Writer, "NOTICE.deflated"); + } + + private static void WriteEmbeddedResource(TextWriter writer, string streamName) + { + using var stream = typeof(CliInfoPrinter).Assembly.GetManifestResourceStream(streamName) + ?? throw new BicepException($"The resource stream '{streamName}' is missing from this executable."); + + using var decompressor = new DeflateStream(stream, CompressionMode.Decompress); + + using var reader = new StreamReader(decompressor); + string? line = null; + while ((line = reader.ReadLine()) is not null) + { + writer.WriteLine(line); + } + } + } +} diff --git a/src/Bicep.Cli/Helpers/IServiceCollectionExtensions.cs b/src/Bicep.Cli/Helpers/IServiceCollectionExtensions.cs index 927df27f243..5780baf3f94 100644 --- a/src/Bicep.Cli/Helpers/IServiceCollectionExtensions.cs +++ b/src/Bicep.Cli/Helpers/IServiceCollectionExtensions.cs @@ -63,6 +63,5 @@ public static IServiceCollection AddCommands(this IServiceCollection services) = .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton(); } diff --git a/src/Bicep.Cli/Program.cs b/src/Bicep.Cli/Program.cs index 2f255be248f..e61627eb306 100644 --- a/src/Bicep.Cli/Program.cs +++ b/src/Bicep.Cli/Program.cs @@ -1,8 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Immutable; +using System.CommandLine; +using System.CommandLine.Help; +using System.CommandLine.Invocation; using System.Diagnostics; -using System.IO.Abstractions; using System.Runtime; using Bicep.Cli.Arguments; using Bicep.Cli.Commands; @@ -12,6 +15,8 @@ using Bicep.Cli.Services; using Bicep.Core; using Bicep.Core.Emit; +using Bicep.Core.Emit.Options; +using Bicep.Core.PrettyPrintV2; using Bicep.Core.Exceptions; using Bicep.Core.Features; using Bicep.Core.Tracing; @@ -20,6 +25,17 @@ using Microsoft.Extensions.Logging; using Spectre.Console; +using SclRootCommand = System.CommandLine.RootCommand; +using System.CommandLine.Parsing; + +public class HelpExamplesAction : SynchronousCommandLineAction +{ + public override int Invoke(ParseResult parseResult) + { + throw new NotImplementedException(); + } +} + namespace Bicep.Cli { public record IOContext( @@ -81,71 +97,1058 @@ public async Task RunAsync(string[] args, CancellationToken cancellationTok var environment = services.GetRequiredService(); Trace.WriteLine($"Bicep version: {environment.GetVersionString()}, OS: {environment.CurrentPlatform?.ToString() ?? "unknown"}, Architecture: {environment.CurrentArchitecture}, CLI arguments: \"{string.Join(' ', args)}\""); - try + var rootCommand = BuildCommandLine(cancellationToken); + var invocationConfig = new InvocationConfiguration + { + Output = io.Output.Writer, + Error = io.Error.Writer, + }; + return await rootCommand.Parse(args).InvokeAsync(invocationConfig, cancellationToken); + } + + private SclRootCommand BuildCommandLine(CancellationToken cancellationToken) + { + var rootCommand = new SclRootCommand("Bicep CLI"); + + // System.CommandLine adds --version and --help to RootCommand by default. + // Replace both with custom options so their output goes through io.Output.Writer + // (not Console.Out) and --version prints the Bicep-specific version string. + var builtInVersion = rootCommand.Options.FirstOrDefault(o => o.Name == Constants.Option.Version); + if (builtInVersion is not null) { - switch (ArgumentParser.TryParse(args, services.GetRequiredService())) + rootCommand.Options.Remove(builtInVersion); + } + + var versionOption = new Option(Constants.Option.Version, Constants.Option.VersionShort) { Description = "Shows bicep version information." }; + var licenseOption = new Option(Constants.Option.License) { Description = "Prints license information." }; + var thirdPartyNoticesOption = new Option(Constants.Option.ThirdPartyNotices) { Description = "Prints third-party notices." }; + + // rootCommand.Add(helpOption); + rootCommand.Add(versionOption); + rootCommand.Add(licenseOption); + rootCommand.Add(thirdPartyNoticesOption); + + rootCommand.SetAction(async (ParseResult pr, CancellationToken ct) => + { + var environment = services.GetRequiredService(); + var unmatched = pr.UnmatchedTokens; + + if (pr.GetValue(versionOption)) { - case BuildArguments buildArguments when buildArguments.CommandName == Constants.Command.Build: // bicep build [options] - return await services.GetRequiredService().RunAsync(buildArguments); + CliInfoPrinter.PrintVersion(io, environment); + return 0; + } + + if (pr.GetValue(licenseOption)) + { + CliInfoPrinter.PrintLicense(io); + return 0; + } + + if (pr.GetValue(thirdPartyNoticesOption)) + { + CliInfoPrinter.PrintThirdPartyNotices(io); + return 0; + } - case TestArguments testArguments when testArguments.CommandName == Constants.Command.Test: // bicep test [options] - return await services.GetRequiredService().RunAsync(testArguments); + await io.Error.Writer.WriteLineAsync( + string.Format(CliResources.UnrecognizedArgumentsFormat, string.Join(' ', unmatched), ThisAssembly.AssemblyName)); + return 1; + }); + + rootCommand.Add(CreateBuildCommand()); + + rootCommand.Add(CreateTestCommand()); + + rootCommand.Add(CreateBuildParamsCommand()); + + rootCommand.Add(CreateFormatCommand()); + + rootCommand.Add(CreateGenerateParamsFileCommand()); + + rootCommand.Add(CreateDecompileCommand()); + + rootCommand.Add(CreateDecompileParamsCommand()); + + rootCommand.Add(CreatePublishCommand()); + + rootCommand.Add(CreatePublishExtensionCommand()); + + rootCommand.Add(CreateRestoreCommand()); + + rootCommand.Add(CreateLintCommand()); + + rootCommand.Add(CreateJsonRpcCommand()); + + rootCommand.Add(CreateLocalDeployCommand()); + + rootCommand.Add(CreateSnapshotCommand()); + + rootCommand.Add(CreateDeployCommand()); + + rootCommand.Add(CreateWhatIfCommand()); + + rootCommand.Add(CreateTeardownCommand()); + + rootCommand.Add(CreateConsoleCommand()); + + return rootCommand; + } + + private Command CreateBuildCommand() + { + var command = new Command(Constants.Command.Build, "Builds a .bicep file.") + { + TreatUnmatchedTokensAsErrors = true, + }; - case BuildParamsArguments buildParamsArguments when buildParamsArguments.CommandName == Constants.Command.BuildParams: // bicep build-params [options] - return await services.GetRequiredService().RunAsync(buildParamsArguments); + var inputFileArgument = new Argument(Constants.Argument.InputFile) + { + Description = "The path to the input .bicep file.", + Arity = ArgumentArity.ZeroOrOne, + }; + var stdoutOption = new Option(Constants.Option.Stdout) + { + Description = "Prints the output to stdout.", + }; + var noRestoreOption = new Option(Constants.Option.NoRestore) + { + Description = "Builds the bicep file without restoring external modules.", + }; + var outDirOption = new Option(Constants.Option.OutDir) + { + Description = "Saves the output at the specified directory.", + }; + var outFileOption = new Option(Constants.Option.OutFile) + { + Description = "Saves the output as the specified file path.", + }; + var filePatternOption = new Option(Constants.Option.Pattern) + { + Description = "Builds all files matching the specified glob pattern.", + }; + var diagnosticsFormatOption = new Option(Constants.Option.DiagnosticsFormat) + { + Description = "Sets the diagnostics format. Valid values are (Default, SARIF).", + }; - case FormatArguments formatArguments when formatArguments.CommandName == Constants.Command.Format: // bicep format [options] - return services.GetRequiredService().Run(formatArguments); + command.Add(inputFileArgument); + command.Add(stdoutOption); + command.Add(noRestoreOption); + command.Add(outDirOption); + command.Add(outFileOption); + command.Add(filePatternOption); + command.Add(diagnosticsFormatOption); + command.Validators.Add(result => ValidatePositionalArgument(result, inputFileArgument)); - case GenerateParametersFileArguments generateParametersFileArguments when generateParametersFileArguments.CommandName == Constants.Command.GenerateParamsFile: // bicep generate-params [options] - return await services.GetRequiredService().RunAsync(generateParametersFileArguments); + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var outputToStdOut = result.GetValue(stdoutOption); + var outputDir = result.GetValue(outDirOption); + var outputFile = result.GetValue(outFileOption); + var filePattern = result.GetValue(filePatternOption); - case DecompileArguments decompileArguments when decompileArguments.CommandName == Constants.Command.Decompile: // bicep decompile [options] - return await services.GetRequiredService().RunAsync(decompileArguments); + ArgumentHelper.ValidateOutputOptions(outputToStdOut, outputDir, outputFile, filePattern); - case DecompileParamsArguments decompileParamsArguments when decompileParamsArguments.CommandName == Constants.Command.DecompileParams: - return services.GetRequiredService().Run(decompileParamsArguments); + var args = new BuildArguments( + result.GetValue(inputFileArgument), + outputToStdOut, + result.GetValue(noRestoreOption), + outputDir, + outputFile, + filePattern, + result.GetValue(diagnosticsFormatOption)); - case PublishArguments publishArguments when publishArguments.CommandName == Constants.Command.Publish: // bicep publish [options] - return await services.GetRequiredService().RunAsync(publishArguments); + return await services.GetRequiredService().RunAsync(args); + })); - case PublishExtensionArguments publishProviderArguments when publishProviderArguments.CommandName == Constants.Command.PublishExtension: // bicep publish-extension [options] - return await services.GetRequiredService().RunAsync(publishProviderArguments, cancellationToken); + return command; + } - case RestoreArguments restoreArguments when restoreArguments.CommandName == Constants.Command.Restore: // bicep restore - return await services.GetRequiredService().RunAsync(restoreArguments); + private Command CreateTestCommand() + { + var command = new Command(Constants.Command.Test, "Runs tests in a .bicep file.") + { + TreatUnmatchedTokensAsErrors = true, + }; - case LintArguments lintArguments when lintArguments.CommandName == Constants.Command.Lint: // bicep lint [options] - return await services.GetRequiredService().RunAsync(lintArguments); + var inputFileArgument = new Argument(Constants.Argument.InputFile) + { + Description = "The path to the input .bicep file.", + Arity = ArgumentArity.ZeroOrOne, + }; + var noRestoreOption = new Option(Constants.Option.NoRestore) + { + Description = "Do not restore modules prior to running tests.", + }; + var diagnosticsFormatOption = new Option(Constants.Option.DiagnosticsFormat) + { + Description = "Set the format of diagnostics (Default, SARIF).", + }; - case JsonRpcArguments jsonRpcArguments when jsonRpcArguments.CommandName == Constants.Command.JsonRpc: // bicep jsonrpc [options] - return await services.GetRequiredService().RunAsync(jsonRpcArguments, cancellationToken); + command.Add(inputFileArgument); + command.Add(noRestoreOption); + command.Add(diagnosticsFormatOption); + command.Validators.Add(result => ValidatePositionalArgument(result, inputFileArgument)); - case LocalDeployArguments localDeployArguments when localDeployArguments.CommandName == Constants.Command.LocalDeploy: // bicep local-deploy [options] - return await services.GetRequiredService().RunAsync(localDeployArguments, cancellationToken); + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var inputFile = result.GetValue(inputFileArgument) + ?? throw new CommandLineException("The input file path was not specified"); + var args = new TestArguments( + inputFile, + result.GetValue(noRestoreOption), + result.GetValue(diagnosticsFormatOption)); - case SnapshotArguments snapshotArguments when snapshotArguments.CommandName == Constants.Command.Snapshot: // bicep snapshot [options] - return await services.GetRequiredService().RunAsync(snapshotArguments, cancellationToken); + return await services.GetRequiredService().RunAsync(args); + })); - case DeployArguments deployArguments when deployArguments.CommandName == Constants.Command.Deploy: // bicep deploy [options] - return await services.GetRequiredService().RunAsync(deployArguments, cancellationToken); + return command; + } - case WhatIfArguments whatIfArguments when whatIfArguments.CommandName == Constants.Command.WhatIf: // bicep what-if [options] - return await services.GetRequiredService().RunAsync(whatIfArguments, cancellationToken); + private Command CreateBuildParamsCommand() + { + var command = new Command(Constants.Command.BuildParams, "Builds a .json file from a .bicepparam file.") + { + TreatUnmatchedTokensAsErrors = true, + }; - case TeardownArguments teardownArguments when teardownArguments.CommandName == Constants.Command.Teardown: // bicep teardown [options] - return await services.GetRequiredService().RunAsync(teardownArguments, cancellationToken); + var inputFileArgument = new Argument(Constants.Argument.InputFile) + { + Description = "The path to the input .bicepparam file.", + Arity = ArgumentArity.ZeroOrOne, + }; + var stdoutOption = new Option(Constants.Option.Stdout) + { + Description = "Prints the output of building both the parameter file (.bicepparam) and the template it points to (.bicep) as json to stdout.", + }; + var noRestoreOption = new Option(Constants.Option.NoRestore) + { + Description = "Builds the bicep file (referenced in using declaration) without restoring external modules.", + }; + var outDirOption = new Option(Constants.Option.OutDir) + { + Description = "Saves the output of building the parameter file only (.bicepparam) as json to the specified directory.", + }; + var outFileOption = new Option(Constants.Option.OutFile) + { + Description = "Saves the output of building the parameter file only (.bicepparam) as json to the specified file path.", + }; + var filePatternOption = new Option(Constants.Option.Pattern) + { + Description = "Builds all files matching the specified glob pattern.", + }; + var bicepFileOption = new Option(Constants.Option.BicepFile) + { + Description = "Verifies if the specified bicep file path matches the one provided in the params file using declaration.", + }; + var diagnosticsFormatOption = new Option(Constants.Option.DiagnosticsFormat) + { + Description = "Sets the diagnostics format. Valid values are (Default, SARIF).", + }; - case ConsoleArguments consoleArguments when consoleArguments.CommandName == Constants.Command.Console: // bicep console - return await services.GetRequiredService().RunAsync(consoleArguments); + command.Add(inputFileArgument); + command.Add(stdoutOption); + command.Add(noRestoreOption); + command.Add(outDirOption); + command.Add(outFileOption); + command.Add(filePatternOption); + command.Add(bicepFileOption); + command.Add(diagnosticsFormatOption); + command.Validators.Add(result => ValidatePositionalArgument(result, inputFileArgument)); - case RootArguments rootArguments when rootArguments.CommandName == Constants.Command.Root: // bicep [options] - return services.GetRequiredService().Run(rootArguments); + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var outputToStdOut = result.GetValue(stdoutOption); + var outputDir = result.GetValue(outDirOption); + var outputFile = result.GetValue(outFileOption); + var filePattern = result.GetValue(filePatternOption); + var bicepFile = result.GetValue(bicepFileOption); - default: - await io.Error.Writer.WriteLineAsync(string.Format(CliResources.UnrecognizedArgumentsFormat, string.Join(' ', args), ThisAssembly.AssemblyName)); // should probably print help here?? - return 1; + if (filePattern is not null && bicepFile is not null) + { + throw new CommandLineException("The --bicep-file parameter cannot be used with the --pattern parameter"); } + ArgumentHelper.ValidateOutputOptions(outputToStdOut, outputDir, outputFile, filePattern); + + var args = new BuildParamsArguments( + result.GetValue(inputFileArgument), + outputToStdOut, + result.GetValue(noRestoreOption), + outputDir, + outputFile, + filePattern, + bicepFile, + result.GetValue(diagnosticsFormatOption)); + + return await services.GetRequiredService().RunAsync(args); + })); + + return command; + } + + private Command CreateGenerateParamsFileCommand() + { + var command = new Command(Constants.Command.GenerateParamsFile, "Builds parameters file from the given bicep file, updates if there is an existing parameters file.") + { + TreatUnmatchedTokensAsErrors = true, + }; + + var inputFileArgument = new Argument(Constants.Argument.InputFile) + { + Description = "The path to the input .bicep file.", + Arity = ArgumentArity.ZeroOrOne, + }; + var stdoutOption = new Option(Constants.Option.Stdout) + { + Description = "Prints the output to stdout.", + }; + var noRestoreOption = new Option(Constants.Option.NoRestore) + { + Description = "Generates the parameters file without restoring external modules.", + }; + var outDirOption = new Option(Constants.Option.OutDir) + { + Description = "Saves the output at the specified directory.", + }; + var outFileOption = new Option(Constants.Option.OutFile) + { + Description = "Saves the output as the specified file path.", + }; + var outputFormatOption = new Option(Constants.Option.OutputFormat) + { + Description = "Selects the output format (json, bicepparam).", + }; + var includeParamsOption = new Option(Constants.Option.IncludeParams) + { + Description = "Selects which parameters to include into output (requiredonly, all).", + }; + + command.Add(inputFileArgument); + command.Add(stdoutOption); + command.Add(noRestoreOption); + command.Add(outDirOption); + command.Add(outFileOption); + command.Add(outputFormatOption); + command.Add(includeParamsOption); + command.Validators.Add(result => ValidatePositionalArgument(result, inputFileArgument)); + + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var outputToStdOut = result.GetValue(stdoutOption); + var outputDir = result.GetValue(outDirOption); + var outputFile = result.GetValue(outFileOption); + + ArgumentHelper.ValidateOutputOptions(outputToStdOut, outputDir, outputFile); + + var inputFile = result.GetValue(inputFileArgument) + ?? throw new CommandLineException("The input file path was not specified"); + var args = new GenerateParametersFileArguments( + inputFile, + outputToStdOut, + result.GetValue(noRestoreOption), + outputDir, + outputFile, + result.GetValue(outputFormatOption), + result.GetValue(includeParamsOption)); + + return await services.GetRequiredService().RunAsync(args); + })); + + return command; + } + + private Command CreateDecompileCommand() + { + var command = new Command(Constants.Command.Decompile, "Attempts to decompile a template .json file to .bicep.") + { + TreatUnmatchedTokensAsErrors = true, + }; + + var inputFileArgument = new Argument(Constants.Argument.InputFile) + { + Description = "The path to the ARM template .json file.", + Arity = ArgumentArity.ZeroOrOne, + }; + var stdoutOption = new Option(Constants.Option.Stdout) + { + Description = "Prints the output to stdout.", + }; + var forceOption = new Option(Constants.Option.Force) + { + Description = "Allows overwriting the output file if it exists (applies only to 'bicep decompile' or 'bicep decompile-params').", + }; + var outDirOption = new Option(Constants.Option.OutDir) + { + Description = "Saves the output at the specified directory.", + }; + var outFileOption = new Option(Constants.Option.OutFile) + { + Description = "Saves the output as the specified file path.", + }; + + command.Add(inputFileArgument); + command.Add(stdoutOption); + command.Add(forceOption); + command.Add(outDirOption); + command.Add(outFileOption); + command.Validators.Add(result => ValidatePositionalArgument(result, inputFileArgument)); + + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var outputToStdOut = result.GetValue(stdoutOption); + var outputDir = result.GetValue(outDirOption); + var outputFile = result.GetValue(outFileOption); + + ArgumentHelper.ValidateOutputOptions(outputToStdOut, outputDir, outputFile); + + var inputFile = result.GetValue(inputFileArgument) + ?? throw new CommandLineException("The input file path was not specified"); + var args = new DecompileArguments( + inputFile, + outputToStdOut, + result.GetValue(forceOption), + outputDir, + outputFile); + + return await services.GetRequiredService().RunAsync(args); + })); + + return command; + } + + private Command CreateDecompileParamsCommand() + { + var command = new Command(Constants.Command.DecompileParams, "Attempts to decompile a parameters .json file to .bicepparam.") + { + TreatUnmatchedTokensAsErrors = true, + }; + + var inputFileArgument = new Argument(Constants.Argument.InputFile) + { + Description = "The path to the parameters .json file.", + }; + var stdoutOption = new Option(Constants.Option.Stdout) + { + Description = "Prints the output to stdout.", + }; + var forceOption = new Option(Constants.Option.Force) + { + Description = "Allows overwriting the output file if it exists (applies only to 'bicep decompile' or 'bicep decompile-params').", + }; + var outDirOption = new Option(Constants.Option.OutDir) + { + Description = "Saves the output at the specified directory.", + }; + var outFileOption = new Option(Constants.Option.OutFile) + { + Description = "Saves the output as the specified file path.", + }; + var bicepFileOption = new Option(Constants.Option.BicepFile) + { + Description = "Path to the bicep template file that will be referenced in the using declaration.", + }; + + command.Add(inputFileArgument); + command.Add(stdoutOption); + command.Add(forceOption); + command.Add(outDirOption); + command.Add(outFileOption); + command.Add(bicepFileOption); + command.Validators.Add(result => ValidateRequiredPositionalArgument(result, inputFileArgument)); + + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var outputToStdOut = result.GetValue(stdoutOption); + var outputDir = result.GetValue(outDirOption); + var outputFile = result.GetValue(outFileOption); + + ArgumentHelper.ValidateOutputOptions(outputToStdOut, outputDir, outputFile); + + var args = new DecompileParamsArguments( + result.GetRequiredValue(inputFileArgument), + outputToStdOut, + result.GetValue(forceOption), + outputDir, + outputFile, + result.GetValue(bicepFileOption)); + + return await Task.FromResult(services.GetRequiredService().Run(args)); + })); + + return command; + } + + private Command CreateFormatCommand() + { + var command = new Command(Constants.Command.Format, "Formats a .bicep file."); + + var inputFileArgument = new Argument(Constants.Argument.InputFile) + { + Description = "The path to the input .bicep file.", + Arity = ArgumentArity.ZeroOrOne, + }; + var stdoutOption = new Option(Constants.Option.Stdout) + { + Description = "Prints the output to stdout.", + }; + var outDirOption = new Option(Constants.Option.OutDir) + { + Description = "Saves the output at the specified directory.", + }; + var outFileOption = new Option(Constants.Option.OutFile) + { + Description = "Saves the output as the specified file path.", + }; + var filePatternOption = new Option(Constants.Option.Pattern) + { + Description = "Formats all files matching the specified glob pattern.", + }; + var newlineKindOption = new Option(Constants.Option.NewlineKind) + { + Description = "Set newline char. Valid values are (Auto, LF, CRLF, CR).", + }; + var indentKindOption = new Option(Constants.Option.IndentKind) + { + Description = "Set indentation kind. Valid values are (Space, Tab).", + }; + var indentSizeOption = new Option(Constants.Option.IndentSize) + { + Description = "Number of spaces to indent with (only valid with --indent-kind set to Space).", + }; + var insertFinalNewlineOption = new Option(Constants.Option.InsertFinalNewline) + { + Description = "Insert a final newline.", + }; + + command.Add(inputFileArgument); + command.Add(stdoutOption); + command.Add(outDirOption); + command.Add(outFileOption); + command.Add(filePatternOption); + command.Add(newlineKindOption); + command.Add(indentKindOption); + command.Add(indentSizeOption); + command.Add(insertFinalNewlineOption); + command.Validators.Add(result => ValidatePositionalArgument(result, inputFileArgument)); + + command.SetAction((result, ct) => RunCommandAsync(() => + { + var args = new FormatArguments( + result.GetValue(stdoutOption), + result.GetValue(inputFileArgument), + result.GetValue(outDirOption), + result.GetValue(outFileOption), + result.GetValue(filePatternOption), + result.GetValue(newlineKindOption), + result.GetValue(indentKindOption), + result.GetValue(indentSizeOption), + result.GetValue(insertFinalNewlineOption)); + + return Task.FromResult(services.GetRequiredService().Run(args)); + })); + + return command; + } + + private Command CreatePublishCommand() + { + var command = new Command(Constants.Command.Publish, "Publishes the .bicep file to the module registry."); + + var inputFileArgument = new Argument(Constants.Argument.InputFile) + { + Description = "The path to the .bicep file to publish.", + Arity = ArgumentArity.ZeroOrOne, + }; + var targetOption = new Option(Constants.Option.Target) + { + Description = "The target module reference.", + }; + var documentationUriOption = new Option(Constants.Option.DocumentationUri) + { + Description = "Module documentation URI.", + Arity = ArgumentArity.ZeroOrMore, + }; + var noRestoreOption = new Option(Constants.Option.NoRestore) + { + Description = "Do not restore modules prior to publishing.", + }; + var forceOption = new Option(Constants.Option.Force) + { + Description = "Overwrite existing published module or file.", + }; + var withSourceOption = new Option(Constants.Option.WithSource) + { + Description = "[Experimental] Publish source code with the module.", + }; + + command.Add(inputFileArgument); + command.Add(targetOption); + command.Add(documentationUriOption); + command.Add(noRestoreOption); + command.Add(forceOption); + command.Add(withSourceOption); + command.Validators.Add(result => ValidatePositionalArgument(result, inputFileArgument)); + + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var inputFile = result.GetValue(inputFileArgument) + ?? throw new CommandLineException("The input file path was not specified"); + var target = result.GetValue(targetOption) + ?? throw new CommandLineException("The target module was not specified."); + var docUriResult = result.GetResult(documentationUriOption); + if (docUriResult is not null) + { + if (docUriResult.Tokens.Count == 0) + { + throw new CommandLineException("The --documentation-uri parameter expects an argument."); + } + if (docUriResult.Tokens.Count > 1) + { + throw new CommandLineException("The --documentation-uri parameter cannot be specified more than once."); + } + } + var documentationUri = docUriResult?.Tokens.Count == 1 ? docUriResult.Tokens[0].Value : null; + if (documentationUri is not null && !Uri.IsWellFormedUriString(documentationUri, UriKind.Absolute)) + { + throw new CommandLineException("The --documentation-uri should be a well formed uri string."); + } + var args = new PublishArguments( + inputFile, + target, + documentationUri, + result.GetValue(noRestoreOption), + result.GetValue(forceOption), + result.GetValue(withSourceOption)); + + return await services.GetRequiredService().RunAsync(args); + })); + + return command; + } + + private Command CreatePublishExtensionCommand() + { + var command = new Command(Constants.Command.PublishExtension, "[Experimental] Publishes a Bicep extension to a registry."); + + var indexFileArgument = new Argument(Constants.Argument.IndexFile) + { + Description = "The path to the index file.", + Arity = ArgumentArity.ZeroOrOne, + }; + var targetOption = new Option(Constants.Option.Target) + { + Description = "The target extension reference.", + }; + var forceOption = new Option(Constants.Option.Force) + { + Description = "Force publish even if the extension already exists.", + }; + // Per-architecture binary options. + var binLinuxX64Option = new Option(Constants.Option.BinLinuxX64) { Description = "Path to the linux-x64 binary." }; + var binLinuxArm64Option = new Option(Constants.Option.BinLinuxArm64) { Description = "Path to the linux-arm64 binary." }; + var binOsxX64Option = new Option(Constants.Option.BinOsxX64) { Description = "Path to the osx-x64 binary." }; + var binOsxArm64Option = new Option(Constants.Option.BinOsxArm64) { Description = "Path to the osx-arm64 binary." }; + var binWinX64Option = new Option(Constants.Option.BinWinX64) { Description = "Path to the win-x64 binary." }; + var binWinArm64Option = new Option(Constants.Option.BinWinArm64) { Description = "Path to the win-arm64 binary." }; + + command.Add(indexFileArgument); + command.Add(targetOption); + command.Add(forceOption); + command.Add(binLinuxX64Option); + command.Add(binLinuxArm64Option); + command.Add(binOsxX64Option); + command.Add(binOsxArm64Option); + command.Add(binWinX64Option); + command.Add(binWinArm64Option); + command.Validators.Add(result => ValidatePositionalArgument(result, indexFileArgument)); + + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var binaries = new Dictionary(); + if (result.GetValue(binLinuxX64Option) is { } p1) { binaries["linux-x64"] = p1; } + if (result.GetValue(binLinuxArm64Option) is { } p2) { binaries["linux-arm64"] = p2; } + if (result.GetValue(binOsxX64Option) is { } p3) { binaries["osx-x64"] = p3; } + if (result.GetValue(binOsxArm64Option) is { } p4) { binaries["osx-arm64"] = p4; } + if (result.GetValue(binWinX64Option) is { } p5) { binaries["win-x64"] = p5; } + if (result.GetValue(binWinArm64Option) is { } p6) { binaries["win-arm64"] = p6; } + + var args = new PublishExtensionArguments( + result.GetValue(indexFileArgument), + result.GetValue(targetOption), + binaries, + result.GetValue(forceOption)); + + return await services.GetRequiredService().RunAsync(args, ct); + })); + + return command; + } + + private Command CreateRestoreCommand() + { + var command = new Command(Constants.Command.Restore, "Restores external modules from the specified Bicep file to the local module cache."); + + var inputFileArgument = new Argument(Constants.Argument.InputFile) + { + Description = "The path to the .bicep file.", + Arity = ArgumentArity.ZeroOrOne, + }; + var filePatternOption = new Option(Constants.Option.Pattern) + { + Description = "Restores all files matching the specified glob pattern.", + }; + var forceOption = new Option(Constants.Option.Force) + { + Description = "Force restore even if modules are already cached.", + }; + + command.Add(inputFileArgument); + command.Add(filePatternOption); + command.Add(forceOption); + command.Validators.Add(result => ValidatePositionalArgument(result, inputFileArgument)); + + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var args = new RestoreArguments( + result.GetValue(inputFileArgument), + result.GetValue(filePatternOption), + result.GetValue(forceOption)); + + return await services.GetRequiredService().RunAsync(args); + })); + + return command; + } + + private Command CreateLintCommand() + { + var command = new Command(Constants.Command.Lint, "Lints a .bicep file."); + + var inputFileArgument = new Argument(Constants.Argument.InputFile) + { + Description = "The path to the .bicep file.", + Arity = ArgumentArity.ZeroOrOne, + }; + var filePatternOption = new Option(Constants.Option.Pattern) + { + Description = "Lints all files matching the specified glob pattern.", + }; + var noRestoreOption = new Option(Constants.Option.NoRestore) + { + Description = "Skips restoring external modules.", + }; + var diagnosticsFormatOption = new Option(Constants.Option.DiagnosticsFormat) + { + Description = "Sets the diagnostics format. Valid values are (Default, SARIF).", + }; + + command.Add(inputFileArgument); + command.Add(filePatternOption); + command.Add(noRestoreOption); + command.Add(diagnosticsFormatOption); + command.Validators.Add(result => ValidatePositionalArgument(result, inputFileArgument)); + + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var diagnosticsFormat = result.GetValue(diagnosticsFormatOption) ?? Arguments.DiagnosticsFormat.Default; + var args = new LintArguments( + result.GetValue(inputFileArgument), + result.GetValue(filePatternOption), + diagnosticsFormat, + result.GetValue(noRestoreOption)); + + return await services.GetRequiredService().RunAsync(args); + })); + + return command; + } + + private Command CreateJsonRpcCommand() + { + var command = new Command(Constants.Command.JsonRpc, "Starts the Bicep CLI listening for JSONRPC messages, for programatically interacting with Bicep."); + + var pipeOption = new Option(Constants.Option.Pipe) + { + Description = "Bicep CLI will connect to the supplied named pipe as a client, and start listening for JSONRPC requests.", + }; + var socketOption = new Option(Constants.Option.Socket) + { + Description = "Bicep CLI will connect to the supplied TCP port on the loopback interface as a client, and start listening for JSONRPC requests.", + }; + var stdioOption = new Option(Constants.Option.Stdio) + { + Description = "Bicep CLI will use stdin/stdout for JSONRPC requests.", + }; + + command.Add(pipeOption); + command.Add(socketOption); + command.Add(stdioOption); + + command.Validators.Add(result => + { + var hasPipe = result.GetResult(pipeOption) is { Implicit: false }; + var hasSocket = result.GetResult(socketOption) is { Implicit: false }; + var hasStdio = result.GetResult(stdioOption) is { Implicit: false }; + + if ((hasPipe ? 1 : 0) + (hasSocket ? 1 : 0) + (hasStdio ? 1 : 0) > 1) + { + result.AddError("Only one of --pipe, --socket, or --stdio may be specified."); + } + }); + + command.SetAction(async (result, cancellationToken) => + { + JsonRpcArguments args = new( + Pipe: result.GetValue(pipeOption), + Socket: result.GetValue(socketOption), + Stdio: result.GetValue(stdioOption)); + + return await services.GetRequiredService().RunAsync(args, cancellationToken); + }); + + return command; + } + + private Command CreateDeployCommand() + { + var command = new Command(Constants.Command.Deploy, "[Experimental] Deploys infrastructure using a .bicepparam file.") + { + TreatUnmatchedTokensAsErrors = false, + }; + + var inputFileArgument = new Argument(Constants.Argument.ParametersFile) + { + Description = "The path to the .bicepparam file.", + }; + var noRestoreOption = new Option(Constants.Option.NoRestore) + { + Description = "Do not restore modules prior to deploying.", + }; + var formatOption = new Option(Constants.Option.Format) + { + Description = "Output format for deployment results (Default, Json).", + }; + + command.Add(inputFileArgument); + command.Add(noRestoreOption); + command.Add(formatOption); + command.Validators.Add(result => ValidateRequiredPositionalArgument(result, inputFileArgument)); + + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var additionalArguments = ParseAdditionalArguments(result.UnmatchedTokens); + var args = new DeployArguments( + result.GetRequiredValue(inputFileArgument), + result.GetValue(noRestoreOption), + additionalArguments, + result.GetValue(formatOption)); + + return await services.GetRequiredService().RunAsync(args, ct); + })); + + return command; + } + + private Command CreateWhatIfCommand() + { + var command = new Command(Constants.Command.WhatIf, "[Experimental] Previews the changes a deployment would make.") + { + TreatUnmatchedTokensAsErrors = false, + }; + + var inputFileArgument = new Argument(Constants.Argument.ParametersFile) + { + Description = "The path to the .bicepparam file.", + }; + var noRestoreOption = new Option(Constants.Option.NoRestore) + { + Description = "Do not restore modules prior to running what-if.", + }; + + command.Add(inputFileArgument); + command.Add(noRestoreOption); + command.Validators.Add(result => ValidateRequiredPositionalArgument(result, inputFileArgument)); + + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var additionalArguments = ParseAdditionalArguments(result.UnmatchedTokens); + var args = new WhatIfArguments( + result.GetRequiredValue(inputFileArgument), + result.GetValue(noRestoreOption), + additionalArguments); + + return await services.GetRequiredService().RunAsync(args, ct); + })); + + return command; + } + + private Command CreateTeardownCommand() + { + var command = new Command(Constants.Command.Teardown, "[Experimental] Tears down resources deployed by a .bicepparam file.") + { + TreatUnmatchedTokensAsErrors = false, + }; + + var inputFileArgument = new Argument(Constants.Argument.ParametersFile) + { + Description = "The path to the .bicepparam file.", + }; + var noRestoreOption = new Option(Constants.Option.NoRestore) + { + Description = "Do not restore modules prior to tearing down.", + }; + + command.Add(inputFileArgument); + command.Add(noRestoreOption); + command.Validators.Add(result => ValidateRequiredPositionalArgument(result, inputFileArgument)); + + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var additionalArguments = ParseAdditionalArguments(result.UnmatchedTokens); + var args = new TeardownArguments( + result.GetRequiredValue(inputFileArgument), + result.GetValue(noRestoreOption), + additionalArguments); + + return await services.GetRequiredService().RunAsync(args, ct); + })); + + return command; + } + + private Command CreateLocalDeployCommand() + { + var command = new Command(Constants.Command.LocalDeploy, "[Experimental] Performs a local deployment."); + + var inputFileArgument = new Argument(Constants.Argument.ParametersFile) + { + Description = "The path to the .bicepparam file.", + }; + var noRestoreOption = new Option(Constants.Option.NoRestore) + { + Description = "Do not restore modules prior to deploying.", + }; + var formatOption = new Option(Constants.Option.Format) + { + Description = "Output format for deployment results (Default, Json).", + }; + + command.Add(inputFileArgument); + command.Add(noRestoreOption); + command.Add(formatOption); + command.Validators.Add(result => ValidateRequiredPositionalArgument(result, inputFileArgument)); + + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var args = new LocalDeployArguments( + result.GetRequiredValue(inputFileArgument), + result.GetValue(noRestoreOption), + result.GetValue(formatOption)); + + return await services.GetRequiredService().RunAsync(args, ct); + })); + + return command; + } + + private Command CreateSnapshotCommand() + { + var command = new Command(Constants.Command.Snapshot, "Generates or validates a deployment snapshot from a .bicepparam file."); + + var inputFileArgument = new Argument(Constants.Argument.ParametersFile) + { + Description = "The path to the .bicepparam file.", + }; + var modeOption = new Option(Constants.Option.Mode) + { + Description = "Sets the snapshot mode. Valid values are (overwrite, validate).", + }; + var tenantIdOption = new Option(Constants.Option.TenantId) + { + Description = "The tenant ID to use for the deployment.", + }; + var subscriptionIdOption = new Option(Constants.Option.SubscriptionId) + { + Description = "The subscription ID to use for the deployment.", + }; + var locationOption = new Option(Constants.Option.Location) + { + Description = "The location to use for the deployment.", + }; + var resourceGroupOption = new Option(Constants.Option.ResourceGroup) + { + Description = "The resource group name to use for the deployment.", + }; + var managementGroupIdOption = new Option(Constants.Option.ManagementGroupId) + { + Description = "The management group ID to use for the deployment.", + }; + var deploymentNameOption = new Option(Constants.Option.DeploymentName) + { + Description = "The deployment name to use.", + }; + + command.Add(inputFileArgument); + command.Add(modeOption); + command.Add(tenantIdOption); + command.Add(subscriptionIdOption); + command.Add(managementGroupIdOption); + command.Add(locationOption); + command.Add(resourceGroupOption); + command.Add(deploymentNameOption); + command.Validators.Add(result => ValidateRequiredPositionalArgument(result, inputFileArgument)); + + command.SetAction((result, ct) => RunCommandAsync(async () => + { + var args = new SnapshotArguments( + result.GetRequiredValue(inputFileArgument), + result.GetValue(modeOption), + result.GetValue(tenantIdOption), + result.GetValue(subscriptionIdOption), + result.GetValue(managementGroupIdOption), + result.GetValue(locationOption), + result.GetValue(resourceGroupOption), + result.GetValue(deploymentNameOption)); + + return await services.GetRequiredService().RunAsync(args, ct); + })); + + return command; + } + + private Command CreateConsoleCommand() + { + var command = new Command(Constants.Command.Console, "Opens an interactive Bicep console."); + + command.SetAction((result, ct) => RunCommandAsync( + () => services.GetRequiredService().RunAsync(new ConsoleArguments()))); + + return command; + } + + private static void ValidatePositionalArgument(CommandResult result, Argument argument) + { + if (result.GetValue(argument) is {} inputValue && inputValue.StartsWith("--", StringComparison.Ordinal)) + { + result.AddError($"Unrecognized parameter \"{inputValue}\""); + } + } + + private static void ValidateRequiredPositionalArgument(CommandResult result, Argument argument) + { + if (result.GetValue(argument) is {} inputValue && inputValue.StartsWith("--", StringComparison.Ordinal)) + { + result.AddError($"Unrecognized parameter \"{inputValue}\""); + } + } + + private async Task RunCommandAsync(Func> action) + { + try + { + return await action(); } catch (BicepException exception) { @@ -154,6 +1157,34 @@ public async Task RunAsync(string[] args, CancellationToken cancellationTok } } + private static ImmutableDictionary ParseAdditionalArguments(IReadOnlyList unmatchedTokens) + { + var additionalArguments = new Dictionary(); + for (var i = 0; i < unmatchedTokens.Count; i++) + { + var token = unmatchedTokens[i]; + if (token.StartsWith(ArgumentConstants.CliArgPrefix, StringComparison.OrdinalIgnoreCase)) + { + var key = token[ArgumentConstants.CliArgPrefix.Length..]; + if (additionalArguments.ContainsKey(key)) + { + throw new CommandLineException($"Parameter \"{token}\" cannot be specified multiple times."); + } + if (i + 1 >= unmatchedTokens.Count) + { + throw new CommandLineException($"Parameter \"{token}\" requires a value."); + } + additionalArguments[key] = unmatchedTokens[++i]; + } + else + { + throw new CommandLineException($"Unrecognized parameter \"{token}\""); + } + } + + return additionalArguments.ToImmutableDictionary(); + } + private static ILoggerFactory CreateLoggerFactory(IOContext io) { // apparently logging requires a factory factory 🤦‍ diff --git a/src/Bicep.Cli/Services/ArgumentParser.cs b/src/Bicep.Cli/Services/ArgumentParser.cs deleted file mode 100644 index 7d8b464c536..00000000000 --- a/src/Bicep.Cli/Services/ArgumentParser.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.IO.Abstractions; -using System.Text.RegularExpressions; -using Bicep.Cli.Arguments; - -namespace Bicep.Cli.Services -{ - public static class ArgumentParser - { - public static ArgumentsBase? TryParse(string[] args, IFileSystem fileSystem) - { - if (args.Length < 1) - { - return null; - } - - // parse root arguments - if (new Regex(Constants.Argument.VersionRegex).IsMatch(args[0]) || - new Regex(Constants.Argument.HelpRegex).IsMatch(args[0]) || - new Regex(Constants.Argument.LicenseRegex).IsMatch(args[0]) || - new Regex(Constants.Argument.ThirdPartyNoticesRegex).IsMatch(args[0])) - { - return new RootArguments(args[0], Constants.Command.Root); - } - - // parse verb - return (args[0].ToLowerInvariant()) switch - { - Constants.Command.Build => new BuildArguments(args[1..]), - Constants.Command.Test => new TestArguments(args[1..]), - Constants.Command.BuildParams => new BuildParamsArguments(args[1..]), - Constants.Command.Format => new FormatArguments(args[1..]), - Constants.Command.GenerateParamsFile => new GenerateParametersFileArguments(args[1..]), - Constants.Command.Decompile => new DecompileArguments(args[1..]), - Constants.Command.DecompileParams => new DecompileParamsArguments(args[1..]), - Constants.Command.PublishExtension => new PublishExtensionArguments(args[1..]), - Constants.Command.Publish => new PublishArguments(args[1..]), - Constants.Command.Restore => new RestoreArguments(args[1..]), - Constants.Command.Lint => new LintArguments(args[1..]), - Constants.Command.JsonRpc => new JsonRpcArguments(args[1..]), - Constants.Command.LocalDeploy => new LocalDeployArguments(args[1..]), - Constants.Command.Snapshot => new SnapshotArguments(args[1..]), - Constants.Command.Deploy => new DeployArguments(args[1..]), - Constants.Command.WhatIf => new WhatIfArguments(args[1..]), - Constants.Command.Teardown => new TeardownArguments(args[1..]), - Constants.Command.Console => new ConsoleArguments(args[1..]), - _ => null, - }; - } - } -} diff --git a/src/Bicep.RegistryModuleTool.IntegrationTests/Commands/GenerateCommandTests.cs b/src/Bicep.RegistryModuleTool.IntegrationTests/Commands/GenerateCommandTests.cs index cd846398faf..d916166f1c1 100644 --- a/src/Bicep.RegistryModuleTool.IntegrationTests/Commands/GenerateCommandTests.cs +++ b/src/Bicep.RegistryModuleTool.IntegrationTests/Commands/GenerateCommandTests.cs @@ -5,6 +5,7 @@ using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using Bicep.RegistryModuleTool.Commands; +using Bicep.RegistryModuleTool.Extensions; using Bicep.RegistryModuleTool.ModuleFiles; using Bicep.RegistryModuleTool.TestFixtures.Assertions; using Bicep.RegistryModuleTool.TestFixtures.Extensions; @@ -25,7 +26,7 @@ public async Task InvokeAsync_OnSuccess_ReturnsZero(MockFileSystem fileSystemBef { var sut = CreateGenerateCommand(fileSystemBeforeGeneration); - var exitCode = await sut.InvokeAsync(""); + var exitCode = await sut.Parse("").InvokeAsync(); exitCode.Should().Be(0); } @@ -36,7 +37,7 @@ public async Task InvokeAsync_OnSuccess_ProducesExpectedFiles(MockFileSystem fil { var sut = CreateGenerateCommand(fileSystemBeforeGeneration); - await sut.InvokeAsync(""); + await sut.Parse("").InvokeAsync(); fileSystemBeforeGeneration.Should().HaveSameFilesAs(fileSystemAfterGeneration); } @@ -49,7 +50,7 @@ public async Task InvokeAsync_RepeatOnSuccess_ProducesSameFiles(MockFileSystem f for (int i = 0; i < 2; i++) { - await sut.InvokeAsync(""); + await sut.Parse("").InvokeAsync(); fileSystemBeforeGeneration.Should().HaveSameFilesAs(fileSystemAfterGeneration); } @@ -63,7 +64,7 @@ public async Task InvokeAsync_BicepBuildError_ReturnsOne() fileSystem.File.WriteAllText(MainBicepFile.FileName, "something"); - var exitCode = await sut.InvokeAsync(""); + var exitCode = await sut.Parse("").InvokeAsync(); exitCode.Should().Be(1); } @@ -72,15 +73,15 @@ public async Task InvokeAsync_BicepBuildError_ReturnsOne() public async Task InvokeAsync_BicepBuildError_PrintDiagnostics() { var fileSystem = MockFileSystemFactory.CreateForSample(Sample.Valid); - var sut = CreateGenerateCommand(fileSystem); var mainBicepFilePath = fileSystem.Path.GetFullPath(MainBicepFile.FileName); var console = new MockConsole().ExpectErrorLines( @$"{mainBicepFilePath}(1,1) : Error BCP007: This declaration type is not recognized. Specify a metadata, parameter, variable, resource, or output declaration. [https://aka.ms/bicep/core-diagnostics#BCP007]", @$"Failed to build ""{mainBicepFilePath}""."); + var sut = CreateGenerateCommand(fileSystem, console); fileSystem.File.WriteAllText(MainBicepFile.FileName, "something"); - await sut.InvokeAsync("", console); + await sut.Parse("").InvokeAsync(); console.Verify(); } @@ -98,7 +99,7 @@ static object[] CreateTestCase(Sample before, Sample after) => ]; } - private static GenerateCommand CreateGenerateCommand(IFileSystem fileSystem) + private static GenerateCommand CreateGenerateCommand(IFileSystem fileSystem, IConsole? console = null) { var serviceCollection = new ServiceCollection() .AddBicepCompilerWithFileSystem(fileSystem) @@ -107,11 +108,13 @@ private static GenerateCommand CreateGenerateCommand(IFileSystem fileSystem) var serviceProvider = serviceCollection.BuildServiceProvider(); var handler = serviceProvider.GetRequiredService(); + var effectiveConsole = console ?? new MockConsole(); - return new GenerateCommand() - { - Handler = handler, - }; + var command = new GenerateCommand(); + command.SetAction(async (ParseResult _, CancellationToken ct) => + await handler.InvokeAsync(effectiveConsole, ct)); + + return command; } } } diff --git a/src/Bicep.RegistryModuleTool.IntegrationTests/Commands/ValidateCommandTests.cs b/src/Bicep.RegistryModuleTool.IntegrationTests/Commands/ValidateCommandTests.cs index 182856254f3..54feaa810f5 100644 --- a/src/Bicep.RegistryModuleTool.IntegrationTests/Commands/ValidateCommandTests.cs +++ b/src/Bicep.RegistryModuleTool.IntegrationTests/Commands/ValidateCommandTests.cs @@ -3,7 +3,9 @@ using System.CommandLine; using System.IO.Abstractions; +using Bicep.Core.Parsing; using Bicep.RegistryModuleTool.Commands; +using Bicep.RegistryModuleTool.Extensions; using Bicep.RegistryModuleTool.ModuleFiles; using Bicep.RegistryModuleTool.TestFixtures.Extensions; using Bicep.RegistryModuleTool.TestFixtures.MockFactories; @@ -23,7 +25,7 @@ public async Task InvokeAsync_ValidFiles_ReturnsZero() var fileSystem = MockFileSystemFactory.CreateForSample(Sample.Valid); var sut = CreateValidateCommand(fileSystem); - var exitCode = await sut.InvokeAsync(""); + var exitCode = await sut.Parse("").InvokeAsync(); exitCode.Should().Be(0); } @@ -34,7 +36,7 @@ public async Task InvokeAsync_InvalidFiles_ReturnsOne() var fileSystem = MockFileSystemFactory.CreateForSample(Sample.Invalid); var sut = CreateValidateCommand(fileSystem); - var exitCode = await sut.InvokeAsync(""); + var exitCode = await sut.Parse("").InvokeAsync(); exitCode.Should().Be(1); } @@ -43,9 +45,8 @@ public async Task InvokeAsync_InvalidFiles_ReturnsOne() public async Task InvokeAsync_InvalidFiles_WritesErrorsToConsole() { var fileSystem = MockFileSystemFactory.CreateForSample(Sample.Invalid); - var sut = CreateValidateCommand(fileSystem); var testFile = MainBicepTestFile.Open(fileSystem); - var console = new MockConsole().ExpectErrorLines( + var console = new MockConsole().ExpectErrorLines(StringUtils.SplitOnNewLine( $""" The file "{fileSystem.Path.GetFullPath(MainBicepFile.FileName)}" is invalid: - A description must be specified for parameter "dnsPrefix". @@ -53,30 +54,24 @@ public async Task InvokeAsync_InvalidFiles_WritesErrorsToConsole() - A description must be specified for output "controlPlaneFQDN". - Metadata "description" must contain at least 10 characters. - """, - $""" The file "{testFile.Path}" is invalid: - Could not find tests in the file. Please make sure to add at least one module referencing the main Bicep file. - """, - $""" The file "{fileSystem.Path.GetFullPath(MainArmTemplateFile.FileName)}" is invalid: - The file is modified or outdated. Please run "brm generate" to regenerate it. - """, - $""" The file "{fileSystem.Path.GetFullPath(ReadmeFile.FileName)}" is invalid: - The file is modified or outdated. Please run "brm generate" to regenerate it. - """, - $""" The file "{fileSystem.Path.GetFullPath(VersionFile.FileName)}" is invalid: - #: Required properties ["$schema","version"] are not present. - The file is modified or outdated. Please run "brm generate" to regenerate it. - """); + """)); + + var sut = CreateValidateCommand(fileSystem, console); - await sut.InvokeAsync("", console); + await sut.Parse("").InvokeAsync(); console.Verify(); } @@ -85,12 +80,10 @@ public async Task InvokeAsync_InvalidFiles_WritesErrorsToConsole() public async Task InvokeAsync_BicepBuildError_ReturnsOne() { var fileSystem = MockFileSystemFactory.CreateForSample(Sample.Valid); - var sut = CreateValidateCommand(fileSystem); - var console = new MockConsole(); - fileSystem.File.WriteAllText(MainBicepFile.FileName, "something"); + var sut = CreateValidateCommand(fileSystem); - var exitCode = await sut.InvokeAsync("", console); + var exitCode = await sut.Parse("").InvokeAsync(); exitCode.Should().Be(1); } @@ -99,15 +92,14 @@ public async Task InvokeAsync_BicepBuildError_ReturnsOne() public async Task InvokeAsync_BicepBuildError_PrintDiagnostics() { var fileSystem = MockFileSystemFactory.CreateForSample(Sample.Valid); - var sut = CreateValidateCommand(fileSystem); var mainBicepFilePath = fileSystem.Path.GetFullPath(MainBicepFile.FileName); var console = new MockConsole().ExpectErrorLines( @$"{mainBicepFilePath}(1,1) : Error BCP007: This declaration type is not recognized. Specify a metadata, parameter, variable, resource, or output declaration. [https://aka.ms/bicep/core-diagnostics#BCP007]", @$"Failed to build ""{mainBicepFilePath}""."); - fileSystem.File.WriteAllText(MainBicepFile.FileName, "something"); + var sut = CreateValidateCommand(fileSystem, console); - await sut.InvokeAsync("", console); + await sut.Parse("").InvokeAsync(); console.Verify(); } @@ -116,13 +108,11 @@ public async Task InvokeAsync_BicepBuildError_PrintDiagnostics() public async Task InvokeAsync_BicepTestBuildError_ReturnsOne() { var fileSystem = MockFileSystemFactory.CreateForSample(Sample.Valid); - var sut = CreateValidateCommand(fileSystem); - var console = new MockConsole(); var bicepTestFile = MainBicepTestFile.Open(fileSystem); - fileSystem.File.WriteAllText(bicepTestFile.Path, "something"); + var sut = CreateValidateCommand(fileSystem); - var exitCode = await sut.InvokeAsync("", console); + var exitCode = await sut.Parse("").InvokeAsync(); exitCode.Should().Be(1); } @@ -131,21 +121,19 @@ public async Task InvokeAsync_BicepTestBuildError_ReturnsOne() public async Task InvokeAsync_BicepTestBuildError_PrintDiagnostics() { var fileSystem = MockFileSystemFactory.CreateForSample(Sample.Valid); - var sut = CreateValidateCommand(fileSystem); var bicepTestFile = MainBicepTestFile.Open(fileSystem); - fileSystem.File.WriteAllText(bicepTestFile.Path, "something"); - var console = new MockConsole().ExpectErrorLines( @$"{bicepTestFile.Path}(1,1) : Error BCP007: This declaration type is not recognized. Specify a metadata, parameter, variable, resource, or output declaration. [https://aka.ms/bicep/core-diagnostics#BCP007]", @$"Failed to build ""{bicepTestFile.Path}""."); + var sut = CreateValidateCommand(fileSystem, console); - await sut.InvokeAsync("", console); + await sut.Parse("").InvokeAsync(); console.Verify(); } - private static ValidateCommand CreateValidateCommand(IFileSystem fileSystem) + private static ValidateCommand CreateValidateCommand(IFileSystem fileSystem, IConsole? console = null) { var serviceCollection = new ServiceCollection() .AddBicepCompilerWithFileSystem(fileSystem) @@ -154,11 +142,13 @@ private static ValidateCommand CreateValidateCommand(IFileSystem fileSystem) var serviceProvider = serviceCollection.BuildServiceProvider(); var handler = serviceProvider.GetRequiredService(); + var effectiveConsole = console ?? new MockConsole(); + + var command = new ValidateCommand(); + command.SetAction(async (ParseResult _, CancellationToken ct) => + await handler.InvokeAsync(effectiveConsole, ct)); - return new ValidateCommand() - { - Handler = handler, - }; + return command; } } } diff --git a/src/Bicep.RegistryModuleTool.TestFixtures/Mocks/MockConsole.cs b/src/Bicep.RegistryModuleTool.TestFixtures/Mocks/MockConsole.cs index 7084a63be1f..7c94253b915 100644 --- a/src/Bicep.RegistryModuleTool.TestFixtures/Mocks/MockConsole.cs +++ b/src/Bicep.RegistryModuleTool.TestFixtures/Mocks/MockConsole.cs @@ -1,26 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.CommandLine; -using System.CommandLine.IO; +using Bicep.RegistryModuleTool.Extensions; using FluentAssertions; -using Moq; namespace Bicep.RegistryModuleTool.TestFixtures.Mocks { public class MockConsole : IConsole { - private readonly Mock outMock = StrictMock.Of(); + private readonly StringWriter outWriter = new(); - private readonly Mock errorMock = StrictMock.Of(); + private readonly StringWriter errorWriter = new(); private readonly List expectedOutLines = new(); private readonly List expectedErrorLines = new(); - private readonly List actualOutLines = new(); + public TextWriter Out => this.outWriter; - private readonly List actualErrorLines = new(); + public TextWriter Error => this.errorWriter; public MockConsole ExpectOutLines(params string[] expectedOutLines) => this.ExpectOutLines((IEnumerable)expectedOutLines); @@ -36,12 +34,7 @@ public MockConsole ExpectOutLines(IEnumerable expectedOutLines) public MockConsole ExpectOutLine(string expectedOutLine) { - this.expectedOutLines.Add(expectedOutLine + Environment.NewLine); - - this.outMock.Setup(x => x.Write(It.IsAny())).Callback(actualOutLine => - { - this.actualOutLines.Add(actualOutLine); - }); + this.expectedOutLines.Add(expectedOutLine); return this; } @@ -60,27 +53,31 @@ public MockConsole ExpectErrorLines(IEnumerable expectedErrorLines) public MockConsole ExpectErrorLine(string expectedErrorLine) { - this.expectedErrorLines.Add($"{expectedErrorLine.ReplaceLineEndings()}{Environment.NewLine}"); - - this.errorMock.Setup(x => x.Write(It.IsAny())).Callback(this.actualErrorLines.Add); + this.expectedErrorLines.Add(expectedErrorLine.ReplaceLineEndings()); return this; } - public IStandardStreamWriter Out => this.outMock.Object; - - public IStandardStreamWriter Error => this.errorMock.Object; - - public bool IsOutputRedirected => false; + public void Verify() + { + var actualOutLines = GetLines(this.outWriter); + var actualErrorLines = GetLines(this.errorWriter); + VerifyStringList(actualOutLines, this.expectedOutLines); + VerifyStringList(actualErrorLines, this.expectedErrorLines); + } - public bool IsErrorRedirected => false; + private static List GetLines(StringWriter writer) + { + var content = writer.ToString().ReplaceLineEndings(Environment.NewLine); + var lines = content.Split(Environment.NewLine).ToList(); - public bool IsInputRedirected => false; + // Remove trailing empty entry produced by a final newline + if (lines.Count > 0 && lines[^1] == string.Empty) + { + lines.RemoveAt(lines.Count - 1); + } - public void Verify() - { - VerifyStringList(this.actualOutLines, this.expectedOutLines); - VerifyStringList(this.actualErrorLines, this.expectedErrorLines); + return lines; } private static void VerifyStringList(List a, List b) @@ -99,3 +96,4 @@ private static void VerifyStringList(List a, List b) } } } + diff --git a/src/Bicep.RegistryModuleTool.UnitTests/ModuleFiles/BaseCommandHandlerTests.cs b/src/Bicep.RegistryModuleTool.UnitTests/ModuleFiles/BaseCommandHandlerTests.cs index 031eaad7321..6000161dfa6 100644 --- a/src/Bicep.RegistryModuleTool.UnitTests/ModuleFiles/BaseCommandHandlerTests.cs +++ b/src/Bicep.RegistryModuleTool.UnitTests/ModuleFiles/BaseCommandHandlerTests.cs @@ -1,10 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.CommandLine; -using System.CommandLine.Invocation; using Bicep.Core.Exceptions; using Bicep.RegistryModuleTool.Commands; +using Bicep.RegistryModuleTool.Extensions; using Bicep.RegistryModuleTool.TestFixtures.MockFactories; using FluentAssertions; using Microsoft.Extensions.Logging; @@ -65,14 +64,10 @@ public async Task InvokeAsync_CaughtUnexpectedException_LogsCritical(Exception e It.IsAny>())); } - private static Task InvokeAsync(ICommandHandler handler) + private static Task InvokeAsync(BaseCommandHandler handler) { - var command = new TestCommand() - { - Handler = handler, - }; - - return command.InvokeAsync(""); + var console = new StringWriterConsole(); + return handler.InvokeAsync(console, CancellationToken.None); } private static IEnumerable GetExceptionData() => GetExpectedExceptionData().Concat(GetUnexpectedExceptionData()); @@ -113,12 +108,10 @@ private static IEnumerable GetUnexpectedExceptionData() }; } - private class TestCommand : Command + private sealed class StringWriterConsole : IConsole { - public TestCommand() - : base("test") - { - } + public TextWriter Out { get; } = new StringWriter(); + public TextWriter Error { get; } = new StringWriter(); } private class PassThroughCommandHandler : BaseCommandHandler @@ -131,7 +124,7 @@ public PassThroughCommandHandler(int exitCode) this.exitCode = exitCode; } - protected override Task InvokeInternalAsync(InvocationContext context) => Task.FromResult(this.exitCode); + protected override Task InvokeInternalAsync(IConsole console, CancellationToken cancellationToken) => Task.FromResult(this.exitCode); } private class ThrowExceptionCommandHandler : BaseCommandHandler @@ -144,10 +137,11 @@ public ThrowExceptionCommandHandler(Exception exceptionToThrow, ILogger? logger this.exceptionToThrow = exceptionToThrow; } - protected override Task InvokeInternalAsync(InvocationContext context) + protected override Task InvokeInternalAsync(IConsole console, CancellationToken cancellationToken) { throw exceptionToThrow; } } } } + diff --git a/src/Bicep.RegistryModuleTool/Bicep.RegistryModuleTool.csproj b/src/Bicep.RegistryModuleTool/Bicep.RegistryModuleTool.csproj index e65e284ac11..a1d9a0dcb8a 100644 --- a/src/Bicep.RegistryModuleTool/Bicep.RegistryModuleTool.csproj +++ b/src/Bicep.RegistryModuleTool/Bicep.RegistryModuleTool.csproj @@ -23,14 +23,12 @@ + - - - diff --git a/src/Bicep.RegistryModuleTool/Commands/BaseCommandHandler.cs b/src/Bicep.RegistryModuleTool/Commands/BaseCommandHandler.cs index f391753d43d..3b8a181ba63 100644 --- a/src/Bicep.RegistryModuleTool/Commands/BaseCommandHandler.cs +++ b/src/Bicep.RegistryModuleTool/Commands/BaseCommandHandler.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.CommandLine.Invocation; using System.IO.Abstractions; using System.Security; using Bicep.Core.Exceptions; @@ -10,7 +9,7 @@ namespace Bicep.RegistryModuleTool.Commands { - public abstract class BaseCommandHandler : ICommandHandler + public abstract class BaseCommandHandler { protected BaseCommandHandler(IFileSystem fileSystem, ILogger logger) { @@ -22,7 +21,7 @@ protected BaseCommandHandler(IFileSystem fileSystem, ILogger logger) protected ILogger Logger { get; } - public async Task InvokeAsync(InvocationContext context) + public async Task InvokeAsync(IConsole console, CancellationToken cancellationToken = default) { try { @@ -34,7 +33,7 @@ public async Task InvokeAsync(InvocationContext context) // module folder structure. this.ValidateWorkingDirectoryPath(); - return await this.InvokeInternalAsync(context); + return await this.InvokeInternalAsync(console, cancellationToken); } catch (Exception exception) { @@ -44,7 +43,7 @@ public async Task InvokeAsync(InvocationContext context) case IOException: case UnauthorizedAccessException: this.Logger.LogDebug(exception, "Command failure."); - context.Console.WriteError(exception.Message); + console.WriteError(exception.Message); break; @@ -57,9 +56,7 @@ public async Task InvokeAsync(InvocationContext context) } } - public int Invoke(InvocationContext context) => throw new NotImplementedException(); - - protected abstract Task InvokeInternalAsync(InvocationContext context); + protected abstract Task InvokeInternalAsync(IConsole console, CancellationToken cancellationToken); private void ValidateWorkingDirectoryPath() { diff --git a/src/Bicep.RegistryModuleTool/Commands/GenerateCommand.cs b/src/Bicep.RegistryModuleTool/Commands/GenerateCommand.cs index 9d8327b5afb..c7d515062c0 100644 --- a/src/Bicep.RegistryModuleTool/Commands/GenerateCommand.cs +++ b/src/Bicep.RegistryModuleTool/Commands/GenerateCommand.cs @@ -2,9 +2,9 @@ // Licensed under the MIT License. using System.CommandLine; -using System.CommandLine.Invocation; using System.IO.Abstractions; using Bicep.Core; +using Bicep.RegistryModuleTool.Extensions; using Bicep.RegistryModuleTool.ModuleFiles; using Microsoft.Extensions.Logging; @@ -27,10 +27,10 @@ public CommandHandler(IFileSystem fileSystem, ILogger logger, B this.compiler = compiler; } - protected override async Task InvokeInternalAsync(InvocationContext context) + protected override async Task InvokeInternalAsync(IConsole console, CancellationToken cancellationToken) { var mainBicepFile = await this.GenerateAndLogAsync( - MainBicepFile.FileName, () => MainBicepFile.GenerateAsync(this.FileSystem, this.compiler, context.Console)); + MainBicepFile.FileName, () => MainBicepFile.GenerateAsync(this.FileSystem, this.compiler, console)); await this.GenerateAndLogAsync( MainArmTemplateFile.FileName, () => MainArmTemplateFile.GenerateAsync(this.FileSystem, mainBicepFile)); diff --git a/src/Bicep.RegistryModuleTool/Commands/ValidateCommand.cs b/src/Bicep.RegistryModuleTool/Commands/ValidateCommand.cs index ba1b4ff1a41..8120181f429 100644 --- a/src/Bicep.RegistryModuleTool/Commands/ValidateCommand.cs +++ b/src/Bicep.RegistryModuleTool/Commands/ValidateCommand.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.CommandLine; -using System.CommandLine.Invocation; using System.IO.Abstractions; using Bicep.Core; using Bicep.RegistryModuleTool.Exceptions; @@ -30,39 +29,39 @@ public CommandHandler(IFileSystem fileSystem, BicepCompiler compiler, ILogger InvokeInternalAsync(InvocationContext context) + protected override async Task InvokeInternalAsync(IConsole console, CancellationToken cancellationToken) { var valid = true; this.Logger.LogInformation("Validating main Bicep file..."); - var mainBicepFile = await MainBicepFile.OpenAsync(this.FileSystem, this.compiler, context.Console); + var mainBicepFile = await MainBicepFile.OpenAsync(this.FileSystem, this.compiler, console); var descriptionsValidator = new DescriptionsValidator(this.Logger); var metadataValidator = new BicepMetadataValidator(this.Logger); - valid &= await ValidateFileAsync(context.Console, () => mainBicepFile.ValidatedByAsync(descriptionsValidator, metadataValidator)); + valid &= await ValidateFileAsync(console, () => mainBicepFile.ValidatedByAsync(descriptionsValidator, metadataValidator)); this.Logger.LogInformation("Validating main Bicep test file..."); var mainBicepTestFile = MainBicepTestFile.Open(this.FileSystem); - var testValidator = new TestValidator(this.Logger, context.Console, this.compiler, mainBicepFile); - valid &= await ValidateFileAsync(context.Console, () => mainBicepTestFile.ValidatedByAsync(testValidator)); + var testValidator = new TestValidator(this.Logger, console, this.compiler, mainBicepFile); + valid &= await ValidateFileAsync(console, () => mainBicepTestFile.ValidatedByAsync(testValidator)); this.Logger.LogInformation("Validating main ARM template file..."); var diffValidator = new DiffValidator(this.FileSystem, this.Logger, mainBicepFile); var mainArmTemplateFile = await MainArmTemplateFile.OpenAsync(this.FileSystem); - valid &= await ValidateFileAsync(context.Console, () => mainArmTemplateFile.ValidatedByAsync(diffValidator)); + valid &= await ValidateFileAsync(console, () => mainArmTemplateFile.ValidatedByAsync(diffValidator)); this.Logger.LogInformation("Validating README file..."); var readmeFile = await ReadmeFile.OpenAsync(this.FileSystem); - valid &= await ValidateFileAsync(context.Console, () => readmeFile.ValidatedByAsync(diffValidator)); + valid &= await ValidateFileAsync(console, () => readmeFile.ValidatedByAsync(diffValidator)); this.Logger.LogInformation("Validating version file..."); var versionFile = await VersionFile.OpenAsync(this.FileSystem); var jsonSchemaValidator = new JsonSchemaValidator(this.Logger); - valid &= await ValidateFileAsync(context.Console, () => versionFile.ValidatedByAsync(jsonSchemaValidator, diffValidator)); + valid &= await ValidateFileAsync(console, () => versionFile.ValidatedByAsync(jsonSchemaValidator, diffValidator)); return valid ? 0 : 1; } diff --git a/src/Bicep.RegistryModuleTool/Extensions/BicepCompilerExtensions.cs b/src/Bicep.RegistryModuleTool/Extensions/BicepCompilerExtensions.cs index 1fa11dbd0f8..60e4068f2b5 100644 --- a/src/Bicep.RegistryModuleTool/Extensions/BicepCompilerExtensions.cs +++ b/src/Bicep.RegistryModuleTool/Extensions/BicepCompilerExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.CommandLine; using Bicep.Core; using Bicep.Core.Diagnostics; using Bicep.Core.Exceptions; diff --git a/src/Bicep.RegistryModuleTool/Extensions/CommandExtensions.cs b/src/Bicep.RegistryModuleTool/Extensions/CommandExtensions.cs index f89c96e9e92..2a92e615987 100644 --- a/src/Bicep.RegistryModuleTool/Extensions/CommandExtensions.cs +++ b/src/Bicep.RegistryModuleTool/Extensions/CommandExtensions.cs @@ -9,7 +9,7 @@ public static class CommandExtensions { public static Command AddSubcommand(this Command command, Command subcommand) { - command.AddCommand(subcommand); + command.Add(subcommand); return command; } diff --git a/src/Bicep.RegistryModuleTool/Extensions/CommandLineBuilderExtensions.cs b/src/Bicep.RegistryModuleTool/Extensions/CommandLineBuilderExtensions.cs index a1f701b66ef..cc72e1b9f7c 100644 --- a/src/Bicep.RegistryModuleTool/Extensions/CommandLineBuilderExtensions.cs +++ b/src/Bicep.RegistryModuleTool/Extensions/CommandLineBuilderExtensions.cs @@ -1,23 +1,22 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.CommandLine.Builder; +using System.CommandLine; using Bicep.RegistryModuleTool.Options; namespace Bicep.RegistryModuleTool.Extensions { public static class CommandLineBuilderExtensions { - public static CommandLineBuilder UseVerboseOption(this CommandLineBuilder builder) + /// Adds as a global option on the root command. + public static Command UseVerboseOption(this Command command) { - if (builder.Command.Children.Any(x => x is VerboseOption)) + if (!command.Options.Any(x => x is VerboseOption)) { - return builder; + command.Add(GlobalOptions.Verbose); } - builder.Command.AddGlobalOption(GlobalOptions.Verbose); - - return builder; + return command; } } } diff --git a/src/Bicep.RegistryModuleTool/Extensions/ConsoleExtensions.cs b/src/Bicep.RegistryModuleTool/Extensions/ConsoleExtensions.cs index eac558265a8..9b0b36abc85 100644 --- a/src/Bicep.RegistryModuleTool/Extensions/ConsoleExtensions.cs +++ b/src/Bicep.RegistryModuleTool/Extensions/ConsoleExtensions.cs @@ -1,15 +1,19 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.CommandLine; -using System.CommandLine.IO; -using System.CommandLine.Rendering; using Bicep.Core.Diagnostics; using Bicep.Core.SourceGraph; using Bicep.Core.Text; namespace Bicep.RegistryModuleTool.Extensions { + /// Local replacement for the removed System.CommandLine.IConsole interface. + public interface IConsole + { + TextWriter Out { get; } + TextWriter Error { get; } + } + public static class ConsoleExtensions { public static void WriteDiagnostic(this IConsole console, BicepSourceFile file, IDiagnostic diagnostic) @@ -23,7 +27,7 @@ public static void WriteDiagnostic(this IConsole console, BicepSourceFile file, case DiagnosticLevel.Off: break; case DiagnosticLevel.Info: - console.WriteLine(message); + console.Out.WriteLine(message); break; case DiagnosticLevel.Warning: console.WriteWarning(message); @@ -36,25 +40,23 @@ public static void WriteDiagnostic(this IConsole console, BicepSourceFile file, } } - public static void WriteWarning(this IConsole console, string warning) => console.WriteMessage(console.Out, ConsoleColor.Yellow, warning); + public static void WriteWarning(this IConsole console, string warning) => WriteMessage(console.Error, ConsoleColor.Yellow, warning); - public static void WriteError(this IConsole console, string error) => console.WriteMessage(console.Error, ConsoleColor.Red, error); + public static void WriteError(this IConsole console, string error) => WriteMessage(console.Error, ConsoleColor.Red, error); - private static void WriteMessage(this IConsole console, IStandardStreamWriter writer, ConsoleColor color, string message) + private static void WriteMessage(TextWriter writer, ConsoleColor color, string message) { - var terminal = console.GetTerminal(preferVirtualTerminal: false); - var originalForegroundColor = terminal?.ForegroundColor ?? Console.ForegroundColor; - - if (terminal is not null) + // Only apply color when writing to a real console stream. + if (writer == Console.Out || writer == Console.Error) { - terminal.ForegroundColor = color; + var previous = Console.ForegroundColor; + Console.ForegroundColor = color; + writer.WriteLine(message); + Console.ForegroundColor = previous; } - - writer.WriteLine(message); - - if (terminal is not null) + else { - terminal.ForegroundColor = originalForegroundColor; + writer.WriteLine(message); } } } diff --git a/src/Bicep.RegistryModuleTool/Extensions/HostBuilderExtensions.cs b/src/Bicep.RegistryModuleTool/Extensions/HostBuilderExtensions.cs index 8797877316a..4577690783b 100644 --- a/src/Bicep.RegistryModuleTool/Extensions/HostBuilderExtensions.cs +++ b/src/Bicep.RegistryModuleTool/Extensions/HostBuilderExtensions.cs @@ -1,10 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.CommandLine; -using System.CommandLine.Hosting; using System.Reflection; using Bicep.RegistryModuleTool.Commands; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace Bicep.RegistryModuleTool.Extensions @@ -13,20 +12,18 @@ public static class HostBuilderExtensions { public static IHostBuilder UseCommandHandlers(this IHostBuilder builder) { - var baseCommandType = typeof(Command); var baseCommandHandlerType = typeof(BaseCommandHandler); - var commandTypes = Assembly.GetExecutingAssembly().GetTypes() - .Where(t => t.Namespace == baseCommandHandlerType.Namespace && baseCommandType.IsAssignableFrom(t)); + var commandHandlerTypes = Assembly.GetExecutingAssembly().GetTypes() + .Where(t => baseCommandHandlerType.IsAssignableFrom(t) && !t.IsAbstract); - foreach (var commandType in commandTypes) + return builder.ConfigureServices(services => { - var commandHandlerType = commandType.GetNestedTypes().First(t => baseCommandHandlerType.IsAssignableFrom(t)); - - builder.UseCommandHandler(commandType, commandHandlerType); - } - - return builder; + foreach (var handlerType in commandHandlerTypes) + { + services.AddScoped(handlerType); + } + }); } } } diff --git a/src/Bicep.RegistryModuleTool/ModuleFileValidators/TestValidator.cs b/src/Bicep.RegistryModuleTool/ModuleFileValidators/TestValidator.cs index e19bd0b8cb9..3aa2ceb9919 100644 --- a/src/Bicep.RegistryModuleTool/ModuleFileValidators/TestValidator.cs +++ b/src/Bicep.RegistryModuleTool/ModuleFileValidators/TestValidator.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.CommandLine; using Bicep.Core; using Bicep.RegistryModuleTool.Extensions; using Bicep.RegistryModuleTool.ModuleFiles; diff --git a/src/Bicep.RegistryModuleTool/ModuleFiles/MainBicepFile.cs b/src/Bicep.RegistryModuleTool/ModuleFiles/MainBicepFile.cs index b70a3b91b57..d105248c2aa 100644 --- a/src/Bicep.RegistryModuleTool/ModuleFiles/MainBicepFile.cs +++ b/src/Bicep.RegistryModuleTool/ModuleFiles/MainBicepFile.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.CommandLine; using System.IO.Abstractions; using System.Text; using Bicep.Core; diff --git a/src/Bicep.RegistryModuleTool/Program.cs b/src/Bicep.RegistryModuleTool/Program.cs index edc77e9afbb..e41c88c0eb4 100644 --- a/src/Bicep.RegistryModuleTool/Program.cs +++ b/src/Bicep.RegistryModuleTool/Program.cs @@ -2,11 +2,6 @@ // Licensed under the MIT License. using System.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.Hosting; -using System.CommandLine.Invocation; -using System.CommandLine.NamingConventionBinder; -using System.CommandLine.Parsing; using Bicep.RegistryModuleTool.Commands; using Bicep.RegistryModuleTool.Extensions; using Bicep.RegistryModuleTool.Options; @@ -19,44 +14,56 @@ namespace Bicep.RegistryModuleTool { public class Program { - public static Task Main(string[] args) => CreateParser().InvokeAsync(args); - - private static Parser CreateParser() + public static async Task Main(string[] args) { - var rootCommand = new RootCommand("Bicep registry module tool") - .AddSubcommand(new ValidateCommand()) - .AddSubcommand(new GenerateCommand()); - - var parser = new CommandLineBuilder(rootCommand) - .UseHost(Host.CreateDefaultBuilder, ConfigureHost) - .UseDefaults() - .UseVerboseOption() - .Build(); + var host = CreateHost(args); + await host.StartAsync(); - // Have to use parser.Invoke instead of rootCommand.Invoke due to the - // System.CommandLine bug: https://github.com/dotnet/command-line-api/issues/1691. - rootCommand.Handler = CommandHandler.Create(() => parser.Invoke("-h")); + var rootCommand = BuildRootCommand(host.Services); + var exitCode = await rootCommand.Parse(args).InvokeAsync(); - return parser; + await host.StopAsync(); + return exitCode; } - private static void ConfigureHost(IHostBuilder builder) => builder + private static IHost CreateHost(string[] args) => Host.CreateDefaultBuilder() .ConfigureServices(services => services.AddBicepCore()) - .UseSerilog((context, logging) => logging - .MinimumLevel.Is(GetMinimumLogEventLevel(context)) + .UseSerilog((_, logging) => logging + .MinimumLevel.Is(GetMinimumLogEventLevel(args)) .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("System", LogEventLevel.Warning) .WriteTo.Console()) - .UseCommandHandlers(); + .UseCommandHandlers() + .Build(); - private static LogEventLevel GetMinimumLogEventLevel(HostBuilderContext context) + private static RootCommand BuildRootCommand(IServiceProvider services) { - var verboseSpecified = - context.Properties.TryGetValue(typeof(InvocationContext), out var value) && - value is InvocationContext invocationContext && - invocationContext.ParseResult.FindResultFor(GlobalOptions.Verbose) is not null; + var rootCommand = new RootCommand("Bicep registry module tool"); + rootCommand.UseVerboseOption(); + + var console = new SystemConsole(); + + var generateCmd = new GenerateCommand(); + generateCmd.SetAction(async (ParseResult _, CancellationToken ct) => + await services.GetRequiredService().InvokeAsync(console, ct)); + + var validateCmd = new ValidateCommand(); + validateCmd.SetAction(async (ParseResult _, CancellationToken ct) => + await services.GetRequiredService().InvokeAsync(console, ct)); - return verboseSpecified ? LogEventLevel.Debug : LogEventLevel.Fatal; + rootCommand.Add(generateCmd); + rootCommand.Add(validateCmd); + + return rootCommand; + } + + private static LogEventLevel GetMinimumLogEventLevel(string[] args) => + args.Contains("--verbose") ? LogEventLevel.Debug : LogEventLevel.Fatal; + + private sealed class SystemConsole : IConsole + { + public TextWriter Out => Console.Out; + public TextWriter Error => Console.Error; } } } diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 98ff78c1ca3..b40ddf5a6de 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -95,10 +95,7 @@ - - - - +