Skip to content

Commit fd8528d

Browse files
committed
Omit ansi codes if output is redirected
1 parent e2f9095 commit fd8528d

File tree

19 files changed

+133
-81
lines changed

19 files changed

+133
-81
lines changed

docs/experimental/console-command.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ true
9494
- 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
9595

9696

97-
### Piping and standard input redirection
97+
### Piping and standard input/output redirection
9898
The console command supports evaluating expressions provided through piping or redirected standard input, i.e.:
9999

100100
**Powershell**:
@@ -119,6 +119,11 @@ Multi-line input is also supported, i.e:
119119
# Output: bar
120120
```
121121

122+
Output redirection is also supported:
123+
```sh
124+
"toObject([{name:'Evie', age:4},{name:'Casper', age:3}], x => x.name)" | bicep console > output.json
125+
```
126+
122127
## Limitations
123128
- No support for expressions requiring Azure context, e.g. `resourceGroup()`
124129
- No support for for-loop expressions, e.g. `[for i in range(0, x): i]`

src/Bicep.Cli.IntegrationTests/Commands/ConsoleCommandTests.cs

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ public class ConsoleCommandTests : TestBase
1414
[TestMethod]
1515
public async Task Redirected_input_context_with_single_line_input_should_succeed()
1616
{
17+
// "concat('Hello', ' ', 'World', '!')" | bicep console
1718
var input = "concat('Hello', ' ', 'World', '!')";
19+
var result = await Bicep(
20+
(@out, err) => new IOContext(
21+
new(new StringReader(input), IsRedirected: true),
22+
new(@out, IsRedirected: false),
23+
new(err, IsRedirected: false)),
24+
"console");
1825

19-
var inputContext = new InputContext(
20-
Reader: new StringReader(input),
21-
IsRedirected: true);
22-
23-
var result = await Bicep(inputContext, "console");
24-
2526
result.Should().Succeed();
2627
result.WithoutAnsi().Stdout.Should().BeEquivalentToIgnoringNewlines("""
2728
'Hello World!'
@@ -32,25 +33,51 @@ public async Task Redirected_input_context_with_single_line_input_should_succeed
3233
[TestMethod]
3334
public async Task Redirected_input_context_with_multi_line_input_should_succeed()
3435
{
36+
/*
37+
var greeting = 'Hello'
38+
var target = {
39+
value: 'World'
40+
}
41+
42+
'${greeting} ${target.value}!' | bicep console
43+
*/
3544
var input = """
3645
var greeting = 'Hello'
3746
var target = {
3847
value: 'World'
3948
}
40-
49+
4150
'${greeting} ${target.value}!'
4251
""";
4352

44-
var inputContext = new InputContext(
45-
Reader: new StringReader(input),
46-
IsRedirected: true);
47-
48-
var result = await Bicep(inputContext, "console");
53+
var result = await Bicep(
54+
(@out, err) => new IOContext(
55+
new(new StringReader(input), IsRedirected: true),
56+
new(@out, IsRedirected: false),
57+
new(err, IsRedirected: false)),
58+
"console");
4959

5060
result.Should().Succeed();
5161
result.WithoutAnsi().Stdout.Should().BeEquivalentToIgnoringNewlines("""
5262
'Hello World!'
5363
5464
""");
5565
}
66+
67+
[TestMethod]
68+
public async Task Redirected_output_context_should_not_have_ansi_codes()
69+
{
70+
// "concat('Hello', ' ', 'World', '!')" | bicep console >
71+
var input = "concat('Hello', ' ', 'World', '!')";
72+
var result = await Bicep(
73+
(@out, err) => new IOContext(
74+
new(new StringReader(input), IsRedirected: true),
75+
new(@out, IsRedirected: true),
76+
new(err, IsRedirected: false)),
77+
"console");
78+
79+
result.Should().Succeed();
80+
var withoutAnsi = result.WithoutAnsi();
81+
result.Stdout.Should().Be(withoutAnsi.Stdout);
82+
}
5683
}

src/Bicep.Cli.IntegrationTests/TestBase.cs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,9 @@ protected static Task<CliResult> Bicep(Action<IServiceCollection> registerAction
9292
protected static Task<CliResult> Bicep(InvocationSettings settings, params string?[] args /*null args are ignored*/)
9393
=> Bicep(settings, null, CancellationToken.None, args);
9494

95-
protected static Task<CliResult> Bicep(InputContext inputContext, params string[] args)
96-
=> BicepInternal(InvocationSettings.Default, null, inputContext, CancellationToken.None, args);
95+
protected static Task<CliResult> Bicep(
96+
Func<TextWriter, TextWriter, IOContext> ioContextFactory, params string[] args)
97+
=> BicepInternal(InvocationSettings.Default, null, ioContextFactory, CancellationToken.None, args);
9798

9899
protected static void AssertNoErrors(string error)
99100
{
@@ -165,10 +166,18 @@ protected static IEnvironment CreateDefaultEnvironment() => TestEnvironment.Defa
165166
("boolEnvironmentVariable", "true")
166167
);
167168

168-
private static Task<CliResult> BicepInternal(InvocationSettings settings, Action<IServiceCollection>? registerAction, InputContext? inputContext, CancellationToken cancellationToken, params string?[] args /*null args are ignored*/)
169-
=> TextWriterHelper.InvokeWriterAction((@out, err)
170-
=> new Program(
171-
new(Input: inputContext ?? new(new StringReader(string.Empty), false), Output: @out, Error: err),
169+
private static Task<CliResult> BicepInternal(
170+
InvocationSettings settings,
171+
Action<IServiceCollection>? registerAction,
172+
Func<TextWriter, TextWriter, IOContext>? ioContextFactory,
173+
CancellationToken cancellationToken, params string?[] args /*null args are ignored*/)
174+
=> TextWriterHelper.InvokeWriterAction((@out, err) =>
175+
{
176+
var ioContext = ioContextFactory?.Invoke(@out, err) ?? new IOContext(
177+
Input: new(new StringReader(string.Empty), false),
178+
Output: new(@out, false),
179+
Error: new(err, false));
180+
return new Program(ioContext,
172181
services =>
173182
{
174183
if (settings.FeatureOverrides is { })
@@ -192,7 +201,7 @@ private static Task<CliResult> BicepInternal(InvocationSettings settings, Action
192201

193202
registerAction?.Invoke(services);
194203
}
195-
)
196-
.RunAsync(args.ToArrayExcludingNull(), cancellationToken));
204+
).RunAsync(args.ToArrayExcludingNull(), cancellationToken);
205+
});
197206
}
198207
}

src/Bicep.Cli.UnitTests/Bicep.Cli.UnitTests.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
<OutputType>Exe</OutputType>
66
</PropertyGroup>
77

8+
<ItemGroup>
9+
<Compile Remove="Utils\**" />
10+
<EmbeddedResource Remove="Utils\**" />
11+
<None Remove="Utils\**" />
12+
</ItemGroup>
13+
814
<ItemGroup>
915
<PackageReference Include="Moq" />
1016
<PackageReference Include="FluentAssertions" />

src/Bicep.Cli/Commands/ConsoleCommand.cs

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ public class ConsoleCommand(
2626
ILogger logger,
2727
IOContext io,
2828
IEnvironment environment,
29-
ReplEnvironment replEnvironment,
30-
IAnsiConsole console) : ICommand
29+
ReplEnvironment replEnvironment) : ICommand
3130
{
3231
private const string FirstLinePrefix = "> ";
3332

@@ -90,20 +89,19 @@ public async Task<int> RunAsync(ConsoleArguments args)
9089
outputBuilder.Append(replEnvironment.EvaluateAndGetOutput(inputBuffer.ToString()));
9190
}
9291

93-
await io.Output.WriteAsync(outputBuilder.ToString());
92+
var output = outputBuilder.ToString();
93+
if (io.Output.IsRedirected)
94+
{
95+
output = AnsiHelper.RemoveCodes(output);
96+
}
97+
await io.Output.Writer.WriteAsync(output);
9498
return 0;
9599
}
96100

97-
if (!console.Profile.Capabilities.Interactive)
98-
{
99-
logger.LogError($"The '{args.CommandName}' CLI command requires an interactive console.");
100-
return 1;
101-
}
102-
103-
await io.Output.WriteLineAsync($"Bicep Console version {environment.GetVersionString()}");
104-
await io.Output.WriteLineAsync("Type 'help' for available commands, press ESC to quit.");
105-
await io.Output.WriteLineAsync("Multi-line input supported.");
106-
await io.Output.WriteLineAsync(string.Empty);
101+
await io.Output.Writer.WriteLineAsync($"Bicep Console version {environment.GetVersionString()}");
102+
await io.Output.Writer.WriteLineAsync("Type 'help' for available commands, press ESC to quit.");
103+
await io.Output.Writer.WriteLineAsync("Multi-line input supported.");
104+
await io.Output.Writer.WriteLineAsync(string.Empty);
107105

108106
var buffer = new StringBuilder();
109107

@@ -129,8 +127,8 @@ public async Task<int> RunAsync(ConsoleArguments args)
129127

130128
if (rawLine.Equals("help", StringComparison.OrdinalIgnoreCase))
131129
{
132-
await io.Output.WriteLineAsync("Enter expressions or 'var name = <expr>'. Multi-line supported until structure closes.");
133-
await io.Output.WriteLineAsync("Commands: exit, clear");
130+
await io.Output.Writer.WriteLineAsync("Enter expressions or 'var name = <expr>'. Multi-line supported until structure closes.");
131+
await io.Output.Writer.WriteLineAsync("Commands: exit, clear");
134132
continue;
135133
}
136134
}
@@ -146,7 +144,7 @@ public async Task<int> RunAsync(ConsoleArguments args)
146144

147145
// evaluate input
148146
var output = replEnvironment.EvaluateAndGetOutput(current);
149-
await io.Output.WriteAsync(output);
147+
await io.Output.Writer.WriteAsync(output);
150148
}
151149
}
152150

@@ -169,8 +167,8 @@ private async Task<int> PrintHistory(StringBuilder buffer, List<Rune> lineBuffer
169167
cursorOffset = lineBuffer.Count;
170168

171169
var output2 = replEnvironment.HighlightInputLine(FirstLinePrefix, buffer.ToString(), lineBuffer, cursorOffset, printPrevLines: true);
172-
await io.Output.WriteAsync(PrintHelper.MoveCursorUp(prevBufferLineCount));
173-
await io.Output.WriteAsync(output2);
170+
await io.Output.Writer.WriteAsync(PrintHelper.MoveCursorUp(prevBufferLineCount));
171+
await io.Output.Writer.WriteAsync(output2);
174172
return cursorOffset;
175173
}
176174

@@ -182,7 +180,7 @@ private string GetPrefix(StringBuilder buffer)
182180

183181
private async Task<string?> ReadLine(StringBuilder buffer)
184182
{
185-
await io.Output.WriteAsync(GetPrefix(buffer));
183+
await io.Output.Writer.WriteAsync(GetPrefix(buffer));
186184

187185
var lineBuffer = new List<Rune>();
188186
var cursorOffset = 0;
@@ -217,7 +215,7 @@ private string GetPrefix(StringBuilder buffer)
217215
}
218216
if (keyInfo.Key == ConsoleKey.Enter)
219217
{
220-
await io.Output.FlushAsync();
218+
await io.Output.Writer.FlushAsync();
221219
break;
222220
}
223221
else if (keyInfo.Key == ConsoleKey.Backspace)
@@ -246,10 +244,10 @@ private string GetPrefix(StringBuilder buffer)
246244
}
247245

248246
var output = replEnvironment.HighlightInputLine(GetPrefix(buffer), buffer.ToString(), lineBuffer, cursorOffset, printPrevLines: false);
249-
await io.Output.WriteAsync(output);
247+
await io.Output.Writer.WriteAsync(output);
250248
}
251249

252-
await io.Output.WriteAsync("\n");
250+
await io.Output.Writer.WriteAsync("\n");
253251

254252
return string.Concat(lineBuffer);
255253
}

src/Bicep.Cli/Commands/DecompileCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public async Task<int> RunAsync(DecompileArguments args)
8888
}
8989
catch (Exception exception)
9090
{
91-
await io.Error.WriteLineAsync(string.Format(CliResources.DecompilationFailedFormat, inputUri, exception.Message));
91+
await io.Error.Writer.WriteLineAsync(string.Format(CliResources.DecompilationFailedFormat, inputUri, exception.Message));
9292
return 1;
9393
}
9494
}

src/Bicep.Cli/Commands/DecompileParamsCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public int Run(DecompileParamsArguments args)
6464
}
6565
catch (Exception exception)
6666
{
67-
io.Error.WriteLine(string.Format(CliResources.DecompilationFailedFormat, inputUri, exception.Message));
67+
io.Error.Writer.WriteLine(string.Format(CliResources.DecompilationFailedFormat, inputUri, exception.Message));
6868
return 1;
6969
}
7070
}

src/Bicep.Cli/Commands/FormatCommand.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ public void Format(FormatArguments args, IOUri inputUri, IOUri outputUri, bool o
5757

5858
if (outputToStdOut)
5959
{
60-
io.Output.Write(output);
61-
io.Output.Flush();
60+
io.Output.Writer.Write(output);
61+
io.Output.Writer.Flush();
6262
}
6363
else
6464
{
@@ -73,8 +73,8 @@ public void Format(FormatArguments args, IOUri inputUri, IOUri outputUri, bool o
7373

7474
if (outputToStdOut)
7575
{
76-
PrettyPrinterV2.PrintTo(io.Output, sourceFile.ProgramSyntax, context);
77-
io.Output.Flush();
76+
PrettyPrinterV2.PrintTo(io.Output.Writer, sourceFile.ProgramSyntax, context);
77+
io.Output.Writer.Flush();
7878
}
7979
else
8080
{

src/Bicep.Cli/Commands/PublishCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public async Task<int> RunAsync(PublishArguments args)
5959
{
6060
if (publishSource)
6161
{
62-
await ioContext.Error.WriteLineAsync($"Cannot publish with source when the target is an ARM template file.");
62+
await ioContext.Error.Writer.WriteLineAsync($"Cannot publish with source when the target is an ARM template file.");
6363
return 1;
6464
}
6565

src/Bicep.Cli/Commands/RootCommand.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -253,26 +253,26 @@ bicep jsonrpc --stdio
253253
254254
"; // this newline is intentional
255255

256-
io.Output.Write(output);
257-
io.Output.Flush();
256+
io.Output.Writer.Write(output);
257+
io.Output.Writer.Flush();
258258
}
259259

260260
private void PrintVersion()
261261
{
262262
var output = $@"Bicep CLI version {environment.GetVersionString()}{System.Environment.NewLine}";
263263

264-
io.Output.Write(output);
265-
io.Output.Flush();
264+
io.Output.Writer.Write(output);
265+
io.Output.Writer.Flush();
266266
}
267267

268268
private void PrintLicense()
269269
{
270-
WriteEmbeddedResource(io.Output, "LICENSE.deflated");
270+
WriteEmbeddedResource(io.Output.Writer, "LICENSE.deflated");
271271
}
272272

273273
private void PrintThirdPartyNotices()
274274
{
275-
WriteEmbeddedResource(io.Output, "NOTICE.deflated");
275+
WriteEmbeddedResource(io.Output.Writer, "NOTICE.deflated");
276276
}
277277

278278
private static void WriteEmbeddedResource(TextWriter writer, string streamName)

0 commit comments

Comments
 (0)