From 87577c6ee13690cbfb7f4f9cebb110af46a912be Mon Sep 17 00:00:00 2001
From: Anthony Martin <38542602+anthony-c-martin@users.noreply.github.com>
Date: Tue, 31 Mar 2026 15:36:00 -0400
Subject: [PATCH 1/8] Initial commit with boilerplate
---
src/Bicep.Cli/Bicep.Cli.csproj | 1 +
src/Bicep.Cli/Commands/RootCommand.cs | 2 +-
src/Bicep.Cli/Program.cs | 261 ++++++++++++++----
.../Bicep.RegistryModuleTool.csproj | 4 +-
.../Commands/BaseCommandHandler.cs | 13 +-
.../Commands/GenerateCommand.cs | 6 +-
.../Commands/ValidateCommand.cs | 17 +-
.../Extensions/BicepCompilerExtensions.cs | 1 -
.../Extensions/CommandExtensions.cs | 2 +-
.../CommandLineBuilderExtensions.cs | 13 +-
.../Extensions/ConsoleExtensions.cs | 36 +--
.../Extensions/HostBuilderExtensions.cs | 21 +-
.../ModuleFileValidators/TestValidator.cs | 1 -
.../ModuleFiles/MainBicepFile.cs | 1 -
src/Bicep.RegistryModuleTool/Program.cs | 69 ++---
src/Directory.Packages.props | 5 +-
16 files changed, 299 insertions(+), 154 deletions(-)
diff --git a/src/Bicep.Cli/Bicep.Cli.csproj b/src/Bicep.Cli/Bicep.Cli.csproj
index 009a3e3c215..237221b0a40 100644
--- a/src/Bicep.Cli/Bicep.Cli.csproj
+++ b/src/Bicep.Cli/Bicep.Cli.csproj
@@ -28,6 +28,7 @@
+
diff --git a/src/Bicep.Cli/Commands/RootCommand.cs b/src/Bicep.Cli/Commands/RootCommand.cs
index c91fe2fd5f6..5693ce25b1b 100644
--- a/src/Bicep.Cli/Commands/RootCommand.cs
+++ b/src/Bicep.Cli/Commands/RootCommand.cs
@@ -41,7 +41,7 @@ public int Run(RootArguments args)
return 1;
}
- private void PrintHelp()
+ internal void PrintHelp()
{
var exeName = ThisAssembly.AssemblyName;
var versionString = environment.GetVersionString();
diff --git a/src/Bicep.Cli/Program.cs b/src/Bicep.Cli/Program.cs
index 2f255be248f..f1c05aea4e1 100644
--- a/src/Bicep.Cli/Program.cs
+++ b/src/Bicep.Cli/Program.cs
@@ -1,8 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using System.CommandLine;
+using System.CommandLine.Help;
using System.Diagnostics;
-using System.IO.Abstractions;
using System.Runtime;
using Bicep.Cli.Arguments;
using Bicep.Cli.Commands;
@@ -20,6 +21,9 @@
using Microsoft.Extensions.Logging;
using Spectre.Console;
+// Avoid naming conflict with Bicep.Cli.Commands.RootCommand
+using SclRootCommand = System.CommandLine.RootCommand;
+
namespace Bicep.Cli
{
public record IOContext(
@@ -81,77 +85,224 @@ 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
- {
- switch (ArgumentParser.TryParse(args, services.GetRequiredService()))
- {
- case BuildArguments buildArguments when buildArguments.CommandName == Constants.Command.Build: // bicep build [options]
- return await services.GetRequiredService().RunAsync(buildArguments);
-
- case TestArguments testArguments when testArguments.CommandName == Constants.Command.Test: // bicep test [options]
- return await services.GetRequiredService().RunAsync(testArguments);
-
- case BuildParamsArguments buildParamsArguments when buildParamsArguments.CommandName == Constants.Command.BuildParams: // bicep build-params [options]
- return await services.GetRequiredService().RunAsync(buildParamsArguments);
-
- case FormatArguments formatArguments when formatArguments.CommandName == Constants.Command.Format: // bicep format [options]
- return services.GetRequiredService().Run(formatArguments);
+ var rootCommand = BuildCommandLine(cancellationToken);
+ return await rootCommand.Parse(args).InvokeAsync(cancellationToken: cancellationToken);
+ }
- case GenerateParametersFileArguments generateParametersFileArguments when generateParametersFileArguments.CommandName == Constants.Command.GenerateParamsFile: // bicep generate-params [options]
- return await services.GetRequiredService().RunAsync(generateParametersFileArguments);
+ ///
+ /// Builds the System.CommandLine command hierarchy. Each existing subcommand is currently
+ /// registered as a legacy pass-through stub via . To migrate a
+ /// command, replace its LegacyCommand call with a that
+ /// declares its own and members and
+ /// invokes the command handler directly.
+ ///
+ private SclRootCommand BuildCommandLine(CancellationToken cancellationToken)
+ {
+ var rootCommand = new SclRootCommand("Bicep CLI")
+ {
+ // Collect unknown tokens so we can produce a custom error message that matches
+ // the format of the original CLI instead of letting System.CommandLine error out.
+ TreatUnmatchedTokensAsErrors = false,
+ };
- case DecompileArguments decompileArguments when decompileArguments.CommandName == Constants.Command.Decompile: // bicep decompile [options]
- return await services.GetRequiredService().RunAsync(decompileArguments);
+ // 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 == "--version");
+ if (builtInVersion is not null)
+ {
+ rootCommand.Options.Remove(builtInVersion);
+ }
+ var builtInHelp = rootCommand.Options.OfType().FirstOrDefault();
+ if (builtInHelp is not null)
+ {
+ rootCommand.Options.Remove(builtInHelp);
+ }
- case DecompileParamsArguments decompileParamsArguments when decompileParamsArguments.CommandName == Constants.Command.DecompileParams:
- return services.GetRequiredService().Run(decompileParamsArguments);
+ var helpOption = new Option("--help", "-?", "-h") { Description = "Show help and usage information." };
+ var versionOption = new Option("--version", "-v") { Description = "Show version information." };
+ var licenseOption = new Option("--license") { Description = "Print license information." };
+ var thirdPartyNoticesOption = new Option("--third-party-notices") { Description = "Print third-party notice information." };
- case PublishArguments publishArguments when publishArguments.CommandName == Constants.Command.Publish: // bicep publish [options]
- return await services.GetRequiredService().RunAsync(publishArguments);
+ rootCommand.Add(helpOption);
+ rootCommand.Add(versionOption);
+ rootCommand.Add(licenseOption);
+ rootCommand.Add(thirdPartyNoticesOption);
- case PublishExtensionArguments publishProviderArguments when publishProviderArguments.CommandName == Constants.Command.PublishExtension: // bicep publish-extension [options]
- return await services.GetRequiredService().RunAsync(publishProviderArguments, cancellationToken);
+ rootCommand.SetAction(async (ParseResult pr, CancellationToken ct) =>
+ {
+ var bicepRootCommand = services.GetRequiredService();
- case RestoreArguments restoreArguments when restoreArguments.CommandName == Constants.Command.Restore: // bicep restore
- return await services.GetRequiredService().RunAsync(restoreArguments);
+ var unmatched = pr.UnmatchedTokens;
- case LintArguments lintArguments when lintArguments.CommandName == Constants.Command.Lint: // bicep lint [options]
- return await services.GetRequiredService().RunAsync(lintArguments);
+ // Show help when explicitly requested or when called with no arguments at all.
+ if (pr.GetValue(helpOption) || unmatched.Count == 0)
+ {
+ bicepRootCommand.PrintHelp();
+ return 0;
+ }
- case JsonRpcArguments jsonRpcArguments when jsonRpcArguments.CommandName == Constants.Command.JsonRpc: // bicep jsonrpc [options]
- return await services.GetRequiredService().RunAsync(jsonRpcArguments, cancellationToken);
+ if (pr.GetValue(versionOption))
+ {
+ return bicepRootCommand.Run(new RootArguments("--version", Constants.Command.Root));
+ }
- case LocalDeployArguments localDeployArguments when localDeployArguments.CommandName == Constants.Command.LocalDeploy: // bicep local-deploy [options]
- return await services.GetRequiredService().RunAsync(localDeployArguments, cancellationToken);
+ if (pr.GetValue(licenseOption))
+ {
+ return bicepRootCommand.Run(new RootArguments("--license", Constants.Command.Root));
+ }
- case SnapshotArguments snapshotArguments when snapshotArguments.CommandName == Constants.Command.Snapshot: // bicep snapshot [options]
- return await services.GetRequiredService().RunAsync(snapshotArguments, cancellationToken);
+ if (pr.GetValue(thirdPartyNoticesOption))
+ {
+ return bicepRootCommand.Run(new RootArguments("--third-party-notices", Constants.Command.Root));
+ }
- case DeployArguments deployArguments when deployArguments.CommandName == Constants.Command.Deploy: // bicep deploy [options]
- return await services.GetRequiredService().RunAsync(deployArguments, cancellationToken);
+ await io.Error.Writer.WriteLineAsync(
+ string.Format(CliResources.UnrecognizedArgumentsFormat, string.Join(' ', unmatched), ThisAssembly.AssemblyName));
+ return 1;
+ });
- case WhatIfArguments whatIfArguments when whatIfArguments.CommandName == Constants.Command.WhatIf: // bicep what-if [options]
- return await services.GetRequiredService().RunAsync(whatIfArguments, cancellationToken);
+ // Each subcommand below is a legacy pass-through stub. Arguments not recognized by
+ // System.CommandLine are collected in ParseResult.UnmatchedTokens and forwarded to
+ // the existing argument class for parsing. Once a command is fully migrated, replace
+ // the LegacyCommand call with a Command that has explicit Option/Argument
+ // members and calls the command handler with the bound values directly.
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.Build,
+ "Builds a .bicep file.",
+ args => services.GetRequiredService().RunAsync(new BuildArguments(args))));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.Test,
+ "Runs tests in a .bicep file.",
+ args => services.GetRequiredService().RunAsync(new TestArguments(args))));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.BuildParams,
+ "Builds a .bicepparam file.",
+ args => services.GetRequiredService().RunAsync(new BuildParamsArguments(args))));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.Format,
+ "Formats a .bicep file.",
+ args => Task.FromResult(services.GetRequiredService().Run(new FormatArguments(args)))));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.GenerateParamsFile,
+ "Generates a parameters file for a .bicep file.",
+ args => services.GetRequiredService().RunAsync(new GenerateParametersFileArguments(args))));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.Decompile,
+ "Attempts to decompile a template .json file to .bicep.",
+ args => services.GetRequiredService().RunAsync(new DecompileArguments(args))));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.DecompileParams,
+ "Attempts to decompile a parameters .json file to .bicepparam.",
+ args => Task.FromResult(services.GetRequiredService().Run(new DecompileParamsArguments(args)))));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.Publish,
+ "Publishes a .bicep file to a registry.",
+ args => services.GetRequiredService().RunAsync(new PublishArguments(args))));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.PublishExtension,
+ "Publishes a Bicep extension to a registry.",
+ args => services.GetRequiredService().RunAsync(new PublishExtensionArguments(args), cancellationToken)));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.Restore,
+ "Restores external modules for a .bicep file.",
+ args => services.GetRequiredService().RunAsync(new RestoreArguments(args))));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.Lint,
+ "Lints a .bicep file.",
+ args => services.GetRequiredService().RunAsync(new LintArguments(args))));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.JsonRpc,
+ "Starts the Bicep JSON-RPC server.",
+ args => services.GetRequiredService().RunAsync(new JsonRpcArguments(args), cancellationToken)));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.LocalDeploy,
+ "Performs a local deployment.",
+ args => services.GetRequiredService().RunAsync(new LocalDeployArguments(args), cancellationToken)));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.Snapshot,
+ "Creates an extension snapshot.",
+ args => services.GetRequiredService().RunAsync(new SnapshotArguments(args), cancellationToken)));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.Deploy,
+ "Deploys infrastructure using a .bicepparam file.",
+ args => services.GetRequiredService().RunAsync(new DeployArguments(args), cancellationToken)));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.WhatIf,
+ "Previews the changes a deployment would make.",
+ args => services.GetRequiredService().RunAsync(new WhatIfArguments(args), cancellationToken)));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.Teardown,
+ "Tears down resources deployed by a .bicepparam file.",
+ args => services.GetRequiredService().RunAsync(new TeardownArguments(args), cancellationToken)));
+
+ rootCommand.Add(LegacyCommand(
+ Constants.Command.Console,
+ "Opens an interactive Bicep console.",
+ args => services.GetRequiredService().RunAsync(new ConsoleArguments(args))));
+
+ return rootCommand;
+ }
- case TeardownArguments teardownArguments when teardownArguments.CommandName == Constants.Command.Teardown: // bicep teardown [options]
- return await services.GetRequiredService().RunAsync(teardownArguments, cancellationToken);
+ ///
+ /// Creates a pass-through command stub that forwards all unrecognized tokens to an
+ /// existing argument class and command handler. System.CommandLine's built-in options
+ /// (e.g. --help) are still handled; everything else lands in
+ /// and is passed to
+ /// as a plain string[].
+ ///
+ /// Once a command is fully migrated, replace this stub with a that
+ /// declares its own and members.
+ ///
+ ///
+ private Command LegacyCommand(string name, string description, Func> handler)
+ {
+ var command = new Command(name, description)
+ {
+ TreatUnmatchedTokensAsErrors = false,
+ };
- case ConsoleArguments consoleArguments when consoleArguments.CommandName == Constants.Command.Console: // bicep console
- return await services.GetRequiredService().RunAsync(consoleArguments);
+ // Remove the built-in HelpOption so that --help output goes through io.Output.Writer
+ // (not Console.Out) and shows the custom Bicep help text.
+ var builtInHelp = command.Options.OfType().FirstOrDefault();
+ if (builtInHelp is not null)
+ {
+ command.Options.Remove(builtInHelp);
+ }
- case RootArguments rootArguments when rootArguments.CommandName == Constants.Command.Root: // bicep [options]
- return services.GetRequiredService().Run(rootArguments);
+ var helpOption = new Option("--help", "-?", "-h") { Description = "Show help and usage information." };
+ command.Add(helpOption);
- default:
- await io.Error.Writer.WriteLineAsync(string.Format(CliResources.UnrecognizedArgumentsFormat, string.Join(' ', args), ThisAssembly.AssemblyName)); // should probably print help here??
- return 1;
- }
- }
- catch (BicepException exception)
+ command.SetAction(async (ParseResult pr, CancellationToken ct) =>
{
- await io.Error.Writer.WriteLineAsync(exception.Message);
- return 1;
- }
+ try
+ {
+ return await handler(pr.UnmatchedTokens.ToArray());
+ }
+ catch (BicepException exception)
+ {
+ await io.Error.Writer.WriteLineAsync(exception.Message);
+ return 1;
+ }
+ });
+
+ return command;
}
private static ILoggerFactory CreateLoggerFactory(IOContext io)
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 309edde41f5..50994ccb67e 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -95,10 +95,7 @@
-
-
-
-
+
From 84c254ea5bfb5639b8f9b608b944a4f64d332ef2 Mon Sep 17 00:00:00 2001
From: Anthony Martin <38542602+anthony-c-martin@users.noreply.github.com>
Date: Tue, 31 Mar 2026 16:19:59 -0400
Subject: [PATCH 2/8] Implement jsonrpc command
---
src/Bicep.Cli/Arguments/JsonRpcArguments.cs | 58 ++-----------
src/Bicep.Cli/Program.cs | 92 ++++++++++++++-------
src/Bicep.Cli/Services/ArgumentParser.cs | 1 -
3 files changed, 65 insertions(+), 86 deletions(-)
diff --git a/src/Bicep.Cli/Arguments/JsonRpcArguments.cs b/src/Bicep.Cli/Arguments/JsonRpcArguments.cs
index 44097d00feb..4b9d6026e51 100644
--- a/src/Bicep.Cli/Arguments/JsonRpcArguments.cs
+++ b/src/Bicep.Cli/Arguments/JsonRpcArguments.cs
@@ -3,59 +3,11 @@
namespace Bicep.Cli.Arguments;
-public class JsonRpcArguments : ArgumentsBase
+public record JsonRpcArguments
{
- 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;
+ public string? Pipe { get; init; }
- 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 int? Socket { get; init; }
- public string? Pipe { get; set; }
-
- public int? Socket { get; set; }
-
- public bool? Stdio { get; set; }
-}
+ public bool? Stdio { get; init; }
+}
\ No newline at end of file
diff --git a/src/Bicep.Cli/Program.cs b/src/Bicep.Cli/Program.cs
index f1c05aea4e1..066fc0157a8 100644
--- a/src/Bicep.Cli/Program.cs
+++ b/src/Bicep.Cli/Program.cs
@@ -3,6 +3,7 @@
using System.CommandLine;
using System.CommandLine.Help;
+using System.CommandLine.Invocation;
using System.Diagnostics;
using System.Runtime;
using Bicep.Cli.Arguments;
@@ -24,6 +25,14 @@
// Avoid naming conflict with Bicep.Cli.Commands.RootCommand
using SclRootCommand = System.CommandLine.RootCommand;
+public class HelpExamplesAction : SynchronousCommandLineAction
+{
+ public override int Invoke(ParseResult parseResult)
+ {
+ throw new NotImplementedException();
+ }
+}
+
namespace Bicep.Cli
{
public record IOContext(
@@ -100,9 +109,7 @@ private SclRootCommand BuildCommandLine(CancellationToken cancellationToken)
{
var rootCommand = new SclRootCommand("Bicep CLI")
{
- // Collect unknown tokens so we can produce a custom error message that matches
- // the format of the original CLI instead of letting System.CommandLine error out.
- TreatUnmatchedTokensAsErrors = false,
+ TreatUnmatchedTokensAsErrors = true,
};
// System.CommandLine adds --version and --help to RootCommand by default.
@@ -113,18 +120,12 @@ private SclRootCommand BuildCommandLine(CancellationToken cancellationToken)
{
rootCommand.Options.Remove(builtInVersion);
}
- var builtInHelp = rootCommand.Options.OfType().FirstOrDefault();
- if (builtInHelp is not null)
- {
- rootCommand.Options.Remove(builtInHelp);
- }
- var helpOption = new Option("--help", "-?", "-h") { Description = "Show help and usage information." };
var versionOption = new Option("--version", "-v") { Description = "Show version information." };
var licenseOption = new Option("--license") { Description = "Print license information." };
var thirdPartyNoticesOption = new Option("--third-party-notices") { Description = "Print third-party notice information." };
- rootCommand.Add(helpOption);
+ // rootCommand.Add(helpOption);
rootCommand.Add(versionOption);
rootCommand.Add(licenseOption);
rootCommand.Add(thirdPartyNoticesOption);
@@ -135,13 +136,6 @@ private SclRootCommand BuildCommandLine(CancellationToken cancellationToken)
var unmatched = pr.UnmatchedTokens;
- // Show help when explicitly requested or when called with no arguments at all.
- if (pr.GetValue(helpOption) || unmatched.Count == 0)
- {
- bicepRootCommand.PrintHelp();
- return 0;
- }
-
if (pr.GetValue(versionOption))
{
return bicepRootCommand.Run(new RootArguments("--version", Constants.Command.Root));
@@ -222,10 +216,7 @@ await io.Error.Writer.WriteLineAsync(
"Lints a .bicep file.",
args => services.GetRequiredService().RunAsync(new LintArguments(args))));
- rootCommand.Add(LegacyCommand(
- Constants.Command.JsonRpc,
- "Starts the Bicep JSON-RPC server.",
- args => services.GetRequiredService().RunAsync(new JsonRpcArguments(args), cancellationToken)));
+ rootCommand.Add(CreateJsonRpcCommand());
rootCommand.Add(LegacyCommand(
Constants.Command.LocalDeploy,
@@ -260,6 +251,54 @@ await io.Error.Writer.WriteLineAsync(
return rootCommand;
}
+ private Command CreateJsonRpcCommand()
+ {
+ var command = new Command(Constants.Command.JsonRpc, "Starts the Bicep JSON-RPC server.");
+
+ var pipeOption = new Option("--pipe")
+ {
+ Description = "Connect via a named pipe with the given name.",
+ };
+ var socketOption = new Option("--socket")
+ {
+ Description = "Connect via a TCP socket on the specified port.",
+ };
+ var stdioOption = new Option("--stdio")
+ {
+ Description = "Use standard input/output for communication (default when no transport is specified).",
+ };
+
+ 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;
+ }
+
///
/// Creates a pass-through command stub that forwards all unrecognized tokens to an
/// existing argument class and command handler. System.CommandLine's built-in options
@@ -278,17 +317,6 @@ private Command LegacyCommand(string name, string description, Func().FirstOrDefault();
- if (builtInHelp is not null)
- {
- command.Options.Remove(builtInHelp);
- }
-
- var helpOption = new Option("--help", "-?", "-h") { Description = "Show help and usage information." };
- command.Add(helpOption);
-
command.SetAction(async (ParseResult pr, CancellationToken ct) =>
{
try
diff --git a/src/Bicep.Cli/Services/ArgumentParser.cs b/src/Bicep.Cli/Services/ArgumentParser.cs
index 7d8b464c536..de67be2b218 100644
--- a/src/Bicep.Cli/Services/ArgumentParser.cs
+++ b/src/Bicep.Cli/Services/ArgumentParser.cs
@@ -39,7 +39,6 @@ public static class ArgumentParser
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..]),
From 8f48119c9e8f4bbf8de8255b7048786b73b8c14c Mon Sep 17 00:00:00 2001
From: Anthony Martin <38542602+anthony-c-martin@users.noreply.github.com>
Date: Tue, 31 Mar 2026 16:38:43 -0400
Subject: [PATCH 3/8] More commands
---
src/Bicep.Cli/Arguments/DeployArguments.cs | 29 +--
.../Arguments/DeployArgumentsBase.cs | 62 +-----
src/Bicep.Cli/Arguments/JsonRpcArguments.cs | 12 +-
src/Bicep.Cli/Arguments/TeardownArguments.cs | 12 +-
src/Bicep.Cli/Arguments/WhatIfArguments.cs | 12 +-
.../Commands/DeploymentsCommandsBase.cs | 4 +-
src/Bicep.Cli/Program.cs | 186 +++++++++++++++---
src/Bicep.Cli/Services/ArgumentParser.cs | 3 -
8 files changed, 186 insertions(+), 134 deletions(-)
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/JsonRpcArguments.cs b/src/Bicep.Cli/Arguments/JsonRpcArguments.cs
index 4b9d6026e51..9e52f3aa6cc 100644
--- a/src/Bicep.Cli/Arguments/JsonRpcArguments.cs
+++ b/src/Bicep.Cli/Arguments/JsonRpcArguments.cs
@@ -3,11 +3,7 @@
namespace Bicep.Cli.Arguments;
-public record JsonRpcArguments
-{
- public string? Pipe { get; init; }
-
- public int? Socket { get; init; }
-
- public bool? Stdio { get; init; }
-}
\ No newline at end of file
+public record JsonRpcArguments(
+ string? Pipe,
+ int? Socket,
+ bool? Stdio);
\ No newline at end of file
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/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/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/Program.cs b/src/Bicep.Cli/Program.cs
index 066fc0157a8..9f8a3cf2efa 100644
--- a/src/Bicep.Cli/Program.cs
+++ b/src/Bicep.Cli/Program.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using System.Collections.Immutable;
using System.CommandLine;
using System.CommandLine.Help;
using System.CommandLine.Invocation;
@@ -228,20 +229,11 @@ await io.Error.Writer.WriteLineAsync(
"Creates an extension snapshot.",
args => services.GetRequiredService().RunAsync(new SnapshotArguments(args), cancellationToken)));
- rootCommand.Add(LegacyCommand(
- Constants.Command.Deploy,
- "Deploys infrastructure using a .bicepparam file.",
- args => services.GetRequiredService().RunAsync(new DeployArguments(args), cancellationToken)));
+ rootCommand.Add(CreateDeployCommand());
- rootCommand.Add(LegacyCommand(
- Constants.Command.WhatIf,
- "Previews the changes a deployment would make.",
- args => services.GetRequiredService().RunAsync(new WhatIfArguments(args), cancellationToken)));
+ rootCommand.Add(CreateWhatIfCommand());
- rootCommand.Add(LegacyCommand(
- Constants.Command.Teardown,
- "Tears down resources deployed by a .bicepparam file.",
- args => services.GetRequiredService().RunAsync(new TeardownArguments(args), cancellationToken)));
+ rootCommand.Add(CreateTeardownCommand());
rootCommand.Add(LegacyCommand(
Constants.Command.Console,
@@ -286,12 +278,10 @@ private Command CreateJsonRpcCommand()
command.SetAction(async (result, cancellationToken) =>
{
- JsonRpcArguments args = new()
- {
- Pipe = result.GetValue(pipeOption),
- Socket = result.GetValue(socketOption),
- Stdio = result.GetValue(stdioOption)
- };
+ JsonRpcArguments args = new(
+ Pipe: result.GetValue(pipeOption),
+ Socket: result.GetValue(socketOption),
+ Stdio: result.GetValue(stdioOption));
return await services.GetRequiredService().RunAsync(args, cancellationToken);
});
@@ -299,6 +289,152 @@ private Command CreateJsonRpcCommand()
return command;
}
+ private Command CreateDeployCommand()
+ {
+ var command = new Command(Constants.Command.Deploy, "Deploys infrastructure using a .bicepparam file.")
+ {
+ TreatUnmatchedTokensAsErrors = false,
+ };
+
+ var parametersFileArgument = new Argument("parameters-file")
+ {
+ Description = "The path to the .bicepparam file.",
+ };
+ var noRestoreOption = new Option("--no-restore")
+ {
+ Description = "Do not restore modules prior to deploying.",
+ };
+ var formatOption = new Option("--format")
+ {
+ Description = "Output format for deployment results (Default, Json).",
+ };
+
+ command.Add(parametersFileArgument);
+ command.Add(noRestoreOption);
+ command.Add(formatOption);
+
+ command.SetAction((result, ct) => RunCommandAsync(async () =>
+ {
+ var additionalArguments = ParseAdditionalArguments(result.UnmatchedTokens);
+ var args = new DeployArguments(
+ result.GetValue(parametersFileArgument)!,
+ 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, "Previews the changes a deployment would make.")
+ {
+ TreatUnmatchedTokensAsErrors = false,
+ };
+
+ var parametersFileArgument = new Argument("parameters-file")
+ {
+ Description = "The path to the .bicepparam file.",
+ };
+ var noRestoreOption = new Option("--no-restore")
+ {
+ Description = "Do not restore modules prior to running what-if.",
+ };
+
+ command.Add(parametersFileArgument);
+ command.Add(noRestoreOption);
+
+ command.SetAction((result, ct) => RunCommandAsync(async () =>
+ {
+ var additionalArguments = ParseAdditionalArguments(result.UnmatchedTokens);
+ var args = new WhatIfArguments(
+ result.GetValue(parametersFileArgument)!,
+ result.GetValue(noRestoreOption),
+ additionalArguments);
+
+ return await services.GetRequiredService().RunAsync(args, ct);
+ }));
+
+ return command;
+ }
+
+ private Command CreateTeardownCommand()
+ {
+ var command = new Command(Constants.Command.Teardown, "Tears down resources deployed by a .bicepparam file.")
+ {
+ TreatUnmatchedTokensAsErrors = false,
+ };
+
+ var parametersFileArgument = new Argument("parameters-file")
+ {
+ Description = "The path to the .bicepparam file.",
+ };
+ var noRestoreOption = new Option("--no-restore")
+ {
+ Description = "Do not restore modules prior to tearing down.",
+ };
+
+ command.Add(parametersFileArgument);
+ command.Add(noRestoreOption);
+
+ command.SetAction((result, ct) => RunCommandAsync(async () =>
+ {
+ var additionalArguments = ParseAdditionalArguments(result.UnmatchedTokens);
+ var args = new TeardownArguments(
+ result.GetValue(parametersFileArgument)!,
+ result.GetValue(noRestoreOption),
+ additionalArguments);
+
+ return await services.GetRequiredService().RunAsync(args, ct);
+ }));
+
+ return command;
+ }
+
+ private async Task RunCommandAsync(Func> action)
+ {
+ try
+ {
+ return await action();
+ }
+ catch (BicepException exception)
+ {
+ await io.Error.Writer.WriteLineAsync(exception.Message);
+ return 1;
+ }
+ }
+
+ 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();
+ }
+
///
/// Creates a pass-through command stub that forwards all unrecognized tokens to an
/// existing argument class and command handler. System.CommandLine's built-in options
@@ -317,18 +453,8 @@ private Command LegacyCommand(string name, string description, Func
- {
- try
- {
- return await handler(pr.UnmatchedTokens.ToArray());
- }
- catch (BicepException exception)
- {
- await io.Error.Writer.WriteLineAsync(exception.Message);
- return 1;
- }
- });
+ command.SetAction((ParseResult pr, CancellationToken ct) =>
+ RunCommandAsync(() => handler(pr.UnmatchedTokens.ToArray())));
return command;
}
diff --git a/src/Bicep.Cli/Services/ArgumentParser.cs b/src/Bicep.Cli/Services/ArgumentParser.cs
index de67be2b218..2067a94c85f 100644
--- a/src/Bicep.Cli/Services/ArgumentParser.cs
+++ b/src/Bicep.Cli/Services/ArgumentParser.cs
@@ -41,9 +41,6 @@ public static class ArgumentParser
Constants.Command.Lint => new LintArguments(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,
};
From 848e70309207333a7ab8af41fa96e5379b05978a Mon Sep 17 00:00:00 2001
From: Anthony Martin <38542602+anthony-c-martin@users.noreply.github.com>
Date: Tue, 31 Mar 2026 16:49:30 -0400
Subject: [PATCH 4/8] More
---
src/Bicep.Cli/Arguments/ConsoleArguments.cs | 8 +-
.../Arguments/LocalDeployArguments.cs | 53 +-------
src/Bicep.Cli/Arguments/SnapshotArguments.cs | 94 ++-----------
src/Bicep.Cli/Program.cs | 123 +++++++++++++++---
src/Bicep.Cli/Services/ArgumentParser.cs | 3 -
5 files changed, 122 insertions(+), 159 deletions(-)
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/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/SnapshotArguments.cs b/src/Bicep.Cli/Arguments/SnapshotArguments.cs
index fc08bfb892e..fda42c28dc4 100644
--- a/src/Bicep.Cli/Arguments/SnapshotArguments.cs
+++ b/src/Bicep.Cli/Arguments/SnapshotArguments.cs
@@ -1,98 +1,20 @@
// 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? Location,
+ string? ResourceGroup,
+ string? DeploymentName) : IInputArguments
{
- private const string TenantIdArgument = "--tenant-id";
- private const string SubscriptionIdArgument = "--subscription-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 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? Location { get; }
-
- public string? ResourceGroup { get; }
-
- public string? DeploymentName { get; }
}
diff --git a/src/Bicep.Cli/Program.cs b/src/Bicep.Cli/Program.cs
index 9f8a3cf2efa..3a4b808abc0 100644
--- a/src/Bicep.Cli/Program.cs
+++ b/src/Bicep.Cli/Program.cs
@@ -219,15 +219,9 @@ await io.Error.Writer.WriteLineAsync(
rootCommand.Add(CreateJsonRpcCommand());
- rootCommand.Add(LegacyCommand(
- Constants.Command.LocalDeploy,
- "Performs a local deployment.",
- args => services.GetRequiredService().RunAsync(new LocalDeployArguments(args), cancellationToken)));
+ rootCommand.Add(CreateLocalDeployCommand());
- rootCommand.Add(LegacyCommand(
- Constants.Command.Snapshot,
- "Creates an extension snapshot.",
- args => services.GetRequiredService().RunAsync(new SnapshotArguments(args), cancellationToken)));
+ rootCommand.Add(CreateSnapshotCommand());
rootCommand.Add(CreateDeployCommand());
@@ -235,10 +229,7 @@ await io.Error.Writer.WriteLineAsync(
rootCommand.Add(CreateTeardownCommand());
- rootCommand.Add(LegacyCommand(
- Constants.Command.Console,
- "Opens an interactive Bicep console.",
- args => services.GetRequiredService().RunAsync(new ConsoleArguments(args))));
+ rootCommand.Add(CreateConsoleCommand());
return rootCommand;
}
@@ -317,7 +308,7 @@ private Command CreateDeployCommand()
{
var additionalArguments = ParseAdditionalArguments(result.UnmatchedTokens);
var args = new DeployArguments(
- result.GetValue(parametersFileArgument)!,
+ result.GetRequiredValue(parametersFileArgument),
result.GetValue(noRestoreOption),
additionalArguments,
result.GetValue(formatOption));
@@ -351,7 +342,7 @@ private Command CreateWhatIfCommand()
{
var additionalArguments = ParseAdditionalArguments(result.UnmatchedTokens);
var args = new WhatIfArguments(
- result.GetValue(parametersFileArgument)!,
+ result.GetRequiredValue(parametersFileArgument),
result.GetValue(noRestoreOption),
additionalArguments);
@@ -384,7 +375,7 @@ private Command CreateTeardownCommand()
{
var additionalArguments = ParseAdditionalArguments(result.UnmatchedTokens);
var args = new TeardownArguments(
- result.GetValue(parametersFileArgument)!,
+ result.GetRequiredValue(parametersFileArgument),
result.GetValue(noRestoreOption),
additionalArguments);
@@ -394,6 +385,108 @@ private Command CreateTeardownCommand()
return command;
}
+ private Command CreateLocalDeployCommand()
+ {
+ var command = new Command(Constants.Command.LocalDeploy, "Performs a local deployment.");
+
+ var paramsFileArgument = new Argument("parameters-file")
+ {
+ Description = "The path to the .bicepparam file.",
+ };
+ var noRestoreOption = new Option("--no-restore")
+ {
+ Description = "Do not restore modules prior to deploying.",
+ };
+ var formatOption = new Option("--format")
+ {
+ Description = "Output format for deployment results (Default, Json).",
+ };
+
+ command.Add(paramsFileArgument);
+ command.Add(noRestoreOption);
+ command.Add(formatOption);
+
+ command.SetAction((result, ct) => RunCommandAsync(async () =>
+ {
+ var args = new LocalDeployArguments(
+ result.GetRequiredValue(paramsFileArgument),
+ 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, "Creates an extension snapshot.");
+
+ var inputFileArgument = new Argument("input-file")
+ {
+ Description = "The path to the .bicepparam file.",
+ };
+ var modeOption = new Option("--mode")
+ {
+ Description = "Snapshot mode (Overwrite, Validate).",
+ };
+ var tenantIdOption = new Option("--tenant-id")
+ {
+ Description = "The tenant ID.",
+ };
+ var subscriptionIdOption = new Option("--subscription-id")
+ {
+ Description = "The subscription ID.",
+ };
+ var locationOption = new Option("--location")
+ {
+ Description = "The location.",
+ };
+ var resourceGroupOption = new Option("--resource-group")
+ {
+ Description = "The resource group.",
+ };
+ var deploymentNameOption = new Option("--deployment-name")
+ {
+ Description = "The deployment name.",
+ };
+
+ command.Add(inputFileArgument);
+ command.Add(modeOption);
+ command.Add(tenantIdOption);
+ command.Add(subscriptionIdOption);
+ command.Add(locationOption);
+ command.Add(resourceGroupOption);
+ command.Add(deploymentNameOption);
+
+ command.SetAction((result, ct) => RunCommandAsync(async () =>
+ {
+ var args = new SnapshotArguments(
+ result.GetRequiredValue(inputFileArgument),
+ result.GetValue(modeOption),
+ result.GetValue(tenantIdOption),
+ result.GetValue(subscriptionIdOption),
+ 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 async Task RunCommandAsync(Func> action)
{
try
diff --git a/src/Bicep.Cli/Services/ArgumentParser.cs b/src/Bicep.Cli/Services/ArgumentParser.cs
index 2067a94c85f..743f69ccbf6 100644
--- a/src/Bicep.Cli/Services/ArgumentParser.cs
+++ b/src/Bicep.Cli/Services/ArgumentParser.cs
@@ -39,9 +39,6 @@ public static class ArgumentParser
Constants.Command.Publish => new PublishArguments(args[1..]),
Constants.Command.Restore => new RestoreArguments(args[1..]),
Constants.Command.Lint => new LintArguments(args[1..]),
- Constants.Command.LocalDeploy => new LocalDeployArguments(args[1..]),
- Constants.Command.Snapshot => new SnapshotArguments(args[1..]),
- Constants.Command.Console => new ConsoleArguments(args[1..]),
_ => null,
};
}
From e55d65dfb3fe4592b90b26f56b81c7e73a90a7be Mon Sep 17 00:00:00 2001
From: Anthony Martin <38542602+anthony-c-martin@users.noreply.github.com>
Date: Tue, 31 Mar 2026 17:06:28 -0400
Subject: [PATCH 5/8] More
---
.../ArgumentParserTests.cs | 222 -----------
src/Bicep.Cli/Arguments/BuildArguments.cs | 118 +-----
.../Arguments/BuildParamsArguments.cs | 142 +------
src/Bicep.Cli/Arguments/DecompileArguments.cs | 82 +---
.../Arguments/DecompileParamsArguments.cs | 98 +----
.../GenerateParametersFileArguments.cs | 126 +-----
src/Bicep.Cli/Arguments/TestArguments.cs | 58 +--
src/Bicep.Cli/Program.cs | 363 ++++++++++++++++--
src/Bicep.Cli/Services/ArgumentParser.cs | 6 -
9 files changed, 395 insertions(+), 820 deletions(-)
diff --git a/src/Bicep.Cli.UnitTests/ArgumentParserTests.cs b/src/Bicep.Cli.UnitTests/ArgumentParserTests.cs
index 93694e9b596..0d09d88502c 100644
--- a/src/Bicep.Cli.UnitTests/ArgumentParserTests.cs
+++ b/src/Bicep.Cli.UnitTests/ArgumentParserTests.cs
@@ -28,50 +28,6 @@ public void Wrong_command_should_return_null()
}
[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\"")]
@@ -103,113 +59,6 @@ public void Invalid_args_trigger_validation_exceptions(string[] parameters, stri
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()
{
@@ -300,77 +149,6 @@ public void Help_argument_should_return_HelpShortArguments_instance()
}
}
- [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()
{
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/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/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/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/Program.cs b/src/Bicep.Cli/Program.cs
index 3a4b808abc0..5f9f615fc9e 100644
--- a/src/Bicep.Cli/Program.cs
+++ b/src/Bicep.Cli/Program.cs
@@ -15,6 +15,7 @@
using Bicep.Cli.Services;
using Bicep.Core;
using Bicep.Core.Emit;
+using Bicep.Core.Emit.Options;
using Bicep.Core.Exceptions;
using Bicep.Core.Features;
using Bicep.Core.Tracing;
@@ -162,40 +163,22 @@ await io.Error.Writer.WriteLineAsync(
// the existing argument class for parsing. Once a command is fully migrated, replace
// the LegacyCommand call with a Command that has explicit Option/Argument
// members and calls the command handler with the bound values directly.
- rootCommand.Add(LegacyCommand(
- Constants.Command.Build,
- "Builds a .bicep file.",
- args => services.GetRequiredService().RunAsync(new BuildArguments(args))));
+ rootCommand.Add(CreateBuildCommand());
- rootCommand.Add(LegacyCommand(
- Constants.Command.Test,
- "Runs tests in a .bicep file.",
- args => services.GetRequiredService().RunAsync(new TestArguments(args))));
+ rootCommand.Add(CreateTestCommand());
- rootCommand.Add(LegacyCommand(
- Constants.Command.BuildParams,
- "Builds a .bicepparam file.",
- args => services.GetRequiredService().RunAsync(new BuildParamsArguments(args))));
+ rootCommand.Add(CreateBuildParamsCommand());
rootCommand.Add(LegacyCommand(
Constants.Command.Format,
"Formats a .bicep file.",
args => Task.FromResult(services.GetRequiredService().Run(new FormatArguments(args)))));
- rootCommand.Add(LegacyCommand(
- Constants.Command.GenerateParamsFile,
- "Generates a parameters file for a .bicep file.",
- args => services.GetRequiredService().RunAsync(new GenerateParametersFileArguments(args))));
+ rootCommand.Add(CreateGenerateParamsFileCommand());
- rootCommand.Add(LegacyCommand(
- Constants.Command.Decompile,
- "Attempts to decompile a template .json file to .bicep.",
- args => services.GetRequiredService().RunAsync(new DecompileArguments(args))));
+ rootCommand.Add(CreateDecompileCommand());
- rootCommand.Add(LegacyCommand(
- Constants.Command.DecompileParams,
- "Attempts to decompile a parameters .json file to .bicepparam.",
- args => Task.FromResult(services.GetRequiredService().Run(new DecompileParamsArguments(args)))));
+ rootCommand.Add(CreateDecompileParamsCommand());
rootCommand.Add(LegacyCommand(
Constants.Command.Publish,
@@ -234,6 +217,338 @@ await io.Error.Writer.WriteLineAsync(
return rootCommand;
}
+ private Command CreateBuildCommand()
+ {
+ var command = new Command(Constants.Command.Build, "Builds a .bicep file.")
+ {
+ TreatUnmatchedTokensAsErrors = true,
+ };
+
+ var inputFileArgument = new Argument("input-file")
+ {
+ Description = "The path to the input .bicep file.",
+ Arity = ArgumentArity.ZeroOrOne,
+ };
+ var stdoutOption = new Option("--stdout")
+ {
+ Description = "Print output to stdout.",
+ };
+ var noRestoreOption = new Option("--no-restore")
+ {
+ Description = "Do not restore modules prior to building.",
+ };
+ var outDirOption = new Option("--outdir")
+ {
+ Description = "Save output to the specified directory.",
+ };
+ var outFileOption = new Option("--outfile")
+ {
+ Description = "Save output to the specified file path.",
+ };
+ var filePatternOption = new Option("--pattern")
+ {
+ Description = "Build all files matching the specified pattern.",
+ };
+ var diagnosticsFormatOption = new Option("--diagnostics-format")
+ {
+ Description = "Set the format of diagnostics (Default, SARIF).",
+ };
+
+ command.Add(inputFileArgument);
+ command.Add(stdoutOption);
+ command.Add(noRestoreOption);
+ command.Add(outDirOption);
+ command.Add(outFileOption);
+ command.Add(filePatternOption);
+ command.Add(diagnosticsFormatOption);
+
+ command.SetAction((result, ct) => RunCommandAsync(async () =>
+ {
+ var args = new BuildArguments(
+ result.GetValue(inputFileArgument),
+ result.GetValue(stdoutOption),
+ result.GetValue(noRestoreOption),
+ result.GetValue(outDirOption),
+ result.GetValue(outFileOption),
+ result.GetValue(filePatternOption),
+ result.GetValue(diagnosticsFormatOption));
+
+ return await services.GetRequiredService().RunAsync(args);
+ }));
+
+ return command;
+ }
+
+ private Command CreateTestCommand()
+ {
+ var command = new Command(Constants.Command.Test, "Runs tests in a .bicep file.")
+ {
+ TreatUnmatchedTokensAsErrors = true,
+ };
+
+ var inputFileArgument = new Argument("input-file")
+ {
+ Description = "The path to the input .bicep file.",
+ };
+ var noRestoreOption = new Option("--no-restore")
+ {
+ Description = "Do not restore modules prior to running tests.",
+ };
+ var diagnosticsFormatOption = new Option("--diagnostics-format")
+ {
+ Description = "Set the format of diagnostics (Default, SARIF).",
+ };
+
+ command.Add(inputFileArgument);
+ command.Add(noRestoreOption);
+ command.Add(diagnosticsFormatOption);
+
+ command.SetAction((result, ct) => RunCommandAsync(async () =>
+ {
+ var args = new TestArguments(
+ result.GetRequiredValue(inputFileArgument),
+ result.GetValue(noRestoreOption),
+ result.GetValue(diagnosticsFormatOption));
+
+ return await services.GetRequiredService().RunAsync(args);
+ }));
+
+ return command;
+ }
+
+ private Command CreateBuildParamsCommand()
+ {
+ var command = new Command(Constants.Command.BuildParams, "Builds a .bicepparam file.")
+ {
+ TreatUnmatchedTokensAsErrors = true,
+ };
+
+ var inputFileArgument = new Argument("input-file")
+ {
+ Description = "The path to the input .bicepparam file.",
+ Arity = ArgumentArity.ZeroOrOne,
+ };
+ var stdoutOption = new Option("--stdout")
+ {
+ Description = "Print output to stdout.",
+ };
+ var noRestoreOption = new Option("--no-restore")
+ {
+ Description = "Do not restore modules prior to building.",
+ };
+ var outDirOption = new Option("--outdir")
+ {
+ Description = "Save output to the specified directory.",
+ };
+ var outFileOption = new Option("--outfile")
+ {
+ Description = "Save output to the specified file path.",
+ };
+ var filePatternOption = new Option("--pattern")
+ {
+ Description = "Build all files matching the specified pattern.",
+ };
+ var bicepFileOption = new Option("--bicep-file")
+ {
+ Description = "Path to the .bicep template file that will be used to validate the .bicepparam file.",
+ };
+ var diagnosticsFormatOption = new Option("--diagnostics-format")
+ {
+ Description = "Set the format of diagnostics (Default, SARIF).",
+ };
+
+ 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.SetAction((result, ct) => RunCommandAsync(async () =>
+ {
+ var args = new BuildParamsArguments(
+ result.GetValue(inputFileArgument),
+ result.GetValue(stdoutOption),
+ result.GetValue(noRestoreOption),
+ result.GetValue(outDirOption),
+ result.GetValue(outFileOption),
+ result.GetValue(filePatternOption),
+ result.GetValue(bicepFileOption),
+ result.GetValue(diagnosticsFormatOption));
+
+ return await services.GetRequiredService().RunAsync(args);
+ }));
+
+ return command;
+ }
+
+ private Command CreateGenerateParamsFileCommand()
+ {
+ var command = new Command(Constants.Command.GenerateParamsFile, "Generates a parameters file for a .bicep file.")
+ {
+ TreatUnmatchedTokensAsErrors = true,
+ };
+
+ var inputFileArgument = new Argument("input-file")
+ {
+ Description = "The path to the input .bicep file.",
+ };
+ var stdoutOption = new Option("--stdout")
+ {
+ Description = "Print output to stdout.",
+ };
+ var noRestoreOption = new Option("--no-restore")
+ {
+ Description = "Do not restore modules prior to generating.",
+ };
+ var outDirOption = new Option("--outdir")
+ {
+ Description = "Save output to the specified directory.",
+ };
+ var outFileOption = new Option("--outfile")
+ {
+ Description = "Save output to the specified file path.",
+ };
+ var outputFormatOption = new Option("--output-format")
+ {
+ Description = "Output format (Json, BicepParam).",
+ };
+ var includeParamsOption = new Option("--include-params")
+ {
+ Description = "Which parameters to include (RequiredOnly, All).",
+ };
+
+ command.Add(inputFileArgument);
+ command.Add(stdoutOption);
+ command.Add(noRestoreOption);
+ command.Add(outDirOption);
+ command.Add(outFileOption);
+ command.Add(outputFormatOption);
+ command.Add(includeParamsOption);
+
+ command.SetAction((result, ct) => RunCommandAsync(async () =>
+ {
+ var args = new GenerateParametersFileArguments(
+ result.GetRequiredValue(inputFileArgument),
+ result.GetValue(stdoutOption),
+ result.GetValue(noRestoreOption),
+ result.GetValue(outDirOption),
+ result.GetValue(outFileOption),
+ 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("input-file")
+ {
+ Description = "The path to the ARM template .json file.",
+ };
+ var stdoutOption = new Option("--stdout")
+ {
+ Description = "Print output to stdout.",
+ };
+ var forceOption = new Option("--force")
+ {
+ Description = "Allow overwriting existing files.",
+ };
+ var outDirOption = new Option("--outdir")
+ {
+ Description = "Save output to the specified directory.",
+ };
+ var outFileOption = new Option("--outfile")
+ {
+ Description = "Save output to the specified file path.",
+ };
+
+ command.Add(inputFileArgument);
+ command.Add(stdoutOption);
+ command.Add(forceOption);
+ command.Add(outDirOption);
+ command.Add(outFileOption);
+
+ command.SetAction((result, ct) => RunCommandAsync(async () =>
+ {
+ var args = new DecompileArguments(
+ result.GetRequiredValue(inputFileArgument),
+ result.GetValue(stdoutOption),
+ result.GetValue(forceOption),
+ result.GetValue(outDirOption),
+ result.GetValue(outFileOption));
+
+ 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("input-file")
+ {
+ Description = "The path to the parameters .json file.",
+ };
+ var stdoutOption = new Option("--stdout")
+ {
+ Description = "Print output to stdout.",
+ };
+ var forceOption = new Option("--force")
+ {
+ Description = "Allow overwriting existing files.",
+ };
+ var outDirOption = new Option("--outdir")
+ {
+ Description = "Save output to the specified directory.",
+ };
+ var outFileOption = new Option("--outfile")
+ {
+ Description = "Save output to the specified file path.",
+ };
+ var bicepFileOption = new Option("--bicep-file")
+ {
+ Description = "Path to the .bicep template file associated with the parameters file.",
+ };
+
+ command.Add(inputFileArgument);
+ command.Add(stdoutOption);
+ command.Add(forceOption);
+ command.Add(outDirOption);
+ command.Add(outFileOption);
+ command.Add(bicepFileOption);
+
+ command.SetAction((result, ct) => RunCommandAsync(async () =>
+ {
+ var args = new DecompileParamsArguments(
+ result.GetRequiredValue(inputFileArgument),
+ result.GetValue(stdoutOption),
+ result.GetValue(forceOption),
+ result.GetValue(outDirOption),
+ result.GetValue(outFileOption),
+ result.GetValue(bicepFileOption));
+
+ return await Task.FromResult(services.GetRequiredService().Run(args));
+ }));
+
+ return command;
+ }
+
private Command CreateJsonRpcCommand()
{
var command = new Command(Constants.Command.JsonRpc, "Starts the Bicep JSON-RPC server.");
diff --git a/src/Bicep.Cli/Services/ArgumentParser.cs b/src/Bicep.Cli/Services/ArgumentParser.cs
index 743f69ccbf6..1850e547135 100644
--- a/src/Bicep.Cli/Services/ArgumentParser.cs
+++ b/src/Bicep.Cli/Services/ArgumentParser.cs
@@ -28,13 +28,7 @@ public static class ArgumentParser
// 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..]),
From 12bf3dc74b959ace9311b8e6a4d75b10278cb345 Mon Sep 17 00:00:00 2001
From: Anthony Martin <38542602+anthony-c-martin@users.noreply.github.com>
Date: Tue, 31 Mar 2026 18:01:46 -0400
Subject: [PATCH 6/8] All commands
---
.../ArgumentParserTests.cs | 198 -----------
src/Bicep.Cli/Arguments/FormatArguments.cs | 198 +----------
src/Bicep.Cli/Arguments/LintArguments.cs | 71 +---
src/Bicep.Cli/Arguments/PublishArguments.cs | 107 +-----
.../Arguments/PublishExtensionArguments.cs | 86 +----
src/Bicep.Cli/Arguments/RestoreArguments.cs | 59 +---
src/Bicep.Cli/Arguments/RootArguments.cs | 38 --
src/Bicep.Cli/Commands/CliInfoPrinter.cs | 45 +++
.../Commands/PublishExtensionCommand.cs | 6 +-
src/Bicep.Cli/Commands/RootCommand.cs | 315 -----------------
.../Helpers/IServiceCollectionExtensions.cs | 3 +-
src/Bicep.Cli/Program.cs | 334 ++++++++++++++----
src/Bicep.Cli/Services/ArgumentParser.cs | 40 ---
13 files changed, 358 insertions(+), 1142 deletions(-)
delete mode 100644 src/Bicep.Cli.UnitTests/ArgumentParserTests.cs
delete mode 100644 src/Bicep.Cli/Arguments/RootArguments.cs
create mode 100644 src/Bicep.Cli/Commands/CliInfoPrinter.cs
delete mode 100644 src/Bicep.Cli/Commands/RootCommand.cs
delete mode 100644 src/Bicep.Cli/Services/ArgumentParser.cs
diff --git a/src/Bicep.Cli.UnitTests/ArgumentParserTests.cs b/src/Bicep.Cli.UnitTests/ArgumentParserTests.cs
deleted file mode 100644
index 0d09d88502c..00000000000
--- a/src/Bicep.Cli.UnitTests/ArgumentParserTests.cs
+++ /dev/null
@@ -1,198 +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]
- // 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 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 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/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/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/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/Commands/CliInfoPrinter.cs b/src/Bicep.Cli/Commands/CliInfoPrinter.cs
new file mode 100644
index 00000000000..4dc64d571f5
--- /dev/null
+++ b/src/Bicep.Cli/Commands/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.Commands
+{
+ 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/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 5693ce25b1b..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;
- }
-
- internal 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/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 5f9f615fc9e..bda5cae8910 100644
--- a/src/Bicep.Cli/Program.cs
+++ b/src/Bicep.Cli/Program.cs
@@ -16,6 +16,7 @@
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;
@@ -24,7 +25,6 @@
using Microsoft.Extensions.Logging;
using Spectre.Console;
-// Avoid naming conflict with Bicep.Cli.Commands.RootCommand
using SclRootCommand = System.CommandLine.RootCommand;
public class HelpExamplesAction : SynchronousCommandLineAction
@@ -100,13 +100,6 @@ public async Task RunAsync(string[] args, CancellationToken cancellationTok
return await rootCommand.Parse(args).InvokeAsync(cancellationToken: cancellationToken);
}
- ///
- /// Builds the System.CommandLine command hierarchy. Each existing subcommand is currently
- /// registered as a legacy pass-through stub via . To migrate a
- /// command, replace its LegacyCommand call with a that
- /// declares its own and members and
- /// invokes the command handler directly.
- ///
private SclRootCommand BuildCommandLine(CancellationToken cancellationToken)
{
var rootCommand = new SclRootCommand("Bicep CLI")
@@ -134,23 +127,25 @@ private SclRootCommand BuildCommandLine(CancellationToken cancellationToken)
rootCommand.SetAction(async (ParseResult pr, CancellationToken ct) =>
{
- var bicepRootCommand = services.GetRequiredService();
-
+ var environment = services.GetRequiredService();
var unmatched = pr.UnmatchedTokens;
if (pr.GetValue(versionOption))
{
- return bicepRootCommand.Run(new RootArguments("--version", Constants.Command.Root));
+ Commands.CliInfoPrinter.PrintVersion(io, environment);
+ return 0;
}
if (pr.GetValue(licenseOption))
{
- return bicepRootCommand.Run(new RootArguments("--license", Constants.Command.Root));
+ Commands.CliInfoPrinter.PrintLicense(io);
+ return 0;
}
if (pr.GetValue(thirdPartyNoticesOption))
{
- return bicepRootCommand.Run(new RootArguments("--third-party-notices", Constants.Command.Root));
+ Commands.CliInfoPrinter.PrintThirdPartyNotices(io);
+ return 0;
}
await io.Error.Writer.WriteLineAsync(
@@ -158,21 +153,13 @@ await io.Error.Writer.WriteLineAsync(
return 1;
});
- // Each subcommand below is a legacy pass-through stub. Arguments not recognized by
- // System.CommandLine are collected in ParseResult.UnmatchedTokens and forwarded to
- // the existing argument class for parsing. Once a command is fully migrated, replace
- // the LegacyCommand call with a Command that has explicit Option/Argument
- // members and calls the command handler with the bound values directly.
rootCommand.Add(CreateBuildCommand());
rootCommand.Add(CreateTestCommand());
rootCommand.Add(CreateBuildParamsCommand());
- rootCommand.Add(LegacyCommand(
- Constants.Command.Format,
- "Formats a .bicep file.",
- args => Task.FromResult(services.GetRequiredService().Run(new FormatArguments(args)))));
+ rootCommand.Add(CreateFormatCommand());
rootCommand.Add(CreateGenerateParamsFileCommand());
@@ -180,25 +167,13 @@ await io.Error.Writer.WriteLineAsync(
rootCommand.Add(CreateDecompileParamsCommand());
- rootCommand.Add(LegacyCommand(
- Constants.Command.Publish,
- "Publishes a .bicep file to a registry.",
- args => services.GetRequiredService().RunAsync(new PublishArguments(args))));
+ rootCommand.Add(CreatePublishCommand());
- rootCommand.Add(LegacyCommand(
- Constants.Command.PublishExtension,
- "Publishes a Bicep extension to a registry.",
- args => services.GetRequiredService().RunAsync(new PublishExtensionArguments(args), cancellationToken)));
+ rootCommand.Add(CreatePublishExtensionCommand());
- rootCommand.Add(LegacyCommand(
- Constants.Command.Restore,
- "Restores external modules for a .bicep file.",
- args => services.GetRequiredService().RunAsync(new RestoreArguments(args))));
+ rootCommand.Add(CreateRestoreCommand());
- rootCommand.Add(LegacyCommand(
- Constants.Command.Lint,
- "Lints a .bicep file.",
- args => services.GetRequiredService().RunAsync(new LintArguments(args))));
+ rootCommand.Add(CreateLintCommand());
rootCommand.Add(CreateJsonRpcCommand());
@@ -549,6 +524,265 @@ private Command CreateDecompileParamsCommand()
return command;
}
+ private Command CreateFormatCommand()
+ {
+ var command = new Command(Constants.Command.Format, "Formats a .bicep file.");
+
+ var inputFileArgument = new Argument("input-file")
+ {
+ Description = "The path to the input .bicep file.",
+ Arity = ArgumentArity.ZeroOrOne,
+ };
+ var stdoutOption = new Option("--stdout")
+ {
+ Description = "Print output to stdout.",
+ };
+ var outDirOption = new Option("--outdir")
+ {
+ Description = "Save output to the specified directory.",
+ };
+ var outFileOption = new Option("--outfile")
+ {
+ Description = "Save output to the specified file path.",
+ };
+ var filePatternOption = new Option("--pattern")
+ {
+ Description = "Format all files matching the specified pattern.",
+ };
+ var newlineKindOption = new Option("--newline-kind")
+ {
+ Description = "Set the newline kind (Auto, LF, CRLF, CR).",
+ };
+ var indentKindOption = new Option("--indent-kind")
+ {
+ Description = "Set the indent kind (Space, Tab).",
+ };
+ var indentSizeOption = new Option("--indent-size")
+ {
+ Description = "Set the indent size (number of spaces).",
+ };
+ var insertFinalNewlineOption = new Option("--insert-final-newline")
+ {
+ 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.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 a .bicep file to a registry.");
+
+ var inputFileArgument = new Argument("input-file")
+ {
+ Description = "The path to the .bicep file to publish.",
+ };
+ var targetOption = new Option("--target")
+ {
+ Description = "The target module reference.",
+ };
+ var documentationUriOption = new Option("--documentation-uri")
+ {
+ Description = "The documentation URI.",
+ };
+ var noRestoreOption = new Option]