Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/experimental/console-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ The console automatically detects when expressions are structurally complete:
> config.settings.debug
true
```
To exit out of a multi-line expression input (e.g. if you get stuck due to an invalid syntax), simply hit 'Enter' twice.


### Complex Expressions
```bicep
Expand Down
4 changes: 0 additions & 4 deletions src/Bicep.Cli.UnitTests/Bicep.Cli.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,4 @@
<ItemGroup>
<PackageReference Update="Nerdbank.GitVersioning" />
</ItemGroup>

<ItemGroup>
<Folder Include="Helpers\" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using Bicep.Cli.Helpers.Repl;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Bicep.Cli.UnitTests.Helpers.Repl;

[TestClass]
public class StructuralCompletenessHelperTests
{
[DataTestMethod]
[DataRow("42", true)]
[DataRow("'hello'", true)]
[DataRow("true", true)]
[DataRow("items([1, 2, 3])", true)]
[DataRow("var a = 'complete'", true)]
[DataRow("length(['a', 'b'])", true)]
[DataRow(
"""
'''
Hello
World
'''
""", true)]
[DataRow("items({", false)]
[DataRow("var a = items({", false)]
[DataRow("var b = length([", false)]
[DataRow("[", false)]
[DataRow("{", false)]
[DataRow("(length(", false)]
[DataRow("filter(map([1,2],", false)]
[DataRow(
"""
filter(map([1,2],
x => x + 1),
y => y % 2 == 0
""", false)]
[DataRow(
"""
'''
Hello
World
""", false)]
public void IsStructurallyComplete_CompleteExpressions_ReturnsExpectedResult(string text, bool expected)
{
StructuralCompletenessHelper.IsStructurallyComplete(text).Should().Be(expected);
}
}
52 changes: 21 additions & 31 deletions src/Bicep.Cli/Commands/ConsoleCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ private IEnumerable<Rune> GetRunes(string text)

public async Task<int> 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.");
logger.LogWarning("WARNING: The '{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.", args.CommandName);

if (!console.Profile.Capabilities.Interactive)
{
logger.LogError($"The '{args.CommandName}' CLI command requires an interactive console.");
logger.LogError("The '{commandName}' CLI command requires an interactive console.", args.CommandName);
return 1;
}

await io.Output.WriteLineAsync("Bicep Console v1.0.0");
await io.Output.WriteLineAsync("Type 'help' for available commands, press ESC to quit.");
await io.Output.WriteLineAsync("Multi-line input supported.");
Expand Down Expand Up @@ -91,8 +91,16 @@ public async Task<int> RunAsync(ConsoleArguments args)

if (rawLine.Equals("help", StringComparison.OrdinalIgnoreCase))
{
await io.Output.WriteLineAsync("Enter expressions or 'var name = <expr>'. Multi-line supported until structure closes.");
await io.Output.WriteLineAsync("Commands: exit, clear");
await io.Output.WriteLineAsync(
"""
Enter Bicep expressions or variable declarations (var a = <expr>).
If stuck in a multi-line expression, Press 'Enter' twice
Special commands:
exit Exit the console
clear Clear the console
help Show this help message
"""
);
continue;
}
}
Expand All @@ -109,7 +117,7 @@ public async Task<int> RunAsync(ConsoleArguments args)
}

// Auto-submit when structure complete immediately after this line.
if (ShouldTerminateWithNewLine(current))
if (StructuralCompletenessHelper.IsStructurallyComplete(current))
{
await SubmitBuffer(buffer, current);
}
Expand Down Expand Up @@ -142,7 +150,7 @@ private async Task<bool> PrintHistory(StringBuilder buffer, List<Rune> lineBuffe
return false;
}

private string GetPrefix(StringBuilder buffer)
private static string GetPrefix(StringBuilder buffer)
=> buffer.Length == 0 ? FirstLinePrefix : "";

