Skip to content

Commit 2b1a61d

Browse files
levimatheritsmallig33anthony-c-martin
authored
Merge Bicep REPL to main branch (#18152)
## Description Introduces Bicep REPL (exposed via `bicep console` command) experimental feature ## Example Usage Included in docs ## Checklist - [x] I have read and adhere to the [contribution guide](https://github.com/Azure/bicep/blob/main/CONTRIBUTING.md). ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/Azure/bicep/pull/18152) --------- Co-authored-by: Tate Smalligan <[email protected]> Co-authored-by: Anthony Martin <[email protected]>
1 parent ad7459f commit 2b1a61d

29 files changed

+1751
-453
lines changed

.vscode/launch.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@
4040
"console": "internalConsole",
4141
"stopAtEntry": false
4242
},
43+
{
44+
"name": "REPL",
45+
"type": "coreclr",
46+
"request": "launch",
47+
"preLaunchTask": "Build CLI",
48+
"program": "${workspaceFolder}/src/Bicep.Cli/bin/Debug/net8.0/bicep",
49+
"args": [
50+
"console",
51+
],
52+
"env": {
53+
"BICEP_TRACING_ENABLED": "true",
54+
"BICEP_TRACING_VERBOSITY": "basic",
55+
},
56+
"cwd": "${workspaceFolder}/src/Bicep.Cli",
57+
"console": "integratedTerminal",
58+
"stopAtEntry": false
59+
},
4360
{
4461
"name": "Playground",
4562
"type": "node",
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Using the `console` command (Experimental!)
2+
3+
## What is it?
4+
The `console` command provides an interactive Read-Eval-Print Loop (REPL) environment for Bicep expressions. It allows you to experiment with Bicep functions and expressions in an interactive console session.
5+
6+
## Features
7+
- **Interactive Expression Evaluation**: Enter Bicep expressions and see their evaluated results immediately
8+
- **Variable Declarations**: Define variables using `var name = expression` syntax and reuse them in subsequent expressions
9+
- **Multi-line Input**: Support for complex multi-line expressions with automatic structural completion detection
10+
- **Syntax Highlighting**: Real-time syntax highlighting for input and output
11+
12+
## Usage
13+
```sh
14+
bicep console
15+
```
16+
17+
## Examples
18+
19+
### Simple Expressions
20+
```bicep
21+
> 1 + 2
22+
3
23+
24+
> 'Hello, ' + 'World!'
25+
'Hello, World!'
26+
27+
> length(['a', 'b', 'c'])
28+
3
29+
```
30+
31+
### Variable Declarations
32+
```bicep
33+
> var myName = 'John'
34+
> var greeting = 'Hello, ${myName}!'
35+
> greeting
36+
'Hello, John!'
37+
```
38+
39+
### Multi-line Expressions
40+
The console automatically detects when expressions are structurally complete:
41+
42+
```bicep
43+
> var config = {
44+
name: 'myApp'
45+
version: '1.0.0'
46+
settings: {
47+
debug: true
48+
timeout: 30
49+
}
50+
}
51+
> config.settings.debug
52+
true
53+
```
54+
55+
### Complex Expressions
56+
```bicep
57+
> var users = [
58+
{ name: 'Alice', age: 30 }
59+
{ name: 'Bob', age: 25 }
60+
]
61+
> map(users, user => user.name)
62+
['Alice', 'Bob']
63+
64+
> filter(users, user => user.age > 26)
65+
[
66+
{
67+
age: 30
68+
name: 'Alice'
69+
}
70+
]
71+
```
72+
73+
## Limitations
74+
- No support for expressions requiring Azure context, e.g. `resourceGroup()`
75+
- No file system access or external dependencies
76+
- Limited to expression evaluation and variable declarations
77+
- No persistent state between console sessions
78+
- No completions support
79+
80+
## Raising bugs or feature requests
81+
Please raise bug reports or feature requests under [Bicep REPL issue](https://github.com/Azure/bicep/issues/11963).
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Drawing;
5+
using System.Text;
6+
using System.Web.Services.Description;
7+
using Bicep.Cli.Helpers.Repl;
8+
using Bicep.Cli.Services;
9+
using Bicep.Core;
10+
using Bicep.Core.Parsing;
11+
using Bicep.Core.UnitTests;
12+
using Bicep.Core.UnitTests.Utils;
13+
using FluentAssertions;
14+
using Microsoft.VisualStudio.TestTools.UnitTesting;
15+
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
16+
using PrintHelper = Bicep.Cli.Helpers.Repl.PrintHelper;
17+
18+
namespace Bicep.Cli.UnitTests.Helpers.Repl;
19+
20+
[TestClass]
21+
public class PrintHelperTests
22+
{
23+
[TestMethod]
24+
public void PrintInputLine_returns_expected_output()
25+
{
26+
var input = "var test = 'foo'";
27+
28+
var result = PrintHelper.PrintInputLine("> ", input, input.Length);
29+
AnsiHelper.ReplaceCodes(result).Should().Be(string.Concat(
30+
"[HideCursor]",
31+
"[MoveCursorToLineStart]",
32+
"[ClearToEndOfScreen]",
33+
"> var test = 'foo'",
34+
"[MoveCursorToLineStart]",
35+
"[MoveCursorRight(2)]",
36+
"[MoveCursorRight(16)]",
37+
"[ShowCursor]"));
38+
}
39+
40+
[TestMethod]
41+
public void PrintWithSyntaxHighlighting_returns_expected_output()
42+
{
43+
var compilation = CompilationHelper.Compile("""
44+
var foo = {
45+
abc: 'def'
46+
ghi: 123
47+
}
48+
""").Compilation;
49+
50+
var model = compilation.GetEntrypointSemanticModel();
51+
52+
var result = PrintHelper.PrintWithSyntaxHighlighting(model, model.SourceFile.ProgramSyntax.ToString());
53+
AnsiHelper.ReplaceCodes(result).Should().Be("""
54+
[Orange]var[Reset] [Purple]foo[Reset] = {
55+
[Orange]abc[Reset]: [Orange]'def'[Reset]
56+
[Orange]ghi[Reset]: [Orange]123[Reset]
57+
}
58+
""");
59+
}
60+
61+
[TestMethod]
62+
public void PrintWithAnnotations_allows_annotating_highlighted_syntax()
63+
{
64+
var sourceText = """
65+
var foo = {
66+
abc: def
67+
ghi: 123,
68+
}
69+
""";
70+
var compilation = CompilationHelper.Compile(sourceText).Compilation;
71+
72+
var model = compilation.GetEntrypointSemanticModel();
73+
var highlighted = PrintHelper.PrintWithSyntaxHighlighting(model, model.SourceFile.ProgramSyntax.ToString());
74+
75+
var result = PrintHelper.PrintWithAnnotations(
76+
sourceText,
77+
model.GetAllDiagnostics().Select(x => new PrintHelper.AnnotatedDiagnostic(x)),
78+
highlighted);
79+
80+
result = AnsiHelper.ReplaceCodes(result);
81+
82+
AnsiHelper.ReplaceCodes(result).Should().Be("""
83+
[Orange]var[Reset] [Purple]foo[Reset] = {
84+
[Orange]~~~ Variable "foo" is declared but never used.[Reset]
85+
[Orange]abc[Reset]: [Purple]def[Reset]
86+
[Red]~~~ The name "def" does not exist in the current context.[Reset]
87+
[Orange]ghi[Reset]: [Orange]123[Reset][Reset],[Reset]
88+
[Red]^ Unexpected new line character after a comma.[Reset]
89+
90+
""");
91+
}
92+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text;
5+
using Bicep.Cli.Services;
6+
using Bicep.Core;
7+
using Bicep.Core.Parsing;
8+
using Bicep.Core.UnitTests;
9+
using FluentAssertions;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.VisualStudio.TestTools.UnitTesting;
12+
using Microsoft.WindowsAzure.ResourceStack.Common.Json;
13+
14+
namespace Bicep.Cli.UnitTests.Services;
15+
16+
[TestClass]
17+
public class ReplEnvironmentTests
18+
{
19+
private static ReplEnvironment CreateReplEnvironment()
20+
=> ServiceBuilder.Create(x => x.AddSingleton<ReplEnvironment>())
21+
.Construct<ReplEnvironment>();
22+
23+
[TestMethod]
24+
public void HighlightInputLine_succeeds()
25+
{
26+
var replEnvironment = CreateReplEnvironment();
27+
28+
var lines = StringUtils.SplitOnNewLine("""
29+
var test = /*
30+
foo
31+
bar
32+
*/
33+
v
34+
""").ToArray();
35+
36+
var output = new List<string>();
37+
for (var i = 0; i < lines.Length; i++)
38+
{
39+
var prevLines = string.Concat(lines.Take(i).Select(x => x + "\n"));
40+
var runes = lines[i].Select(x => new Rune(x)).ToArray();
41+
42+
var prefix = i == 0 ? "> " : "";
43+
output.Add(replEnvironment.HighlightInputLine(prefix, prevLines, runes, runes.Length, printPrevLines: false));
44+
}
45+
46+
output.Select(AnsiHelper.ReplaceCodes).Should().BeEquivalentTo([
47+
"[HideCursor][MoveCursorToLineStart][ClearToEndOfScreen]> [Orange]var[Reset] [Purple]test[Reset] = [Green]/*[Reset][MoveCursorToLineStart][MoveCursorRight(2)][MoveCursorRight(13)][ShowCursor]",
48+
"[HideCursor][MoveCursorToLineStart][ClearToEndOfScreen][Green]foo[Reset][MoveCursorToLineStart][MoveCursorRight(3)][ShowCursor]",
49+
"[HideCursor][MoveCursorToLineStart][ClearToEndOfScreen][Green]bar[Reset][MoveCursorToLineStart][MoveCursorRight(3)][ShowCursor]",
50+
"[HideCursor][MoveCursorToLineStart][ClearToEndOfScreen][Green]*/[Reset][MoveCursorToLineStart][MoveCursorRight(2)][ShowCursor]",
51+
"[HideCursor][MoveCursorToLineStart][ClearToEndOfScreen][Purple]v[Reset][MoveCursorToLineStart][MoveCursorRight(1)][ShowCursor]",
52+
]);
53+
}
54+
}

src/Bicep.Cli.UnitTests/Utils/AnsiHelper.cs

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,62 @@
22
// Licensed under the MIT License.
33

44
using System.Collections.Immutable;
5+
using System.Text.RegularExpressions;
6+
using Bicep.Cli.Helpers.Repl;
57
using Bicep.Cli.Helpers.WhatIf;
6-
78
public static class AnsiHelper
89
{
9-
private static readonly ImmutableDictionary<string, string> namesByEscapeCode = new Dictionary<string, string>
10+
private static readonly ImmutableDictionary<string, string> escapeCodeByName = new Dictionary<string, string>
1011
{
11-
[Color.Orange.ToString()] = nameof(Color.Orange),
12-
[Color.Green.ToString()] = nameof(Color.Green),
13-
[Color.Purple.ToString()] = nameof(Color.Purple),
14-
[Color.Blue.ToString()] = nameof(Color.Blue),
15-
[Color.Gray.ToString()] = nameof(Color.Gray),
16-
[Color.Reset.ToString()] = nameof(Color.Reset),
17-
[Color.Red.ToString()] = nameof(Color.Red),
18-
[Color.DarkYellow.ToString()] = nameof(Color.DarkYellow),
19-
[$"{Color.Esc}[1m"] = "Bold",
20-
[$"{Color.Esc}[1;32m"] = "Bold,Green",
21-
[$"{Color.Esc}[1;91m"] = "Bold,Red",
12+
[nameof(Color.Orange)] = Color.Orange.ToString(),
13+
[nameof(Color.Green)] = Color.Green.ToString(),
14+
[nameof(Color.Purple)] = Color.Purple.ToString(),
15+
[nameof(Color.Blue)] = Color.Blue.ToString(),
16+
[nameof(Color.Gray)] = Color.Gray.ToString(),
17+
[nameof(Color.Reset)] = Color.Reset.ToString(),
18+
[nameof(Color.Red)] = Color.Red.ToString(),
19+
[nameof(Color.DarkYellow)] = Color.DarkYellow.ToString(),
20+
["Bold"] = $"{Color.Esc}[1m",
21+
["Bold,Green"] = $"{Color.Esc}[1;32m",
22+
["Bold,Red"] = $"{Color.Esc}[1;91m",
23+
[nameof(PrintHelper.HideCursor)] = PrintHelper.HideCursor,
24+
[nameof(PrintHelper.ShowCursor)] = PrintHelper.ShowCursor,
25+
[nameof(PrintHelper.ClearToEndOfScreen)] = PrintHelper.ClearToEndOfScreen,
26+
[nameof(PrintHelper.MoveCursorToLineStart)] = PrintHelper.MoveCursorToLineStart,
27+
}.ToImmutableDictionary();
28+
29+
private static readonly ImmutableDictionary<string, string> escapeCodeRegexByName = new Dictionary<string, string>
30+
{
31+
[nameof(PrintHelper.MoveCursorRight)] = "\u001b\\[(\\d+)C",
2232
}.ToImmutableDictionary();
2333

2434
public static string ReplaceCodes(string input)
2535
{
26-
foreach (var (escapeCode, name) in namesByEscapeCode)
36+
foreach (var (name, code) in escapeCodeByName)
37+
{
38+
input = input.Replace(code, $"[{name}]");
39+
}
40+
41+
foreach (var (name, pattern) in escapeCodeRegexByName)
2742
{
28-
input = input.Replace(escapeCode, $"[{name}]");
43+
input = Regex.Replace(input, pattern, $"[{name}($1)]");
2944
}
3045

3146
return input;
3247
}
3348

3449
public static string RemoveCodes(string input)
3550
{
36-
foreach (var (escapeCode, name) in namesByEscapeCode)
51+
foreach (var (name, escapeCode) in escapeCodeByName)
3752
{
3853
input = input.Replace(escapeCode, "");
3954
}
4055

56+
foreach (var (name, pattern) in escapeCodeRegexByName)
57+
{
58+
input = Regex.Replace(input, pattern, "");
59+
}
60+
4161
return input;
4262
}
43-
}
63+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Bicep.Cli.Arguments;
5+
6+
public class ConsoleArguments : ArgumentsBase
7+
{
8+
public ConsoleArguments(string[] args) : base(Constants.Command.Console)
9+
{
10+
// Currently no options. Future flags (e.g. --subscription, --resource-group) can be added.
11+
}
12+
}

0 commit comments

Comments
 (0)