diff --git a/CosmosDBShell.Analyzer/DirectConsoleOutputAnalyzer.cs b/CosmosDBShell.Analyzer/DirectConsoleOutputAnalyzer.cs
new file mode 100644
index 00000000..c7729c93
--- /dev/null
+++ b/CosmosDBShell.Analyzer/DirectConsoleOutputAnalyzer.cs
@@ -0,0 +1,107 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using System.Collections.Immutable;
+
+namespace CosmosShell.Analyzer
+{
+ ///
+ /// Requires user-facing console output to go through ShellOutput, which applies the
+ /// quiet and machine-mode presentation policies. Direct writes bypass those
+ /// policies and can corrupt structured stdout.
+ ///
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class DirectConsoleOutputAnalyzer : DiagnosticAnalyzer
+ {
+ public const string DiagnosticId = "CZ0003";
+
+ private const string Category = "Usage";
+ private const string AllowedType = "Azure.Data.Cosmos.Shell.Core.ShellOutput";
+
+ private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
+ DiagnosticId,
+ "Direct console output",
+ "'{0}' bypasses the shell output policy; write through ShellOutput instead",
+ Category,
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "User-facing output must go through ShellOutput so --quiet and machine mode are honored.");
+
+ public override ImmutableArray SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
+ }
+
+ private static void Analyze(SyntaxNodeAnalysisContext context)
+ {
+ var invocation = (InvocationExpressionSyntax)context.Node;
+ if (!(context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol is IMethodSymbol method))
+ {
+ return;
+ }
+
+ if (!IsDirectOutput(context.SemanticModel, invocation, method, context))
+ {
+ return;
+ }
+
+ var containingType = context.ContainingSymbol?.ContainingType;
+ while (containingType != null)
+ {
+ if (containingType.ToDisplayString() == AllowedType)
+ {
+ return;
+ }
+
+ containingType = containingType.ContainingType;
+ }
+
+ context.ReportDiagnostic(Diagnostic.Create(Rule, invocation.GetLocation(), method.ContainingType.Name + "." + method.Name));
+ }
+
+ private static bool IsDirectOutput(SemanticModel model, InvocationExpressionSyntax invocation, IMethodSymbol method, SyntaxNodeAnalysisContext context)
+ {
+ var type = method.ContainingType?.ToDisplayString();
+
+ // Console.Write / Console.WriteLine / Console.Clear.
+ if (method.IsStatic && type == "System.Console" && (method.Name.StartsWith("Write") || method.Name == "Clear"))
+ {
+ return true;
+ }
+
+ // Every static AnsiConsole call except Create renders to the global console.
+ if (method.IsStatic && type == "Spectre.Console.AnsiConsole" && method.Name != "Create")
+ {
+ return true;
+ }
+
+ // Console.Out.Write*, Console.Error.Write*, and AnsiConsole.Console..
+ if (invocation.Expression is MemberAccessExpressionSyntax access
+ && model.GetSymbolInfo(access.Expression, context.CancellationToken).Symbol is IPropertySymbol receiver
+ && receiver.IsStatic)
+ {
+ var owner = receiver.ContainingType?.ToDisplayString();
+ if (owner == "System.Console" && (receiver.Name == "Out" || receiver.Name == "Error") && method.Name.StartsWith("Write"))
+ {
+ return true;
+ }
+
+ if (owner == "Spectre.Console.AnsiConsole" && receiver.Name == "Console")
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/CosmosDBShell.Analyzer/GlobalSuppressions.cs b/CosmosDBShell.Analyzer/GlobalSuppressions.cs
index f6ec9e97..db9f4d45 100644
--- a/CosmosDBShell.Analyzer/GlobalSuppressions.cs
+++ b/CosmosDBShell.Analyzer/GlobalSuppressions.cs
@@ -11,3 +11,4 @@
[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking", Justification = "", Scope = "member", Target = "~F:CosmosShell.Analyzer.EmptyCatchClauseAnalyzer.Rule")]
[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking", Justification = "", Scope = "member", Target = "~F:CosmosShell.Analyzer.WriteLineAnalyzer.Rule")]
+[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking", Justification = "", Scope = "member", Target = "~F:CosmosShell.Analyzer.DirectConsoleOutputAnalyzer.Rule")]
diff --git a/CosmosDBShell.Tests/CommandTests/BatchCommandTests.cs b/CosmosDBShell.Tests/CommandTests/BatchCommandTests.cs
index 3850dce3..2e438b4f 100644
--- a/CosmosDBShell.Tests/CommandTests/BatchCommandTests.cs
+++ b/CosmosDBShell.Tests/CommandTests/BatchCommandTests.cs
@@ -249,7 +249,7 @@ await Assert.ThrowsAsync(
public async Task Execute_WithoutOperations_Throws()
{
await Assert.ThrowsAsync(
- () => BatchExecutor.ExecuteAsync("batch", null!, default, [], CancellationToken.None));
+ () => BatchExecutor.ExecuteAsync(null!, "batch", null!, default, [], CancellationToken.None));
}
[Fact]
@@ -259,7 +259,7 @@ public async Task Execute_OverMaximumOperations_Throws()
var operations = Enumerable.Repeat(operation, BatchExecutor.MaxOperations + 1).ToArray();
await Assert.ThrowsAsync(
- () => BatchExecutor.ExecuteAsync("batch", null!, default, operations, CancellationToken.None));
+ () => BatchExecutor.ExecuteAsync(null!, "batch", null!, default, operations, CancellationToken.None));
}
[Fact]
@@ -271,7 +271,8 @@ public void CreateResultState_PreservesBatchResultAndRequestCharge()
requestCharge = 4.25,
});
- var state = BatchExecutor.CreateResultState(summary, "Batch committed.", 4.25);
+ using var shell = ShellInterpreter.CreateInstance();
+ var state = BatchExecutor.CreateResultState(shell, summary, "Batch committed.", 4.25);
Assert.Equal(4.25, state.RequestCharge);
var result = Assert.IsType(state.Result).Value;
diff --git a/CosmosDBShell.Tests/CommandTests/BucketCommandTests.cs b/CosmosDBShell.Tests/CommandTests/BucketCommandTests.cs
index 97f068ed..aa1d7c02 100644
--- a/CosmosDBShell.Tests/CommandTests/BucketCommandTests.cs
+++ b/CosmosDBShell.Tests/CommandTests/BucketCommandTests.cs
@@ -4,18 +4,81 @@
namespace CosmosShell.Tests.CommandTests;
+using System.Text.Json;
using Azure.Data.Cosmos.Shell.Commands;
using Azure.Data.Cosmos.Shell.Core;
+using Azure.Data.Cosmos.Shell.Parser;
using Azure.Data.Cosmos.Shell.States;
using Azure.Data.Cosmos.Shell.Util;
using Microsoft.Azure.Cosmos;
+using Spectre.Console;
///
/// Unit tests for . Covers the pure validation helper and
/// the offline state visitors that do not require a live Cosmos DB connection.
///
+[Collection(ConsoleOutputTestCollection.Name)]
public class BucketCommandTests
{
+ [Theory]
+ [InlineData(null, false)]
+ [InlineData("3", false)]
+ [InlineData("0", false)]
+ [InlineData(null, true)]
+ [InlineData("3", true)]
+ [InlineData("0", true)]
+ public async Task ClientSelection_UsesExecutingShellOutput(string? action, bool machineMode)
+ {
+ using var shell = ShellInterpreter.CreateInstance();
+ using var client = CreateTestClient();
+ client.ClientOptions.ThroughputBucket = 2;
+ shell.State = new DatabaseState("TestDatabase", client);
+ shell.Options = new Program.CosmosShellOptions { Output = machineMode ? "json" : "user", Quiet = machineMode };
+ var singletonOptions = ShellInterpreter.Instance.Options;
+ var originalOut = Console.Out;
+ var originalConsole = AnsiConsole.Console;
+ using var output = new StringWriter();
+ try
+ {
+ ShellInterpreter.Instance.Options = new Program.CosmosShellOptions { Output = machineMode ? "user" : "json", Quiet = !machineMode };
+ Console.SetOut(output);
+ AnsiConsole.Console = AnsiConsole.Create(new AnsiConsoleSettings
+ {
+ Ansi = AnsiSupport.No,
+ ColorSystem = ColorSystemSupport.NoColors,
+ Out = new AnsiConsoleOutput(output),
+ });
+ var state = await new BucketCommand { Action = action }.ExecuteAsync(
+ shell, new CommandState(), "bucket", TestContext.Current.CancellationToken);
+ shell.PrintState(state);
+
+ if (machineMode)
+ {
+ using var result = JsonDocument.Parse(output.ToString());
+ var selection = result.RootElement.GetProperty("clientBucket");
+ if (action == "0")
+ {
+ Assert.Equal(JsonValueKind.Null, selection.ValueKind);
+ }
+ else
+ {
+ Assert.Equal(action == "3" ? 3 : 2, selection.GetInt32());
+ }
+ }
+ else
+ {
+ Assert.NotEmpty(output.ToString());
+ Assert.DoesNotContain("clientBucket", output.ToString());
+ }
+ }
+ finally
+ {
+ Console.SetOut(originalOut);
+ AnsiConsole.Console = originalConsole;
+ ShellInterpreter.Instance.Options = singletonOptions;
+ }
+ }
+
[Theory]
[InlineData(0)]
[InlineData(1)]
@@ -68,6 +131,7 @@ public async Task ExecuteAsync_InDatabase_SetsBucket()
Assert.False(state.IsError);
Assert.Equal(3, client.ClientOptions.ThroughputBucket);
+ Assert.Equal(3, Assert.IsType(state.Result).Value.GetProperty("clientBucket").GetInt32());
}
[Fact]
@@ -83,12 +147,14 @@ public async Task ExecuteAsync_InDatabase_ZeroResetsBucket()
Assert.False(state.IsError);
Assert.Null(client.ClientOptions.ThroughputBucket);
+ Assert.Equal(JsonValueKind.Null, Assert.IsType(state.Result).Value.GetProperty("clientBucket").ValueKind);
}
[Fact]
public async Task ExecuteAsync_InContainer_NoArgs_ShowsCurrent()
{
var client = CreateTestClient();
+ client.ClientOptions.ThroughputBucket = 2;
using var shell = ShellInterpreter.CreateInstance();
shell.State = new ContainerState("TestContainer", "TestDatabase", client);
var command = new BucketCommand();
@@ -96,19 +162,20 @@ public async Task ExecuteAsync_InContainer_NoArgs_ShowsCurrent()
var state = await command.ExecuteAsync(shell, new CommandState(), "bucket", CancellationToken.None);
Assert.False(state.IsError);
+ Assert.Equal(2, Assert.IsType(state.Result).Value.GetProperty("clientBucket").GetInt32());
}
[Fact]
- public async Task ExecuteAsync_InDatabase_InvalidValue_ReturnsEmptyState()
+ public async Task ExecuteAsync_InDatabase_InvalidValue_ThrowsCommandException()
{
var client = CreateTestClient();
using var shell = ShellInterpreter.CreateInstance();
shell.State = new DatabaseState("TestDatabase", client);
var command = new BucketCommand { Action = "99" };
- var state = await command.ExecuteAsync(shell, new CommandState(), "bucket 99", CancellationToken.None);
+ await Assert.ThrowsAsync(
+ () => command.ExecuteAsync(shell, new CommandState(), "bucket 99", CancellationToken.None));
- Assert.False(state.IsError);
Assert.Null(client.ClientOptions.ThroughputBucket);
}
diff --git a/CosmosDBShell.Tests/CommandTests/CommandStatusOutputTests.cs b/CosmosDBShell.Tests/CommandTests/CommandStatusOutputTests.cs
index eb77c162..ece28c07 100644
--- a/CosmosDBShell.Tests/CommandTests/CommandStatusOutputTests.cs
+++ b/CosmosDBShell.Tests/CommandTests/CommandStatusOutputTests.cs
@@ -18,6 +18,66 @@ namespace CosmosShell.Tests.CommandTests;
[Collection(ConsoleOutputTestCollection.Name)]
public class CommandStatusOutputTests
{
+ [Theory]
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public async Task BatchFailure_ErrorRedirectPreservesSingleDiagnostic(bool append, bool positional)
+ {
+ using var shell = ShellInterpreter.CreateInstance();
+ shell.Options = new Program.CosmosShellOptions { Output = "user", Verbose = !positional };
+ var file = Path.GetTempFileName();
+ var originalOut = Console.Out;
+ using var output = new StringWriter();
+ try
+ {
+ Console.SetOut(output);
+ await File.WriteAllTextAsync(file, "existing\n", TestContext.Current.CancellationToken);
+ shell.ErrOutRedirect = file;
+ shell.AppendErrRedirection = append;
+ var container = Substitute.For();
+ var batch = Substitute.For();
+ using var response = Substitute.For();
+ response.StatusCode.Returns(HttpStatusCode.Conflict);
+ response.IsSuccessStatusCode.Returns(false);
+ response.RequestCharge.Returns(1.25);
+ container.CreateTransactionalBatch(Arg.Any()).Returns(batch);
+ batch.ExecuteAsync(Arg.Any()).Returns(response);
+ var operations = BatchOperationParser.Parse("batch", """{"op":"delete","id":"missing"}""");
+ var state = Assert.IsType(await BatchExecutor.ExecuteAsync(
+ shell, "batch", container, new PartitionKey("pk"), operations, TestContext.Current.CancellationToken));
+ if (positional)
+ {
+ state.Exception = new PositionalException("batch.csh", state.Exception, 4, 7, "batch execute");
+ }
+
+ shell.PrintState(state);
+
+ var content = await File.ReadAllTextAsync(file, TestContext.Current.CancellationToken);
+ Assert.Empty(output.ToString());
+ Assert.Equal(append, content.StartsWith("existing", StringComparison.Ordinal));
+ if (positional)
+ {
+ Assert.Contains("batch.csh:4:7", content);
+ }
+ else
+ {
+ Assert.Contains("CommandException", content);
+ }
+
+ var beforeRenderer = content;
+ state.RenderUser!();
+ Assert.Equal(beforeRenderer, await File.ReadAllTextAsync(file, TestContext.Current.CancellationToken));
+ Assert.Empty(output.ToString());
+ }
+ finally
+ {
+ Console.SetOut(originalOut);
+ File.Delete(file);
+ }
+ }
+
[Theory]
[InlineData(".json", false)]
[InlineData(".csv", false)]
diff --git a/CosmosDBShell.Tests/CommandTests/CommandUsageErrorTests.cs b/CosmosDBShell.Tests/CommandTests/CommandUsageErrorTests.cs
index 83c5ed27..1b99be1a 100644
--- a/CosmosDBShell.Tests/CommandTests/CommandUsageErrorTests.cs
+++ b/CosmosDBShell.Tests/CommandTests/CommandUsageErrorTests.cs
@@ -130,6 +130,10 @@ public async Task EveryBuiltInCommand_InvalidBindingIsUsageError(Type type, stri
[InlineData("rm")]
[InlineData("rm '*' --etag value")]
[InlineData("watch --interval=NaN")]
+ [InlineData("bucket 99")]
+ [InlineData("bucket -1")]
+ [InlineData("query 'SELECT * FROM c' --bucket=99")]
+ [InlineData("query 'SELECT * FROM c' --bucket=-1")]
public async Task CommandValidation_ReturnsUsageError(string text)
{
using var shell = ShellInterpreter.CreateInstance();
diff --git a/CosmosDBShell.Tests/CommandTests/MachineModeWriteErrorTests.cs b/CosmosDBShell.Tests/CommandTests/MachineModeWriteErrorTests.cs
new file mode 100644
index 00000000..137f5672
--- /dev/null
+++ b/CosmosDBShell.Tests/CommandTests/MachineModeWriteErrorTests.cs
@@ -0,0 +1,321 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+namespace CosmosShell.Tests.CommandTests;
+
+using System.Net;
+using System.Text.Json;
+using Azure.Data.Cosmos.Shell.Commands;
+using Azure.Data.Cosmos.Shell.Core;
+using Azure.Data.Cosmos.Shell.States;
+using Microsoft.Azure.Cosmos;
+using NSubstitute;
+
+[Collection(ConsoleOutputTestCollection.Name)]
+public class MachineModeWriteErrorTests
+{
+ [Theory]
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public async Task Import_PreservesLineStatusAndServiceDiagnostics(bool upsert, bool quiet)
+ {
+ var (shell, container) = CreateShell(quiet);
+ using (shell)
+ {
+ ConfigureWrites(container);
+ var path = Path.Join(Path.GetTempPath(), $"cosmosshell-import-errors-{Guid.NewGuid():N}.jsonl");
+ try
+ {
+ await File.WriteAllTextAsync(
+ path,
+ "{\"id\":\"bad-status\",\"pk\":\"x\"}\n{\"id\":\"denied\",\"pk\":\"x\"}\n",
+ TestContext.Current.CancellationToken);
+ var command = $"import '{path}' --continue-on-error --concurrency=2"
+ + (upsert ? " --mode=upsert" : string.Empty);
+ var error = await CaptureMachineErrorAsync(shell, command);
+
+ Assert.Contains("Line 1", error);
+ Assert.Contains("BadRequest", error);
+ Assert.Contains("Line 2", error);
+ Assert.Contains("Forbidden", error);
+ Assert.Contains("bulk service denied", error);
+ }
+ finally
+ {
+ File.Delete(path);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task Import_ParseFailure_PreservesEarlierWriteDiagnostics()
+ {
+ var (shell, container) = CreateShell(quiet: true);
+ using (shell)
+ {
+ ConfigureWrites(container);
+ var path = Path.Join(Path.GetTempPath(), $"cosmosshell-import-parse-errors-{Guid.NewGuid():N}.jsonl");
+ try
+ {
+ await File.WriteAllTextAsync(
+ path,
+ "{\"id\":\"bad-status\",\"pk\":\"x\"}\nnot-json\n",
+ TestContext.Current.CancellationToken);
+ var error = await CaptureMachineErrorAsync(shell, $"import '{path}' --continue-on-error --concurrency=2");
+
+ Assert.Contains("Line 1", error);
+ Assert.Contains("BadRequest", error);
+ Assert.Contains("2", error);
+ Assert.Contains("JSON", error, StringComparison.OrdinalIgnoreCase);
+ }
+ finally
+ {
+ File.Delete(path);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task Import_ArrayParseFailure_PreservesEarlierWriteDiagnostics()
+ {
+ var (shell, container) = CreateShell(quiet: true);
+ using (shell)
+ {
+ ConfigureWrites(container);
+ var path = Path.Join(Path.GetTempPath(), $"cosmosshell-import-array-errors-{Guid.NewGuid():N}.json");
+ try
+ {
+ await File.WriteAllTextAsync(
+ path,
+ "[{\"id\":\"bad-status\",\"pk\":\"x\"},{\"id\":\"denied\",\"pk\":\"x\"},",
+ TestContext.Current.CancellationToken);
+ var error = await CaptureMachineErrorAsync(shell, $"import '{path}' --continue-on-error --concurrency=2");
+
+ Assert.Contains("Line 1", error);
+ Assert.Contains("BadRequest", error);
+ Assert.Contains("Line 2", error);
+ Assert.Contains("Forbidden", error);
+ Assert.Contains("bulk service denied", error);
+ Assert.Contains("Expected start of a property name or value", error);
+ }
+ finally
+ {
+ File.Delete(path);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task Import_TooManyFailures_BoundsDiagnosticsAndReportsOmittedCount()
+ {
+ var (shell, container) = CreateShell(quiet: true);
+ using (shell)
+ {
+ ConfigureWrites(container);
+ var path = Path.Join(Path.GetTempPath(), $"cosmosshell-import-many-errors-{Guid.NewGuid():N}.jsonl");
+ try
+ {
+ var records = Enumerable.Repeat("{\"id\":\"denied\",\"pk\":\"x\"}", BulkWriteDiagnostics.MaximumCount + 3);
+ await File.WriteAllLinesAsync(path, records, TestContext.Current.CancellationToken);
+ var error = await CaptureMachineErrorAsync(shell, $"import '{path}' --continue-on-error --concurrency=16");
+
+ Assert.Equal(
+ BulkWriteDiagnostics.MaximumCount,
+ error.Split("Line ", StringSplitOptions.None).Length - 1);
+ Assert.Contains("3 additional item diagnostics omitted", error);
+ }
+ finally
+ {
+ File.Delete(path);
+ }
+ }
+ }
+
+ [Fact]
+ public void BulkWriteDiagnostics_TruncatesLongMessages()
+ {
+ var diagnostics = new BulkWriteDiagnostics();
+ diagnostics.Add(new string('x', BulkWriteDiagnostics.MaximumLength + 100));
+
+ var message = diagnostics.GetMessage();
+
+ Assert.True(message.Length <= BulkWriteDiagnostics.MaximumLength);
+ Assert.Contains("[diagnostic truncated]", message);
+ }
+
+ [Theory]
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public async Task MakeItem_Bulk_PreservesStatusAndServiceDiagnostics(bool upsert, bool quiet)
+ {
+ var (shell, container) = CreateShell(quiet);
+ using (shell)
+ {
+ ConfigureWrites(container);
+ var command = "mkitem '[{\"id\":\"bad-status\",\"pk\":\"x\"},{\"id\":\"denied\",\"pk\":\"x\"}]'"
+ + (upsert ? " --force" : string.Empty);
+ var error = await CaptureMachineErrorAsync(shell, command);
+
+ Assert.Contains("Item 1", error);
+ Assert.Contains("BadRequest", error);
+ Assert.Contains("Item 2", error);
+ Assert.Contains("Forbidden", error);
+ Assert.Contains("bulk service denied", error);
+ }
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task MakeItem_SingleFailedStatus_ReportsStructuredError(bool upsert)
+ {
+ var (shell, container) = CreateShell(quiet: true);
+ using (shell)
+ {
+ ConfigureWrites(container);
+ var command = "mkitem '{\"id\":\"bad-status\",\"pk\":\"x\"}'" + (upsert ? " --force" : string.Empty);
+ Assert.Contains("BadRequest", await CaptureMachineErrorAsync(shell, command));
+ }
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task Replace_Bulk_PreservesValidationStatusAndServiceDiagnostics(bool quiet)
+ {
+ var (shell, container) = CreateShell(quiet);
+ using (shell)
+ {
+ ConfigureWrites(container);
+ var error = await CaptureMachineErrorAsync(
+ shell,
+ "replace '[{\"pk\":\"x\"},{\"id\":\"bad-status\",\"pk\":\"x\"},{\"id\":\"denied\",\"pk\":\"x\"}]'");
+
+ Assert.Contains("Item 1", error);
+ Assert.Contains("non-empty 'id'", error);
+ Assert.Contains("Item 2", error);
+ Assert.Contains("BadRequest", error);
+ Assert.Contains("Item 3", error);
+ Assert.Contains("Forbidden", error);
+ Assert.Contains("bulk service denied", error);
+ }
+ }
+
+ [Theory]
+ [InlineData("mkitem")]
+ [InlineData("replace")]
+ public async Task BulkWrite_TooManyFailures_BoundsDiagnosticsAndReportsOmittedCount(string commandName)
+ {
+ var (shell, container) = CreateShell(quiet: true);
+ using (shell)
+ {
+ ConfigureWrites(container);
+ var items = string.Join(',', Enumerable.Repeat("{\"id\":\"denied\",\"pk\":\"x\"}", BulkWriteDiagnostics.MaximumCount + 3));
+ var error = await CaptureMachineErrorAsync(shell, $"{commandName} '[{items}]'");
+
+ Assert.Equal(
+ BulkWriteDiagnostics.MaximumCount,
+ error.Split("Item ", StringSplitOptions.None).Length - 1);
+ Assert.Contains("3 additional item diagnostics omitted", error);
+ }
+ }
+
+ private static (ShellInterpreter Shell, Container Container) CreateShell(bool quiet)
+ {
+ var client = Substitute.For();
+ client.Endpoint.Returns(new Uri("https://account.documents.azure.com:443/"));
+ var database = Substitute.For();
+ var container = Substitute.For();
+ client.GetDatabase("db").Returns(database);
+ database.GetContainer("items").Returns(container);
+ var databaseResponse = Substitute.For();
+ databaseResponse.StatusCode.Returns(HttpStatusCode.OK);
+ database.ReadAsync(Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult(databaseResponse));
+ var containerResponse = Substitute.For();
+ containerResponse.StatusCode.Returns(HttpStatusCode.OK);
+ containerResponse.Resource.Returns(new ContainerProperties("items", "/pk"));
+ container.ReadContainerAsync(Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult(containerResponse));
+
+ var shell = ShellInterpreter.CreateInstance();
+ shell.Options = new Program.CosmosShellOptions { Output = "json", Quiet = quiet };
+ shell.State = new ContainerState("items", "db", client);
+ return (shell, container);
+ }
+
+ private static void ConfigureWrites(Container container)
+ {
+ Task> Write(JsonElement item)
+ {
+ if (item.GetProperty("id").GetString() == "bad-status")
+ {
+ var response = Substitute.For>();
+ response.StatusCode.Returns(HttpStatusCode.BadRequest);
+ return Task.FromResult(response);
+ }
+
+ return Task.FromException>(
+ new CosmosException("bulk service denied", HttpStatusCode.Forbidden, 0, "test", 0));
+ }
+
+ container.CreateItemAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(call => Write(call.ArgAt(0)));
+ container.UpsertItemAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(call => Write(call.ArgAt(0)));
+ container.ReplaceItemAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(call => Write(call.ArgAt(0)));
+
+ static Task WriteStream(Stream body)
+ {
+ using var document = JsonDocument.Parse(body);
+ if (document.RootElement.GetProperty("id").GetString() == "bad-status")
+ {
+ return Task.FromException(
+ new CosmosException("bad status", HttpStatusCode.BadRequest, 0, "test", 0));
+ }
+
+ return Task.FromException(
+ new CosmosException("bulk service denied", HttpStatusCode.Forbidden, 0, "test", 0));
+ }
+
+ container.CreateItemStreamAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(call => WriteStream(call.ArgAt(0)));
+ container.UpsertItemStreamAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(call => WriteStream(call.ArgAt(0)));
+ container.ReplaceItemStreamAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(call => WriteStream(call.ArgAt(0)));
+ }
+
+ private static async Task CaptureMachineErrorAsync(ShellInterpreter shell, string command)
+ {
+ var savedOut = Console.Out;
+ var savedError = Console.Error;
+ using var stdout = new StringWriter();
+ using var stderr = new StringWriter();
+ try
+ {
+ Console.SetOut(stdout);
+ Console.SetError(stderr);
+ var state = await shell.ExecuteCommandAsync(command, TestContext.Current.CancellationToken);
+
+ Assert.True(state.IsError);
+ Assert.Empty(stdout.ToString());
+ var line = Assert.Single(stderr.ToString().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries));
+ using var json = JsonDocument.Parse(line);
+ Assert.Equal("error", json.RootElement.GetProperty("status").GetString());
+ Assert.DoesNotContain("\u001b", line, StringComparison.Ordinal);
+ return Assert.IsType(json.RootElement.GetProperty("error").GetString());
+ }
+ finally
+ {
+ Console.SetOut(savedOut);
+ Console.SetError(savedError);
+ }
+ }
+}
diff --git a/CosmosDBShell.Tests/CommandTests/QueryCommandTests.cs b/CosmosDBShell.Tests/CommandTests/QueryCommandTests.cs
index 5cdc6e45..0be198bd 100644
--- a/CosmosDBShell.Tests/CommandTests/QueryCommandTests.cs
+++ b/CosmosDBShell.Tests/CommandTests/QueryCommandTests.cs
@@ -729,6 +729,65 @@ public async Task ExecuteQueryAsync_ResumableQueryAtLimit_ReportsLimitWithoutRes
Assert.DoesNotContain("cannot be resumed", output.Text);
}
+ [Theory]
+ [InlineData(false, null)]
+ [InlineData(true, null)]
+ [InlineData(false, false)]
+ [InlineData(true, false)]
+ [InlineData(false, true)]
+ [InlineData(true, true)]
+ public async Task ExecuteQueryAsync_LimitWarnings_UseExecutingShellOutput(bool quiet, bool? append)
+ {
+ using var shell = ShellInterpreter.CreateInstance();
+ shell.Options = new Program.CosmosShellOptions { Output = "json", Quiet = quiet };
+ var singletonOptions = ShellInterpreter.Instance.Options;
+ var originalOut = Console.Out;
+ var originalError = Console.Error;
+ using var stdout = new StringWriter();
+ using var stderr = new StringWriter();
+ var errorFile = Path.Join(Path.GetTempPath(), $"cosmosshell-query-warnings-{Guid.NewGuid():N}.log");
+ try
+ {
+ ShellInterpreter.Instance.Options = new Program.CosmosShellOptions { Output = "user" };
+ Console.SetOut(stdout);
+ Console.SetError(stderr);
+ if (append.HasValue)
+ {
+ await File.WriteAllTextAsync(errorFile, "existing\n", TestContext.Current.CancellationToken);
+ shell.ErrOutRedirect = errorFile;
+ shell.AppendErrRedirection = append.Value;
+ }
+
+ using var iterator = new FakeFeedIterator(
+ NonResumablePage("Continuation tokens are not supported.", 1, "1", "2", "3"));
+ var command = new QueryCommand { Query = "SELECT * FROM c", Max = 2 };
+ var output = await CaptureConsoleAsync(
+ () => command.ExecuteQueryAsync(CreateContainer(iterator), shell, TestContext.Current.CancellationToken));
+
+ Assert.Equal(["1", "2"], ReadIds(output.Result));
+ Assert.Empty(output.Text);
+ Assert.Empty(stdout.ToString());
+ var diagnostics = append.HasValue
+ ? await File.ReadAllTextAsync(errorFile, TestContext.Current.CancellationToken)
+ : stderr.ToString();
+ Assert.Contains("Results limited to 2 items", diagnostics);
+ Assert.Contains("cannot be resumed", diagnostics);
+ Assert.DoesNotContain("\u001b", diagnostics, StringComparison.Ordinal);
+ if (append.HasValue)
+ {
+ Assert.Empty(stderr.ToString());
+ Assert.Equal(append.Value, diagnostics.StartsWith("existing", StringComparison.Ordinal));
+ }
+ }
+ finally
+ {
+ Console.SetOut(originalOut);
+ Console.SetError(originalError);
+ ShellInterpreter.Instance.Options = singletonOptions;
+ File.Delete(errorFile);
+ }
+ }
+
private static Container CreateContainer(FeedIterator iterator)
{
var container = Substitute.For();
diff --git a/CosmosDBShell.Tests/CommandTests/ThemeCommandDispatchTests.cs b/CosmosDBShell.Tests/CommandTests/ThemeCommandDispatchTests.cs
index 4c9bd737..a35614f9 100644
--- a/CosmosDBShell.Tests/CommandTests/ThemeCommandDispatchTests.cs
+++ b/CosmosDBShell.Tests/CommandTests/ThemeCommandDispatchTests.cs
@@ -123,9 +123,7 @@ public async Task Show_KnownTheme_PreviewsWithoutChangingActive(string name)
[Fact]
public async Task Show_UnknownTheme_ReturnsError()
{
- var state = await RunAsync(new ThemeCommand { Action = "show", Name = "does-not-exist-xyz" });
-
- Assert.IsType(state);
+ await Assert.ThrowsAsync(() => RunAsync(new ThemeCommand { Action = "show", Name = "does-not-exist-xyz" }));
}
[Fact]
@@ -167,25 +165,19 @@ public async Task Set_AliasOfUse_AppliesTheme()
[Fact]
public async Task Use_MissingName_ReturnsError()
{
- var state = await RunAsync(new ThemeCommand { Action = "use" });
-
- Assert.IsType(state);
+ await Assert.ThrowsAsync(() => RunAsync(new ThemeCommand { Action = "use" }));
}
[Fact]
public async Task Use_UnknownName_ReturnsError()
{
- var state = await RunAsync(new ThemeCommand { Action = "use", Name = "nope-xyz" });
-
- Assert.IsType(state);
+ await Assert.ThrowsAsync(() => RunAsync(new ThemeCommand { Action = "use", Name = "nope-xyz" }));
}
[Fact]
public async Task Load_MissingPath_ReturnsError()
{
- var state = await RunAsync(new ThemeCommand { Action = "load" });
-
- Assert.IsType(state);
+ await Assert.ThrowsAsync(() => RunAsync(new ThemeCommand { Action = "load" }));
}
[Fact]
@@ -214,9 +206,7 @@ public async Task Load_NonExistentFile_ReturnsError()
{
var path = Path.Combine(Path.GetTempPath(), $"missing-{Guid.NewGuid():N}.toml");
- var state = await RunAsync(new ThemeCommand { Action = "load", Name = path });
-
- Assert.IsType(state);
+ await Assert.ThrowsAsync(() => RunAsync(new ThemeCommand { Action = "load", Name = path }));
}
[Fact]
@@ -225,9 +215,7 @@ public async Task Load_InvalidToml_ReturnsError()
var path = WriteTempToml($"bad-{Guid.NewGuid():N}", "this is = = not valid toml [[[");
try
{
- var state = await RunAsync(new ThemeCommand { Action = "load", Name = path });
-
- Assert.IsType(state);
+ await Assert.ThrowsAsync(() => RunAsync(new ThemeCommand { Action = "load", Name = path }));
}
finally
{
@@ -238,17 +226,13 @@ public async Task Load_InvalidToml_ReturnsError()
[Fact]
public async Task Save_MissingName_ReturnsError()
{
- var state = await RunAsync(new ThemeCommand { Action = "save" });
-
- Assert.IsType(state);
+ await Assert.ThrowsAsync(() => RunAsync(new ThemeCommand { Action = "save" }));
}
[Fact]
public async Task Save_InvalidName_ReturnsError()
{
- var state = await RunAsync(new ThemeCommand { Action = "save", Name = "../escape" });
-
- Assert.IsType(state);
+ await Assert.ThrowsAsync(() => RunAsync(new ThemeCommand { Action = "save", Name = "../escape" }));
}
[Fact]
@@ -280,9 +264,7 @@ public async Task Save_ExistingFileWithoutForce_ReturnsError()
File.WriteAllText(path, "name = \"existing\"");
try
{
- var state = await RunAsync(new ThemeCommand { Action = "save", Name = "exported", Path = path });
-
- Assert.IsType(state);
+ await Assert.ThrowsAsync(() => RunAsync(new ThemeCommand { Action = "save", Name = "exported", Path = path }));
}
finally
{
@@ -321,9 +303,7 @@ public async Task Reload_RescansUserThemesDirectory()
[Fact]
public async Task UnknownAction_ReturnsError()
{
- var state = await RunAsync(new ThemeCommand { Action = "frobnicate" });
-
- Assert.IsType(state);
+ await Assert.ThrowsAsync(() => RunAsync(new ThemeCommand { Action = "frobnicate" }));
}
private static Task RunAsync(ThemeCommand command) =>
diff --git a/CosmosDBShell.Tests/CommandTests/ThemeCommandTests.cs b/CosmosDBShell.Tests/CommandTests/ThemeCommandTests.cs
index ec163e6f..581c0ad2 100644
--- a/CosmosDBShell.Tests/CommandTests/ThemeCommandTests.cs
+++ b/CosmosDBShell.Tests/CommandTests/ThemeCommandTests.cs
@@ -81,10 +81,44 @@ public async Task Validate_ScansDirectoryAndReportsCounts()
Name = dir.Path,
};
- var state = await command.ExecuteAsync(ShellInterpreter.Instance, new CommandState(), "", CancellationToken.None);
- var error = Assert.IsType(state);
+ var error = await Assert.ThrowsAsync(() => command.ExecuteAsync(ShellInterpreter.Instance, new CommandState(), "", CancellationToken.None));
- Assert.Contains("1 of 2", error.Exception.Message);
+ Assert.Contains("1 of 2", error.Message);
+ }
+
+ [Fact]
+ public async Task Validate_DirectoryUsesExecutingShellMachineMode()
+ {
+ using var dir = new TempDirectory();
+ var broken = Path.Join(dir.Path, "broken.toml");
+ File.WriteAllText(broken,
+ """
+ name = "broken"
+
+ [colors]
+ literal = "lightyellow3"
+ """);
+ using var shell = ShellInterpreter.CreateInstance();
+ shell.Options = new Program.CosmosShellOptions { Output = "json" };
+ var command = new ThemeCommand
+ {
+ Action = "validate",
+ Name = dir.Path,
+ };
+ var savedOut = Console.Out;
+ using var stdout = new StringWriter();
+ try
+ {
+ Console.SetOut(stdout);
+ var error = await Assert.ThrowsAsync(() => command.ExecuteAsync(shell, new CommandState(), "", CancellationToken.None));
+
+ Assert.Contains(broken, error.Message);
+ Assert.Empty(stdout.ToString());
+ }
+ finally
+ {
+ Console.SetOut(savedOut);
+ }
}
[Fact]
@@ -109,9 +143,8 @@ public async Task Validate_StrictTreatsWarningsAsErrors()
Strict = true,
};
- var state = await command.ExecuteAsync(ShellInterpreter.Instance, new CommandState(), "", CancellationToken.None);
- var error = Assert.IsType(state);
- Assert.Contains("strict mode", error.Exception.Message);
+ var error = await Assert.ThrowsAsync(() => command.ExecuteAsync(ShellInterpreter.Instance, new CommandState(), "", CancellationToken.None));
+ Assert.Contains("strict mode", error.Message);
}
finally
{
diff --git a/CosmosDBShell.Tests/CommandTests/WatchCommandTests.cs b/CosmosDBShell.Tests/CommandTests/WatchCommandTests.cs
index 2dc9671e..68c631a8 100644
--- a/CosmosDBShell.Tests/CommandTests/WatchCommandTests.cs
+++ b/CosmosDBShell.Tests/CommandTests/WatchCommandTests.cs
@@ -4,12 +4,13 @@
namespace CosmosShell.Tests.CommandTests;
+using System.Net;
+using System.Text;
+using System.Text.Json;
using Azure.Data.Cosmos.Shell.Commands;
using Azure.Data.Cosmos.Shell.Core;
using Azure.Data.Cosmos.Shell.Parser;
using Azure.Data.Cosmos.Shell.States;
-using System.Net;
-using System.Text;
using Microsoft.Azure.Cosmos;
using NSubstitute;
@@ -22,6 +23,73 @@ namespace CosmosShell.Tests.CommandTests;
[Collection(ConsoleOutputTestCollection.Name)]
public class WatchCommandTests
{
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ExecuteAsync_BoundedMachineWatch_ReturnsExactlyMaxChangesOnce(bool quiet)
+ {
+ var client = Substitute.For();
+ var database = Substitute.For();
+ var container = Substitute.For();
+ client.GetDatabase("db").Returns(database);
+ database.GetContainer("items").Returns(container);
+ container.Id.Returns("items");
+ var databaseResponse = Substitute.For();
+ databaseResponse.StatusCode.Returns(HttpStatusCode.OK);
+ database.ReadAsync(Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult(databaseResponse));
+ var containerResponse = Substitute.For();
+ containerResponse.StatusCode.Returns(HttpStatusCode.OK);
+ container.ReadContainerAsync(Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult(containerResponse));
+
+ using var firstPage = new ResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new MemoryStream(Encoding.UTF8.GetBytes("{\"Documents\":[{\"id\":\"1\"},{\"id\":\"2\"}]}")),
+ };
+ using var secondPage = new ResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new MemoryStream(Encoding.UTF8.GetBytes("{\"Documents\":[{\"id\":\"3\"},{\"id\":\"4\"}]}")),
+ };
+ var iterator = Substitute.For();
+ iterator.ReadNextAsync(Arg.Any())
+ .Returns(Task.FromResult(firstPage), Task.FromResult(secondPage));
+ container.GetChangeFeedStreamIterator(
+ Arg.Any(), ChangeFeedMode.Incremental, Arg.Any())
+ .Returns(iterator);
+
+ using var shell = ShellInterpreter.CreateInstance();
+ shell.Options = new Program.CosmosShellOptions { Quiet = quiet, Output = "json" };
+ shell.State = new ContainerState("items", "db", client);
+ var command = new WatchCommand { Max = 3 };
+ var savedOut = Console.Out;
+ var savedError = Console.Error;
+ using var stdout = new StringWriter();
+ using var stderr = new StringWriter();
+ try
+ {
+ Console.SetOut(stdout);
+ Console.SetError(stderr);
+ var state = await command.ExecuteAsync(shell, new CommandState(), "watch --max=3", TestContext.Current.CancellationToken);
+ Assert.Empty(stdout.ToString());
+ Assert.Empty(stderr.ToString());
+
+ shell.PrintState(state);
+ using var json = JsonDocument.Parse(stdout.ToString());
+ Assert.Equal("item", json.RootElement.GetProperty("type").GetString());
+ Assert.Equal(
+ ["1", "2", "3"],
+ json.RootElement.GetProperty("values").EnumerateArray().Select(item => item.GetProperty("id").GetString()));
+ Assert.Empty(stderr.ToString());
+ await iterator.Received(2).ReadNextAsync(Arg.Any());
+ }
+ finally
+ {
+ Console.SetOut(savedOut);
+ Console.SetError(savedError);
+ }
+ }
+
[Fact]
public void ParseChangeFeedDocuments_ReturnsAllDocuments()
{
@@ -125,6 +193,63 @@ await Assert.ThrowsAsync(
() => command.ExecuteAsync(shell, new CommandState(), "watch", TestContext.Current.CancellationToken));
}
+ [Theory]
+ [InlineData(true, null, null)]
+ [InlineData(true, null, 0)]
+ [InlineData(true, null, -1)]
+ [InlineData(false, "json", null)]
+ [InlineData(false, "json", 0)]
+ [InlineData(false, "csv", -1)]
+ public async Task ExecuteAsync_UnboundedMachineWatch_RejectsBeforeConnection(bool quiet, string? output, int? max)
+ {
+ using var shell = ShellInterpreter.CreateInstance();
+ shell.Options = new Program.CosmosShellOptions { Quiet = quiet, Output = output };
+ shell.State = new DisconnectedState();
+ var command = new WatchCommand { Max = max };
+
+ var error = await Assert.ThrowsAsync(
+ () => command.ExecuteAsync(shell, new CommandState(), "watch", TestContext.Current.CancellationToken));
+
+ Assert.Contains("positive --max", error.Message);
+ Assert.Equal(ShellExitCode.UsageError, ShellExitCode.FromException(error));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("changes.json")]
+ public async Task ExecuteAsync_UnboundedExecuteAndQuitWatch_RejectsIncludingRedirection(string? redirectedPath)
+ {
+ using var shell = ShellInterpreter.CreateInstance();
+ shell.Options = new Program.CosmosShellOptions { ExecuteAndQuit = "watch" };
+ shell.StdOutRedirect = redirectedPath;
+ shell.State = new DisconnectedState();
+ var command = new WatchCommand();
+
+ var error = await Assert.ThrowsAsync(
+ () => command.ExecuteAsync(shell, new CommandState(), "watch", TestContext.Current.CancellationToken));
+
+ Assert.Contains("positive --max", error.Message);
+ Assert.Equal(ShellExitCode.UsageError, ShellExitCode.FromException(error));
+ }
+
+ [Theory]
+ [InlineData(true, null, 1)]
+ [InlineData(false, "json", 100)]
+ [InlineData(false, "csv", 1)]
+ [InlineData(false, "user", null)]
+ [InlineData(false, "table", 0)]
+ [InlineData(false, null, -1)]
+ public async Task ExecuteAsync_BoundedMachineOrInteractiveWatch_PreservesConnectionGuard(bool quiet, string? output, int? max)
+ {
+ using var shell = ShellInterpreter.CreateInstance();
+ shell.Options = new Program.CosmosShellOptions { Quiet = quiet, Output = output };
+ shell.State = new DisconnectedState();
+ var command = new WatchCommand { Max = max };
+
+ await Assert.ThrowsAsync(
+ () => command.ExecuteAsync(shell, new CommandState(), "watch", TestContext.Current.CancellationToken));
+ }
+
[Theory]
[InlineData(false, false, false)]
[InlineData(true, false, false)]
diff --git a/CosmosDBShell.Tests/CosmosDBShell.Tests.csproj b/CosmosDBShell.Tests/CosmosDBShell.Tests.csproj
index 036b04d4..d5b19433 100644
--- a/CosmosDBShell.Tests/CosmosDBShell.Tests.csproj
+++ b/CosmosDBShell.Tests/CosmosDBShell.Tests.csproj
@@ -24,6 +24,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
@@ -34,6 +35,7 @@
+
diff --git a/CosmosDBShell.Tests/DirectConsoleOutputAnalyzerTests.cs b/CosmosDBShell.Tests/DirectConsoleOutputAnalyzerTests.cs
new file mode 100644
index 00000000..450b6eed
--- /dev/null
+++ b/CosmosDBShell.Tests/DirectConsoleOutputAnalyzerTests.cs
@@ -0,0 +1,76 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+namespace CosmosShell.Tests;
+
+using System.Collections.Immutable;
+using CosmosShell.Analyzer;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+public class DirectConsoleOutputAnalyzerTests
+{
+ [Theory]
+ [InlineData("Console.Clear();", true)]
+ [InlineData("Console.Write(\"text\");", true)]
+ [InlineData("Console.WriteLine(\"text\");", true)]
+ [InlineData("Console.Out.WriteLine(\"text\");", true)]
+ [InlineData("Console.Error.WriteLine(\"text\");", true)]
+ [InlineData("Console.ReadLine();", false)]
+ [InlineData("var width = Console.WindowWidth;", false)]
+ [InlineData("using var writer = new System.IO.StringWriter(); writer.WriteLine(\"text\");", false)]
+ public async Task DirectConsoleOperations_EnforceOutputBoundary(string statement, bool reportsDiagnostic)
+ {
+ var diagnostics = await AnalyzeAsync("Example", statement);
+
+ if (reportsDiagnostic)
+ {
+ Assert.Equal(DirectConsoleOutputAnalyzer.DiagnosticId, Assert.Single(diagnostics).Id);
+ }
+ else
+ {
+ Assert.Empty(diagnostics);
+ }
+ }
+
+ [Theory]
+ [InlineData("Console.Clear();")]
+ [InlineData("Console.WriteLine(\"text\");")]
+ public async Task ShellOutput_CanAccessConsole(string statement)
+ {
+ Assert.Empty(await AnalyzeAsync("Azure.Data.Cosmos.Shell.Core.ShellOutput", statement));
+ }
+
+ private static async Task> AnalyzeAsync(string typeName, string statement)
+ {
+ var separator = typeName.LastIndexOf('.');
+ var namespaceName = separator < 0 ? "ExampleNamespace" : typeName[..separator];
+ var className = typeName[(separator + 1)..];
+ var source = $$"""
+ using System;
+ namespace {{namespaceName}};
+ internal class {{className}}
+ {
+ internal void Run()
+ {
+ {{statement}}
+ }
+ }
+ """;
+ var assemblies = (string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")
+ ?? throw new InvalidOperationException("Runtime assembly references are unavailable.");
+ var references = assemblies.Split(Path.PathSeparator)
+ .Select(path => MetadataReference.CreateFromFile(path));
+ var compilation = CSharpCompilation.Create(
+ "AnalyzerTest",
+ [CSharpSyntaxTree.ParseText(source)],
+ references,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ Assert.DoesNotContain(compilation.GetDiagnostics(), diagnostic => diagnostic.Severity == DiagnosticSeverity.Error);
+
+ return await compilation.WithAnalyzers([new DirectConsoleOutputAnalyzer()])
+ .GetAnalyzerDiagnosticsAsync(TestContext.Current.CancellationToken);
+ }
+}
diff --git a/CosmosDBShell.Tests/Integration/AdditionalCommandsTests.cs b/CosmosDBShell.Tests/Integration/AdditionalCommandsTests.cs
index 24097fe0..63d37829 100644
--- a/CosmosDBShell.Tests/Integration/AdditionalCommandsTests.cs
+++ b/CosmosDBShell.Tests/Integration/AdditionalCommandsTests.cs
@@ -71,12 +71,10 @@ public async Task Bucket_ShowsCurrent_SetsAndResets()
}
[Fact]
- public async Task Bucket_InvalidValue_DoesNotErrorButDoesNotSet()
+ public async Task Bucket_InvalidValue_ReturnsError()
{
- // CheckBucket prints a message for out-of-range values but returns an empty state
- // rather than throwing. We still ensure it does not crash the shell.
var state = await ExecuteAsync("bucket 99");
- Assert.False(state.IsError, IntegrationTestBase.FormatError(state));
+ Assert.True(state.IsError);
}
[Fact]
diff --git a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs
index a84e6fc3..1b4d28c6 100644
--- a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs
+++ b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs
@@ -186,6 +186,10 @@ public async Task ReturnedCommandError_ThroughScriptOrExpression_IsReportedOnce(
[InlineData("ftab --take=invalid")]
[InlineData("ftab --take=999999999999999999999999999")]
[InlineData("echo --unknown-usage-test-option")]
+ [InlineData("query 'SELECT * FROM c' --bucket=99")]
+ [InlineData("watch")]
+ [InlineData("watch --max=0")]
+ [InlineData("watch --max=-1")]
public async Task InvalidCommandArguments_ReturnUsageExitCode(string command)
{
var result = await RunShellAsync(
@@ -222,6 +226,114 @@ public async Task ImportDryRun_InvalidConcurrency_ReturnsUsageExitCode()
}
}
+ [Theory]
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public async Task MachineMode_OutputFailure_RemainsStructuredAndHonorsStderrRedirection(bool verbose, bool redirectError)
+ {
+ var directory = Path.Join(Path.GetTempPath(), $"cosmosshell-output-errors-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(directory);
+ var outputFile = Path.Join(directory, "missing", "out.txt");
+ var errorFile = Path.Join(directory, "error.json");
+ try
+ {
+ var command = $"echo ok > '{outputFile}'" + (redirectError ? $" 2> '{errorFile}'" : string.Empty);
+ var result = await RunShellAsync(
+ stdinScript: null,
+ extraArgs: verbose ? ["--verbose", "-c", command] : ["-c", command],
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.Equal(1, result.ExitCode);
+ Assert.Empty(result.StdOut);
+ var errors = redirectError
+ ? await File.ReadAllTextAsync(errorFile, TestContext.Current.CancellationToken)
+ : result.StdErr;
+ if (redirectError)
+ {
+ Assert.Empty(result.StdErr);
+ }
+
+ var line = Assert.Single(errors.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries));
+ using var error = JsonDocument.Parse(line);
+ Assert.Equal("error", error.RootElement.GetProperty("status").GetString());
+ Assert.Contains(outputFile, error.RootElement.GetProperty("error").GetString());
+ Assert.DoesNotContain("\u001b", errors + result.RawStdOut + result.RawStdErr, StringComparison.Ordinal);
+ }
+ finally
+ {
+ File.Delete(errorFile);
+ Directory.Delete(directory);
+ }
+ }
+
+ [Theory]
+ [InlineData("theme use MACHINE_NOPE", "MACHINE_NOPE", ShellExitCode.GeneralFailure)]
+ [InlineData("help MACHINE_MISSING", "MACHINE_MISSING", ShellExitCode.UsageError)]
+ [InlineData("edit machine.txt", "interactive terminal", ShellExitCode.GeneralFailure)]
+ public async Task MachineMode_CommandFailure_ReportsSingleStructuredError(string command, string expected, int expectedExitCode)
+ {
+ var result = await RunShellAsync(
+ stdinScript: null,
+ extraArgs: ["-c", command],
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.Equal(expectedExitCode, result.ExitCode);
+ Assert.Empty(result.StdOut);
+ var line = Assert.Single(result.StdErr.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries));
+ using var error = JsonDocument.Parse(line);
+ Assert.Equal("error", error.RootElement.GetProperty("status").GetString());
+ Assert.Contains(expected, error.RootElement.GetProperty("error").GetString(), StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public async Task Quiet_PreservesResultsAndStartupWarningsWithoutAnsi()
+ {
+ var result = await RunShellAsync(
+ stdinScript: null,
+ extraArgs: ["--quiet", "--theme", "QUIET_UNKNOWN_THEME", "-c", "echo QUIET_RESULT"],
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.Equal(0, result.ExitCode);
+ Assert.Equal("QUIET_RESULT", result.StdOut.Trim());
+ Assert.Contains("QUIET_UNKNOWN_THEME", result.StdErr);
+ Assert.DoesNotContain("Cosmos Shell version:", result.StdOut);
+ Assert.DoesNotContain("\u001b", result.RawStdOut + result.RawStdErr, StringComparison.Ordinal);
+ }
+
+ [Theory]
+ [InlineData("save", "saved")]
+ [InlineData("load", "loaded")]
+ [InlineData("validate", "valid")]
+ public async Task Quiet_SuppressesCommandInformationButPreservesStructuredResult(string action, string resultProperty)
+ {
+ var file = Path.Join(Path.GetTempPath(), $"cosmosshell-output-{Guid.NewGuid():N}.toml");
+ try
+ {
+ if (action != "save")
+ {
+ await File.WriteAllTextAsync(file, "name = \"OUTPUT_TEST\"\n[colors]\nliteral = \"purple\"\n", TestContext.Current.CancellationToken);
+ }
+
+ var command = action == "save"
+ ? $"theme save OUTPUT_TEST '{file}' --force"
+ : $"theme {action} '{file}'";
+ var result = await RunShellAsync(
+ stdinScript: null,
+ extraArgs: ["--quiet", "-c", command],
+ cancellationToken: TestContext.Current.CancellationToken);
+ Assert.Equal(0, result.ExitCode);
+ using var json = JsonDocument.Parse(result.StdOut);
+ Assert.True(json.RootElement.GetProperty(resultProperty).GetBoolean());
+ Assert.Empty(result.StdErr);
+ }
+ finally
+ {
+ File.Delete(file);
+ }
+ }
+
[Theory]
[InlineData(false, "; ")]
[InlineData(true, "; ")]
@@ -578,6 +690,171 @@ public async Task ImportDryRun_InvalidId_ReturnsUsageExitCodeAndOfflineResult(st
}
}
+ [Theory]
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public async Task ThemeValidation_Success_PreservesWarningsOnStderr(bool quiet, bool directory)
+ {
+ var path = Path.Join(Path.GetTempPath(), $"cosmosshell-theme-warnings-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(path);
+ try
+ {
+ var file = Path.Join(path, "warning.toml");
+ await File.WriteAllTextAsync(
+ file,
+ "name = \"WARNING_TEST\"\n[colors]\nunknown_warning_slot = \"red\"\n",
+ TestContext.Current.CancellationToken);
+ var target = directory ? path : file;
+ var command = $"theme validate '{target}'";
+ var result = await RunShellAsync(
+ stdinScript: null,
+ extraArgs: quiet ? ["--quiet", "-c", command] : ["-c", command],
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.Equal(0, result.ExitCode);
+ using var json = JsonDocument.Parse(result.StdOut);
+ var validation = directory ? json.RootElement.GetProperty("values")[0] : json.RootElement;
+ Assert.True(validation.GetProperty("valid").GetBoolean());
+ Assert.NotEqual(0, validation.GetProperty("warnings").GetArrayLength());
+ Assert.Contains("unknown_warning_slot", result.StdErr);
+ Assert.DoesNotContain("\u001b", result.RawStdOut + result.RawStdErr, StringComparison.Ordinal);
+ }
+ finally
+ {
+ File.Delete(Path.Join(path, "warning.toml"));
+ Directory.Delete(path);
+ }
+ }
+
+ [Theory]
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public async Task ThemeValidation_FailedDirectoryScan_ReportsOnlyStructuredError(bool quiet, bool strict)
+ {
+ var path = Path.Join(Path.GetTempPath(), $"cosmosshell-theme-errors-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(path);
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Join(path, "a-warning.toml"),
+ "name = \"WARNING_TEST\"\n[colors]\nunknown_warning_slot = \"red\"\n",
+ TestContext.Current.CancellationToken);
+ if (!strict)
+ {
+ await File.WriteAllTextAsync(
+ Path.Join(path, "z-broken.toml"),
+ "name = \"BROKEN_TEST\"\n[colors]\nliteral = \"not_a_color\"\n",
+ TestContext.Current.CancellationToken);
+ }
+
+ var command = $"theme validate '{path}'" + (strict ? " --strict" : string.Empty);
+ var result = await RunShellAsync(
+ stdinScript: null,
+ extraArgs: quiet ? ["--quiet", "-c", command] : ["-c", command],
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.Equal(1, result.ExitCode);
+ Assert.Empty(result.StdOut);
+ var line = Assert.Single(result.StdErr.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries));
+ using var error = JsonDocument.Parse(line);
+ Assert.Equal("error", error.RootElement.GetProperty("status").GetString());
+ var message = error.RootElement.GetProperty("error").GetString();
+ Assert.NotNull(message);
+ if (strict)
+ {
+ Assert.Contains(Path.Join(path, "a-warning.toml"), message);
+ Assert.Contains("unknown_warning_slot", message);
+ }
+ else
+ {
+ Assert.Contains(Path.Join(path, "z-broken.toml"), message);
+ Assert.Contains("not_a_color", message);
+ Assert.DoesNotContain("unknown_warning_slot", message);
+ }
+
+ Assert.DoesNotContain("\u001b", result.RawStdOut + result.RawStdErr, StringComparison.Ordinal);
+ }
+ finally
+ {
+ File.Delete(Path.Join(path, "a-warning.toml"));
+ File.Delete(Path.Join(path, "z-broken.toml"));
+ Directory.Delete(path);
+ }
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ThemeValidation_StrictFileFailure_IncludesDiagnosticsInSingleStructuredError(bool quiet)
+ {
+ var file = Path.Join(Path.GetTempPath(), $"cosmosshell-theme-strict-{Guid.NewGuid():N}.toml");
+ try
+ {
+ await File.WriteAllTextAsync(
+ file,
+ "name = \"STRICT_WARNING_TEST\"\n[colors]\nunknown_warning_slot = \"red\"\n[styles]\nunknown_style_slot = \"bold\"\n",
+ TestContext.Current.CancellationToken);
+ var command = $"theme validate '{file}' --strict";
+ var result = await RunShellAsync(
+ stdinScript: null,
+ extraArgs: quiet ? ["--quiet", "-c", command] : ["--output", "json", "-c", command],
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.Equal(1, result.ExitCode);
+ Assert.Empty(result.StdOut);
+ var line = Assert.Single(result.StdErr.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries));
+ using var error = JsonDocument.Parse(line);
+ Assert.Equal("error", error.RootElement.GetProperty("status").GetString());
+ var message = error.RootElement.GetProperty("error").GetString();
+ Assert.NotNull(message);
+ Assert.Contains(file, message);
+ Assert.Contains("unknown_warning_slot", message);
+ Assert.Contains("unknown_style_slot", message);
+ Assert.DoesNotContain("\u001b", result.RawStdOut + result.RawStdErr, StringComparison.Ordinal);
+ }
+ finally
+ {
+ File.Delete(file);
+ }
+ }
+
+ [Theory]
+ [InlineData("--help", "USAGE:")]
+ [InlineData("--version", "Cosmos")]
+ public async Task Quiet_ExplicitStartupResult_RemainsVisible(string option, string expected)
+ {
+ var result = await RunShellAsync(
+ stdinScript: null,
+ extraArgs: ["--quiet", option],
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.Equal(0, result.ExitCode);
+ Assert.Contains(expected, result.StdOut);
+ Assert.Empty(result.StdErr);
+ }
+
+ [Theory]
+ [InlineData("help theme")]
+ [InlineData("theme list")]
+ [InlineData("version")]
+ public async Task MachineMode_HumanViews_ReturnOnlyStructuredResult(string command)
+ {
+ var result = await RunShellAsync(
+ stdinScript: null,
+ extraArgs: ["--output", "json", "-c", command],
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.Equal(0, result.ExitCode);
+ using var json = JsonDocument.Parse(result.StdOut);
+ Assert.NotEqual(JsonValueKind.Undefined, json.RootElement.ValueKind);
+ Assert.Empty(result.StdErr);
+ Assert.DoesNotContain("\u001b", result.RawStdOut, StringComparison.Ordinal);
+ }
+
[Theory]
[InlineData("value", "identity", "1 argument, got 0")]
[InlineData("value", "identity 1 2", "1 argument, got 2")]
@@ -1174,6 +1451,7 @@ private static async Task RunShellAsync(
process.ExitCode,
Strip(stdOut.ToString()),
Strip(rawStdErr),
+ stdOut.ToString(),
rawStdErr);
}
finally
@@ -1210,5 +1488,5 @@ private static string Strip(string text)
return AnsiEscape.Replace(text, string.Empty);
}
- private sealed record ShellProcessResult(int ExitCode, string StdOut, string StdErr, string RawStdErr);
+ private sealed record ShellProcessResult(int ExitCode, string StdOut, string StdErr, string RawStdOut, string RawStdErr);
}
diff --git a/CosmosDBShell.Tests/Parser/CommandStatementTests.cs b/CosmosDBShell.Tests/Parser/CommandStatementTests.cs
index 68e31a9d..ef21a867 100644
--- a/CosmosDBShell.Tests/Parser/CommandStatementTests.cs
+++ b/CosmosDBShell.Tests/Parser/CommandStatementTests.cs
@@ -127,6 +127,38 @@ public override Task ExecuteAsync(ShellInterpreter shell, CommandS
}
}
+ [CosmosCommand("output-warning-test")]
+ internal sealed class OutputWarningTestCommand : CosmosCommand
+ {
+ [CosmosOption("fail")]
+ public bool? Fail { get; init; }
+
+ public override Task ExecuteAsync(ShellInterpreter shell, CommandState commandState, string commandText, CancellationToken token)
+ {
+ shell.Output.WriteLine(ShellMessageKind.Warning, "first warning");
+ shell.Output.MarkupLine(ShellMessageKind.RequiredInstruction, "second [bold]instruction[/]");
+ if (this.Fail == true)
+ {
+ throw new CommandException("output-warning-test", "output failure");
+ }
+
+ commandState.Result = new ShellJson(JsonSerializer.SerializeToElement(new { value = "ok" }));
+ return Task.FromResult(commandState);
+ }
+ }
+
+ [CosmosCommand("interactive-diagnostic-test")]
+ internal sealed class InteractiveDiagnosticTestCommand : CosmosCommand
+ {
+ public override Task ExecuteAsync(ShellInterpreter shell, CommandState commandState, string commandText, CancellationToken token)
+ {
+ shell.Output.WriteLine(ShellMessageKind.Warning, "first warning");
+ shell.Output.MarkupLine(ShellMessageKind.Error, "second [bold]error[/]");
+ shell.Output.WriteLine(ShellMessageKind.RequiredInstruction, "terminal prompt");
+ return Task.FromResult(commandState);
+ }
+ }
+
[CosmosCommand("warning-false-test")]
internal sealed class WarningFalseTestCommand : CosmosCommand
{
@@ -622,6 +654,107 @@ await File.WriteAllTextAsync(
}
}
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task MachineMode_ShellOutputStandardErrorHonorsErrorRedirect(bool append)
+ {
+ using var shell = ShellInterpreter.CreateInstance();
+ Assert.True(CommandFactory.TryCreateFactory(typeof(OutputWarningTestCommand), out var factory));
+ shell.App.Commands["output-warning-test"] = factory;
+ shell.Options = new Program.CosmosShellOptions { Output = "json" };
+ var script = Path.GetTempFileName();
+ var stderr = Path.GetTempFileName();
+ try
+ {
+ await File.WriteAllTextAsync(stderr, "existing" + Environment.NewLine, TestContext.Current.CancellationToken);
+ await File.WriteAllTextAsync(
+ script,
+ $"output-warning-test {(append ? "2>>" : "2>")} {ShellLiteral.Quote(stderr.Replace('\\', '/'))}",
+ TestContext.Current.CancellationToken);
+ var command = new CommandStatement(new Token(TokenType.Identifier, script, 0, script.Length));
+
+ var result = await command.RunScriptAsync(shell, new CommandState(), TestContext.Current.CancellationToken);
+
+ Assert.False(result.IsError, (result as ErrorCommandState)?.Exception.ToString());
+ var expected = $"first warning{Environment.NewLine}second instruction{Environment.NewLine}";
+ Assert.Equal(
+ (append ? "existing" + Environment.NewLine : string.Empty) + expected,
+ await File.ReadAllTextAsync(stderr, TestContext.Current.CancellationToken));
+ }
+ finally
+ {
+ File.Delete(script);
+ File.Delete(stderr);
+ }
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task Interactive_ShellOutputDiagnosticsHonorErrorRedirect(bool append)
+ {
+ using var shell = ShellInterpreter.CreateInstance();
+ Assert.True(CommandFactory.TryCreateFactory(typeof(InteractiveDiagnosticTestCommand), out var factory));
+ shell.App.Commands["interactive-diagnostic-test"] = factory;
+ shell.Options = new Program.CosmosShellOptions();
+ Assert.False(shell.IsMachineMode);
+ var script = Path.GetTempFileName();
+ var stderr = Path.GetTempFileName();
+ try
+ {
+ await File.WriteAllTextAsync(stderr, "existing" + Environment.NewLine, TestContext.Current.CancellationToken);
+ await File.WriteAllTextAsync(
+ script,
+ $"interactive-diagnostic-test {(append ? "2>>" : "2>")} {ShellLiteral.Quote(stderr.Replace('\\', '/'))}",
+ TestContext.Current.CancellationToken);
+ var command = new CommandStatement(new Token(TokenType.Identifier, script, 0, script.Length));
+
+ var result = await command.RunScriptAsync(shell, new CommandState(), TestContext.Current.CancellationToken);
+
+ Assert.False(result.IsError, (result as ErrorCommandState)?.Exception.ToString());
+ var expected = $"first warning{Environment.NewLine}second error{Environment.NewLine}";
+ Assert.Equal(
+ (append ? "existing" + Environment.NewLine : string.Empty) + expected,
+ await File.ReadAllTextAsync(stderr, TestContext.Current.CancellationToken));
+ }
+ finally
+ {
+ File.Delete(script);
+ File.Delete(stderr);
+ }
+ }
+
+ [Fact]
+ public async Task MachineMode_ErrorAfterShellOutputWarningAppendsToErrorRedirect()
+ {
+ using var shell = ShellInterpreter.CreateInstance();
+ Assert.True(CommandFactory.TryCreateFactory(typeof(OutputWarningTestCommand), out var factory));
+ shell.App.Commands["output-warning-test"] = factory;
+ shell.Options = new Program.CosmosShellOptions { Output = "json" };
+ var stderr = Path.GetTempFileName();
+ shell.ErrOutRedirect = stderr;
+ try
+ {
+ var result = await shell.ExecuteCommandAsync("output-warning-test --fail", TestContext.Current.CancellationToken);
+
+ Assert.True(result.IsError);
+ var lines = (await File.ReadAllTextAsync(stderr, TestContext.Current.CancellationToken))
+ .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
+ Assert.Equal(3, lines.Length);
+ Assert.Equal("first warning", lines[0]);
+ Assert.Equal("second instruction", lines[1]);
+ using var document = JsonDocument.Parse(lines[2]);
+ Assert.Equal("error", document.RootElement.GetProperty("status").GetString());
+ Assert.Contains("output failure", document.RootElement.GetProperty("error").GetString());
+ }
+ finally
+ {
+ shell.ErrOutRedirect = null;
+ File.Delete(stderr);
+ }
+ }
+
[Fact]
public async Task Script_PreservesWarningTrackingForInheritedRedirect()
{
diff --git a/CosmosDBShell.Tests/Parser/StatementPositionalErrorTests.cs b/CosmosDBShell.Tests/Parser/StatementPositionalErrorTests.cs
index b5dcf161..24d93f89 100644
--- a/CosmosDBShell.Tests/Parser/StatementPositionalErrorTests.cs
+++ b/CosmosDBShell.Tests/Parser/StatementPositionalErrorTests.cs
@@ -7,6 +7,7 @@ namespace CosmosShell.Tests.Parser;
using System.Threading;
using System.Threading.Tasks;
+using Azure.Data.Cosmos.Shell.Commands;
using Azure.Data.Cosmos.Shell.Core;
using Azure.Data.Cosmos.Shell.Parser;
@@ -35,18 +36,20 @@ public async Task ThrownExpressionError_UsesCommandLocation(string source)
}
[Theory]
- [InlineData("$value = (help totallyunknowncmd999)")]
- [InlineData("if (help totallyunknowncmd999) {}")]
- [InlineData("for $item in (help totallyunknowncmd999) {}")]
+ [InlineData("$value = (returned-error-test)")]
+ [InlineData("if (returned-error-test) {}")]
+ [InlineData("for $item in (returned-error-test) {}")]
public async Task ReturnedExpressionError_UsesCommandLocation(string source)
{
+ Assert.True(CommandFactory.TryCreateFactory(typeof(ReturnedErrorTestCommand), out var factory));
+ Shell.App.Commands["returned-error-test"] = factory;
Shell.CurrentScriptFileName = "expression.csh";
Shell.CurrentScriptContent = source;
var statement = new StatementParser(source).ParseStatement()!;
var exception = await Assert.ThrowsAsync(() => statement.RunAsync(Shell, new(), TestContext.Current.CancellationToken));
var frame = PositionalException.GetSourceTrace(exception)[0];
Assert.Equal("expression.csh", frame.FileName);
- Assert.Equal(source.IndexOf("help", StringComparison.Ordinal) + 1, frame.Column);
+ Assert.Equal(source.IndexOf("returned-error-test", StringComparison.Ordinal) + 1, frame.Column);
var failure = Assert.IsType(frame.InnerException);
Assert.IsType(failure.State);
}
@@ -66,6 +69,13 @@ public async Task InteractiveFunction_RetainsScriptModeWithoutBorrowingCallerOff
Assert.Equal("\nprobe", Shell.CurrentScriptContent);
}
+ [CosmosCommand("returned-error-test")]
+ internal sealed class ReturnedErrorTestCommand : CosmosCommand
+ {
+ public override Task ExecuteAsync(ShellInterpreter shell, CommandState commandState, string commandText, CancellationToken token)
+ => Task.FromResult(new ErrorCommandState(new CommandException("returned-error-test", "failed")));
+ }
+
private sealed class ScriptContextProbe : Statement
{
public override int Start => 100;
diff --git a/CosmosDBShell.Tests/Shell/NonAnsiFallbackTests.cs b/CosmosDBShell.Tests/Shell/NonAnsiFallbackTests.cs
index c5a7d041..ec751f1a 100644
--- a/CosmosDBShell.Tests/Shell/NonAnsiFallbackTests.cs
+++ b/CosmosDBShell.Tests/Shell/NonAnsiFallbackTests.cs
@@ -10,6 +10,55 @@ namespace CosmosShell.Tests.Shell;
[Collection(ThemeStateTestCollection.Name)]
public class NonAnsiFallbackTests
{
+ [Theory]
+ [InlineData("echo QUIET_HISTORY", "echo QUIET_HISTORY")]
+ [InlineData("connect 'AccountKey=example-key==;'", "connect 'AccountKey=[REDACTED];'")]
+ public void PrintCommand_Quiet_RecordsInExistingEditorHistoryWithoutEcho(string command, string expectedHistory)
+ {
+ var configPath = CreateConfigPath();
+ var savedConsole = AnsiConsole.Console;
+ var savedOut = Console.Out;
+ var savedError = Console.Error;
+ using var output = new StringWriter();
+ using var error = new StringWriter();
+ try
+ {
+ var ansiConsole = AnsiConsole.Create(new AnsiConsoleSettings
+ {
+ Ansi = AnsiSupport.Yes,
+ ColorSystem = ColorSystemSupport.NoColors,
+ Out = new AnsiConsoleOutput(output),
+ Enrichment = new ProfileEnrichment { UseDefaultEnrichers = false },
+ });
+ AnsiConsole.Console = ansiConsole;
+ Console.SetOut(output);
+ Console.SetError(error);
+ using var shell = new ShellInterpreter(configPath)
+ {
+ Options = new Program.CosmosShellOptions { Quiet = true },
+ LineEditorTerminal = ansiConsole,
+ };
+ var editor = shell.Editor;
+ Assert.NotNull(editor);
+ var historyCount = editor.History.Count;
+
+ shell.PrintCommand(command);
+
+ Assert.Equal(historyCount + 1, editor.History.Count);
+ Assert.Contains(expectedHistory, shell.History);
+ Assert.Contains(expectedHistory, File.ReadAllText(shell.HistoryFile));
+ Assert.Empty(output.ToString());
+ Assert.Empty(error.ToString());
+ }
+ finally
+ {
+ Console.SetOut(savedOut);
+ Console.SetError(savedError);
+ AnsiConsole.Console = savedConsole;
+ DeleteConfigPath(configPath);
+ }
+ }
+
[Fact]
public async Task RunAsync_WhenLineEditorCannotBeCreated_UsesPromptFallbackAndExitsOnEof()
{
diff --git a/CosmosDBShell.Tests/UtilTest/ShellOutputTests.cs b/CosmosDBShell.Tests/UtilTest/ShellOutputTests.cs
new file mode 100644
index 00000000..4978a66b
--- /dev/null
+++ b/CosmosDBShell.Tests/UtilTest/ShellOutputTests.cs
@@ -0,0 +1,152 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------
+
+namespace CosmosShell.Tests.UtilTest;
+
+using Azure.Data.Cosmos.Shell.Core;
+using Spectre.Console;
+
+public class ShellOutputTests
+{
+ [Theory]
+ [InlineData(false, false)]
+ [InlineData(true, false)]
+ [InlineData(false, true)]
+ [InlineData(true, true)]
+ public void Presentation_UsesCategoryPolicyForTextMarkupAndRenderables(bool quiet, bool machineMode)
+ {
+ foreach (var kind in Enum.GetValues())
+ {
+ using var stdout = new StringWriter();
+ using var stderr = new StringWriter();
+ var output = CreateOutput(quiet, machineMode, stdout, stderr);
+ var marker = kind.ToString();
+
+ output.Write(kind, marker + "_text");
+ output.WriteLine(kind, marker + "_line");
+ output.Markup(kind, $"[red]{marker}_markup[/]");
+ output.MarkupLine(kind, $"[red]{marker}_markupLine[/]");
+ output.Render(kind, new Text(marker + "_renderable"));
+ output.RenderLine(kind, marker + "_renderLine");
+ output.WriteException(kind, new InvalidOperationException(marker + "_exception"));
+
+ var suppressed = ((quiet || machineMode) && kind is ShellMessageKind.Information or ShellMessageKind.Progress)
+ || (machineMode && kind == ShellMessageKind.Result);
+ if (suppressed)
+ {
+ Assert.Empty(stdout.ToString());
+ Assert.Empty(stderr.ToString());
+ continue;
+ }
+
+ var useStderr = machineMode && kind is ShellMessageKind.Warning or ShellMessageKind.Error or ShellMessageKind.RequiredInstruction;
+ var visible = (useStderr ? stderr : stdout).ToString();
+ Assert.Empty((useStderr ? stdout : stderr).ToString());
+ foreach (var suffix in new[] { "_text", "_line", "_markup", "_markupLine", "_renderable", "_renderLine", "_exception" })
+ {
+ Assert.Contains(marker + suffix, visible);
+ }
+
+ if (machineMode)
+ {
+ Assert.DoesNotContain("\u001b", visible, StringComparison.Ordinal);
+ }
+ }
+ }
+
+ [Fact]
+ public void StructuredMode_RoutesWarningsErrorsAndAuthenticationToStderr()
+ {
+ using var stdout = new StringWriter();
+ using var stderr = new StringWriter();
+ var output = CreateOutput(false, true, stdout, stderr);
+ output.MarkupLine(ShellMessageKind.Warning, "[yellow]warning[/]");
+ output.WriteLine(ShellMessageKind.Error, "error");
+ output.WriteLine(ShellMessageKind.RequiredInstruction, "login");
+ output.WriteLine(ShellMessageKind.Result, "eager table");
+ output.Render(ShellMessageKind.Result, new Text("eager renderable"));
+ output.WriteResult("{\"result\":true}");
+ Assert.Equal("{\"result\":true}" + Environment.NewLine, stdout.ToString());
+ Assert.DoesNotContain("eager", stderr.ToString(), StringComparison.Ordinal);
+ Assert.Contains("warning", stderr.ToString());
+ Assert.Contains("error", stderr.ToString());
+ Assert.Contains("login", stderr.ToString());
+ Assert.DoesNotContain("\u001b", stderr.ToString(), StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Modes_AreEvaluatedAtWriteTime()
+ {
+ using var stdout = new StringWriter();
+ using var stderr = new StringWriter();
+ var quiet = false;
+ var machineMode = false;
+ var output = new ShellOutput(() => quiet, () => machineMode, () => stdout, () => stderr);
+ output.WriteLine(ShellMessageKind.Information, "before");
+ quiet = true;
+ machineMode = true;
+ output.WriteLine(ShellMessageKind.Information, "hidden");
+ output.WriteLine(ShellMessageKind.Result, "command-level result hidden in machine mode");
+ output.WriteResult("result");
+ output.WriteLine(ShellMessageKind.RequiredInstruction, "device code");
+ Assert.Equal("before" + Environment.NewLine + "result" + Environment.NewLine, stdout.ToString());
+ Assert.Equal("device code" + Environment.NewLine, stderr.ToString());
+ }
+
+ [Theory]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ public void ClearScreen_OnlyTouchesInteractiveTerminal(bool machineMode, bool cleared)
+ {
+ using var stdout = new StringWriter();
+ using var stderr = new StringWriter();
+ var terminal = AnsiConsole.Create(new AnsiConsoleSettings
+ {
+ Ansi = AnsiSupport.Yes,
+ ColorSystem = ColorSystemSupport.NoColors,
+ Out = new AnsiConsoleOutput(stdout),
+ });
+ var output = new ShellOutput(() => false, () => machineMode, () => stdout, () => stderr, () => terminal);
+ output.ClearScreen();
+ Assert.Equal(cleared, stdout.ToString().Length > 0);
+ Assert.Empty(stderr.ToString());
+ }
+
+ [Fact]
+ public void Text_PreservesBracesAndExplicitFormatting()
+ {
+ using var stdout = new StringWriter();
+ using var stderr = new StringWriter();
+ var output = CreateOutput(false, false, stdout, stderr);
+ output.WriteLine(ShellMessageKind.Result, "{\"value\":1}");
+ output.Write(ShellMessageKind.Result, "{0}={1}", "value", 2);
+ Assert.Equal("{\"value\":1}" + Environment.NewLine + "value=2", stdout.ToString());
+ }
+
+ [Fact]
+ public void Markup_PreservesLiteralBracesWithoutFormattingArguments()
+ {
+ using var stdout = new StringWriter();
+ using var stderr = new StringWriter();
+ var output = CreateOutput(false, false, stdout, stderr);
+ output.Markup(ShellMessageKind.Result, "[red]{\"value\":1}[/]");
+ output.MarkupLine(ShellMessageKind.Result, "[red]{\"value\":2}[/]");
+ output.MarkupLine(ShellMessageKind.Result, "[red]{0}[/]", "formatted");
+ Assert.Equal("{\"value\":1}{\"value\":2}" + Environment.NewLine + "formatted" + Environment.NewLine, stdout.ToString());
+ }
+
+ private static ShellOutput CreateOutput(bool quiet, bool machineMode, StringWriter stdout, StringWriter stderr)
+ => new(
+ () => quiet,
+ () => machineMode,
+ () => stdout,
+ () => stderr,
+ () => AnsiConsole.Create(new AnsiConsoleSettings
+ {
+ Ansi = AnsiSupport.No,
+ ColorSystem = ColorSystemSupport.NoColors,
+ Interactive = InteractionSupport.No,
+ Out = new AnsiConsoleOutput(stdout),
+ }));
+}
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BatchCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BatchCommand.cs
index 51c52f77..538bc508 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BatchCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BatchCommand.cs
@@ -95,7 +95,7 @@ private static CommandState Cancel(ShellInterpreter shell)
"command-batch-cancelled",
"count",
count);
- return new CommandState { RenderUser = () => ShellInterpreter.WriteLine(message) };
+ return new CommandState { RenderUser = () => ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, message) };
}
private static CommandState Status(ShellInterpreter shell)
@@ -106,7 +106,7 @@ private static CommandState Status(ShellInterpreter shell)
if (batch is null)
{
root = new JsonObject { ["active"] = false };
- renderUser = () => ShellInterpreter.WriteLine(MessageService.GetString("command-batch-status-inactive"));
+ renderUser = () => ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, MessageService.GetString("command-batch-status-inactive"));
}
else
{
@@ -155,7 +155,7 @@ void AddDetail(string labelKey, string value) =>
AddDetail("command-batch-status-target", $"{batch.DatabaseName}/{batch.ContainerName}");
AddDetail("command-batch-status-partition-key", batch.PartitionKeyArgument);
AddDetail("command-batch-status-operation-count", batch.Operations.Count.ToString(CultureInfo.InvariantCulture));
- AnsiConsole.Write(details);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, details);
if (batch.Operations.Count == 0)
{
@@ -175,7 +175,7 @@ void AddDetail(string labelKey, string value) =>
Theme.FormatTableValue(Markup.Escape(operation.Id ?? string.Empty)));
}
- AnsiConsole.Write(operations);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, operations);
}
private static CommandState Show(ShellInterpreter shell)
@@ -219,7 +219,7 @@ private async Task RunAsync(ShellInterpreter shell, CommandState c
var partitionKey = ParsePartitionKey(this.PartitionKeyArgument);
try
{
- return await BatchExecutor.ExecuteAsync("batch", container, partitionKey, specs, token);
+ return await BatchExecutor.ExecuteAsync(shell, "batch", container, partitionKey, specs, token);
}
catch (CosmosException ce)
{
@@ -269,7 +269,7 @@ private async Task BeginAsync(ShellInterpreter shell, Cancellation
databaseName!,
"container",
containerName!);
- return new CommandState { RenderUser = () => ShellInterpreter.WriteLine(message) };
+ return new CommandState { RenderUser = () => ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, message) };
}
private CommandState Add(ShellInterpreter shell, CommandState commandState)
@@ -297,7 +297,7 @@ private CommandState Add(ShellInterpreter shell, CommandState commandState)
specs.Count,
"total",
batch.Operations.Count);
- return new CommandState { RenderUser = () => ShellInterpreter.WriteLine(message) };
+ return new CommandState { RenderUser = () => ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, message) };
}
private async Task ExecuteBatchAsync(ShellInterpreter shell, CancellationToken token)
@@ -319,7 +319,7 @@ private async Task ExecuteBatchAsync(ShellInterpreter shell, Cance
try
{
- var result = await BatchExecutor.ExecuteAsync("batch", container, batch.PartitionKey, batch.Operations, token);
+ var result = await BatchExecutor.ExecuteAsync(shell, "batch", container, batch.PartitionKey, batch.Operations, token);
shell.CurrentBatch = null;
return result;
}
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BatchExecutor.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BatchExecutor.cs
index 0ace3efb..2713c656 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BatchExecutor.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BatchExecutor.cs
@@ -17,6 +17,7 @@ internal static class BatchExecutor
internal const int MaxOperations = 100;
public static async Task ExecuteAsync(
+ ShellInterpreter shell,
string commandName,
Container container,
PartitionKey partitionKey,
@@ -74,7 +75,7 @@ public static async Task ExecuteAsync(
operations.Count,
"charge",
response.RequestCharge.ToString("F2", CultureInfo.InvariantCulture));
- return CreateResultState(summary, successMessage, response.RequestCharge);
+ return CreateResultState(shell, summary, successMessage, response.RequestCharge);
}
var errorMessage = MessageService.GetArgsString(
@@ -92,18 +93,24 @@ public static async Task ExecuteAsync(
{
RequestCharge = response.RequestCharge,
};
- errorState.RenderUser = () => ShellInterpreter.WriteLine(errorMessage);
+ errorState.RenderUser = () =>
+ {
+ if (shell.ErrOutRedirect == null)
+ {
+ shell.Output.WriteLine(ShellMessageKind.Error, errorMessage);
+ }
+ };
return errorState;
}
- internal static CommandState CreateResultState(JsonElement summary, string message, double requestCharge)
+ internal static CommandState CreateResultState(ShellInterpreter shell, JsonElement summary, string message, double requestCharge)
{
var state = new CommandState
{
Result = new ShellJson(summary),
RequestCharge = requestCharge,
};
- state.RenderUser = () => ShellInterpreter.WriteLine(message);
+ state.RenderUser = () => shell.Output.WriteLine(ShellMessageKind.Result, message);
return state;
}
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BucketCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BucketCommand.cs
index 7388e7ef..dfb42e1b 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BucketCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BucketCommand.cs
@@ -71,15 +71,7 @@ private enum BucketActionKind
public bool? Yes { get; init; }
public static bool CheckBucket(int bucket)
- {
- var isValid = bucket >= 0 && bucket <= 5;
- if (!isValid)
- {
- AnsiConsole.MarkupLine(MessageService.GetString("error-invalid_bucket_value", new Dictionary { { "bucket", bucket } }));
- }
-
- return isValid;
- }
+ => bucket >= 0 && bucket <= 5;
public override Task ExecuteAsync(ShellInterpreter shell, CommandState commandState, string commandText, CancellationToken token) =>
shell.State.AcceptAsync(this, shell, token);
@@ -108,7 +100,7 @@ async Task IStateVisitor.VisitData
return await this.ExecuteServerAsync(state, shell, database, NormalizeOption(this.Container), token);
}
- return this.RunClientCommand(state.Client);
+ return this.RunClientCommand(shell, state.Client);
}
async Task IStateVisitor.VisitContainerStateAsync(ContainerState state, ShellInterpreter shell, CancellationToken token)
@@ -120,7 +112,7 @@ async Task IStateVisitor.VisitCont
return await this.ExecuteServerAsync(state, shell, database, containerName, token);
}
- return this.RunClientCommand(state.Client);
+ return this.RunClientCommand(shell, state.Client);
}
// Treat an explicitly empty option (e.g. --container "") as "not provided" so it
@@ -138,11 +130,11 @@ private static bool ConfirmWrite(ShellInterpreter shell, bool yes, string summar
return true;
}
- AnsiConsole.MarkupLine(MessageService.GetArgsString(summaryKey, args));
+ shell.Output.MarkupLine(ShellMessageKind.RequiredInstruction, MessageService.GetArgsString(summaryKey, args));
return ShellInterpreter.Confirm("command-bucket-confirm", shell);
}
- private static CommandState BuildShowResult(ConnectedState state, ThroughputBucketsView view)
+ private static CommandState BuildShowResult(ShellInterpreter shell, ConnectedState state, ThroughputBucketsView view)
{
var bucketsArray = new JsonArray();
foreach (var bucket in view.Buckets)
@@ -177,7 +169,7 @@ private static CommandState BuildShowResult(ConnectedState state, ThroughputBuck
{
if (view.Buckets.Count == 0)
{
- AnsiConsole.MarkupLine(MessageService.GetArgsString("command-bucket-no_limits", "resource", Markup.Escape(view.ResourceName)));
+ shell.Output.MarkupLine(ShellMessageKind.Result, MessageService.GetArgsString("command-bucket-no_limits", "resource", Markup.Escape(view.ResourceName)));
}
else
{
@@ -191,23 +183,23 @@ private static CommandState BuildShowResult(ConnectedState state, ThroughputBuck
Theme.FormatTableValue(bucket.MaxThroughputPercentage.ToString(CultureInfo.InvariantCulture) + "%"));
}
- AnsiConsole.Write(table);
+ shell.Output.Render(ShellMessageKind.Result, table);
}
- WriteClientSelection(clientBucket);
+ WriteClientSelection(shell, clientBucket);
},
};
}
- private static void WriteClientSelection(int? clientBucket)
+ private static void WriteClientSelection(ShellInterpreter shell, int? clientBucket)
{
if (clientBucket.HasValue)
{
- AnsiConsole.MarkupLine(MessageService.GetString("command-bucket-current", new Dictionary { { "bucket", Theme.FormatTableValue(clientBucket.Value.ToString(CultureInfo.InvariantCulture)) } }));
+ shell.Output.MarkupLine(ShellMessageKind.Information, MessageService.GetString("command-bucket-current", new Dictionary { { "bucket", Theme.FormatTableValue(clientBucket.Value.ToString(CultureInfo.InvariantCulture)) } }));
}
else
{
- AnsiConsole.MarkupLine(MessageService.GetString("command-bucket-no_bucket"));
+ shell.Output.MarkupLine(ShellMessageKind.Information, MessageService.GetString("command-bucket-no_bucket"));
}
}
@@ -242,7 +234,7 @@ private BucketActionKind Classify()
private bool IsServerAction() =>
this.Classify() is BucketActionKind.ServerShow or BucketActionKind.ServerSet or BucketActionKind.ServerClear;
- private CommandState RunClientCommand(CosmosClient client)
+ private CommandState RunClientCommand(ShellInterpreter shell, CosmosClient client)
{
if (this.Id.HasValue || this.Percent.HasValue)
{
@@ -254,26 +246,35 @@ private CommandState RunClientCommand(CosmosClient client)
int bucket = int.Parse(this.Action!.Trim(), CultureInfo.InvariantCulture);
if (!CheckBucket(bucket))
{
- return new CommandState();
+ throw CommandException.Usage(
+ "bucket",
+ MessageService.GetString("error-invalid_bucket_value", new Dictionary { { "bucket", bucket } }));
}
if (bucket == 0)
{
client.ClientOptions.ThroughputBucket = null;
- AnsiConsole.MarkupLine(MessageService.GetString("command-bucket-reset_bucket"));
+ shell.Output.MarkupLine(ShellMessageKind.Information, MessageService.GetString("command-bucket-reset_bucket"));
}
else
{
client.ClientOptions.ThroughputBucket = bucket;
- AnsiConsole.MarkupLine(MessageService.GetString("command-bucket-switched_bucket", new Dictionary { { "bucket", Theme.FormatTableValue(bucket.ToString(CultureInfo.InvariantCulture)) } }));
+ shell.Output.MarkupLine(ShellMessageKind.Information, MessageService.GetString("command-bucket-switched_bucket", new Dictionary { { "bucket", Theme.FormatTableValue(bucket.ToString(CultureInfo.InvariantCulture)) } }));
}
}
else
{
- WriteClientSelection(client.ClientOptions.ThroughputBucket);
+ WriteClientSelection(shell, client.ClientOptions.ThroughputBucket);
}
- return new CommandState();
+ return new CommandState
+ {
+ Result = new ShellJson(JsonSerializer.SerializeToElement(new JsonObject
+ {
+ ["clientBucket"] = client.ClientOptions.ThroughputBucket,
+ })),
+ RenderUser = () => { },
+ };
}
private async Task ExecuteServerAsync(ConnectedState state, ShellInterpreter shell, string databaseName, string? containerName, CancellationToken token)
@@ -303,7 +304,7 @@ private async Task ExecuteServerAsync(ConnectedState state, ShellI
case BucketActionKind.ServerShow:
default:
var view = await CosmosResourceFacade.GetThroughputBucketsAsync(state, databaseName, containerName, token);
- return BuildShowResult(state, view);
+ return BuildShowResult(shell, state, view);
}
}
catch (ThroughputBucketsNotSupportedException ex)
@@ -330,16 +331,16 @@ private async Task ExecuteSetAsync(ConnectedState state, ShellInte
return new CommandState
{
Result = new ShellJson(cancelledDoc.RootElement.Clone()),
- RenderUser = () => ShellInterpreter.WriteLine(MessageService.GetString("command-bucket-cancelled")),
+ RenderUser = () => shell.Output.WriteLine(ShellMessageKind.Result, MessageService.GetString("command-bucket-cancelled")),
};
}
var view = await CosmosResourceFacade.SetThroughputBucketAsync(state, databaseName, containerName, bucketId, percent, token);
- var setResult = BuildShowResult(state, view);
+ var setResult = BuildShowResult(shell, state, view);
var renderSet = setResult.RenderUser;
setResult.RenderUser = () =>
{
- ShellInterpreter.WriteLine(MessageService.GetString("command-bucket-set_done"));
+ shell.Output.WriteLine(ShellMessageKind.Result, MessageService.GetString("command-bucket-set_done"));
renderSet?.Invoke();
};
return setResult;
@@ -355,16 +356,16 @@ private async Task ExecuteClearAsync(ConnectedState state, ShellIn
return new CommandState
{
Result = new ShellJson(cancelledDoc.RootElement.Clone()),
- RenderUser = () => ShellInterpreter.WriteLine(MessageService.GetString("command-bucket-cancelled")),
+ RenderUser = () => shell.Output.WriteLine(ShellMessageKind.Result, MessageService.GetString("command-bucket-cancelled")),
};
}
var view = await CosmosResourceFacade.ClearThroughputBucketAsync(state, databaseName, containerName, bucketId, token);
- var clearResult = BuildShowResult(state, view);
+ var clearResult = BuildShowResult(shell, state, view);
var renderClear = clearResult.RenderUser;
clearResult.RenderUser = () =>
{
- ShellInterpreter.WriteLine(MessageService.GetString("command-bucket-clear_done"));
+ shell.Output.WriteLine(ShellMessageKind.Result, MessageService.GetString("command-bucket-clear_done"));
renderClear?.Invoke();
};
return clearResult;
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BulkCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BulkCommand.cs
index c24f0051..4de090c8 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BulkCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/BulkCommand.cs
@@ -566,14 +566,14 @@ private static CommandState Status(ShellInterpreter shell)
["uncertain"] = uncertain,
["operations"] = operations,
};
- renderUser = () => RenderStatus(bulk, succeeded, failed, uncertain);
+ renderUser = () => RenderStatus(shell, bulk, succeeded, failed, uncertain);
}
using var document = JsonDocument.Parse(root.ToJsonString());
return new CommandState { Result = new ShellJson(document.RootElement.Clone()), RenderUser = renderUser };
}
- private static void RenderStatus(PendingBulkState bulk, int succeeded, int failed, int uncertain)
+ private static void RenderStatus(ShellInterpreter shell, PendingBulkState bulk, int succeeded, int failed, int uncertain)
{
var details = new Table().HideHeaders();
details.AddColumn(string.Empty);
@@ -593,7 +593,7 @@ void AddDetail(string labelKey, string value) =>
AddDetail("command-bulk-status-succeeded", succeeded.ToString(CultureInfo.InvariantCulture));
AddDetail("command-bulk-status-failed", failed.ToString(CultureInfo.InvariantCulture));
AddDetail("command-bulk-status-uncertain", uncertain.ToString(CultureInfo.InvariantCulture));
- AnsiConsole.Write(details);
+ shell.Output.Render(ShellMessageKind.Result, details);
if (bulk.Operations.Count == 0)
{
return;
@@ -611,10 +611,10 @@ void AddDetail(string labelKey, string value) =>
Theme.FormatTableValue(Markup.Escape(operation.Id)));
}
- AnsiConsole.Write(operations);
+ shell.Output.Render(ShellMessageKind.Result, operations);
if (bulk.Operations.Count > StatusPreviewCount)
{
- ShellInterpreter.WriteLine(MessageService.GetArgsString("command-bulk-status-more", "count", bulk.Operations.Count - StatusPreviewCount));
+ shell.Output.WriteLine(ShellMessageKind.Result, MessageService.GetArgsString("command-bulk-status-more", "count", bulk.Operations.Count - StatusPreviewCount));
}
}
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/CanICommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/CanICommand.cs
index ee69b069..d4a8b761 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/CanICommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/CanICommand.cs
@@ -214,7 +214,7 @@ internal CommandState Build(
private void RenderTable(string action, string? databaseName, string? containerName, string decision, string method, int? statusCode, string? note)
{
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-can-i-title")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(MessageService.GetString("command-can-i-title")));
var table = new Table();
table.AddColumns(string.Empty, string.Empty);
@@ -233,11 +233,11 @@ private void RenderTable(string action, string? databaseName, string? containerN
table.AddRow(MessageService.GetString("command-can-i-status"), Theme.FormatTableValue(code.ToString()));
}
- AnsiConsole.Write(table);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, table);
if (!string.IsNullOrEmpty(note))
{
- AnsiConsole.MarkupLine(Theme.FormatMuted(note));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatMuted(note));
}
}
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/CdCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/CdCommand.cs
index 6cba9f8e..6a9c7018 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/CdCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/CdCommand.cs
@@ -83,7 +83,7 @@ public override async Task ExecuteAsync(ShellInterpreter shell, Co
SetState(shell, new ConnectedState(connectedState.Client, connectedState.ArmContext));
if (!this.Quiet)
{
- ShellInterpreter.WriteLine(MessageService.GetString("command-cd-changed_to_connected_state"));
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Information, MessageService.GetString("command-cd-changed_to_connected_state"));
}
CosmosCompleteCommand.ClearContainers();
@@ -100,7 +100,7 @@ public override async Task ExecuteAsync(ShellInterpreter shell, Co
SetState(shell, new DatabaseState(targetDatabase, connectedState.Client, connectedState.ArmContext));
if (!this.Quiet)
{
- ShellInterpreter.WriteLine(MessageService.GetString("command-cd-changed_to_db", new Dictionary { { "db", targetDatabase } }));
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Information, MessageService.GetString("command-cd-changed_to_db", new Dictionary { { "db", targetDatabase } }));
}
CosmosCompleteCommand.ClearContainers();
@@ -112,7 +112,7 @@ public override async Task ExecuteAsync(ShellInterpreter shell, Co
SetState(shell, new ContainerState(targetContainer, targetDatabase, connectedState.Client, connectedState.ArmContext));
if (!this.Quiet)
{
- ShellInterpreter.WriteLine(MessageService.GetString("command-cd-changed_to_container", new Dictionary { { "container", targetContainer } }));
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Information, MessageService.GetString("command-cd-changed_to_container", new Dictionary { { "container", targetContainer } }));
}
CosmosCompleteCommand.ClearContainers();
@@ -132,7 +132,7 @@ public override async Task ExecuteAsync(ShellInterpreter shell, Co
SetState(shell, new ContainerState(targetContainer, dbName, connectedState.Client, connectedState.ArmContext));
if (!this.Quiet)
{
- ShellInterpreter.WriteLine(MessageService.GetString("command-cd-changed_to_container", new Dictionary { { "container", targetContainer } }));
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Information, MessageService.GetString("command-cd-changed_to_container", new Dictionary { { "container", targetContainer } }));
}
CosmosCompleteCommand.ClearContainers();
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ClsCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ClsCommand.cs
index fd808511..f13bbcbb 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ClsCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ClsCommand.cs
@@ -18,7 +18,7 @@ public override Task ExecuteAsync(ShellInterpreter shell, CommandS
{
try
{
- AnsiConsole.Clear();
+ shell.Output.ClearScreen();
}
catch (IOException)
{
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConflictCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConflictCommand.cs
index 30463d96..e2bdb32c 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConflictCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConflictCommand.cs
@@ -63,7 +63,7 @@ async Task IStateVisitor.VisitConn
string? container = NormalizeOption(this.Container);
if (database is not null && container is not null)
{
- return await this.ExecuteOnContainerAsync(state, database, container, token);
+ return await this.ExecuteOnContainerAsync(state, shell, database, container, token);
}
throw new NotInContainerException("conflict");
@@ -76,7 +76,7 @@ async Task IStateVisitor.VisitData
if (container is not null)
{
- return await this.ExecuteOnContainerAsync(state, databaseName, container, token);
+ return await this.ExecuteOnContainerAsync(state, shell, databaseName, container, token);
}
throw new NotInContainerException("conflict");
@@ -87,7 +87,7 @@ async Task IStateVisitor.VisitCont
string databaseName = NormalizeOption(this.Database) ?? state.DatabaseName;
string containerName = NormalizeOption(this.Container) ?? state.ContainerName;
- return await this.ExecuteOnContainerAsync(state, databaseName, containerName, token);
+ return await this.ExecuteOnContainerAsync(state, shell, databaseName, containerName, token);
}
///
@@ -145,7 +145,7 @@ private static CommandState BuildResult(string containerName, ConflictResolution
};
}
- private async Task ExecuteOnContainerAsync(ConnectedState state, string databaseName, string containerName, CancellationToken token)
+ private async Task ExecuteOnContainerAsync(ConnectedState state, ShellInterpreter shell, string databaseName, string containerName, CancellationToken token)
{
// Validate the subcommand and options that do not depend on the current policy
// before touching the network so bad input fails fast without a round-trip.
@@ -173,7 +173,7 @@ private async Task ExecuteOnContainerAsync(ConnectedState state, s
return BuildResult(containerName, current);
}
- return await this.SetAsync(state, databaseName, containerName, normalizedMode, token);
+ return await this.SetAsync(state, shell, databaseName, containerName, normalizedMode, token);
}
///
@@ -208,7 +208,7 @@ private async Task ExecuteOnContainerAsync(ConnectedState state, s
return normalizedMode;
}
- private async Task SetAsync(ConnectedState state, string databaseName, string containerName, string? normalizedMode, CancellationToken token)
+ private async Task SetAsync(ConnectedState state, ShellInterpreter shell, string databaseName, string containerName, string? normalizedMode, CancellationToken token)
{
var current = await CosmosResourceFacade.GetConflictResolutionPolicyAsync(state, databaseName, containerName, token);
string effectiveMode = normalizedMode ?? (IsCustom(current.Mode) ? "custom" : "lastWriterWins");
@@ -245,7 +245,7 @@ private async Task SetAsync(ConnectedState state, string databaseN
var view = await CosmosResourceFacade.ReplaceConflictResolutionPolicyAsync(state, databaseName, containerName, update, token);
var result = BuildResult(containerName, view);
- result.RenderUser = () => ShellInterpreter.WriteLine(MessageService.GetString("command-conflict-updated"));
+ result.RenderUser = () => shell.Output.WriteLine(ShellMessageKind.Result, MessageService.GetString("command-conflict-updated"));
return result;
}
}
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs
index 85f859da..4e350801 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs
@@ -76,7 +76,7 @@ public async override Task ExecuteAsync(ShellInterpreter shell, Co
var previousEndpoint = cs.Client.Endpoint.Host;
if (!isMachineMode)
{
- AnsiConsole.MarkupLine(MessageService.GetArgsString("command-connect-switching", "endpoint", previousEndpoint));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Information, MessageService.GetArgsString("command-connect-switching", "endpoint", previousEndpoint));
}
}
@@ -153,8 +153,8 @@ internal static bool TryGetPrincipalIdFromRbacException(Exception e, out string?
internal static void AskForRBacPermissions(string principalId, string permission)
{
- AnsiConsole.Markup(Theme.FormatError(MessageService.GetString("error")) + " ");
- ShellInterpreter.WriteLine(MessageService.GetArgsString("command-connect-rbac-error", "id", principalId, "permission", permission));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Error, Theme.FormatError(MessageService.GetString("error")) + " ");
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Error, MessageService.GetArgsString("command-connect-rbac-error", "id", principalId, "permission", permission));
}
///
@@ -165,7 +165,7 @@ internal static void AskForRBacPermissions(string principalId, string permission
///
internal static void PrintConnectUsageHint(ShellInterpreter shell)
{
- AnsiConsole.MarkupLine(Markup.Escape(MessageService.GetString("command-connect-not_connected-usage-header")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Markup.Escape(MessageService.GetString("command-connect-not_connected-usage-header")));
if (shell.App.Commands.TryGetValue("connect", out var factory))
{
@@ -185,17 +185,17 @@ internal static void PrintConnectUsageHint(ShellInterpreter shell)
}
var highlighted = shell.BuildHighlightedMarkup(example);
- AnsiConsole.MarkupLine($" {highlighted}");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, $" {highlighted}");
if (!string.IsNullOrWhiteSpace(description))
{
- AnsiConsole.MarkupLine($" {Theme.FormatMuted(description)}");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, $" {Theme.FormatMuted(description)}");
}
shown++;
}
}
- AnsiConsole.MarkupLine(Markup.Escape(MessageService.GetString("command-connect-not_connected-usage-footer")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Markup.Escape(MessageService.GetString("command-connect-not_connected-usage-footer")));
}
private static async Task PrintConnectionInfoAsync(ShellInterpreter shell, CommandState commandState, CancellationToken token)
@@ -210,7 +210,7 @@ private static async Task PrintConnectionInfoAsync(ShellInterprete
commandState.Result = new ShellJson(JsonSerializer.SerializeToElement(notConnectedJson));
commandState.RenderUser = () =>
{
- AnsiConsole.MarkupLine(MessageService.GetString("command-connect-not_connected"));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, MessageService.GetString("command-connect-not_connected"));
PrintConnectUsageHint(shell);
};
return commandState;
@@ -259,7 +259,7 @@ private static async Task PrintConnectionInfoAsync(ShellInterprete
};
commandState.RenderUser = () =>
{
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-connect-info-title")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(MessageService.GetString("command-connect-info-title")));
var table = new Table();
table.AddColumns(string.Empty, string.Empty);
@@ -283,7 +283,7 @@ private static async Task PrintConnectionInfoAsync(ShellInterprete
// Show current navigation state
table.AddRow(MessageService.GetString("command-connect-info-location"), Theme.ConnectedStatePromt(currentLocation));
- AnsiConsole.Write(table);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, table);
};
return commandState;
}
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/DirCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/DirCommand.cs
index 73486157..d7b05fa3 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/DirCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/DirCommand.cs
@@ -149,11 +149,11 @@ private void RenderEntries(List entries)
// Simple list mode: just show the name
if (entry.IsDirectory)
{
- AnsiConsole.MarkupLine(Theme.FormatDirectory(entry.Name + "/"));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatDirectory(entry.Name + "/"));
}
else
{
- AnsiConsole.WriteLine(entry.Name);
+ ShellInterpreter.Instance.Output.RenderLine(ShellMessageKind.Result, entry.Name);
}
}
else
@@ -161,19 +161,19 @@ private void RenderEntries(List entries)
// Detailed mode: show date, size, and name
if (entry.IsDirectory)
{
- AnsiConsole.MarkupLine(Theme.FormatDirectory(entry.Name + "/"));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatDirectory(entry.Name + "/"));
}
else
{
var sizeStr = FormatFileSize(entry.Size ?? 0);
- AnsiConsole.MarkupLine($"{Theme.FormatMuted($"{entry.LastModified:yyyy-MM-dd HH:mm}")} {Theme.FormatTableValue(sizeStr.PadLeft(10))} {Markup.Escape(entry.Name)}");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, $"{Theme.FormatMuted($"{entry.LastModified:yyyy-MM-dd HH:mm}")} {Theme.FormatTableValue(sizeStr.PadLeft(10))} {Markup.Escape(entry.Name)}");
}
}
}
if (!this.ListOnly)
{
- AnsiConsole.MarkupLine(MessageService.GetArgsString(
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, MessageService.GetArgsString(
"command-dir-summary",
"fileCount",
entries.Count(e => !e.IsDirectory),
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/DisconnectCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/DisconnectCommand.cs
index 97ecd38b..aabbcfd4 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/DisconnectCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/DisconnectCommand.cs
@@ -30,7 +30,7 @@ public override Task ExecuteAsync(ShellInterpreter shell, CommandS
["endpoint"] = endpoint,
};
commandState.Result = new ShellJson(JsonSerializer.SerializeToElement(jsonResult));
- commandState.RenderUser = () => AnsiConsole.MarkupLine(MessageService.GetArgsString("command-disconnect-success", "endpoint", endpoint));
+ commandState.RenderUser = () => ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, MessageService.GetArgsString("command-disconnect-success", "endpoint", endpoint));
}
else
{
@@ -40,7 +40,7 @@ public override Task ExecuteAsync(ShellInterpreter shell, CommandS
["disconnected"] = false,
};
commandState.Result = new ShellJson(JsonSerializer.SerializeToElement(jsonResult));
- commandState.RenderUser = () => AnsiConsole.MarkupLine(MessageService.GetString("command-disconnect-not_connected"));
+ commandState.RenderUser = () => ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, MessageService.GetString("command-disconnect-not_connected"));
}
return Task.FromResult(commandState);
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/DoctorCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/DoctorCommand.cs
index 943c9683..cdb90ca1 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/DoctorCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/DoctorCommand.cs
@@ -478,7 +478,7 @@ internal CommandState CreateResult(List checks, CommandState comman
if (this.IncludeIdentity)
{
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(Message("who-heading")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(Message("who-heading")));
}
var grid = new Grid();
@@ -506,9 +506,9 @@ internal CommandState CreateResult(List checks, CommandState comman
}
}
- AnsiConsole.Write(grid);
- AnsiConsole.WriteLine();
- AnsiConsole.MarkupLine($"{Theme.FormatSectionHeader(Message("summary"))}: {summary.pass} {FormatStatus("PASS")} | {summary.warn} {FormatStatus("WARN")} | {summary.fail} {FormatStatus("FAIL")} | {summary.skip} {FormatStatus("SKIP")} | {durationMs.ToString(CultureInfo.InvariantCulture)} ms | {FormatCharge(result.RequestCharge)} RU");
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, grid);
+ ShellInterpreter.Instance.Output.RenderLine(ShellMessageKind.Result);
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, $"{Theme.FormatSectionHeader(Message("summary"))}: {summary.pass} {FormatStatus("PASS")} | {summary.warn} {FormatStatus("WARN")} | {summary.fail} {FormatStatus("FAIL")} | {summary.skip} {FormatStatus("SKIP")} | {durationMs.ToString(CultureInfo.InvariantCulture)} ms | {FormatCharge(result.RequestCharge)} RU");
};
return result;
}
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/EditCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/EditCommand.cs
index 14a54a85..6ac4e412 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/EditCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/EditCommand.cs
@@ -34,8 +34,7 @@ public override Task ExecuteAsync(ShellInterpreter shell, CommandS
if (shell.IsInputRedirected() || !string.IsNullOrEmpty(shell.CurrentScriptFileName))
{
var message = MessageService.GetString("command-edit-not-interactive");
- AnsiConsole.MarkupLine(Theme.FormatError(message));
- return Task.FromResult(new ErrorCommandState(new CommandException("edit", message)));
+ throw new CommandException("edit", message);
}
var path = this.FilePath;
@@ -56,19 +55,17 @@ public override Task ExecuteAsync(ShellInterpreter shell, CommandS
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException or ArgumentException)
{
var message = MessageService.GetArgsString("command-edit-create-failed", "path", path, "message", ex.Message);
- AnsiConsole.MarkupLine(Theme.FormatError(message));
- return Task.FromResult(new ErrorCommandState(new CommandException("edit", message, ex)));
+ throw new CommandException("edit", message, ex);
}
var editor = ExternalEditor.Resolve(null);
if (editor is null)
{
var message = MessageService.GetString("command-edit-no-editor");
- AnsiConsole.MarkupLine(Theme.FormatError(message));
- return Task.FromResult(new ErrorCommandState(new CommandException("edit", message)));
+ throw new CommandException("edit", message);
}
- AnsiConsole.MarkupLine(Theme.FormatMuted(MessageService.GetArgsString(
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Information, Theme.FormatMuted(MessageService.GetArgsString(
"command-edit-launching",
"path",
path,
@@ -98,8 +95,7 @@ public override Task ExecuteAsync(ShellInterpreter shell, CommandS
path,
"message",
ex.Message);
- AnsiConsole.MarkupLine(Theme.FormatError(message));
- return Task.FromResult(new ErrorCommandState(new CommandException("edit", message, ex)));
+ throw new CommandException("edit", message, ex);
}
if (exitCode != 0)
@@ -110,12 +106,11 @@ public override Task ExecuteAsync(ShellInterpreter shell, CommandS
editor.DisplayName,
"code",
exitCode);
- AnsiConsole.MarkupLine(Theme.FormatWarning(message));
- return Task.FromResult(new ErrorCommandState(new CommandException("edit", message)));
+ throw new CommandException("edit", message);
}
commandState.Result = new ShellJson(JsonSerializer.SerializeToElement(new { type = "file", path, edited = true }));
- commandState.RenderUser = () => AnsiConsole.MarkupLine(Theme.FormatMuted(MessageService.GetArgsString("command-edit-saved", "path", path)));
+ commandState.RenderUser = () => ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatMuted(MessageService.GetArgsString("command-edit-saved", "path", path)));
return Task.FromResult(commandState);
}
}
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ExportCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ExportCommand.cs
index ca67914c..80abbc7c 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ExportCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ExportCommand.cs
@@ -102,7 +102,7 @@ public override async Task ExecuteAsync(ShellInterpreter shell, Co
requestCharge = charge,
}));
resultState.RequestCharge = charge;
- resultState.RenderUser = () => ShellInterpreter.WriteLine(MessageService.GetArgsString(
+ resultState.RenderUser = () => shell.Output.WriteLine(ShellMessageKind.Result, MessageService.GetArgsString(
"command-export-success",
"count",
count,
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/FtabCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/FtabCommand.cs
index ef01d0e6..5b15c754 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/FtabCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/FtabCommand.cs
@@ -409,7 +409,7 @@ private static void RenderSpectreTable(IReadOnlyList headers, IReadOnlyL
table.AddRow(row.Select((cell, index) => FormatStyledCell(headers[index], cell, colorizeRules)).ToArray());
}
- AnsiConsole.Write(table);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, table);
}
private static string RenderPlainText(IReadOnlyList headers, IReadOnlyList> rows)
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/HelpCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/HelpCommand.cs
index 40a7abe5..a1b4f66e 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/HelpCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/HelpCommand.cs
@@ -90,9 +90,7 @@ public static CommandState PrintCommandHelp(string cmdStr, CommandRunner app, bo
}
// Neither command, statement, nor help topic
- AnsiConsole.Markup(Theme.FormatError(MessageService.GetString("error")) + " ");
- ShellInterpreter.WriteLine(MessageService.GetString("error-command-not-found", MessageService.Args("command", cmdStr)));
- return new ErrorCommandState(CommandException.Usage("help", cmdStr + " not found."));
+ throw CommandException.Usage("help", MessageService.GetString("error-command-not-found", MessageService.Args("command", cmdStr)));
}
if (cmd == null)
@@ -107,69 +105,69 @@ public static CommandState PrintCommandHelp(string cmdStr, CommandRunner app, bo
// Plain output (no colors, no panels)
if (!string.IsNullOrEmpty(cmd.Description))
{
- ShellInterpreter.WriteLine(cmd.Description);
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, cmd.Description);
}
- ShellInterpreter.WriteLine();
- ShellInterpreter.WriteLine(MessageService.GetString("help-usage", new System.Collections.Generic.Dictionary { ["command"] = cmd.CommandName }) + " " + BuildPlainUsage(cmd));
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, MessageService.GetString("help-usage", new System.Collections.Generic.Dictionary { ["command"] = cmd.CommandName }) + " " + BuildPlainUsage(cmd));
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
if (cmd.Aliases.Count > 0)
{
- ShellInterpreter.WriteLine($"{MessageService.GetString("help-aliases")} {string.Join(", ", cmd.Aliases)}");
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, $"{MessageService.GetString("help-aliases")} {string.Join(", ", cmd.Aliases)}");
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
}
if (cmd.Parameters.Count > 0)
{
- ShellInterpreter.WriteLine(MessageService.GetString("help-arguments"));
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, MessageService.GetString("help-arguments"));
foreach (var p in cmd.Parameters)
{
var name = p.Name.FirstOrDefault() ?? string.Empty;
- ShellInterpreter.Write(" ");
- ShellInterpreter.Write(p.IsRequired ? name : $"[{name}]");
- ShellInterpreter.Write(" ");
+ ShellInterpreter.Instance.Output.Write(ShellMessageKind.Result, " ");
+ ShellInterpreter.Instance.Output.Write(ShellMessageKind.Result, p.IsRequired ? name : $"[{name}]");
+ ShellInterpreter.Instance.Output.Write(ShellMessageKind.Result, " ");
var desc = p.GetDescription(cmd.CommandName) ?? string.Empty;
if (!p.IsRequired)
{
- ShellInterpreter.Write($"{MessageService.GetString("help-optional")} ");
+ ShellInterpreter.Instance.Output.Write(ShellMessageKind.Result, $"{MessageService.GetString("help-optional")} ");
}
- ShellInterpreter.WriteLine(desc);
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, desc);
}
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
}
if (cmd.Options.Count > 0)
{
- ShellInterpreter.WriteLine(MessageService.GetString("help-options"));
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, MessageService.GetString("help-options"));
foreach (var opt in cmd.Options)
{
var names = string.Join(", ", opt.Name.Select(n => "-" + n));
var desc = opt.GetDescription(cmd.CommandName) ?? string.Empty;
- ShellInterpreter.WriteLine($" {names} {desc}");
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, $" {names} {desc}");
}
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
}
// Use shared examples collection to avoid shadowing
var plainExamples = cmd.ExamplesWithDescriptions;
if (plainExamples.Count > 0)
{
- ShellInterpreter.WriteLine(MessageService.GetString("help-examples"));
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, MessageService.GetString("help-examples"));
for (int i = 0; i < plainExamples.Count; i++)
{
var (ex, desc) = plainExamples[i];
if (!string.IsNullOrEmpty(desc))
{
- ShellInterpreter.WriteLine($" {i + 1}. {desc}");
- ShellInterpreter.WriteLine($" {ex}");
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, $" {i + 1}. {desc}");
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, $" {ex}");
}
else
{
- ShellInterpreter.WriteLine($" {i + 1}. {ex}");
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, $" {i + 1}. {ex}");
}
}
}
@@ -179,31 +177,31 @@ public static CommandState PrintCommandHelp(string cmdStr, CommandRunner app, bo
// Styled output
if (!string.IsNullOrEmpty(cmd.Description))
{
- AnsiConsole.MarkupLine($"{INDENT}{Theme.FormatHelpHeader(cmd.Description)}");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, $"{INDENT}{Theme.FormatHelpHeader(cmd.Description)}");
}
if (cmd.Aliases.Count > 0)
{
- AnsiConsole.MarkupLine($"{INDENT}[dim]{Markup.Escape(MessageService.GetString("help-aliases"))} {Markup.Escape(string.Join(", ", cmd.Aliases))}[/]");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, $"{INDENT}[dim]{Markup.Escape(MessageService.GetString("help-aliases"))} {Markup.Escape(string.Join(", ", cmd.Aliases))}[/]");
}
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
WriteSectionHeader(MessageService.GetString("help-usage-heading"));
- AnsiConsole.Markup(INDENT + Theme.FormatCommand(cmd.CommandName) + " ");
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, INDENT + Theme.FormatCommand(cmd.CommandName) + " ");
}
if (!plain && cmd?.Options != null)
{
foreach (var p in cmd.Options)
{
- AnsiConsole.Markup(INDENT + "[[" + Theme.FormatHelpName("-" + (p.Name.FirstOrDefault() ?? string.Empty)));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, INDENT + "[[" + Theme.FormatHelpName("-" + (p.Name.FirstOrDefault() ?? string.Empty)));
if (!p.PropertyInfo.PropertyType.IsAssignableFrom(typeof(bool)))
{
- AnsiConsole.Markup($" [dim]{MessageService.GetString("help-arg")}[/]");
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, $" [dim]{MessageService.GetString("help-arg")}[/]");
}
- AnsiConsole.Markup("]] ");
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "]] ");
}
}
@@ -219,16 +217,16 @@ public static CommandState PrintCommandHelp(string cmdStr, CommandRunner app, bo
if (p.IsRequired)
{
- AnsiConsole.Markup(INDENT + Theme.FormatHelpName(name) + " ");
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, INDENT + Theme.FormatHelpName(name) + " ");
}
else
{
- AnsiConsole.Markup(INDENT + "[[" + Theme.FormatHelpName(name) + "]] ");
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, INDENT + "[[" + Theme.FormatHelpName(name) + "]] ");
}
}
- ShellInterpreter.WriteLine();
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
if (cmd.Parameters.Count > 0)
{
@@ -266,8 +264,8 @@ public static CommandState PrintCommandHelp(string cmdStr, CommandRunner app, bo
table.AddRow(INDENT + nameDisplay, descDisplay);
}
- AnsiConsole.Write(table);
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, table);
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
}
}
@@ -303,24 +301,24 @@ public static CommandState PrintCommandHelp(string cmdStr, CommandRunner app, bo
table.AddRow(INDENT + Theme.FormatHelpName(sb.ToString()), descDisplay);
}
- AnsiConsole.Write(table);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, table);
}
var examples = cmd?.ExamplesWithDescriptions;
if (!plain && examples != null && examples.Count > 0)
{
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
WriteSectionHeader(MessageService.GetString("help-examples-heading"));
for (int i = 0; i < examples.Count; i++)
{
var (example, description) = examples[i];
if (!string.IsNullOrWhiteSpace(description))
{
- AnsiConsole.MarkupLine(INDENT + $"{Theme.FormatHelpAccent("\u25b6")} {i + 1}. {Theme.FormatHelpDescription(description)}");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, INDENT + $"{Theme.FormatHelpAccent("\u25b6")} {i + 1}. {Theme.FormatHelpDescription(description)}");
}
else
{
- AnsiConsole.MarkupLine(INDENT + $"{Theme.FormatHelpAccent("\u25b6")} {i + 1}.");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, INDENT + $"{Theme.FormatHelpAccent("\u25b6")} {i + 1}.");
}
var parser = new StatementParser(example);
@@ -353,11 +351,11 @@ public static CommandState PrintCommandHelp(string cmdStr, CommandRunner app, bo
Border = BoxBorder.None,
Padding = new Padding(4, 0, 0, 0),
};
- AnsiConsole.Write(panel);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, panel);
if (i < examples.Count - 1)
{
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
}
}
}
@@ -586,8 +584,8 @@ private static void PrintKeyboardShortcuts(bool plain)
BorderStyle = Theme.GetHelpBorderStyle(),
Padding = new Padding(1, 0, 1, 0),
};
- AnsiConsole.Write(headerPanel);
- AnsiConsole.MarkupLine(INDENT + Theme.FormatHelpDescription(MessageService.GetString("help-keyboard-context")));
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, headerPanel);
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, INDENT + Theme.FormatHelpDescription(MessageService.GetString("help-keyboard-context")));
ShellInterpreter.WriteLine();
foreach (var group in groups)
@@ -604,11 +602,11 @@ private static void PrintKeyboardShortcuts(bool plain)
table.AddRow(INDENT + FormatShortcutKeys(shortcut.Shortcut), Theme.FormatHelpDescription(shortcut.Description));
}
- AnsiConsole.Write(table);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, table);
ShellInterpreter.WriteLine();
}
- AnsiConsole.MarkupLine(INDENT + Theme.FormatHelpAccent("\u25b6") + " " + Theme.FormatHelpDescription(MessageService.GetString("help-keyboard-search-note")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, INDENT + Theme.FormatHelpAccent("\u25b6") + " " + Theme.FormatHelpDescription(MessageService.GetString("help-keyboard-search-note")));
ShellInterpreter.WriteLine();
}
@@ -632,7 +630,7 @@ private static void PrintHelpTopics(bool plain)
BorderStyle = Theme.GetHelpBorderStyle(),
Padding = new Padding(1, 0, 1, 0),
};
- AnsiConsole.Write(panel);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, panel);
ShellInterpreter.WriteLine();
var table = new Table()
@@ -642,8 +640,8 @@ private static void PrintHelpTopics(bool plain)
.AddColumn(new TableColumn("Description"));
table.AddRow(INDENT + Theme.FormatHelpAccent(ShortcutsTopic), Theme.FormatHelpDescription(description));
- AnsiConsole.Write(table);
- AnsiConsole.MarkupLine(INDENT + Theme.FormatMuted(MessageService.GetString("help-topics-hint")));
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, table);
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, INDENT + Theme.FormatMuted(MessageService.GetString("help-topics-hint")));
ShellInterpreter.WriteLine();
}
@@ -654,8 +652,8 @@ private static void PrintCommandCategory(string categoryName, List s.Name))
{
- ShellInterpreter.WriteLine($" {s.Name}");
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, $" {s.Name}");
if (!string.IsNullOrWhiteSpace(s.Description))
{
- ShellInterpreter.WriteLine($" {s.Description}");
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, $" {s.Description}");
}
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
}
}
else
@@ -869,8 +867,8 @@ private static void PrintStatementHelps(bool plain)
BorderStyle = Theme.GetHelpBorderStyle(),
Padding = new Padding(1, 0, 1, 0),
};
- AnsiConsole.Write(stmtPanel);
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, stmtPanel);
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
var table = new Table()
.Border(TableBorder.None)
@@ -887,8 +885,8 @@ private static void PrintStatementHelps(bool plain)
table.AddRow(INDENT + Theme.FormatKeyword(s.Name), desc);
}
- AnsiConsole.Write(table);
- AnsiConsole.MarkupLine(string.Empty);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, table);
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, string.Empty);
}
}
@@ -941,7 +939,7 @@ private static void WriteSectionHeader(string title)
Justification = Justify.Left,
Style = Theme.GetMutedStyle(),
};
- AnsiConsole.Write(rule);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, rule);
}
private static string BuildPlainUsage(CommandFactory cmd)
@@ -985,21 +983,21 @@ private static void PrintPlainCategory(string title, List comman
return;
}
- ShellInterpreter.WriteLine(title + ":");
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, title + ":");
foreach (var c in commands)
{
- ShellInterpreter.WriteLine(" " + c.CommandName + " - " + (c.Description ?? string.Empty));
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, " " + c.CommandName + " - " + (c.Description ?? string.Empty));
}
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
}
private void RenderCommandList(CommandRunner app)
{
if (this.Plain)
{
- ShellInterpreter.WriteLine(MessageService.GetString("help-available-commands"));
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result, MessageService.GetString("help-available-commands"));
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
}
else
{
@@ -1010,8 +1008,8 @@ private void RenderCommandList(CommandRunner app)
BorderStyle = Theme.GetHelpBorderStyle(),
Padding = new Padding(1, 0, 1, 0),
};
- AnsiConsole.Write(headerPanel);
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, headerPanel);
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Result);
}
// Group commands by category (basic heuristic based on name patterns)
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ImportCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ImportCommand.cs
index c8cb73ea..1f7b6162 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ImportCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ImportCommand.cs
@@ -732,6 +732,7 @@ public override async Task ExecuteAsync(ShellInterpreter shell, Co
var partitionKeySegments = ParsePartitionKeySegments(this.PartitionKey);
var progress = new ImportProgress();
+ var errors = shell.IsMachineMode ? new BulkWriteDiagnostics() : null;
var reporter = new IngestionProgress(
shell,
selectedFormat,
@@ -758,7 +759,8 @@ await reporter.RunAsync(() => ExecuteImportAsync(
commandState,
progress,
token,
- UpdateProgress));
+ UpdateProgress,
+ errors));
}
catch (DecoderFallbackException ex)
{
@@ -841,12 +843,20 @@ await reporter.RunAsync(() => ExecuteImportAsync(
}
CommandState resultState;
+ var failureMessage = failure?.Message;
+ if (errors is { Count: > 0 })
+ {
+ failureMessage = ReferenceEquals(failure, progress.FirstFailure)
+ ? errors.GetMessage()
+ : failureMessage + Environment.NewLine + errors.GetMessage();
+ }
+
if (failure != null)
{
- payload["error"] = failure.Message;
+ payload["error"] = failureMessage;
payload["exitCode"] = ShellExitCode.FromException(failure);
resultState = new StructuredErrorCommandState(
- new CommandException("import", $"{summary} {failure.Message}", failure),
+ new CommandException("import", $"{summary} {failureMessage}", failure),
new ShellJson(JsonSerializer.SerializeToElement(payload)));
}
else
@@ -864,10 +874,10 @@ await reporter.RunAsync(() => ExecuteImportAsync(
resultState.RequestCharge = charge > 0 ? charge : null;
resultState.RenderUser = () =>
{
- ShellInterpreter.WriteLine(summary);
+ shell.Output.WriteLine(ShellMessageKind.Result, summary);
if (failure != null && shell.ErrOutRedirect == null)
{
- ShellInterpreter.WriteLine(failure.Message);
+ shell.Output.WriteLine(ShellMessageKind.Error, failureMessage!);
}
};
return resultState;
@@ -887,7 +897,8 @@ private static async Task ExecuteImportAsync(
CommandState commandState,
ImportProgress progress,
CancellationToken token,
- Action progressChanged)
+ Action progressChanged,
+ BulkWriteDiagnostics? errors)
{
FileStream? stream = null;
IAsyncEnumerable<(int LineNumber, JsonElement Item)> source;
@@ -928,7 +939,8 @@ await WriteItemsAsync(
token,
warningShell: shell,
progress: progress,
- progressChanged: progressChanged);
+ progressChanged: progressChanged,
+ errors: errors);
}
finally
{
@@ -990,7 +1002,8 @@ private static void ValidateDryRunItem(JsonElement item, int lineNumber)
CancellationToken token,
ShellInterpreter? warningShell = null,
ImportProgress? progress = null,
- Action? progressChanged = null)
+ Action? progressChanged = null,
+ BulkWriteDiagnostics? errors = null)
{
ArgumentOutOfRangeException.ThrowIfLessThan(concurrency, 1);
progress ??= new ImportProgress();
@@ -1009,6 +1022,7 @@ void Record((double Charge, Exception? Error) result)
{
progress.Failed++;
progress.FirstFailure ??= result.Error;
+ errors?.Add(result.Error.Message);
}
progressChanged?.Invoke();
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/IndexCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/IndexCommand.cs
index e97c366d..582dac51 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/IndexCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/IndexCommand.cs
@@ -63,7 +63,7 @@ async Task IStateVisitor.VisitConn
{
if (!string.IsNullOrEmpty(this.Database) && !string.IsNullOrEmpty(this.Container))
{
- return await this.ExecuteOnContainerAsync(state, this.Database, this.Container, token);
+ return await this.ExecuteOnContainerAsync(state, shell, this.Database, this.Container, token);
}
throw new NotInContainerException("index");
@@ -75,7 +75,7 @@ async Task IStateVisitor.VisitData
if (!string.IsNullOrEmpty(this.Container))
{
- return await this.ExecuteOnContainerAsync(state, databaseName, this.Container, token);
+ return await this.ExecuteOnContainerAsync(state, shell, databaseName, this.Container, token);
}
throw new NotInContainerException("index");
@@ -86,7 +86,7 @@ async Task IStateVisitor.VisitCont
string databaseName = this.Database ?? state.DatabaseName;
string containerName = this.Container ?? state.ContainerName;
- return await this.ExecuteOnContainerAsync(state, databaseName, containerName, token);
+ return await this.ExecuteOnContainerAsync(state, shell, databaseName, containerName, token);
}
///
@@ -309,7 +309,7 @@ private static CommandState BuildResult(string json)
};
}
- private async Task ExecuteOnContainerAsync(ConnectedState state, string databaseName, string containerName, CancellationToken token)
+ private async Task ExecuteOnContainerAsync(ConnectedState state, ShellInterpreter shell, string databaseName, string containerName, CancellationToken token)
{
await ValidateContainerExistsAsync(state, databaseName, containerName, "index", token);
@@ -318,11 +318,11 @@ private async Task ExecuteOnContainerAsync(ConnectedState state, s
case "show":
return await this.ShowAsync(state, databaseName, containerName, token);
case "add":
- return await this.AddAsync(state, databaseName, containerName, token);
+ return await this.AddAsync(state, shell, databaseName, containerName, token);
case "remove":
- return await this.RemoveAsync(state, databaseName, containerName, token);
+ return await this.RemoveAsync(state, shell, databaseName, containerName, token);
case "set":
- return await this.SetAsync(state, databaseName, containerName, token);
+ return await this.SetAsync(state, shell, databaseName, containerName, token);
default:
throw CommandException.Usage(
"index",
@@ -342,23 +342,23 @@ private async Task ShowAsync(ConnectedState state, string database
return BuildResult(json);
}
- private async Task AddAsync(ConnectedState state, string databaseName, string containerName, CancellationToken token)
+ private async Task AddAsync(ConnectedState state, ShellInterpreter shell, string databaseName, string containerName, CancellationToken token)
{
var paths = this.RequirePaths();
string current = await this.ReadPolicyAsync(state, databaseName, containerName, token);
string updated = AddIncludedPaths(current, paths);
- return await this.WritePolicyAsync(state, databaseName, containerName, updated, token);
+ return await this.WritePolicyAsync(state, shell, databaseName, containerName, updated, token);
}
- private async Task RemoveAsync(ConnectedState state, string databaseName, string containerName, CancellationToken token)
+ private async Task RemoveAsync(ConnectedState state, ShellInterpreter shell, string databaseName, string containerName, CancellationToken token)
{
var paths = this.RequirePaths();
string current = await this.ReadPolicyAsync(state, databaseName, containerName, token);
string updated = RemovePaths(current, paths);
- return await this.WritePolicyAsync(state, databaseName, containerName, updated, token);
+ return await this.WritePolicyAsync(state, shell, databaseName, containerName, updated, token);
}
- private async Task SetAsync(ConnectedState state, string databaseName, string containerName, CancellationToken token)
+ private async Task SetAsync(ConnectedState state, ShellInterpreter shell, string databaseName, string containerName, CancellationToken token)
{
bool? automatic = ParseAutomatic(this.Automatic);
string? policyArgument = this.PolicyArgument();
@@ -370,7 +370,7 @@ private async Task SetAsync(ConnectedState state, string databaseN
string baseJson = policyArgument ?? await this.ReadPolicyAsync(state, databaseName, containerName, token);
string updated = ApplySettings(baseJson, this.Mode, automatic);
- return await this.WritePolicyAsync(state, databaseName, containerName, updated, token);
+ return await this.WritePolicyAsync(state, shell, databaseName, containerName, updated, token);
}
private string[] RequirePaths()
@@ -406,7 +406,7 @@ private async Task ReadPolicyAsync(ConnectedState state, string database
}
}
- private async Task WritePolicyAsync(ConnectedState state, string databaseName, string containerName, string policyJson, CancellationToken token)
+ private async Task WritePolicyAsync(ConnectedState state, ShellInterpreter shell, string databaseName, string containerName, string policyJson, CancellationToken token)
{
string updatedJson;
try
@@ -419,7 +419,7 @@ private async Task WritePolicyAsync(ConnectedState state, string d
}
var result = BuildResult(updatedJson);
- result.RenderUser = () => ShellInterpreter.WriteLine(MessageService.GetString("command-index-updated"));
+ result.RenderUser = () => shell.Output.WriteLine(ShellMessageKind.Result, MessageService.GetString("command-index-updated"));
return result;
}
}
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/InfoCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/InfoCommand.cs
index 8a55b8fa..7493feab 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/InfoCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/InfoCommand.cs
@@ -196,17 +196,17 @@ private static void AskForRBacPermissions(string principalId, string request, st
var line = lines[i];
if (i == 0)
{
- AnsiConsole.Markup(indent + Theme.FormatError(MessageService.GetString("error")) + " ");
- ShellInterpreter.WriteLine(line);
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Error, indent + Theme.FormatError(MessageService.GetString("error")) + " ");
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Error, line);
}
else if (line.Length == 0)
{
- ShellInterpreter.WriteLine();
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Error);
}
else
{
- AnsiConsole.Markup(indent);
- ShellInterpreter.WriteLine(line);
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Error, indent);
+ ShellInterpreter.Instance.Output.WriteLine(ShellMessageKind.Error, line);
}
}
}
@@ -215,9 +215,9 @@ private static async Task WriteAccountDatabaseBreakdownAsync(ConnectedState stat
{
if (renderOutput)
{
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-stats-account-databases-heading")));
- AnsiConsole.Markup("\t");
- AnsiConsole.MarkupLine(Theme.FormatMuted(MessageService.GetString("command-stats-account-detailed-cost-note")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(MessageService.GetString("command-stats-account-databases-heading")));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "\t");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatMuted(MessageService.GetString("command-stats-account-detailed-cost-note")));
}
long totalContainers = 0;
@@ -277,7 +277,7 @@ private static async Task WriteAccountDatabaseBreakdownAsync(ConnectedState stat
totalsTable.AddRow(MessageService.GetString("command-stats-account-label-total-containers"), Theme.FormatTableValue(totalContainers.ToString(CultureInfo.InvariantCulture)));
totalsTable.AddRow(MessageService.GetString("command-stats-account-label-total-documents"), Theme.FormatTableValue(FormatCount(totalDocuments)));
totalsTable.AddRow(MessageService.GetString("command-stats-account-label-total-size"), Theme.FormatTableValue(FormatSize(totalSizeKb)));
- AnsiConsole.Write(totalsTable);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, totalsTable);
if (rows.Count == 0)
{
@@ -298,7 +298,7 @@ private static async Task WriteAccountDatabaseBreakdownAsync(ConnectedState stat
Theme.FormatTableValue(FormatSize(row.SizeKb)));
}
- AnsiConsole.Write(databaseTable);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, databaseTable);
}
internal static void AddSessionUsage(ShellInterpreter shell, Dictionary mcpTable, bool renderOutput)
@@ -324,7 +324,7 @@ internal static void AddSessionUsage(ShellInterpreter shell, Dictionary ReadContainerUsageAsync(Container container, CancellationToken token)
@@ -396,7 +396,7 @@ private static async Task WriteDatabaseThroughputAsync(Database database, Dictio
return;
}
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-stats-throughput-heading")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(MessageService.GetString("command-stats-throughput-heading")));
if (throughputError is not null)
{
@@ -406,14 +406,14 @@ private static async Task WriteDatabaseThroughputAsync(Database database, Dictio
}
else
{
- AnsiConsole.Markup("\t");
- AnsiConsole.MarkupLine(Theme.FormatError(throughputError));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "\t");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatError(throughputError));
}
}
else if (serverless)
{
- AnsiConsole.Markup("\t");
- AnsiConsole.MarkupLine(Theme.FormatMuted(MessageService.GetString("command-settings-scale-serverless")));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "\t");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatMuted(MessageService.GetString("command-settings-scale-serverless")));
}
else if (min.HasValue || max.HasValue)
{
@@ -430,12 +430,12 @@ private static async Task WriteDatabaseThroughputAsync(Database database, Dictio
table.AddRow(MessageService.GetString("command-stats-throughput-min"), Theme.FormatTableValue(renderedMinValue.ToString(CultureInfo.InvariantCulture)));
}
- AnsiConsole.Write(table);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, table);
}
else
{
- AnsiConsole.Markup("\t");
- AnsiConsole.MarkupLine(Theme.FormatMuted(MessageService.GetString("command-stats-database-shared-throughput-none")));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "\t");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatMuted(MessageService.GetString("command-stats-database-shared-throughput-none")));
}
}
@@ -443,9 +443,9 @@ private static async Task WriteDatabaseThroughputAsync(Database database, Dictio
{
if (renderOutput)
{
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-stats-partitions-heading")));
- AnsiConsole.Markup("\t");
- AnsiConsole.MarkupLine(Theme.FormatMuted(MessageService.GetString("command-stats-partitions-cost-note")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(MessageService.GetString("command-stats-partitions-heading")));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "\t");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatMuted(MessageService.GetString("command-stats-partitions-cost-note")));
}
var ranges = await container.GetFeedRangesAsync(token);
@@ -488,14 +488,14 @@ private static async Task WriteDatabaseThroughputAsync(Database database, Dictio
if (table is not null)
{
- AnsiConsole.Write(table);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, table);
}
if (renderOutput && total > 0 && counts.Count > 1)
{
double largestShare = (double)counts.Max() / total * 100;
- AnsiConsole.Markup("\t");
- AnsiConsole.MarkupLine(Theme.FormatMuted(MessageService.GetArgsString(
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "\t");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatMuted(MessageService.GetArgsString(
"command-stats-partitions-skew",
"percent",
string.Create(CultureInfo.InvariantCulture, $"{largestShare:0.#}"))));
@@ -508,9 +508,9 @@ private static async Task WriteDatabaseThroughputAsync(Database database, Dictio
{
if (renderOutput)
{
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-stats-detailed-heading")));
- AnsiConsole.Markup("\t");
- AnsiConsole.MarkupLine(Theme.FormatMuted(MessageService.GetString("command-stats-detailed-cost-note")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(MessageService.GetString("command-stats-detailed-heading")));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "\t");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatMuted(MessageService.GetString("command-stats-detailed-cost-note")));
}
var result = new List>();
@@ -518,8 +518,8 @@ private static async Task WriteDatabaseThroughputAsync(Database database, Dictio
{
if (renderOutput)
{
- AnsiConsole.Markup("\t");
- AnsiConsole.MarkupLine(Theme.FormatMuted(MessageService.GetString("command-stats-na")));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "\t");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatMuted(MessageService.GetString("command-stats-na")));
}
return result;
@@ -583,7 +583,7 @@ private static async Task WriteDatabaseThroughputAsync(Database database, Dictio
if (table is not null)
{
- AnsiConsole.Write(table);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, table);
}
return result;
@@ -716,7 +716,7 @@ private async Task ShowContainerSettingsAsync(ShellInterpreter she
// Scale section - fail gracefully if it cannot be read
if (renderOutput)
{
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-settings-scale-heading")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(MessageService.GetString("command-settings-scale-heading")));
}
switch (view.Throughput)
@@ -726,8 +726,8 @@ private async Task ShowContainerSettingsAsync(ShellInterpreter she
var maxDisplay = view.MaxThroughput?.ToString(System.Globalization.CultureInfo.InvariantCulture) ?? MessageService.GetString("command-settings-na");
if (renderOutput)
{
- AnsiConsole.Markup("\t");
- AnsiConsole.MarkupLine(MessageService.GetArgsString("command-settings-scale-usage", "min", minDisplay, "max", maxDisplay));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "\t");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, MessageService.GetArgsString("command-settings-scale-usage", "min", minDisplay, "max", maxDisplay));
}
if (view.MinThroughput.HasValue)
@@ -744,16 +744,16 @@ private async Task ShowContainerSettingsAsync(ShellInterpreter she
case ThroughputAvailability.NotConfigured:
if (renderOutput)
{
- AnsiConsole.Markup("\t");
- AnsiConsole.MarkupLine(Theme.FormatMuted(MessageService.GetString("command-settings-na")));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "\t");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatMuted(MessageService.GetString("command-settings-na")));
}
break;
case ThroughputAvailability.Serverless:
if (renderOutput)
{
- AnsiConsole.Markup("\t");
- AnsiConsole.MarkupLine(Theme.FormatMuted(MessageService.GetString("command-settings-scale-serverless")));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "\t");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatMuted(MessageService.GetString("command-settings-scale-serverless")));
}
break;
@@ -766,8 +766,8 @@ private async Task ShowContainerSettingsAsync(ShellInterpreter she
}
else
{
- AnsiConsole.Markup("\t");
- AnsiConsole.MarkupLine(Theme.FormatError(view.ThroughputErrorMessage ?? string.Empty));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "\t");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatError(view.ThroughputErrorMessage ?? string.Empty));
}
}
@@ -776,7 +776,7 @@ private async Task ShowContainerSettingsAsync(ShellInterpreter she
if (renderOutput)
{
- AnsiConsole.MarkupLine(string.Empty);
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, string.Empty);
}
mcpTable["id"] = view.ContainerName;
@@ -804,7 +804,7 @@ private async Task ShowContainerSettingsAsync(ShellInterpreter she
Table? table = null;
if (renderOutput)
{
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-settings-title")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(MessageService.GetString("command-settings-title")));
table = new Table();
table.AddColumns(string.Empty, string.Empty);
table.AddRow(MessageService.GetString("command-settings-ttl-label"), Theme.FormatTableValue(ttl));
@@ -827,13 +827,13 @@ private async Task ShowContainerSettingsAsync(ShellInterpreter she
{
table.AddRow(MessageService.GetString("command-settings-partition-key-label"), Theme.FormatTableValue(string.Join(',', view.PartitionKeyPaths)));
table.HideHeaders();
- AnsiConsole.Write(table);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, table);
}
// Full Text Policy section - show N/A when policy is unset
if (renderOutput)
{
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-settings-fulltext-title")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(MessageService.GetString("command-settings-fulltext-title")));
}
if (view.FullTextPolicy is { } fullText)
@@ -872,19 +872,19 @@ private async Task ShowContainerSettingsAsync(ShellInterpreter she
if (fullTextTable is not null)
{
fullTextTable.HideHeaders();
- AnsiConsole.Write(fullTextTable);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, fullTextTable);
}
}
else if (renderOutput)
{
- AnsiConsole.Markup("\t");
- AnsiConsole.MarkupLine(Theme.FormatMuted(MessageService.GetString("command-settings-na")));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "\t");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatMuted(MessageService.GetString("command-settings-na")));
}
// Indexing Policy section - compact summary; use 'index show' for the full policy
if (renderOutput)
{
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-settings-indexing-title")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(MessageService.GetString("command-settings-indexing-title")));
}
if (view.IndexingPolicy is { } indexing)
@@ -931,7 +931,7 @@ private async Task ShowContainerSettingsAsync(ShellInterpreter she
}
indexingTable.HideHeaders();
- AnsiConsole.Write(indexingTable);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, indexingTable);
}
mcpTable["indexingPolicy"] = new Dictionary
@@ -947,8 +947,8 @@ private async Task ShowContainerSettingsAsync(ShellInterpreter she
}
else if (renderOutput)
{
- AnsiConsole.Markup("\t");
- AnsiConsole.MarkupLine(Theme.FormatMuted(MessageService.GetString("command-settings-na")));
+ ShellInterpreter.Instance.Output.Markup(ShellMessageKind.Result, "\t");
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatMuted(MessageService.GetString("command-settings-na")));
}
// Usage section - document count and storage size
@@ -965,7 +965,7 @@ private async Task ShowContainerSettingsAsync(ShellInterpreter she
if (renderOutput)
{
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-settings-usage-heading")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(MessageService.GetString("command-settings-usage-heading")));
var usageTable = new Table();
usageTable.AddColumns(string.Empty, string.Empty);
@@ -978,7 +978,7 @@ private async Task ShowContainerSettingsAsync(ShellInterpreter she
}
usageTable.AddRow(MessageService.GetString("command-stats-label-total-size"), Theme.FormatTableValue(FormatSize(usage.TotalSizeKb)));
- AnsiConsole.Write(usageTable);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, usageTable);
}
if (this.Partitions)
@@ -1043,7 +1043,7 @@ private async Task ShowDatabaseSettingsAsync(ShellInterpreter shel
if (renderOutput)
{
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-stats-database-heading")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(MessageService.GetString("command-stats-database-heading")));
var table = new Table();
table.AddColumns(string.Empty, string.Empty);
@@ -1052,14 +1052,14 @@ private async Task ShowDatabaseSettingsAsync(ShellInterpreter shel
table.AddRow(MessageService.GetString("command-stats-database-label-container-count"), Theme.FormatTableValue(rows.Count.ToString(CultureInfo.InvariantCulture)));
table.AddRow(MessageService.GetString("command-stats-database-label-total-documents"), Theme.FormatTableValue(FormatCount(totalDocuments)));
table.AddRow(MessageService.GetString("command-stats-database-label-total-size"), Theme.FormatTableValue(FormatSize(totalSizeKb)));
- AnsiConsole.Write(table);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, table);
}
await WriteDatabaseThroughputAsync(database, mcpTable, renderOutput, token);
if (renderOutput && this.Detailed && rows.Count > 0)
{
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-stats-containers-heading")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(MessageService.GetString("command-stats-containers-heading")));
var containerTable = new Table();
containerTable.AddColumn(MessageService.GetString("command-stats-containers-col-name"));
containerTable.AddColumn(MessageService.GetString("command-stats-containers-col-count"));
@@ -1072,7 +1072,7 @@ private async Task ShowDatabaseSettingsAsync(ShellInterpreter shel
Theme.FormatTableValue(FormatSize(row.SizeKb)));
}
- AnsiConsole.Write(containerTable);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, containerTable);
}
if (this.Detailed)
@@ -1099,7 +1099,7 @@ private async Task PrintOverviewAsync(ShellInterpreter shell, Conn
if (renderOutput)
{
- AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-settings-overview")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatSectionHeader(MessageService.GetString("command-settings-overview")));
var table = new Table();
table.AddColumns(string.Empty, string.Empty, string.Empty, string.Empty);
@@ -1107,7 +1107,7 @@ private async Task PrintOverviewAsync(ShellInterpreter shell, Conn
table.AddRow(MessageService.GetString("command-settings-uri"), Theme.FormatTableValue(client.Endpoint.ToString()), MessageService.GetString("command-settings-write_locations"), Theme.FormatTableValue(string.Join(", ", acc.WritableRegions.Select(location => location.Name))));
table.AddRow(MessageService.GetString("command-stats-account-label-database-count"), Theme.FormatTableValue(databaseNames.Count.ToString(CultureInfo.InvariantCulture)), string.Empty, string.Empty);
table.HideHeaders();
- AnsiConsole.Write(table);
+ ShellInterpreter.Instance.Output.Render(ShellMessageKind.Result, table);
}
var mcpTable = new Dictionary
diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ListCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ListCommand.cs
index ae505f93..1eb7b9bc 100644
--- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ListCommand.cs
+++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ListCommand.cs
@@ -113,10 +113,10 @@ async Task IStateVisitor.VisitConn
{
foreach (var name in list)
{
- AnsiConsole.MarkupLine(Theme.DatabaseNamePromt(name));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.DatabaseNamePromt(name));
}
- AnsiConsole.MarkupLine(MessageService.GetString("command-ls-found_databases", new Dictionary
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, MessageService.GetString("command-ls-found_databases", new Dictionary
{
{ "count", list.Count },
{ "display", Theme.FormatTableValue(list.Count.ToString()) },
@@ -124,7 +124,7 @@ async Task IStateVisitor.VisitConn
if (completionList.Count == 0 && hasArmContext)
{
- AnsiConsole.MarkupLine(Theme.FormatWarning(MessageService.GetString("command-ls-empty_databases_hint")));
+ ShellInterpreter.Instance.Output.MarkupLine(ShellMessageKind.Result, Theme.FormatWarning(MessageService.GetString("command-ls-empty_databases_hint")));
}
};
return result;
@@ -183,10 +183,10 @@ private async Task