diff --git a/docs/experimental/console-command.md b/docs/experimental/console-command.md index 6b857a6c464..520e4454d30 100644 --- a/docs/experimental/console-command.md +++ b/docs/experimental/console-command.md @@ -10,6 +10,8 @@ The `console` command provides an interactive Read-Eval-Print Loop (REPL) enviro - **Syntax Highlighting**: Real-time syntax highlighting for input and output ## Usage +To launch the REPL, run the following Bicep command: + ```sh bicep console ``` @@ -53,6 +55,7 @@ true ``` ### Complex Expressions +#### Lambdas ```bicep > var users = [ { name: 'Alice', age: 30 } @@ -69,11 +72,60 @@ true } ] ``` +#### User-defined types and functions +```bicep +> type PersonType = { + name: string + age: int +} +> func sayHi(person PersonType) string => 'Hello ${person.name}, you are ${person.age} years old!' +> var alice = { + name: 'Alice' + age: 30 +} +> [ sayHi(alice), sayHi({ name: 'Bob', age: 25 })] +[ + 'Hello Alice, you are 30 years old!' + 'Hello Bob, you are 25 years old!' +] +``` + +### Loading content from files +- Bicep console also supports the [`load*()` functions](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions-files). Note: The directory from which the `bicep console` command is run is used as the _current directory_ when evaluating the `load*()` functions + + +### Piping and standard input/output redirection +The console command supports evaluating expressions provided through piping or redirected standard input, i.e.: + +**Powershell**: +```pwsh +# piped input +"parseCidr('10.144.0.0/20')" | bicep console +``` + +**Bash**: +```sh +# piped input +echo "parseCidr('10.144.0.0/20')" | bicep console +# stdin redirection from file content +bicep console < test.txt +``` + +Multi-line input is also supported, i.e: +```pwsh +"{ +> foo: 'bar' +> }.foo" | bicep console +# Output: bar +``` + +Output redirection is also supported: +```sh +"toObject([{name:'Evie', age:4},{name:'Casper', age:3}], x => x.name)" | bicep console > output.json +``` ## Limitations - No support for expressions requiring Azure context, e.g. `resourceGroup()` -- No file system access or external dependencies -- Limited to expression evaluation and variable declarations - No support for for-loop expressions, e.g. `[for i in range(0, x): i]` - No persistent state between console sessions - No completions support diff --git a/src/Bicep.Cli.IntegrationTests/Commands/ConsoleCommandTests.cs b/src/Bicep.Cli.IntegrationTests/Commands/ConsoleCommandTests.cs new file mode 100644 index 00000000000..a281bf7765b --- /dev/null +++ b/src/Bicep.Cli.IntegrationTests/Commands/ConsoleCommandTests.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Cli.UnitTests.Assertions; +using Bicep.Core.UnitTests.Assertions; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bicep.Cli.IntegrationTests.Commands; + +[TestClass] +public class ConsoleCommandTests : TestBase +{ + [TestMethod] + public async Task Redirected_input_context_with_single_line_input_should_succeed() + { + // "concat('Hello', ' ', 'World', '!')" | bicep console + var input = "concat('Hello', ' ', 'World', '!')"; + var result = await Bicep( + (@out, err) => new IOContext( + new(new StringReader(input), IsRedirected: true), + new(@out, IsRedirected: false), + new(err, IsRedirected: false)), + "console"); + + result.Should().Succeed(); + result.WithoutAnsi().Stdout.Should().BeEquivalentToIgnoringNewlines(""" +'Hello World!' + +"""); + } + + [TestMethod] + public async Task Redirected_input_context_with_multi_line_input_should_succeed() + { + /* +var greeting = 'Hello' +var target = { + value: 'World' +} + +'${greeting} ${target.value}!' | bicep console + */ + var input = """ + var greeting = 'Hello' + var target = { + value: 'World' + } + + '${greeting} ${target.value}!' + """; + + var result = await Bicep( + (@out, err) => new IOContext( + new(new StringReader(input), IsRedirected: true), + new(@out, IsRedirected: false), + new(err, IsRedirected: false)), + "console"); + + result.Should().Succeed(); + result.WithoutAnsi().Stdout.Should().BeEquivalentToIgnoringNewlines(""" +'Hello World!' + +"""); + } + + [TestMethod] + public async Task Redirected_output_context_should_not_have_ansi_codes() + { + // "concat('Hello', ' ', 'World', '!')" | bicep console > + var input = "concat('Hello', ' ', 'World', '!')"; + var result = await Bicep( + (@out, err) => new IOContext( + new(new StringReader(input), IsRedirected: true), + new(@out, IsRedirected: true), + new(err, IsRedirected: false)), + "console"); + + result.Should().Succeed(); + var withoutAnsi = result.WithoutAnsi(); + result.Stdout.Should().Be(withoutAnsi.Stdout); + } +} diff --git a/src/Bicep.Cli.IntegrationTests/TestBase.cs b/src/Bicep.Cli.IntegrationTests/TestBase.cs index 073880bc3ef..07215e9d5c2 100644 --- a/src/Bicep.Cli.IntegrationTests/TestBase.cs +++ b/src/Bicep.Cli.IntegrationTests/TestBase.cs @@ -79,34 +79,7 @@ this with } protected static Task Bicep(InvocationSettings settings, Action? registerAction, CancellationToken cancellationToken, params string?[] args /*null args are ignored*/) - => TextWriterHelper.InvokeWriterAction((@out, err) - => new Program( - new(Output: @out, Error: err), - services => - { - if (settings.FeatureOverrides is { }) - { - services.WithFeatureOverrides(settings.FeatureOverrides); - } - - IServiceCollectionExtensions.AddMockHttpClientIfNotNull(services, settings.ModuleMetadataClient); - - services - .AddSingletonIfNotNull(settings.Environment ?? BicepTestConstants.EmptyEnvironment) - .AddSingletonIfNotNull(settings.ClientFactory) - .AddSingletonIfNotNull(settings.TemplateSpecRepositoryFactory) - .AddSingleton(AnsiConsole.Create(new() - { - Ansi = AnsiSupport.Yes, - ColorSystem = ColorSystemSupport.Standard, - Interactive = InteractionSupport.No, - Out = new AnsiConsoleOutput(@out), - })); - - registerAction?.Invoke(services); - } - ) - .RunAsync(args.ToArrayExcludingNull(), cancellationToken)); + => BicepInternal(settings, registerAction, null, cancellationToken, args); protected static Task Bicep(params string[] args) => Bicep(InvocationSettings.Default, args); @@ -119,6 +92,10 @@ protected static Task Bicep(Action registerAction protected static Task Bicep(InvocationSettings settings, params string?[] args /*null args are ignored*/) => Bicep(settings, null, CancellationToken.None, args); + protected static Task Bicep( + Func ioContextFactory, params string[] args) + => BicepInternal(InvocationSettings.Default, null, ioContextFactory, CancellationToken.None, args); + protected static void AssertNoErrors(string error) { foreach (var line in error.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries)) @@ -188,5 +165,43 @@ protected static IEnvironment CreateDefaultEnvironment() => TestEnvironment.Defa ("intEnvVariableName", "100"), ("boolEnvironmentVariable", "true") ); + + private static Task BicepInternal( + InvocationSettings settings, + Action? registerAction, + Func? ioContextFactory, + CancellationToken cancellationToken, params string?[] args /*null args are ignored*/) + => TextWriterHelper.InvokeWriterAction((@out, err) => + { + var ioContext = ioContextFactory?.Invoke(@out, err) ?? new IOContext( + Input: new(new StringReader(string.Empty), false), + Output: new(@out, false), + Error: new(err, false)); + return new Program(ioContext, + services => + { + if (settings.FeatureOverrides is { }) + { + services.WithFeatureOverrides(settings.FeatureOverrides); + } + + IServiceCollectionExtensions.AddMockHttpClientIfNotNull(services, settings.ModuleMetadataClient); + + services + .AddSingletonIfNotNull(settings.Environment ?? BicepTestConstants.EmptyEnvironment) + .AddSingletonIfNotNull(settings.ClientFactory) + .AddSingletonIfNotNull(settings.TemplateSpecRepositoryFactory) + .AddSingleton(AnsiConsole.Create(new() + { + Ansi = AnsiSupport.Yes, + ColorSystem = ColorSystemSupport.Standard, + Interactive = InteractionSupport.No, + Out = new AnsiConsoleOutput(@out), + })); + + registerAction?.Invoke(services); + } + ).RunAsync(args.ToArrayExcludingNull(), cancellationToken); + }); } } diff --git a/src/Bicep.Cli/Commands/ConsoleCommand.cs b/src/Bicep.Cli/Commands/ConsoleCommand.cs index bb3a09d3f73..dbd3465aeaa 100644 --- a/src/Bicep.Cli/Commands/ConsoleCommand.cs +++ b/src/Bicep.Cli/Commands/ConsoleCommand.cs @@ -26,8 +26,7 @@ public class ConsoleCommand( ILogger logger, IOContext io, IEnvironment environment, - ReplEnvironment replEnvironment, - IAnsiConsole console) : ICommand + ReplEnvironment replEnvironment) : ICommand { private const string FirstLinePrefix = "> "; @@ -57,16 +56,52 @@ public async Task RunAsync(ConsoleArguments args) { logger.LogWarning($"WARNING: The '{args.CommandName}' CLI command is an experimental feature. Experimental features should be used for testing purposes only, as there are no guarantees about the quality or stability of these features."); - if (!console.Profile.Capabilities.Interactive) + if (io.Input.IsRedirected) { - logger.LogError($"The '{args.CommandName}' CLI command requires an interactive console."); - return 1; + // Read all input from stdin if redirected (via pipe or file redirection) + var input = await io.Input.Reader.ReadToEndAsync(); + + if (string.IsNullOrWhiteSpace(input)) + { + return 0; + } + + // Handle input line by line (to support multi-line strings) + var outputBuilder = new StringBuilder(); + var inputBuffer = new StringBuilder(); + + using var reader = new StringReader(input); + while (await reader.ReadLineAsync() is { } line) + { + inputBuffer.Append(line); + inputBuffer.Append('\n'); + + var current = inputBuffer.ToString(); + if (ReplEnvironment.ShouldSubmitBuffer(current, line)) + { + inputBuffer.Clear(); + outputBuilder.Append(replEnvironment.EvaluateAndGetOutput(current)); + } + } + + if (inputBuffer.Length > 0) + { + outputBuilder.Append(replEnvironment.EvaluateAndGetOutput(inputBuffer.ToString())); + } + + var output = outputBuilder.ToString(); + if (io.Output.IsRedirected) + { + output = AnsiHelper.RemoveCodes(output); + } + await io.Output.Writer.WriteAsync(output); + return 0; } - await io.Output.WriteLineAsync($"Bicep Console version {environment.GetVersionString()}"); - await io.Output.WriteLineAsync("Type 'help' for available commands, press ESC to quit."); - await io.Output.WriteLineAsync("Multi-line input supported."); - await io.Output.WriteLineAsync(string.Empty); + await io.Output.Writer.WriteLineAsync($"Bicep Console version {environment.GetVersionString()}"); + await io.Output.Writer.WriteLineAsync("Type 'help' for available commands, press ESC to quit."); + await io.Output.Writer.WriteLineAsync("Multi-line input supported."); + await io.Output.Writer.WriteLineAsync(string.Empty); var buffer = new StringBuilder(); @@ -92,8 +127,8 @@ public async Task RunAsync(ConsoleArguments args) if (rawLine.Equals("help", StringComparison.OrdinalIgnoreCase)) { - await io.Output.WriteLineAsync("Enter expressions or 'var name = '. Multi-line supported until structure closes."); - await io.Output.WriteLineAsync("Commands: exit, clear"); + await io.Output.Writer.WriteLineAsync("Enter expressions or 'var name = '. Multi-line supported until structure closes."); + await io.Output.Writer.WriteLineAsync("Commands: exit, clear"); continue; } } @@ -109,7 +144,7 @@ public async Task RunAsync(ConsoleArguments args) // evaluate input var output = replEnvironment.EvaluateAndGetOutput(current); - await io.Output.WriteAsync(output); + await io.Output.Writer.WriteAsync(output); } } @@ -132,8 +167,8 @@ private async Task PrintHistory(StringBuilder buffer, List lineBuffer cursorOffset = lineBuffer.Count; var output2 = replEnvironment.HighlightInputLine(FirstLinePrefix, buffer.ToString(), lineBuffer, cursorOffset, printPrevLines: true); - await io.Output.WriteAsync(PrintHelper.MoveCursorUp(prevBufferLineCount)); - await io.Output.WriteAsync(output2); + await io.Output.Writer.WriteAsync(PrintHelper.MoveCursorUp(prevBufferLineCount)); + await io.Output.Writer.WriteAsync(output2); return cursorOffset; } @@ -145,7 +180,7 @@ private string GetPrefix(StringBuilder buffer) private async Task ReadLine(StringBuilder buffer) { - await io.Output.WriteAsync(GetPrefix(buffer)); + await io.Output.Writer.WriteAsync(GetPrefix(buffer)); var lineBuffer = new List(); var cursorOffset = 0; @@ -180,7 +215,7 @@ private string GetPrefix(StringBuilder buffer) } if (keyInfo.Key == ConsoleKey.Enter) { - await io.Output.FlushAsync(); + await io.Output.Writer.FlushAsync(); break; } else if (keyInfo.Key == ConsoleKey.Backspace) @@ -209,10 +244,10 @@ private string GetPrefix(StringBuilder buffer) } var output = replEnvironment.HighlightInputLine(GetPrefix(buffer), buffer.ToString(), lineBuffer, cursorOffset, printPrevLines: false); - await io.Output.WriteAsync(output); + await io.Output.Writer.WriteAsync(output); } - await io.Output.WriteAsync("\n"); + await io.Output.Writer.WriteAsync("\n"); return string.Concat(lineBuffer); } diff --git a/src/Bicep.Cli/Commands/DecompileCommand.cs b/src/Bicep.Cli/Commands/DecompileCommand.cs index 3438d0d643f..555bba8d7c7 100644 --- a/src/Bicep.Cli/Commands/DecompileCommand.cs +++ b/src/Bicep.Cli/Commands/DecompileCommand.cs @@ -88,7 +88,7 @@ public async Task RunAsync(DecompileArguments args) } catch (Exception exception) { - await io.Error.WriteLineAsync(string.Format(CliResources.DecompilationFailedFormat, inputUri, exception.Message)); + await io.Error.Writer.WriteLineAsync(string.Format(CliResources.DecompilationFailedFormat, inputUri, exception.Message)); return 1; } } diff --git a/src/Bicep.Cli/Commands/DecompileParamsCommand.cs b/src/Bicep.Cli/Commands/DecompileParamsCommand.cs index 1ecfdfe6120..db9925d372b 100644 --- a/src/Bicep.Cli/Commands/DecompileParamsCommand.cs +++ b/src/Bicep.Cli/Commands/DecompileParamsCommand.cs @@ -64,7 +64,7 @@ public int Run(DecompileParamsArguments args) } catch (Exception exception) { - io.Error.WriteLine(string.Format(CliResources.DecompilationFailedFormat, inputUri, exception.Message)); + io.Error.Writer.WriteLine(string.Format(CliResources.DecompilationFailedFormat, inputUri, exception.Message)); return 1; } } diff --git a/src/Bicep.Cli/Commands/FormatCommand.cs b/src/Bicep.Cli/Commands/FormatCommand.cs index 899bc3fed1a..56c187a5e2c 100644 --- a/src/Bicep.Cli/Commands/FormatCommand.cs +++ b/src/Bicep.Cli/Commands/FormatCommand.cs @@ -57,8 +57,8 @@ public void Format(FormatArguments args, IOUri inputUri, IOUri outputUri, bool o if (outputToStdOut) { - io.Output.Write(output); - io.Output.Flush(); + io.Output.Writer.Write(output); + io.Output.Writer.Flush(); } else { @@ -73,8 +73,8 @@ public void Format(FormatArguments args, IOUri inputUri, IOUri outputUri, bool o if (outputToStdOut) { - PrettyPrinterV2.PrintTo(io.Output, sourceFile.ProgramSyntax, context); - io.Output.Flush(); + PrettyPrinterV2.PrintTo(io.Output.Writer, sourceFile.ProgramSyntax, context); + io.Output.Writer.Flush(); } else { diff --git a/src/Bicep.Cli/Commands/PublishCommand.cs b/src/Bicep.Cli/Commands/PublishCommand.cs index a1ef1c30d7a..936dc297a76 100644 --- a/src/Bicep.Cli/Commands/PublishCommand.cs +++ b/src/Bicep.Cli/Commands/PublishCommand.cs @@ -59,7 +59,7 @@ public async Task RunAsync(PublishArguments args) { if (publishSource) { - await ioContext.Error.WriteLineAsync($"Cannot publish with source when the target is an ARM template file."); + await ioContext.Error.Writer.WriteLineAsync($"Cannot publish with source when the target is an ARM template file."); return 1; } diff --git a/src/Bicep.Cli/Commands/RootCommand.cs b/src/Bicep.Cli/Commands/RootCommand.cs index af6a1c9adf5..d78157d94fe 100644 --- a/src/Bicep.Cli/Commands/RootCommand.cs +++ b/src/Bicep.Cli/Commands/RootCommand.cs @@ -253,26 +253,26 @@ bicep jsonrpc --stdio "; // this newline is intentional - io.Output.Write(output); - io.Output.Flush(); + io.Output.Writer.Write(output); + io.Output.Writer.Flush(); } private void PrintVersion() { var output = $@"Bicep CLI version {environment.GetVersionString()}{System.Environment.NewLine}"; - io.Output.Write(output); - io.Output.Flush(); + io.Output.Writer.Write(output); + io.Output.Writer.Flush(); } private void PrintLicense() { - WriteEmbeddedResource(io.Output, "LICENSE.deflated"); + WriteEmbeddedResource(io.Output.Writer, "LICENSE.deflated"); } private void PrintThirdPartyNotices() { - WriteEmbeddedResource(io.Output, "NOTICE.deflated"); + WriteEmbeddedResource(io.Output.Writer, "NOTICE.deflated"); } private static void WriteEmbeddedResource(TextWriter writer, string streamName) diff --git a/src/Bicep.Cli/Commands/SnapshotCommand.cs b/src/Bicep.Cli/Commands/SnapshotCommand.cs index 75521060df2..10206ed8c22 100644 --- a/src/Bicep.Cli/Commands/SnapshotCommand.cs +++ b/src/Bicep.Cli/Commands/SnapshotCommand.cs @@ -79,7 +79,7 @@ public async Task RunAsync(SnapshotArguments args, CancellationToken cancel logger.LogWarning("Snapshot validation failed. Expected no changes, but found the following:"); } - await io.Output.WriteAsync(WhatIfOperationResultFormatter.Format(changes)); + await io.Output.Writer.WriteAsync(WhatIfOperationResultFormatter.Format(changes)); return changes.Any() ? 1 : 0; } default: diff --git a/src/Bicep.Cli/Commands/TestCommand.cs b/src/Bicep.Cli/Commands/TestCommand.cs index c9b3766bf77..45f09b40a52 100644 --- a/src/Bicep.Cli/Commands/TestCommand.cs +++ b/src/Bicep.Cli/Commands/TestCommand.cs @@ -48,7 +48,7 @@ public async Task RunAsync(TestArguments args) if (!features.TestFrameworkEnabled) { - await io.Error.WriteLineAsync("TestFrameWork not enabled"); + await io.Error.Writer.WriteLineAsync("TestFrameWork not enabled"); return 1; } @@ -76,30 +76,30 @@ private void LogResults(TestResults testResults) { if (evaluation.Success) { - io.Output.WriteLine($"{SuccessSymbol} Evaluation {testDeclaration.Name} Passed!"); + io.Output.Writer.WriteLine($"{SuccessSymbol} Evaluation {testDeclaration.Name} Passed!"); } else if (evaluation.Skip) { - io.Error.WriteLine($"{SkippedSymbol} Evaluation {testDeclaration.Name} Skipped!"); - io.Error.WriteLine($"Reason: {evaluation.Error}"); + io.Error.Writer.WriteLine($"{SkippedSymbol} Evaluation {testDeclaration.Name} Skipped!"); + io.Error.Writer.WriteLine($"Reason: {evaluation.Error}"); } else { - io.Error.WriteLine($"{FailureSymbol} Evaluation {testDeclaration.Name} Failed at {evaluation.FailedAssertions.Length} / {evaluation.AllAssertions.Length} assertions!"); + io.Error.Writer.WriteLine($"{FailureSymbol} Evaluation {testDeclaration.Name} Failed at {evaluation.FailedAssertions.Length} / {evaluation.AllAssertions.Length} assertions!"); foreach (var (assertion, _) in evaluation.FailedAssertions) { - io.Error.WriteLine($"\t{FailureSymbol} Assertion {assertion} failed!"); + io.Error.Writer.WriteLine($"\t{FailureSymbol} Assertion {assertion} failed!"); } } } if (testResults.Success) { - io.Output.WriteLine($"All {testResults.TotalEvaluations} evaluations passed!"); + io.Output.Writer.WriteLine($"All {testResults.TotalEvaluations} evaluations passed!"); } else { - io.Error.WriteLine($"Evaluation Summary: Failure!"); - io.Error.WriteLine($"Total: {testResults.TotalEvaluations} - Success: {testResults.SuccessfulEvaluations} - Skipped: {testResults.SkippedEvaluations} - Failed: {testResults.FailedEvaluations}"); + io.Error.Writer.WriteLine($"Evaluation Summary: Failure!"); + io.Error.Writer.WriteLine($"Total: {testResults.TotalEvaluations} - Success: {testResults.SuccessfulEvaluations} - Skipped: {testResults.SkippedEvaluations} - Failed: {testResults.FailedEvaluations}"); } } diff --git a/src/Bicep.Cli/Commands/WhatIfCommand.cs b/src/Bicep.Cli/Commands/WhatIfCommand.cs index 2ee0aed13ee..dcc2d1edb7f 100644 --- a/src/Bicep.Cli/Commands/WhatIfCommand.cs +++ b/src/Bicep.Cli/Commands/WhatIfCommand.cs @@ -38,6 +38,6 @@ private async Task WhatIf(SemanticModel model, DeployCommandsConfig config, Canc var changes = result.Properties.Changes.Where(x => x.ChangeType != DeploymentWhatIfChangeType.Ignore); - await io.Output.WriteAsync(WhatIfOperationResultFormatter.Format([.. changes])); + await io.Output.Writer.WriteAsync(WhatIfOperationResultFormatter.Format([.. changes])); } } diff --git a/src/Bicep.Cli/Extensions/IOContextExtensions.cs b/src/Bicep.Cli/Extensions/IOContextExtensions.cs index 37e99a90d71..468b24b4ca1 100644 --- a/src/Bicep.Cli/Extensions/IOContextExtensions.cs +++ b/src/Bicep.Cli/Extensions/IOContextExtensions.cs @@ -7,12 +7,12 @@ public static class IOContextExtensions { public static void WriteCommandDeprecationWarning(this IOContext io, string deprecatingCommand, string newCommand) { - io.Error.WriteLine($"DEPRECATED: The command {deprecatingCommand} is deprecated and will be removed in a future version of Bicep CLI. Use {newCommand} instead."); + io.Error.Writer.WriteLine($"DEPRECATED: The command {deprecatingCommand} is deprecated and will be removed in a future version of Bicep CLI. Use {newCommand} instead."); } public static void WriteParameterDeprecationWarning(this IOContext io, string deprecatingParameter, string newParameter) { - io.Error.WriteLine($"DEPRECATED: The parameter {deprecatingParameter} is deprecated and will be removed in a future version of Bicep CLI. Use {newParameter} instead."); + io.Error.Writer.WriteLine($"DEPRECATED: The parameter {deprecatingParameter} is deprecated and will be removed in a future version of Bicep CLI. Use {newParameter} instead."); } } } diff --git a/src/Bicep.Cli.UnitTests/Utils/AnsiHelper.cs b/src/Bicep.Cli/Helpers/AnsiHelper.cs similarity index 100% rename from src/Bicep.Cli.UnitTests/Utils/AnsiHelper.cs rename to src/Bicep.Cli/Helpers/AnsiHelper.cs diff --git a/src/Bicep.Cli/Logging/DiagnosticLogger.cs b/src/Bicep.Cli/Logging/DiagnosticLogger.cs index 20828150cca..763f9ccf4d0 100644 --- a/src/Bicep.Cli/Logging/DiagnosticLogger.cs +++ b/src/Bicep.Cli/Logging/DiagnosticLogger.cs @@ -47,7 +47,7 @@ public DiagnosticSummary LogDiagnostics(DiagnosticOptions options, ImmutableDict LogDefaultDiagnostics(this.logger, diagnosticsByBicepFile); break; case DiagnosticsFormat.Sarif: - var writer = options.SarifToStdout ? this.ioContext.Output : this.ioContext.Error; + var writer = options.SarifToStdout ? this.ioContext.Output.Writer : this.ioContext.Error.Writer; LogSarifDiagnostics(writer, diagnosticsByBicepFile); break; default: diff --git a/src/Bicep.Cli/Program.cs b/src/Bicep.Cli/Program.cs index f3ca2997831..2f255be248f 100644 --- a/src/Bicep.Cli/Program.cs +++ b/src/Bicep.Cli/Program.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.IO.Abstractions; using System.Runtime; -using Azure.Deployments.Engine.Workers; using Bicep.Cli.Arguments; using Bicep.Cli.Commands; using Bicep.Cli.Helpers; @@ -24,8 +23,19 @@ namespace Bicep.Cli { public record IOContext( - TextWriter Output, - TextWriter Error); + InputContext Input, + OutputContext Output, + ErrorContext Error); + + public record InputContext( + TextReader Reader, + bool IsRedirected); + public record OutputContext( + TextWriter Writer, + bool IsRedirected); + public record ErrorContext( + TextWriter Writer, + bool IsRedirected); public class Program { @@ -55,7 +65,10 @@ public static async Task Main(string[] args) // this event listener picks up SDK events and writes them to Trace.WriteLine() using (FeatureProvider.TracingEnabled ? AzureEventSourceListenerFactory.Create(FeatureProvider.TracingVerbosity) : null) { - var program = new Program(new(Output: Console.Out, Error: Console.Error)); + var program = new Program(new( + Input: new(Console.In, Console.IsInputRedirected), + Output: new(Console.Out, Console.IsOutputRedirected), + Error: new(Console.Error, Console.IsErrorRedirected))); // this must be awaited so dispose of the listener occurs in the continuation // rather than the sync part at the beginning of RunAsync() @@ -130,13 +143,13 @@ public async Task RunAsync(string[] args, CancellationToken cancellationTok return services.GetRequiredService().Run(rootArguments); default: - await io.Error.WriteLineAsync(string.Format(CliResources.UnrecognizedArgumentsFormat, string.Join(' ', args), ThisAssembly.AssemblyName)); // should probably print help here?? + await io.Error.Writer.WriteLineAsync(string.Format(CliResources.UnrecognizedArgumentsFormat, string.Join(' ', args), ThisAssembly.AssemblyName)); // should probably print help here?? return 1; } } catch (BicepException exception) { - await io.Error.WriteLineAsync(exception.Message); + await io.Error.Writer.WriteLineAsync(exception.Message); return 1; } } @@ -146,7 +159,7 @@ private static ILoggerFactory CreateLoggerFactory(IOContext io) // apparently logging requires a factory factory 🤦‍ return LoggerFactory.Create(builder => { - builder.AddProvider(new BicepLoggerProvider(new BicepLoggerOptions(true, ConsoleColor.Red, ConsoleColor.DarkYellow, io.Error))); + builder.AddProvider(new BicepLoggerProvider(new BicepLoggerOptions(true, ConsoleColor.Red, ConsoleColor.DarkYellow, io.Error.Writer))); }); } @@ -194,7 +207,7 @@ private static IServiceCollection ConfigureServices(IOContext io) Ansi = AnsiSupport.Detect, ColorSystem = ColorSystemSupport.Detect, Interactive = InteractionSupport.Detect, - Out = new AnsiConsoleOutput(io.Output), + Out = new AnsiConsoleOutput(io.Output.Writer), })) .AddSingleton() .AddSingleton(); diff --git a/src/Bicep.Cli/Services/OutputWriter.cs b/src/Bicep.Cli/Services/OutputWriter.cs index 8fabf0ebd32..2b468dba783 100644 --- a/src/Bicep.Cli/Services/OutputWriter.cs +++ b/src/Bicep.Cli/Services/OutputWriter.cs @@ -128,7 +128,7 @@ public void DecompileResultToStdout(DecompileResult decompilation) private void WriteToStdout(string contents) { - io.Output.Write(contents); + io.Output.Writer.Write(contents); } public void WriteToFile(Uri fileUri, string contents) diff --git a/src/Bicep.Cli/Services/PlaceholderParametersWriter.cs b/src/Bicep.Cli/Services/PlaceholderParametersWriter.cs index 5b507b11f2b..8847b5dc43f 100644 --- a/src/Bicep.Cli/Services/PlaceholderParametersWriter.cs +++ b/src/Bicep.Cli/Services/PlaceholderParametersWriter.cs @@ -36,7 +36,7 @@ public EmitResult ToFile(Compilation compilation, IFileHandle outputFile, Output public EmitResult ToStdout(Compilation compilation, OutputFormatOption outputFormat, IncludeParamsOption includeParams) { var semanticModel = compilation.GetEntrypointSemanticModel(); - return new TemplateEmitter(semanticModel).EmitTemplateGeneratedParameterFile(io.Output, string.Empty, outputFormat, includeParams); + return new TemplateEmitter(semanticModel).EmitTemplateGeneratedParameterFile(io.Output.Writer, string.Empty, outputFormat, includeParams); } } }