private async Task<string?> ReadLine(StringBuilder buffer)
Expand Down Expand Up @@ -243,36 +251,18 @@ private async Task SubmitBuffer(StringBuilder buffer, string current)
buffer.Clear();
}

/// <summary>
/// Heuristic structural completeness check: ensures bracket/brace/paren balance
/// and not inside (multi-line) string or interpolation expression.
/// Not a full parse; parse errors still reported by real parser.
/// </summary>
private static bool ShouldTerminateWithNewLine(string text)
{
var program = new ReplParser(text).Program();
if (program.Children.Length != 1)
{
return true;
}

return program.Children[0] switch
{
VariableDeclarationSyntax { Value: SkippedTriviaSyntax } => false,
_ => true,
};
}

private static SyntaxBase ParseJToken(JToken value)
=> value switch {
=> value switch
{
JObject jObject => ParseJObject(jObject),
JArray jArray => ParseJArray(jArray),
JValue jValue => ParseJValue(jValue),
_ => throw new NotImplementedException($"Unrecognized token type {value.Type}"),
};

private static SyntaxBase ParseJValue(JValue value)
=> value.Type switch {
private static ExpressionSyntax ParseJValue(JValue value)
=> value.Type switch
{
JTokenType.Integer => SyntaxFactory.CreatePositiveOrNegativeInteger(value.Value<long>()),
JTokenType.String => SyntaxFactory.CreateStringLiteral(value.ToString()),
JTokenType.Boolean => SyntaxFactory.CreateBooleanLiteral(value.Value<bool>()),
Expand All @@ -288,4 +278,4 @@ private static SyntaxBase ParseJObject(JObject jObject)
=> SyntaxFactory.CreateObject(
jObject.Properties()
.Select(x => SyntaxFactory.CreateObjectProperty(x.Name, ParseJToken(x.Value))));
}
}
63 changes: 63 additions & 0 deletions src/Bicep.Cli/Helpers/Repl/StructuralCompletenessHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using Bicep.Core.Parsing;
using Bicep.Core.Syntax;

namespace Bicep.Cli.Helpers.Repl;

public static class StructuralCompletenessHelper
{
public static bool IsStructurallyComplete(string text)
{
var program = new ReplParser(text).Program();
var completenessVisitor = new StructuralCompletenessVisitor();
program.Accept(completenessVisitor);
return completenessVisitor.IsComplete;
}

private sealed class StructuralCompletenessVisitor : CstVisitor
{
public bool IsComplete { get; private set; } = true;

public override void VisitSkippedTriviaSyntax(SkippedTriviaSyntax syntax)
{
IsComplete = false;
// don't visit children - we already know it's incomplete
}

public override void VisitVariableDeclarationSyntax(VariableDeclarationSyntax syntax)
Copy link
Member

@anthony-c-martin anthony-c-martin Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One possible complexity with the visitor pattern is that you can get "stuck" inside a piece of deeply nested syntax - e.g. something like:

{
  foo: {
    bar: [
      'abc'
    abc: 'def'
  }
}

Here I accidentally missed a ], and so my expression is not structurally complete. It's not very clear to me that I need to type a ], and so it could feel like I'm "stuck" - e.g. I can't exit the statement whatever I press.

Is that something to consider here, or do you have a mitigation for this already?

{
if (syntax.Value is SkippedTriviaSyntax)
{
IsComplete = false;
return;
}

base.VisitVariableDeclarationSyntax(syntax);
}

public override void VisitFunctionCallSyntax(FunctionCallSyntax syntax)
{
if (syntax.CloseParen is SkippedTriviaSyntax)
{
IsComplete = false;
return;
}

base.VisitFunctionCallSyntax(syntax);
}

public override void VisitParenthesizedExpressionSyntax(ParenthesizedExpressionSyntax syntax)
{
if (syntax.CloseParen is SkippedTriviaSyntax)
{
IsComplete = false;
return;
}

base.VisitParenthesizedExpressionSyntax(syntax);
}
}
}
Loading