From 020942b3f6e81411064bba3911239f53ce4bcf9a Mon Sep 17 00:00:00 2001 From: KhushviB Date: Sat, 11 Jul 2026 23:43:49 +0530 Subject: [PATCH 01/26] feat: implement deterministic exit codes and structured machine output (A1) - Add --output json/ndjson/table and --quiet flags - Implement deterministic exit codes (0: success, 1: generic, 2: bad args, 3: auth, 4: not found, 5: throttled) - Emit structured JSON errors on stderr during machine modes - Automatically disable ANSI colors and suppress informational output in machine modes --- .../CommandState.cs | 42 +++++++++ .../ErrorCommandState.cs | 31 +++++++ .../OutputFormat.cs | 5 ++ .../ParserErrorCommandState.cs | 4 +- .../ShellInterpreter.cs | 89 ++++++++++++++++--- CosmosDBShell/Program.cs | 59 ++++++++++-- CosmosDBShell/lang/en.ftl | 2 + 7 files changed, 215 insertions(+), 17 deletions(-) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CommandState.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CommandState.cs index 98720118..77a8049a 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CommandState.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CommandState.cs @@ -19,6 +19,11 @@ public partial class CommandState /// public virtual bool IsError => false; + /// + /// Gets the exit code for the command state. Default is 0. + /// + public virtual int ExitCode => 0; + /// /// Gets or sets the output format for the command result. /// @@ -55,6 +60,10 @@ internal void SetFormat(string? outputFormat) { this.OutputFormat = OutputFormat.Table; } + else if (string.Equals(outputFormat, "ndjson", StringComparison.OrdinalIgnoreCase)) + { + this.OutputFormat = OutputFormat.Ndjson; + } else { throw new ShellException(MessageService.GetString("error-invalid_output_format", new Dictionary { { "format", outputFormat } })); @@ -123,6 +132,39 @@ internal string GenerateOutputText() } return ResultToTable(json.EnumerateArray().ToArray()).ToGridString(); + case OutputFormat.Ndjson: + { + var array = json; + if (json.ValueKind == JsonValueKind.Object) + { + if (json.TryGetProperty("documents", out var documents)) + { + array = documents; + } + else if (json.TryGetProperty("items", out var items)) + { + array = items; + } + else + { + return JsonSerializer.Serialize(json) + Environment.NewLine; + } + } + + if (array.ValueKind == JsonValueKind.Array) + { + var sb = new StringBuilder(); + foreach (var element in array.EnumerateArray()) + { + sb.AppendLine(JsonSerializer.Serialize(element)); + } + + return sb.ToString(); + } + + return JsonSerializer.Serialize(array) + Environment.NewLine; + } + default: throw new InvalidOperationException("OutputFormat invalid " + this.OutputFormat); } diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs index f4de5468..ae022b27 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs @@ -15,4 +15,35 @@ internal class ErrorCommandState(Exception exception) : CommandState public Exception Exception { get; init; } = exception; public override bool IsError => true; + + public override int ExitCode + { + get + { + var ex = this.Exception; + if (ex is CommandException ce) + { + ex = ce.InnerException ?? ce; + } + + if (ex is Microsoft.Azure.Cosmos.CosmosException cosmosEx) + { + return cosmosEx.StatusCode switch + { + System.Net.HttpStatusCode.Unauthorized => 3, + System.Net.HttpStatusCode.Forbidden => 3, + System.Net.HttpStatusCode.NotFound => 4, + System.Net.HttpStatusCode.TooManyRequests => 5, + _ => 1, + }; + } + + if (ex is ArgumentException) + { + return 2; + } + + return 1; + } + } } diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/OutputFormat.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/OutputFormat.cs index 6bb6c380..e9b9d1c7 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/OutputFormat.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/OutputFormat.cs @@ -23,4 +23,9 @@ public enum OutputFormat /// Output as an aligned text table. /// Table, + + /// + /// Output in NDJSON (Newline Delimited JSON) format. + /// + Ndjson, } diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ParserErrorCommandState.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ParserErrorCommandState.cs index 399269a4..5057d84c 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ParserErrorCommandState.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ParserErrorCommandState.cs @@ -13,7 +13,9 @@ public ParserErrorCommandState(ErrorList errors) this.Errors = errors; } - public ErrorList Errors { get; } + public ErrorList Errors { get; init; } public override bool IsError => true; + + public override int ExitCode => 2; } \ No newline at end of file diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs index 2770f71d..c0f97220 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs @@ -185,6 +185,11 @@ public static ShellInterpreter CreateInstance() /// An array of objects to format. public static void WriteLine(string message, params object[] par) { + if (Instance?.Options?.Quiet == true) + { + return; + } + Console.WriteLine(message, par); } @@ -194,6 +199,11 @@ public static void WriteLine(string message, params object[] par) /// The message to write. public static void WriteLine(string message) { + if (Instance?.Options?.Quiet == true) + { + return; + } + Console.WriteLine(message); } @@ -202,6 +212,11 @@ public static void WriteLine(string message) /// public static void WriteLine() { + if (Instance?.Options?.Quiet == true) + { + return; + } + Console.WriteLine(); } @@ -212,6 +227,11 @@ public static void WriteLine() /// An array of objects to format. public static void Write(string message, params object[] par) { + if (Instance?.Options?.Quiet == true) + { + return; + } + Console.Write(message, par); } @@ -221,6 +241,11 @@ public static void Write(string message, params object[] par) /// The message to write. public static void Write(string message) { + if (Instance?.Options?.Quiet == true) + { + return; + } + Console.Write(message); } @@ -1361,7 +1386,7 @@ internal CommandState PrintState(CommandState state) { if (string.IsNullOrEmpty(this.StdOutRedirect)) { - WriteLine(output); + Console.Out.WriteLine(output); } else { @@ -1395,18 +1420,30 @@ internal CommandState PrintState(CommandState state) return new ErrorCommandState(e); } - var prefix = MessageService.GetString("runtime-error-prefix") ?? "error"; - AnsiConsole.MarkupLine($"{Theme.FormatError(prefix + ":")} {Markup.Escape(e.Message)}"); - if (e is IShellExceptionWithHint hinted && !string.IsNullOrEmpty(hinted.Hint)) + if (this.Options?.Output is "json" or "ndjson") { - AnsiConsole.MarkupLine(Markup.Escape(hinted.Hint)); + var errObj = new + { + status = "error", + error = e.Message, + }; + Console.Error.WriteLine(JsonSerializer.Serialize(errObj)); } - - var inner = e.InnerException; - while (inner != null) + else { - AnsiConsole.MarkupLine($" {Theme.FormatError("\u2192")} {Markup.Escape(inner.Message)}"); - inner = inner.InnerException; + var prefix = MessageService.GetString("runtime-error-prefix") ?? "error"; + AnsiConsole.MarkupLine($"{Theme.FormatError(prefix + ":")} {Markup.Escape(e.Message)}"); + if (e is IShellExceptionWithHint hinted && !string.IsNullOrEmpty(hinted.Hint)) + { + AnsiConsole.MarkupLine(Markup.Escape(hinted.Hint)); + } + + var inner = e.InnerException; + while (inner != null) + { + AnsiConsole.MarkupLine($" {Theme.FormatError("\u2192")} {Markup.Escape(inner.Message)}"); + inner = inner.InnerException; + } } return new ErrorCommandState(e); @@ -1829,6 +1866,16 @@ internal static bool IsIncompleteInput(string text) private void ReportExecutionError(Exception e, string? sourceText = null) { + if (this.Options?.Output is "json" or "ndjson") + { + var errObj = new + { + status = "error", + error = e.Message, + }; + Console.Error.WriteLine(JsonSerializer.Serialize(errObj)); + return; + } // The command already emitted a friendly diagnostic; do not print again. if (ContainsException(e)) { @@ -2059,6 +2106,28 @@ private string[] SplitIntoLines(string text) private void ReportParserErrors(ErrorList errors, string commandText) { + if (this.Options?.Output is "json" or "ndjson" && errors != null && errors.Count > 0) + { + var errorStrings = new System.Collections.Generic.List(); + foreach (var err in errors) + { + if (err != null && err.ErrorLevel == ErrorLevel.Error) + { + errorStrings.Add(err.Message ?? "Parser error"); + } + } + + if (errorStrings.Count > 0) + { + var errObj = new + { + status = "error", + error = string.Join("; ", errorStrings), + }; + Console.Error.WriteLine(JsonSerializer.Serialize(errObj)); + return; + } + } if (errors == null || errors.Count == 0) { return; diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index e4e8a833..99689953 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -75,12 +75,14 @@ public static async Task Main(string[] args) } ShellInterpreter.WriteLine(BuildHelpText()); - Environment.ExitCode = 1; + Environment.ExitCode = 2; return; } var o = new CosmosShellOptions { + Output = parseResult.GetValueForOption(optionMap.Output), + Quiet = parseResult.GetValueForOption(optionMap.Quiet), ColorSystem = parseResult.GetValueForOption(optionMap.ColorSystem), ExecuteAndQuit = parseResult.GetValueForOption(optionMap.ExecuteAndQuit), ExecuteAndContinue = parseResult.GetValueForOption(optionMap.ExecuteAndContinue), @@ -163,6 +165,11 @@ public static async Task Main(string[] args) var executeAndContinueCommand = string.IsNullOrWhiteSpace(o.ExecuteAndContinue) ? null : o.ExecuteAndContinue; var explicitCommand = executeAndContinueCommand ?? executeAndQuitCommand; + if (string.IsNullOrWhiteSpace(o.Output) && (!string.IsNullOrWhiteSpace(executeAndQuitCommand) || Console.IsInputRedirected)) + { + o.Output = "ndjson"; + } + if (o.ClearHistory) { if (File.Exists(ShellInterpreter.Instance.HistoryFile)) @@ -174,7 +181,14 @@ public static async Task Main(string[] args) return; } - AnsiConsole.Profile.Capabilities.ColorSystem = o.ColorSystem switch + var colorSystemVal = o.ColorSystem; + if (o.Quiet || string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) + { + colorSystemVal = 0; // Force NoColors in machine mode + o.Quiet = true; // Suppress informational messages to keep output clean + } + + AnsiConsole.Profile.Capabilities.ColorSystem = colorSystemVal switch { 1 => ColorSystem.Standard, 2 => ColorSystem.TrueColor, @@ -223,14 +237,29 @@ await ShellInterpreter.Instance.ConnectAsync( } catch (Exception ex) { - Environment.ExitCode = 1; + var errorState = new ErrorCommandState(ex); + Environment.ExitCode = errorState.ExitCode; + if (ConnectCommand.TryGetPrincipalIdFromRbacException(ex, out var id, out var permission)) { ConnectCommand.AskForRBacPermissions(id ?? string.Empty, permission ?? string.Empty); return; } - ShellInterpreter.WriteLine(ex.Message); + if (o.Output is "json" or "ndjson") + { + var errObj = new + { + status = "error", + error = ex.Message, + }; + Console.Error.WriteLine(System.Text.Json.JsonSerializer.Serialize(errObj)); + } + else + { + ShellInterpreter.WriteLine(ex.Message); + } + return; } } @@ -287,7 +316,7 @@ await ShellInterpreter.Instance.ConnectAsync( var state = await ShellInterpreter.Instance.ExecuteCommandAsync(script, default); if (state.IsError) { - Environment.ExitCode = 1; + Environment.ExitCode = state.ExitCode; if (executeAndContinueCommand is null) { await StopHostAsync(host, hostTask); @@ -311,7 +340,7 @@ await ShellInterpreter.Instance.ConnectAsync( var state = await ShellInterpreter.Instance.ExecuteCommandAsync(explicitCommand, default); if (state.IsError) { - Environment.ExitCode = 1; + Environment.ExitCode = state.ExitCode; if (executeAndContinueCommand is null) { await StopHostAsync(host, hostTask); @@ -447,6 +476,14 @@ private static (RootCommand Command, OptionMap Map) BuildRootCommand() getDefaultValue: () => 2, description: MessageService.GetString("help-ColorSystem")); + var output = new Option( + aliases: ["--output", "-o"], + description: MessageService.GetString("help-OutputFormat")); // Ensure this is defined in en.ftl + + var quiet = new Option( + aliases: ["--quiet", "-q"], + description: MessageService.GetString("help-Quiet")); // Ensure this is defined in en.ftl + var executeAndQuit = new Option("-c", MessageService.GetString("help-ExecuteAndQuit")); var executeAndContinue = new Option("-k", MessageService.GetString("help-ExecuteAndContinue")); @@ -513,6 +550,8 @@ private static (RootCommand Command, OptionMap Map) BuildRootCommand() var root = new RootCommand("Cosmos DB Shell") { + output, + quiet, colorSystem, executeAndQuit, executeAndContinue, @@ -536,6 +575,8 @@ private static (RootCommand Command, OptionMap Map) BuildRootCommand() }; var map = new OptionMap( + output, + quiet, colorSystem, executeAndQuit, executeAndContinue, @@ -669,6 +710,8 @@ private static void ApplyTheme(string? themeFromCli) } private sealed record OptionMap( + Option Output, + Option Quiet, Option ColorSystem, Option ExecuteAndQuit, Option ExecuteAndContinue, @@ -731,6 +774,10 @@ private static string GetDisplayName(System.CommandLine.Parsing.SymbolResult sym public class CosmosShellOptions { + public string? Output { get; set; } + + public bool Quiet { get; set; } + public int ColorSystem { get; set; } = 2; public string? ExecuteAndQuit { get; set; } diff --git a/CosmosDBShell/lang/en.ftl b/CosmosDBShell/lang/en.ftl index 97e50ef4..90509fbb 100644 --- a/CosmosDBShell/lang/en.ftl +++ b/CosmosDBShell/lang/en.ftl @@ -85,6 +85,8 @@ help-commands = Commands: help-examples = Examples: help-examples-heading = Examples help-aliases = Aliases: +help-OutputFormat = The output format to use (json, ndjson, table, csv). +help-Quiet = Suppresses standard informational output. command-help-description = Shows help information for commands command-help-description-command = The specific command to get help for From 7b82638edc57a728fc476274a8b8aa30f91d8288 Mon Sep 17 00:00:00 2001 From: KhushviB Date: Sun, 12 Jul 2026 00:02:19 +0530 Subject: [PATCH 02/26] fix: harden machine output contract against edge cases - Handle root System.CommandLine parser errors via JSON serialization to STDERR in machine modes - Propagate COSMOSDB_SHELL_FORMAT automatically to command formatters - Ignore case on JSON/NDJSON format checks - Bypass interactive RBAC prompts in quiet/machine modes - Prevent trailing blank lines in JSON serialization output - Add integration tests for deterministic machine exit codes - Update CLI argument tables in README --- .../Integration/ShellProcessTests.cs | 27 ++++++++++++++ .../UtilTest/OutputFormatTests.cs | 16 ++++++++ .../ShellInterpreter.cs | 15 ++++++-- CosmosDBShell/Program.cs | 37 ++++++++++++++++--- README.md | 17 ++++++++- 5 files changed, 103 insertions(+), 9 deletions(-) diff --git a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs index c073ee48..f7e39aa6 100644 --- a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs +++ b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs @@ -166,6 +166,33 @@ public async Task ConnectOption_WithExecuteAndQuit_RunsCommandAgainstEmulator() Assert.Contains("Current Location", result.StdOut, StringComparison.OrdinalIgnoreCase); } + [Fact] + public async Task BadArguments_WithOutputJson_ReturnsExitCode2_AndJsonOnStdErr() + { + var result = await RunShellAsync( + stdinScript: null, + extraArgs: ["--output", "json", "-c", "not a real command"], + cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal(2, result.ExitCode); + Assert.Empty(result.StdOut.Trim()); + Assert.Contains("\"status\":\"error\"", result.StdErr, StringComparison.OrdinalIgnoreCase); + Assert.Contains("not recognized as an internal or external command", result.StdErr, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ConnectionFailure_WithOutputJson_ReturnsExitCode1_AndJsonOnStdErr() + { + var result = await RunShellAsync( + stdinScript: null, + extraArgs: ["--connect", "AccountEndpoint=https://localhost:8081/;AccountKey=dummy", "--output", "json", "-c", "list databases"], + cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal(1, result.ExitCode); + Assert.Empty(result.StdOut.Trim()); + Assert.Contains("\"status\":\"error\"", result.StdErr, StringComparison.OrdinalIgnoreCase); + } + private static async Task RunShellAsync( string stdinScript, CancellationToken cancellationToken) diff --git a/CosmosDBShell.Tests/UtilTest/OutputFormatTests.cs b/CosmosDBShell.Tests/UtilTest/OutputFormatTests.cs index e2c80302..03c5d083 100644 --- a/CosmosDBShell.Tests/UtilTest/OutputFormatTests.cs +++ b/CosmosDBShell.Tests/UtilTest/OutputFormatTests.cs @@ -31,6 +31,22 @@ void TestJSon() Assert.Equal(StripWS(input), StripWS(output)); } + [Fact] + void TestNdjson() + { + var input = "[{ \"id\": 12 }, { \"id\": 13 }]"; + var element = JsonSerializer.Deserialize(input); + + var commandState = new CommandState(); + commandState.Result = new ShellJson(element); + commandState.OutputFormat = OutputFormat.Ndjson; + + var output = commandState.GenerateOutputText(); + var expected = "{\"id\":12}" + Environment.NewLine + "{\"id\":13}" + Environment.NewLine; + + Assert.Equal(expected, output); + } + [Fact] void TestCSV() { diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs index c0f97220..9f0186c2 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs @@ -1386,7 +1386,14 @@ internal CommandState PrintState(CommandState state) { if (string.IsNullOrEmpty(this.StdOutRedirect)) { - Console.Out.WriteLine(output); + if (output.EndsWith(Environment.NewLine)) + { + Console.Out.Write(output); + } + else + { + Console.Out.WriteLine(output); + } } else { @@ -1866,7 +1873,7 @@ internal static bool IsIncompleteInput(string text) private void ReportExecutionError(Exception e, string? sourceText = null) { - if (this.Options?.Output is "json" or "ndjson") + if (string.Equals(this.Options?.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(this.Options?.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) { var errObj = new { @@ -2106,7 +2113,9 @@ private string[] SplitIntoLines(string text) private void ReportParserErrors(ErrorList errors, string commandText) { - if (this.Options?.Output is "json" or "ndjson" && errors != null && errors.Count > 0) + if ((string.Equals(this.Options?.Output, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(this.Options?.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) + && errors != null && errors.Count > 0) { var errorStrings = new System.Collections.Generic.List(); foreach (var err in errors) diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index 99689953..493c6717 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -69,12 +69,31 @@ public static async Task Main(string[] args) if (parseResult.Errors.Count > 0) { - foreach (var error in parseResult.Errors) + var outputMode = parseResult.GetValueForOption(optionMap.Output); + var isMachineMode = string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) || + string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) || + parseResult.GetValueForOption(optionMap.Quiet); + + if (isMachineMode) { - ShellInterpreter.WriteLine(error.Message); + var errorStrings = parseResult.Errors.Select(e => e.Message).ToList(); + var errObj = new + { + status = "error", + error = string.Join("; ", errorStrings) + }; + Console.Error.WriteLine(System.Text.Json.JsonSerializer.Serialize(errObj)); + } + else + { + foreach (var error in parseResult.Errors) + { + ShellInterpreter.WriteLine(error.Message); + } + + ShellInterpreter.WriteLine(BuildHelpText()); } - ShellInterpreter.WriteLine(BuildHelpText()); Environment.ExitCode = 2; return; } @@ -170,6 +189,11 @@ public static async Task Main(string[] args) o.Output = "ndjson"; } + if (!string.IsNullOrWhiteSpace(o.Output)) + { + Environment.SetEnvironmentVariable("COSMOSDB_SHELL_FORMAT", o.Output); + } + if (o.ClearHistory) { if (File.Exists(ShellInterpreter.Instance.HistoryFile)) @@ -240,13 +264,16 @@ await ShellInterpreter.Instance.ConnectAsync( var errorState = new ErrorCommandState(ex); Environment.ExitCode = errorState.ExitCode; - if (ConnectCommand.TryGetPrincipalIdFromRbacException(ex, out var id, out var permission)) + if (!string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) + && !string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase) + && !o.Quiet + && ConnectCommand.TryGetPrincipalIdFromRbacException(ex, out var id, out var permission)) { ConnectCommand.AskForRBacPermissions(id ?? string.Empty, permission ?? string.Empty); return; } - if (o.Output is "json" or "ndjson") + if (string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) { var errObj = new { diff --git a/README.md b/README.md index e0ecbd56..8890689b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Azure Cosmos DB Shell +# Azure Cosmos DB Shell A terminal-native shell for Azure Cosmos DB — navigate databases like a filesystem, query interactively, and script with variables, loops, and functions. Supports Entra ID, MCP for AI tool integration, and works with the local emulator. @@ -159,6 +159,8 @@ Packaging runs produce preview versions in the form `1.0.-preview.` | Option | Description | | ------ | ----------- | +| `--output ` | The output format to use (`json`, `ndjson`, `table`, `csv`). Alias: `-o` | +| `--quiet` | Suppress standard informational output. Alias: `-q` | | `-c ` | Execute and exit | | `-k ` | Execute and stay | | `--connect ` | Connection string or endpoint URL | @@ -190,6 +192,19 @@ cosmosdbshell --connect "AccountEndpoint=...;AccountKey=..." -c seed.csh mydb my echo "seed.csh mydb mycontainer" | cosmosdbshell --connect "AccountEndpoint=...;AccountKey=..." ``` +## Deterministic Exit Codes + +When running scripts or automation, Cosmos DB Shell maps execution failures to a set of stable exit codes (accessible via `$?`, `%ERRORLEVEL%`, or `$LASTEXITCODE`): + +- **`0`**: Success +- **`1`**: Generic / Unhandled Execution Error +- **`2`**: Bad Arguments or Parser Errors +- **`3`**: Authentication or Connection Failure +- **`4`**: Not Found (e.g. Document or Resource not found) +- **`5`**: Throttled (RU budget exceeded) + +> **Machine Mode**: Using `--output json`, `--output ndjson`, or `--quiet` implicitly disables ANSI colors, suppresses connection/informational banners, and redirects early parser/connection exceptions to `STDERR` as structured JSON, guaranteeing that `STDOUT` can be safely piped to downstream JSON parsers. + ## Theming The shell ships with four built-in color profiles that can be selected at startup or swapped at runtime: From 5cca3755be646e90be316543d52a5cbc9d8c01ff Mon Sep 17 00:00:00 2001 From: KhushviB Date: Mon, 13 Jul 2026 19:39:38 +0530 Subject: [PATCH 03/26] fix/copilot-fix-suggestion --- CosmosDBShell.Tests/Integration/ShellProcessTests.cs | 2 +- .../Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs | 5 +++++ .../Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs | 2 +- CosmosDBShell/Program.cs | 10 +++++++--- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs index f7e39aa6..7bed3861 100644 --- a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs +++ b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs @@ -157,7 +157,7 @@ public async Task ConnectOption_WithExecuteAndQuit_RunsCommandAgainstEmulator() var result = await RunShellAsync( stdinScript: null, - extraArgs: ["--connect", connectionString, "-c", "connect"], + extraArgs: ["--connect", connectionString, "-c", "connect", "--output", "table"], cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(0, result.ExitCode); diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs index ae022b27..66d0e698 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs @@ -38,6 +38,11 @@ public override int ExitCode }; } + if (ex is Azure.Data.Cosmos.Shell.Parser.CommandNotFoundException) + { + return 2; + } + if (ex is ArgumentException) { return 2; diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs index 9f0186c2..3ba76b4b 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs @@ -1427,7 +1427,7 @@ internal CommandState PrintState(CommandState state) return new ErrorCommandState(e); } - if (this.Options?.Output is "json" or "ndjson") + if (string.Equals(this.Options?.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(this.Options?.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) { var errObj = new { diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index 493c6717..de2c4571 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -158,7 +158,7 @@ public static async Task Main(string[] args) if (!string.IsNullOrWhiteSpace(o.OtlpEndpoint) && !Uri.TryCreate(o.OtlpEndpoint, UriKind.Absolute, out _)) { - Environment.ExitCode = 1; + Environment.ExitCode = 2; ShellInterpreter.WriteLine(MessageService.GetArgsString( "otel-error-invalid-endpoint", "endpoint", o.OtlpEndpoint)); return; @@ -175,7 +175,7 @@ public static async Task Main(string[] args) if (!string.IsNullOrWhiteSpace(o.ExecuteAndQuit) && !string.IsNullOrWhiteSpace(o.ExecuteAndContinue)) { - Environment.ExitCode = 1; + Environment.ExitCode = 2; ShellInterpreter.WriteLine(MessageService.GetString("error-mutually-exclusive-options")); return; } @@ -273,7 +273,11 @@ await ShellInterpreter.Instance.ConnectAsync( return; } - if (string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) + var inMachineMode = o.Quiet + || string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase); + + if (inMachineMode) { var errObj = new { From 2c6a7c016d5119a0eb0651dbb4857b6fe97aaf3a Mon Sep 17 00:00:00 2001 From: Khushvi Bamrolia Date: Mon, 13 Jul 2026 19:48:39 +0530 Subject: [PATCH 04/26] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CommandState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CommandState.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CommandState.cs index 77a8049a..802d3f29 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CommandState.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CommandState.cs @@ -66,7 +66,7 @@ internal void SetFormat(string? outputFormat) } else { - throw new ShellException(MessageService.GetString("error-invalid_output_format", new Dictionary { { "format", outputFormat } })); + throw new ArgumentException(MessageService.GetString("error-invalid_output_format", new Dictionary { { "format", outputFormat } })); } } From 96c7c308167ebee11167a99edd4b1aa225545d7f Mon Sep 17 00:00:00 2001 From: Khushvi Bamrolia Date: Mon, 13 Jul 2026 20:04:35 +0530 Subject: [PATCH 05/26] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Integration/ShellProcessTests.cs | 12 +-------- .../ErrorCommandState.cs | 25 +++++++++++++++++-- .../ShellInterpreter.cs | 8 +++--- CosmosDBShell/Program.cs | 24 +++++++++++++++++- 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs index 7bed3861..bc422a96 100644 --- a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs +++ b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs @@ -181,17 +181,7 @@ public async Task BadArguments_WithOutputJson_ReturnsExitCode2_AndJsonOnStdErr() } [Fact] - public async Task ConnectionFailure_WithOutputJson_ReturnsExitCode1_AndJsonOnStdErr() - { - var result = await RunShellAsync( - stdinScript: null, - extraArgs: ["--connect", "AccountEndpoint=https://localhost:8081/;AccountKey=dummy", "--output", "json", "-c", "list databases"], - cancellationToken: TestContext.Current.CancellationToken); - - Assert.Equal(1, result.ExitCode); - Assert.Empty(result.StdOut.Trim()); - Assert.Contains("\"status\":\"error\"", result.StdErr, StringComparison.OrdinalIgnoreCase); - } + Assert.Equal(3, result.ExitCode); private static async Task RunShellAsync( string stdinScript, diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs index 66d0e698..e39ff38a 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs @@ -21,9 +21,30 @@ public override int ExitCode get { var ex = this.Exception; - if (ex is CommandException ce) + while (true) { - ex = ce.InnerException ?? ce; + if (ex is CommandException ce && ce.InnerException is not null) + { + ex = ce.InnerException; + continue; + } + + if (ex is ShellException se && se.InnerException is not null) + { + ex = se.InnerException; + continue; + } + + break; + } + + if (ex is Azure.Identity.AuthenticationFailedException + || ex is Azure.Identity.CredentialUnavailableException + || ex is System.Net.Http.HttpRequestException + || ex is System.Net.Sockets.SocketException + || ex is TimeoutException) + { + return 3; } if (ex is Microsoft.Azure.Cosmos.CosmosException cosmosEx) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs index 3ba76b4b..f1869fbf 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs @@ -1427,7 +1427,7 @@ internal CommandState PrintState(CommandState state) return new ErrorCommandState(e); } - if (string.Equals(this.Options?.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(this.Options?.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) + if (this.Options?.Quiet == true || string.Equals(this.Options?.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(this.Options?.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) { var errObj = new { @@ -1873,7 +1873,7 @@ internal static bool IsIncompleteInput(string text) private void ReportExecutionError(Exception e, string? sourceText = null) { - if (string.Equals(this.Options?.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(this.Options?.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) + if (this.Options?.Quiet == true || string.Equals(this.Options?.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(this.Options?.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) { var errObj = new { @@ -2113,10 +2113,10 @@ private string[] SplitIntoLines(string text) private void ReportParserErrors(ErrorList errors, string commandText) { - if ((string.Equals(this.Options?.Output, "json", StringComparison.OrdinalIgnoreCase) + if (((this.Options?.Quiet == true) + || string.Equals(this.Options?.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(this.Options?.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) && errors != null && errors.Count > 0) - { var errorStrings = new System.Collections.Generic.List(); foreach (var err in errors) { diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index de2c4571..6f7ec870 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -509,8 +509,30 @@ private static (RootCommand Command, OptionMap Map) BuildRootCommand() var output = new Option( aliases: ["--output", "-o"], - description: MessageService.GetString("help-OutputFormat")); // Ensure this is defined in en.ftl + parseArgument: argResult => + { + if (argResult.Tokens.Count == 0) + { + return null; + } + var token = argResult.Tokens[0].Value; + if (string.Equals(token, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(token, "js", StringComparison.OrdinalIgnoreCase) + || string.Equals(token, "ndjson", StringComparison.OrdinalIgnoreCase) + || string.Equals(token, "table", StringComparison.OrdinalIgnoreCase) + || string.Equals(token, "tbl", StringComparison.OrdinalIgnoreCase) + || string.Equals(token, "csv", StringComparison.OrdinalIgnoreCase)) + { + return token; + } + + argResult.ErrorMessage = MessageService.GetString( + "error-invalid_output_format", + new Dictionary { { "format", token } }); + return null; + }, + description: MessageService.GetString("help-OutputFormat")); var quiet = new Option( aliases: ["--quiet", "-q"], description: MessageService.GetString("help-Quiet")); // Ensure this is defined in en.ftl From 6afcabe02166bda7647b840e78ed7cd71f83aa63 Mon Sep 17 00:00:00 2001 From: Khushvi Bamrolia Date: Mon, 13 Jul 2026 20:17:08 +0530 Subject: [PATCH 06/26] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CosmosDBShell.Tests/Integration/ShellProcessTests.cs | 11 ++++++++++- CosmosDBShell/Program.cs | 11 +++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs index bc422a96..2dc9c387 100644 --- a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs +++ b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs @@ -181,8 +181,17 @@ public async Task BadArguments_WithOutputJson_ReturnsExitCode2_AndJsonOnStdErr() } [Fact] - Assert.Equal(3, result.ExitCode); + public async Task ConnectionFailure_WithOutputJson_ReturnsExitCode3_AndJsonOnStdErr() + { + var result = await RunShellAsync( + stdinScript: null, + extraArgs: ["--connect", "https://127.0.0.1:1", "--output", "json", "-c", "version"], + cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal(3, result.ExitCode); + Assert.Empty(result.StdOut.Trim()); + Assert.Contains("\"status\":\"error\"", result.StdErr, StringComparison.OrdinalIgnoreCase); + } private static async Task RunShellAsync( string stdinScript, CancellationToken cancellationToken) diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index 6f7ec870..a6a7c82e 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -517,11 +517,18 @@ private static (RootCommand Command, OptionMap Map) BuildRootCommand() } var token = argResult.Tokens[0].Value; + if (string.Equals(token, "js", StringComparison.OrdinalIgnoreCase)) + { + token = "json"; + } + else if (string.Equals(token, "tbl", StringComparison.OrdinalIgnoreCase)) + { + token = "table"; + } + if (string.Equals(token, "json", StringComparison.OrdinalIgnoreCase) - || string.Equals(token, "js", StringComparison.OrdinalIgnoreCase) || string.Equals(token, "ndjson", StringComparison.OrdinalIgnoreCase) || string.Equals(token, "table", StringComparison.OrdinalIgnoreCase) - || string.Equals(token, "tbl", StringComparison.OrdinalIgnoreCase) || string.Equals(token, "csv", StringComparison.OrdinalIgnoreCase)) { return token; From 9addb340a6421790235be76d5b1c3909d867e7da Mon Sep 17 00:00:00 2001 From: KhushviB Date: Mon, 13 Jul 2026 21:01:28 +0530 Subject: [PATCH 07/26] fix: emit mutually-exclusive options error to STDERR as JSON in machine mode --- CosmosDBShell/Program.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index a6a7c82e..87044f42 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -176,7 +176,23 @@ public static async Task Main(string[] args) if (!string.IsNullOrWhiteSpace(o.ExecuteAndQuit) && !string.IsNullOrWhiteSpace(o.ExecuteAndContinue)) { Environment.ExitCode = 2; - ShellInterpreter.WriteLine(MessageService.GetString("error-mutually-exclusive-options")); + var inMachineMode = o.Quiet + || string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase); + + if (inMachineMode) + { + var errObj = new + { + status = "error", + error = MessageService.GetString("error-mutually-exclusive-options") + }; + Console.Error.WriteLine(System.Text.Json.JsonSerializer.Serialize(errObj)); + } + else + { + ShellInterpreter.WriteLine(MessageService.GetString("error-mutually-exclusive-options")); + } return; } From 6155fde36d34d58837d23168dec81e48d47a975a Mon Sep 17 00:00:00 2001 From: Khushvi Bamrolia Date: Mon, 13 Jul 2026 21:10:25 +0530 Subject: [PATCH 08/26] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs index f1869fbf..7f7a5c81 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs @@ -2113,10 +2113,11 @@ private string[] SplitIntoLines(string text) private void ReportParserErrors(ErrorList errors, string commandText) { - if (((this.Options?.Quiet == true) + if ((this.Options?.Quiet == true || string.Equals(this.Options?.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(this.Options?.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) && errors != null && errors.Count > 0) + { var errorStrings = new System.Collections.Generic.List(); foreach (var err in errors) { From 9adee04199afb6df5a71d8258343a986272a05ff Mon Sep 17 00:00:00 2001 From: Khushvi Bamrolia Date: Mon, 13 Jul 2026 21:18:43 +0530 Subject: [PATCH 09/26] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CosmosDBShell/Program.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index 87044f42..a0a24b0e 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -69,11 +69,12 @@ public static async Task Main(string[] args) if (parseResult.Errors.Count > 0) { - var outputMode = parseResult.GetValueForOption(optionMap.Output); - var isMachineMode = string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) || - string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) || - parseResult.GetValueForOption(optionMap.Quiet); - +var outputMode = parseResult.GetValueForOption(optionMap.Output); +var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; +var isMachineMode = isNonInteractive + || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) + || parseResult.GetValueForOption(optionMap.Quiet); if (isMachineMode) { var errorStrings = parseResult.Errors.Select(e => e.Message).ToList(); From 5c719959033a8e6fb260b2e2fdf1b7f0364c3a3c Mon Sep 17 00:00:00 2001 From: Khushvi Bamrolia Date: Mon, 13 Jul 2026 22:13:38 +0530 Subject: [PATCH 10/26] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CosmosDBShell/Program.cs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index a0a24b0e..da1728cd 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -69,12 +69,12 @@ public static async Task Main(string[] args) if (parseResult.Errors.Count > 0) { -var outputMode = parseResult.GetValueForOption(optionMap.Output); -var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; -var isMachineMode = isNonInteractive - || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) - || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) - || parseResult.GetValueForOption(optionMap.Quiet); + var outputMode = parseResult.GetValueForOption(optionMap.Output); + var isNonInteractive = args.Contains("-c", StringComparer.Ordinal); + var isMachineMode = isNonInteractive + || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) + || parseResult.GetValueForOption(optionMap.Quiet); if (isMachineMode) { var errorStrings = parseResult.Errors.Select(e => e.Message).ToList(); @@ -160,8 +160,20 @@ public static async Task Main(string[] args) && !Uri.TryCreate(o.OtlpEndpoint, UriKind.Absolute, out _)) { Environment.ExitCode = 2; - ShellInterpreter.WriteLine(MessageService.GetArgsString( - "otel-error-invalid-endpoint", "endpoint", o.OtlpEndpoint)); + var msg = MessageService.GetArgsString( + "otel-error-invalid-endpoint", "endpoint", o.OtlpEndpoint); + var inMachineMode = o.Quiet + || string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase); + + if (inMachineMode) + { + Console.Error.WriteLine(System.Text.Json.JsonSerializer.Serialize(new { status = "error", error = msg })); + } + else + { + ShellInterpreter.WriteLine(msg); + } return; } } From d0289b06410501c10450453a8352aca57ae2beb8 Mon Sep 17 00:00:00 2001 From: Khushvi Bamrolia Date: Mon, 13 Jul 2026 22:41:42 +0530 Subject: [PATCH 11/26] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CosmosDBShell/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index da1728cd..486ae753 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -70,7 +70,7 @@ public static async Task Main(string[] args) if (parseResult.Errors.Count > 0) { var outputMode = parseResult.GetValueForOption(optionMap.Output); - var isNonInteractive = args.Contains("-c", StringComparer.Ordinal); + var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; var isMachineMode = isNonInteractive || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) From 562e1f35635621813f3be463affb2bff82104764 Mon Sep 17 00:00:00 2001 From: Khushvi Bamrolia Date: Mon, 13 Jul 2026 23:14:17 +0530 Subject: [PATCH 12/26] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CosmosDBShell/Program.cs | 13 +++++++------ README.md | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index 486ae753..d888bd0d 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -69,12 +69,13 @@ public static async Task Main(string[] args) if (parseResult.Errors.Count > 0) { - var outputMode = parseResult.GetValueForOption(optionMap.Output); - var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; - var isMachineMode = isNonInteractive - || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) - || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) - || parseResult.GetValueForOption(optionMap.Quiet); +var outputMode = parseResult.GetValueForOption(optionMap.Output); +var quiet = parseResult.GetValueForOption(optionMap.Quiet); +var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; +var isMachineMode = quiet + || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) + || (isNonInteractive && string.IsNullOrWhiteSpace(outputMode)); if (isMachineMode) { var errorStrings = parseResult.Errors.Select(e => e.Message).ToList(); diff --git a/README.md b/README.md index 2e6f4ef7..2dd08bd1 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ When running scripts or automation, Cosmos DB Shell maps execution failures to a - **`4`**: Not Found (e.g. Document or Resource not found) - **`5`**: Throttled (RU budget exceeded) -> **Machine Mode**: Using `--output json`, `--output ndjson`, or `--quiet` implicitly disables ANSI colors, suppresses connection/informational banners, and redirects early parser/connection exceptions to `STDERR` as structured JSON, guaranteeing that `STDOUT` can be safely piped to downstream JSON parsers. +> **Machine Mode**: Using `--output json`, `--output ndjson`, `--quiet`, or running non-interactively (e.g. `-c` or piped stdin) disables ANSI colors, suppresses connection/informational banners, and redirects early parser/connection exceptions to `STDERR` as structured JSON, guaranteeing that `STDOUT` can be safely piped to downstream parsers. ## Theming From 971f4dd57bf5b160cafcb8e165c1e25c16527683 Mon Sep 17 00:00:00 2001 From: KhushviB Date: Mon, 13 Jul 2026 23:28:38 +0530 Subject: [PATCH 13/26] fix: 2 changes suggested by copilot --- .../ConnectCommand.cs | 68 +++++++++++-------- .../ShellInterpreter.cs | 39 ++++++----- README.md | 2 +- 3 files changed, 63 insertions(+), 46 deletions(-) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs index e23dbef5..8c6da522 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs @@ -55,10 +55,12 @@ internal partial class ConnectCommand : CosmosCommand public async override Task ExecuteAsync(ShellInterpreter shell, CommandState commandState, string commandText, CancellationToken token) { + var isQuiet = shell.Options?.Quiet == true; + // If no connection string provided, show current connection info if (this.ConnectionString is null) { - return await PrintConnectionInfoAsync(shell, commandState, token); + return await PrintConnectionInfoAsync(shell, commandState, token, isQuiet); } // If already connected, inform user about switching accounts. @@ -67,7 +69,10 @@ public async override Task ExecuteAsync(ShellInterpreter shell, Co if (shell.State is ConnectedState cs) { var previousEndpoint = cs.Client.Endpoint.Host; - AnsiConsole.MarkupLine(MessageService.GetArgsString("command-connect-switching", "endpoint", previousEndpoint)); + if (!isQuiet) + { + AnsiConsole.MarkupLine(MessageService.GetArgsString("command-connect-switching", "endpoint", previousEndpoint)); + } } ConnectionMode? connectionMode = null; @@ -95,7 +100,7 @@ public async override Task ExecuteAsync(ShellInterpreter shell, Co await shell.ConnectAsync(this.ConnectionString, this.LoginHint, connectionMode, tenantId: this.TenantId, authorityHost: this.AuthorityHost, managedIdentityClientId: this.ManagedIdentityClientId, useVSCodeCredential: this.UseVSCodeCredential, subscriptionId: this.SubscriptionId, resourceGroupName: this.ResourceGroupName, token: token); var returnState = new CommandState { - IsPrinted = true, + IsPrinted = !isQuiet, }; var endpoint = ParsedDocDBConnectionString.ExtractEndpoint(this.ConnectionString); var resultElement = JsonSerializer.SerializeToElement(new Dictionary @@ -183,13 +188,16 @@ internal static void PrintConnectUsageHint(ShellInterpreter shell) AnsiConsole.MarkupLine(Markup.Escape(MessageService.GetString("command-connect-not_connected-usage-footer"))); } - private static async Task PrintConnectionInfoAsync(ShellInterpreter shell, CommandState commandState, CancellationToken token) + private static async Task PrintConnectionInfoAsync(ShellInterpreter shell, CommandState commandState, CancellationToken token, bool isQuiet) { if (shell.State is not ConnectedState connectedState) { - AnsiConsole.MarkupLine(MessageService.GetString("command-connect-not_connected")); - PrintConnectUsageHint(shell); - commandState.IsPrinted = true; + if (!isQuiet) + { + AnsiConsole.MarkupLine(MessageService.GetString("command-connect-not_connected")); + PrintConnectUsageHint(shell); + } + commandState.IsPrinted = !isQuiet; var notConnectedJson = new Dictionary { ["connected"] = false, @@ -202,36 +210,40 @@ private static async Task PrintConnectionInfoAsync(ShellInterprete token.ThrowIfCancellationRequested(); var acc = await client.ReadAccountAsync().WaitAsync(token); - AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-connect-info-title"))); + + if (!isQuiet) + { + AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-connect-info-title"))); - var table = new Table(); - table.AddColumns(string.Empty, string.Empty); - table.HideHeaders(); + var table = new Table(); + table.AddColumns(string.Empty, string.Empty); + table.HideHeaders(); - table.AddRow(MessageService.GetString("command-connect-info-account"), Theme.FormatTableValue(acc.Id)); - table.AddRow(MessageService.GetString("command-connect-info-endpoint"), Theme.FormatTableValue(client.Endpoint.ToString())); + table.AddRow(MessageService.GetString("command-connect-info-account"), Theme.FormatTableValue(acc.Id)); + table.AddRow(MessageService.GetString("command-connect-info-endpoint"), Theme.FormatTableValue(client.Endpoint.ToString())); - if (connectedState.ArmContext != null) - { - table.AddRow(MessageService.GetString("command-connect-info-arm-account"), Theme.FormatTableValue(connectedState.ArmContext.AccountResourceId.ToString())); - } + if (connectedState.ArmContext != null) + { + table.AddRow(MessageService.GetString("command-connect-info-arm-account"), Theme.FormatTableValue(connectedState.ArmContext.AccountResourceId.ToString())); + } - // Display the connection mode - var connectionMode = client.ClientOptions.ConnectionMode; - table.AddRow(MessageService.GetString("command-connect-info-mode"), Theme.FormatTableValue(connectionMode.ToString())); + // Display the connection mode + var connectionMode = client.ClientOptions.ConnectionMode; + table.AddRow(MessageService.GetString("command-connect-info-mode"), Theme.FormatTableValue(connectionMode.ToString())); - // Display the readable/writable regions - table.AddRow(MessageService.GetString("command-connect-info-read-regions"), Theme.FormatTableValue(string.Join(", ", acc.ReadableRegions.Select(r => r.Name)))); - table.AddRow(MessageService.GetString("command-connect-info-write-regions"), Theme.FormatTableValue(string.Join(", ", acc.WritableRegions.Select(r => r.Name)))); + // Display the readable/writable regions + table.AddRow(MessageService.GetString("command-connect-info-read-regions"), Theme.FormatTableValue(string.Join(", ", acc.ReadableRegions.Select(r => r.Name)))); + table.AddRow(MessageService.GetString("command-connect-info-write-regions"), Theme.FormatTableValue(string.Join(", ", acc.WritableRegions.Select(r => r.Name)))); - // Show current navigation state - string currentLocation = ShellLocation.GetCurrentLocation(shell.State) ?? ShellLocation.NotConnectedText; + // Show current navigation state + string currentLocation = ShellLocation.GetCurrentLocation(shell.State) ?? ShellLocation.NotConnectedText; - table.AddRow(MessageService.GetString("command-connect-info-location"), Theme.ConnectedStatePromt(currentLocation)); + table.AddRow(MessageService.GetString("command-connect-info-location"), Theme.ConnectedStatePromt(currentLocation)); - AnsiConsole.Write(table); + AnsiConsole.Write(table); + } - commandState.IsPrinted = true; + commandState.IsPrinted = !isQuiet; var jsonResult = new Dictionary { ["connected"] = true, diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs index 7f7a5c81..7b759130 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs @@ -588,26 +588,31 @@ internal ShellObject GetVariable(string name) internal void PrintVersion(CommandState? commandState) { + var isQuiet = this.Options?.Quiet == true; var version = GetDisplayVersion(typeof(VersionCommand).Assembly); - var versionString = MessageService.GetArgsString("command-version", "version", version); - AnsiConsole.MarkupLine(versionString); - var port = this.McpPort; - if (port != null) - { - var mcpPortString = MessageService.GetArgsString("command-version-mcp", "mcp_port", port?.ToString() ?? string.Empty); - AnsiConsole.MarkupLine(Theme.FormatWarning(mcpPortString)); - } - else - { - AnsiConsole.MarkupLine(MessageService.GetString("command-version-mcp-off")); - } - var repoUrl = GetRepositoryUrl(typeof(VersionCommand).Assembly); - if (!string.IsNullOrEmpty(repoUrl)) + + if (!isQuiet) { - var repoString = MessageService.GetArgsString("command-version-repo", "url", repoUrl); - AnsiConsole.MarkupLine(repoString); + var versionString = MessageService.GetArgsString("command-version", "version", version); + AnsiConsole.MarkupLine(versionString); + + if (port != null) + { + var mcpPortString = MessageService.GetArgsString("command-version-mcp", "mcp_port", port?.ToString() ?? string.Empty); + AnsiConsole.MarkupLine(Theme.FormatWarning(mcpPortString)); + } + else + { + AnsiConsole.MarkupLine(MessageService.GetString("command-version-mcp-off")); + } + + if (!string.IsNullOrEmpty(repoUrl)) + { + var repoString = MessageService.GetArgsString("command-version-repo", "url", repoUrl); + AnsiConsole.MarkupLine(repoString); + } } if (commandState != null) @@ -623,7 +628,7 @@ internal void PrintVersion(CommandState? commandState) var jsonElement = System.Text.Json.JsonSerializer.SerializeToElement(json); commandState.Result = new ShellJson(jsonElement); - commandState.IsPrinted = true; + commandState.IsPrinted = !isQuiet; } } diff --git a/README.md b/README.md index 2dd08bd1..11017b3f 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ When running scripts or automation, Cosmos DB Shell maps execution failures to a - **`4`**: Not Found (e.g. Document or Resource not found) - **`5`**: Throttled (RU budget exceeded) -> **Machine Mode**: Using `--output json`, `--output ndjson`, `--quiet`, or running non-interactively (e.g. `-c` or piped stdin) disables ANSI colors, suppresses connection/informational banners, and redirects early parser/connection exceptions to `STDERR` as structured JSON, guaranteeing that `STDOUT` can be safely piped to downstream parsers. +> **Machine Mode**: Using `--output json`, `--output ndjson`, or `--quiet` implicitly disables ANSI colors, suppresses connection/informational banners, and redirects early parser/connection exceptions to `STDERR` as structured JSON. For most data operations, this ensures that `STDOUT` contains only structured JSON and can be safely piped to downstream parsers (though some diagnostic or interactive commands may still emit plain text). ## Theming From ecf01773964d7d748b00bdfc603c298d197157f3 Mon Sep 17 00:00:00 2001 From: KhushviB Date: Tue, 14 Jul 2026 12:57:30 +0530 Subject: [PATCH 14/26] fix: skip stderr JSON payload if command already reported diagnostic --- .../Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs index 7b759130..723f3131 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs @@ -1878,6 +1878,12 @@ internal static bool IsIncompleteInput(string text) private void ReportExecutionError(Exception e, string? sourceText = null) { + // The command already emitted a friendly diagnostic; do not print again. + if (ContainsException(e)) + { + return; + } + if (this.Options?.Quiet == true || string.Equals(this.Options?.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(this.Options?.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) { var errObj = new @@ -1888,11 +1894,6 @@ private void ReportExecutionError(Exception e, string? sourceText = null) Console.Error.WriteLine(JsonSerializer.Serialize(errObj)); return; } - // The command already emitted a friendly diagnostic; do not print again. - if (ContainsException(e)) - { - return; - } if (e is PositionalException pe) { From 96b7f9f072a730177e2153904d0d1b39fa63caba Mon Sep 17 00:00:00 2001 From: Khushvi Bamrolia Date: Tue, 14 Jul 2026 13:08:33 +0530 Subject: [PATCH 15/26] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CosmosDBShell/Program.cs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index d888bd0d..0c852f03 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -69,13 +69,13 @@ public static async Task Main(string[] args) if (parseResult.Errors.Count > 0) { -var outputMode = parseResult.GetValueForOption(optionMap.Output); -var quiet = parseResult.GetValueForOption(optionMap.Quiet); -var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; -var isMachineMode = quiet - || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) - || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) - || (isNonInteractive && string.IsNullOrWhiteSpace(outputMode)); + var outputMode = parseResult.GetValueForOption(optionMap.Output); + var quiet = parseResult.GetValueForOption(optionMap.Quiet); + var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; + var isMachineMode = quiet + || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) + || (isNonInteractive && string.IsNullOrWhiteSpace(outputMode)); if (isMachineMode) { var errorStrings = parseResult.Errors.Select(e => e.Message).ToList(); @@ -165,7 +165,9 @@ public static async Task Main(string[] args) "otel-error-invalid-endpoint", "endpoint", o.OtlpEndpoint); var inMachineMode = o.Quiet || string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) - || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase); + || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase) + || ((args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected) + && string.IsNullOrWhiteSpace(o.Output)); if (inMachineMode) { @@ -192,7 +194,9 @@ public static async Task Main(string[] args) Environment.ExitCode = 2; var inMachineMode = o.Quiet || string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) - || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase); + || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase) + || ((args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected) + && string.IsNullOrWhiteSpace(o.Output)); if (inMachineMode) { From 5dddf317c60e4af0ea97d8df94d71386e2a3687f Mon Sep 17 00:00:00 2001 From: Khushvi Bamrolia Date: Tue, 14 Jul 2026 13:37:13 +0530 Subject: [PATCH 16/26] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CosmosDBShell/Program.cs | 22 +++++++++++----------- README.md | 7 +------ 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index 0c852f03..7668ce7d 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -69,13 +69,13 @@ public static async Task Main(string[] args) if (parseResult.Errors.Count > 0) { - var outputMode = parseResult.GetValueForOption(optionMap.Output); - var quiet = parseResult.GetValueForOption(optionMap.Quiet); - var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; - var isMachineMode = quiet - || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) - || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) - || (isNonInteractive && string.IsNullOrWhiteSpace(outputMode)); +var outputMode = parseResult.GetValueForOption(optionMap.Output); +var quiet = parseResult.GetValueForOption(optionMap.Quiet); +var isNonInteractive = args.Contains("-c", StringComparer.Ordinal); +var isMachineMode = quiet + || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) + || (isNonInteractive && string.IsNullOrWhiteSpace(outputMode)); if (isMachineMode) { var errorStrings = parseResult.Errors.Select(e => e.Message).ToList(); @@ -218,10 +218,10 @@ public static async Task Main(string[] args) var executeAndContinueCommand = string.IsNullOrWhiteSpace(o.ExecuteAndContinue) ? null : o.ExecuteAndContinue; var explicitCommand = executeAndContinueCommand ?? executeAndQuitCommand; - if (string.IsNullOrWhiteSpace(o.Output) && (!string.IsNullOrWhiteSpace(executeAndQuitCommand) || Console.IsInputRedirected)) - { - o.Output = "ndjson"; - } +if (string.IsNullOrWhiteSpace(o.Output) && !string.IsNullOrWhiteSpace(executeAndQuitCommand)) +{ + o.Output = "ndjson"; +} if (!string.IsNullOrWhiteSpace(o.Output)) { diff --git a/README.md b/README.md index 11017b3f..33652563 100644 --- a/README.md +++ b/README.md @@ -198,14 +198,9 @@ echo "seed.csh mydb mycontainer" | cosmosdbshell --connect "AccountEndpoint=...; When running scripts or automation, Cosmos DB Shell maps execution failures to a set of stable exit codes (accessible via `$?`, `%ERRORLEVEL%`, or `$LASTEXITCODE`): -- **`0`**: Success -- **`1`**: Generic / Unhandled Execution Error -- **`2`**: Bad Arguments or Parser Errors -- **`3`**: Authentication or Connection Failure -- **`4`**: Not Found (e.g. Document or Resource not found) - **`5`**: Throttled (RU budget exceeded) -> **Machine Mode**: Using `--output json`, `--output ndjson`, or `--quiet` implicitly disables ANSI colors, suppresses connection/informational banners, and redirects early parser/connection exceptions to `STDERR` as structured JSON. For most data operations, this ensures that `STDOUT` contains only structured JSON and can be safely piped to downstream parsers (though some diagnostic or interactive commands may still emit plain text). +> **Machine Mode**: Using `--output json`, `--output ndjson`, or `--quiet` (or running `-c` without specifying `--output`) disables ANSI colors, suppresses connection/informational banners, and redirects early parser/connection exceptions to `STDERR` as structured JSON. For most data operations, this ensures that `STDOUT` contains only structured JSON and can be safely piped to downstream parsers (though some diagnostic or interactive commands may still emit plain text). ## Theming From d51071b3b7d72b3a568b6d56fbb727c32ebdb1a0 Mon Sep 17 00:00:00 2001 From: Khushvi Bamrolia Date: Tue, 14 Jul 2026 13:58:07 +0530 Subject: [PATCH 17/26] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs | 3 +++ CosmosDBShell/Program.cs | 2 +- README.md | 6 +++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs index e39ff38a..9f040a7a 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ErrorCommandState.cs @@ -55,6 +55,9 @@ public override int ExitCode System.Net.HttpStatusCode.Forbidden => 3, System.Net.HttpStatusCode.NotFound => 4, System.Net.HttpStatusCode.TooManyRequests => 5, + System.Net.HttpStatusCode.RequestTimeout => 3, + System.Net.HttpStatusCode.ServiceUnavailable => 3, + System.Net.HttpStatusCode.GatewayTimeout => 3, _ => 1, }; } diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index 7668ce7d..d09f11e4 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -71,7 +71,7 @@ public static async Task Main(string[] args) { var outputMode = parseResult.GetValueForOption(optionMap.Output); var quiet = parseResult.GetValueForOption(optionMap.Quiet); -var isNonInteractive = args.Contains("-c", StringComparer.Ordinal); +var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; var isMachineMode = quiet || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) diff --git a/README.md b/README.md index 33652563..0bbc35bf 100644 --- a/README.md +++ b/README.md @@ -198,8 +198,12 @@ echo "seed.csh mydb mycontainer" | cosmosdbshell --connect "AccountEndpoint=...; When running scripts or automation, Cosmos DB Shell maps execution failures to a set of stable exit codes (accessible via `$?`, `%ERRORLEVEL%`, or `$LASTEXITCODE`): +- **`0`**: Success +- **`1`**: Generic error +- **`2`**: Bad arguments / parser errors +- **`3`**: Auth / connection failure +- **`4`**: Not found - **`5`**: Throttled (RU budget exceeded) - > **Machine Mode**: Using `--output json`, `--output ndjson`, or `--quiet` (or running `-c` without specifying `--output`) disables ANSI colors, suppresses connection/informational banners, and redirects early parser/connection exceptions to `STDERR` as structured JSON. For most data operations, this ensures that `STDOUT` contains only structured JSON and can be safely piped to downstream parsers (though some diagnostic or interactive commands may still emit plain text). ## Theming From ab8de85a75a0f91b5220bdd444d37a4936ada544 Mon Sep 17 00:00:00 2001 From: Khushvi Bamrolia Date: Tue, 14 Jul 2026 15:08:01 +0530 Subject: [PATCH 18/26] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CosmosDBShell/Program.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index d09f11e4..b021ae8e 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -69,13 +69,13 @@ public static async Task Main(string[] args) if (parseResult.Errors.Count > 0) { -var outputMode = parseResult.GetValueForOption(optionMap.Output); -var quiet = parseResult.GetValueForOption(optionMap.Quiet); -var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; -var isMachineMode = quiet - || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) - || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) - || (isNonInteractive && string.IsNullOrWhiteSpace(outputMode)); + var outputMode = parseResult.GetValueForOption(optionMap.Output); + var quiet = parseResult.GetValueForOption(optionMap.Quiet); + var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; + var isMachineMode = quiet + || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) + || (isNonInteractive && string.IsNullOrWhiteSpace(outputMode)); if (isMachineMode) { var errorStrings = parseResult.Errors.Select(e => e.Message).ToList(); @@ -218,10 +218,10 @@ public static async Task Main(string[] args) var executeAndContinueCommand = string.IsNullOrWhiteSpace(o.ExecuteAndContinue) ? null : o.ExecuteAndContinue; var explicitCommand = executeAndContinueCommand ?? executeAndQuitCommand; -if (string.IsNullOrWhiteSpace(o.Output) && !string.IsNullOrWhiteSpace(executeAndQuitCommand)) -{ - o.Output = "ndjson"; -} + if (string.IsNullOrWhiteSpace(o.Output) && !string.IsNullOrWhiteSpace(executeAndQuitCommand)) + { + o.Output = "ndjson"; + } if (!string.IsNullOrWhiteSpace(o.Output)) { @@ -576,7 +576,7 @@ private static (RootCommand Command, OptionMap Map) BuildRootCommand() description: MessageService.GetString("help-OutputFormat")); var quiet = new Option( aliases: ["--quiet", "-q"], - description: MessageService.GetString("help-Quiet")); // Ensure this is defined in en.ftl + description: MessageService.GetString("help-Quiet")); var executeAndQuit = new Option("-c", MessageService.GetString("help-ExecuteAndQuit")); var executeAndContinue = new Option("-k", MessageService.GetString("help-ExecuteAndContinue")); From 765b194e23707fd8413b746cca3513658172f225 Mon Sep 17 00:00:00 2001 From: Khushvi Bamrolia Date: Tue, 14 Jul 2026 15:13:32 +0530 Subject: [PATCH 19/26] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CosmosDBShell/Program.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index b021ae8e..c2efc686 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -71,11 +71,16 @@ public static async Task Main(string[] args) { var outputMode = parseResult.GetValueForOption(optionMap.Output); var quiet = parseResult.GetValueForOption(optionMap.Quiet); - var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; - var isMachineMode = quiet - || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) - || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) - || (isNonInteractive && string.IsNullOrWhiteSpace(outputMode)); +var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; + +var machineModeRequested = + args.Contains("--quiet", StringComparer.Ordinal) || args.Contains("-q", StringComparer.Ordinal) + || args.Contains("--output", StringComparer.Ordinal) || args.Contains("-o", StringComparer.Ordinal); + +var isMachineMode = machineModeRequested + || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) + || (isNonInteractive && string.IsNullOrWhiteSpace(outputMode)); if (isMachineMode) { var errorStrings = parseResult.Errors.Select(e => e.Message).ToList(); From 70498e5d8000c774601fb2b7ed74840dffb61381 Mon Sep 17 00:00:00 2001 From: Khushvi Bamrolia Date: Tue, 14 Jul 2026 15:21:29 +0530 Subject: [PATCH 20/26] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CosmosDBShell/Program.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index c2efc686..4a3b234f 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -71,16 +71,12 @@ public static async Task Main(string[] args) { var outputMode = parseResult.GetValueForOption(optionMap.Output); var quiet = parseResult.GetValueForOption(optionMap.Quiet); -var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; + var isNonInteractive = args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected; -var machineModeRequested = - args.Contains("--quiet", StringComparer.Ordinal) || args.Contains("-q", StringComparer.Ordinal) - || args.Contains("--output", StringComparer.Ordinal) || args.Contains("-o", StringComparer.Ordinal); - -var isMachineMode = machineModeRequested - || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) - || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) - || (isNonInteractive && string.IsNullOrWhiteSpace(outputMode)); + var isMachineMode = quiet + || string.Equals(outputMode, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(outputMode, "ndjson", StringComparison.OrdinalIgnoreCase) + || (isNonInteractive && string.IsNullOrWhiteSpace(outputMode)); if (isMachineMode) { var errorStrings = parseResult.Errors.Select(e => e.Message).ToList(); From dc5b027366320c30c3b3e97f4dd466718eff5d28 Mon Sep 17 00:00:00 2001 From: KhushviB Date: Wed, 15 Jul 2026 00:16:47 +0530 Subject: [PATCH 21/26] fix: normalize --output to lowercase and fix connectionMode/currentLocation scoping - Normalize the accepted --output token to lowercase in parseArgument so o.Output is always a canonical lowercase value (json, ndjson, table, csv). Previously the raw user-supplied casing was stored, making any plain equality check case-sensitive. - Hoist connectionMode and currentLocation declarations above the isQuiet guard in PrintConnectionInfoAsync so they are in scope for the JSON result dictionary regardless of quiet mode. The previous placement caused CS0841 build errors when isQuiet was true. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ConnectCommand.cs | 7 +++---- CosmosDBShell/Program.cs | 16 ++++++++-------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs index 8c6da522..08235661 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs @@ -210,7 +210,9 @@ private static async Task PrintConnectionInfoAsync(ShellInterprete token.ThrowIfCancellationRequested(); var acc = await client.ReadAccountAsync().WaitAsync(token); - + var connectionMode = client.ClientOptions.ConnectionMode; + string currentLocation = ShellLocation.GetCurrentLocation(shell.State) ?? ShellLocation.NotConnectedText; + if (!isQuiet) { AnsiConsole.MarkupLine(Theme.FormatSectionHeader(MessageService.GetString("command-connect-info-title"))); @@ -228,7 +230,6 @@ private static async Task PrintConnectionInfoAsync(ShellInterprete } // Display the connection mode - var connectionMode = client.ClientOptions.ConnectionMode; table.AddRow(MessageService.GetString("command-connect-info-mode"), Theme.FormatTableValue(connectionMode.ToString())); // Display the readable/writable regions @@ -236,8 +237,6 @@ private static async Task PrintConnectionInfoAsync(ShellInterprete table.AddRow(MessageService.GetString("command-connect-info-write-regions"), Theme.FormatTableValue(string.Join(", ", acc.WritableRegions.Select(r => r.Name)))); // Show current navigation state - string currentLocation = ShellLocation.GetCurrentLocation(shell.State) ?? ShellLocation.NotConnectedText; - table.AddRow(MessageService.GetString("command-connect-info-location"), Theme.ConnectedStatePromt(currentLocation)); AnsiConsole.Write(table); diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index 4a3b234f..f086d9e8 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -299,19 +299,19 @@ await ShellInterpreter.Instance.ConnectAsync( var errorState = new ErrorCommandState(ex); Environment.ExitCode = errorState.ExitCode; - if (!string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) - && !string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase) - && !o.Quiet + var inMachineMode = o.Quiet + || string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase) + || ((args.Contains("-c", StringComparer.Ordinal) || Console.IsInputRedirected) + && string.IsNullOrWhiteSpace(o.Output)); + + if (!inMachineMode && ConnectCommand.TryGetPrincipalIdFromRbacException(ex, out var id, out var permission)) { ConnectCommand.AskForRBacPermissions(id ?? string.Empty, permission ?? string.Empty); return; } - var inMachineMode = o.Quiet - || string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) - || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase); - if (inMachineMode) { var errObj = new @@ -566,7 +566,7 @@ private static (RootCommand Command, OptionMap Map) BuildRootCommand() || string.Equals(token, "table", StringComparison.OrdinalIgnoreCase) || string.Equals(token, "csv", StringComparison.OrdinalIgnoreCase)) { - return token; + return token.ToLowerInvariant(); } argResult.ErrorMessage = MessageService.GetString( From 7f9287d6f67a59253d64b59b21367622625c6e2e Mon Sep 17 00:00:00 2001 From: KhushviB Date: Wed, 15 Jul 2026 00:30:21 +0530 Subject: [PATCH 22/26] test: avoid forced stdin redirection in process harness Only redirect stdin in RunShellAsync when stdinScript is provided. This keeps no-stdin invocations from being misclassified as machine mode via Console.IsInputRedirected and preserves expected root parser behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CosmosDBShell.Tests/Integration/ShellProcessTests.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs index 2dc9c387..e4eda1e4 100644 --- a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs +++ b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs @@ -217,7 +217,7 @@ private static async Task RunShellAsync( FileName = dotnet, WorkingDirectory = AppContext.BaseDirectory, UseShellExecute = false, - RedirectStandardInput = true, + RedirectStandardInput = stdinScript != null, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, @@ -271,10 +271,6 @@ private static async Task RunShellAsync( await process.StandardInput.WriteLineAsync(); process.StandardInput.Close(); } - else - { - process.StandardInput.Close(); - } // Enforce a reasonable timeout so a hang does not block the test run indefinitely. using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); From 162f63586c66b91cad49ccf40521a6d2e92e67d0 Mon Sep 17 00:00:00 2001 From: KhushviB Date: Wed, 15 Jul 2026 00:47:03 +0530 Subject: [PATCH 23/26] fix: keep machine-mode errors off stdout - Gate connect RBAC guidance behind interactive mode so quiet/machine runs do not emit interactive stdout text. - In ShellInterpreter.PrintState, treat OperationCanceledException like other machine-mode failures by emitting structured JSON to stderr instead of a warning on stdout. - Reuse a shared inMachineMode condition in the catch block. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ConnectCommand.cs | 2 +- .../ShellInterpreter.cs | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs index 08235661..b60192d9 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ConnectCommand.cs @@ -116,7 +116,7 @@ public async override Task ExecuteAsync(ShellInterpreter shell, Co } catch (Exception e) { - if (TryGetPrincipalIdFromRbacException(e, out var id, out var permission)) + if (!isQuiet && TryGetPrincipalIdFromRbacException(e, out var id, out var permission)) { AskForRBacPermissions(id ?? string.Empty, permission ?? string.Empty); return commandState; diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs index 723f3131..e594626a 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs @@ -1421,10 +1421,24 @@ internal CommandState PrintState(CommandState state) // exceptions) carry an actionable message; the stack trace is noise // for end users. Show only Message chains and let --verbose surface // the full exception. + var inMachineMode = this.Options?.Quiet == true + || string.Equals(this.Options?.Output, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(this.Options?.Output, "ndjson", StringComparison.OrdinalIgnoreCase); + if (e is OperationCanceledException) { var canceled = MessageService.GetString("runtime-error-canceled"); - if (!string.IsNullOrEmpty(canceled)) + + if (inMachineMode) + { + var errObj = new + { + status = "error", + error = string.IsNullOrEmpty(canceled) ? e.Message : canceled, + }; + Console.Error.WriteLine(JsonSerializer.Serialize(errObj)); + } + else if (!string.IsNullOrEmpty(canceled)) { AnsiConsole.MarkupLine(Theme.FormatWarning(canceled)); } @@ -1432,7 +1446,7 @@ internal CommandState PrintState(CommandState state) return new ErrorCommandState(e); } - if (this.Options?.Quiet == true || string.Equals(this.Options?.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(this.Options?.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) + if (inMachineMode) { var errObj = new { From 564666c602a90503d623d4da53914b277e933141 Mon Sep 17 00:00:00 2001 From: KhushviB Date: Wed, 15 Jul 2026 00:59:07 +0530 Subject: [PATCH 24/26] fix: silence AnsiConsole in structured output modes When --output json or --output ndjson is selected, route AnsiConsole output through TextWriter.Null using a dedicated AnsiConsoleSettings instance. This prevents accidental AnsiConsole writes from commands from polluting machine-readable stdout, while command-state output still uses Console.Out. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CosmosDBShell/Program.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index f086d9e8..ec652e48 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -240,8 +240,12 @@ public static async Task Main(string[] args) return; } + var structuredOutputMode = + string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase); + var colorSystemVal = o.ColorSystem; - if (o.Quiet || string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase)) + if (o.Quiet || structuredOutputMode) { colorSystemVal = 0; // Force NoColors in machine mode o.Quiet = true; // Suppress informational messages to keep output clean @@ -256,6 +260,18 @@ public static async Task Main(string[] args) ApplyTheme(o.Theme); + if (structuredOutputMode) + { + // Keep machine-mode stdout deterministic even if a command + // accidentally writes via AnsiConsole instead of command state output. + AnsiConsole.Console = AnsiConsole.Create(new AnsiConsoleSettings + { + Ansi = AnsiSupport.No, + ColorSystem = ColorSystemSupport.NoColors, + Out = new AnsiConsoleOutput(TextWriter.Null), + }); + } + ShellInterpreter.Instance.Options = o; // Enable diagnostic logging before connecting so the startup --connect From ae681f060c510d7b69f190953e60212c1ba4220e Mon Sep 17 00:00:00 2001 From: KhushviB Date: Wed, 15 Jul 2026 01:14:12 +0530 Subject: [PATCH 25/26] fix: treat quiet startup as machine mode - Apply startup machine-mode handling to quiet mode as well as json/ndjson. - Route startup MCP errors through structured stderr output in machine mode instead of unconditional AnsiConsole stdout writes. - Keep interactive behavior unchanged for non-machine runs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CosmosDBShell/Program.cs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index ec652e48..b737a704 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -243,9 +243,10 @@ public static async Task Main(string[] args) var structuredOutputMode = string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase); + var startupMachineMode = o.Quiet || structuredOutputMode; var colorSystemVal = o.ColorSystem; - if (o.Quiet || structuredOutputMode) + if (startupMachineMode) { colorSystemVal = 0; // Force NoColors in machine mode o.Quiet = true; // Suppress informational messages to keep output clean @@ -260,7 +261,7 @@ public static async Task Main(string[] args) ApplyTheme(o.Theme); - if (structuredOutputMode) + if (startupMachineMode) { // Keep machine-mode stdout deterministic even if a command // accidentally writes via AnsiConsole instead of command state output. @@ -274,6 +275,22 @@ public static async Task Main(string[] args) ShellInterpreter.Instance.Options = o; + void WriteStartupError(string message) + { + if (startupMachineMode) + { + var errObj = new + { + status = "error", + error = message, + }; + Console.Error.WriteLine(System.Text.Json.JsonSerializer.Serialize(errObj)); + return; + } + + AnsiConsole.WriteLine(message); + } + // Enable diagnostic logging before connecting so the startup --connect // event is captured in the log. if (o.EnableDiagnostics) @@ -353,7 +370,7 @@ await ShellInterpreter.Instance.ConnectAsync( { if (mcpPort <= 0) { - AnsiConsole.WriteLine(MessageService.GetString("mcp-error-invalid-port")); + WriteStartupError(MessageService.GetString("mcp-error-invalid-port")); Environment.ExitCode = 1; return; } @@ -364,7 +381,7 @@ await ShellInterpreter.Instance.ConnectAsync( } catch (Exception ex) { - AnsiConsole.WriteLine(MessageService.GetArgsString("mcp-error-creating-server", "message", ex.Message)); + WriteStartupError(MessageService.GetArgsString("mcp-error-creating-server", "message", ex.Message)); Environment.ExitCode = 1; return; } @@ -382,7 +399,7 @@ await ShellInterpreter.Instance.ConnectAsync( } catch (Exception ex) { - AnsiConsole.WriteLine(MessageService.GetArgsString("mcp-error-server-failed-start", "message", ex.Message)); + WriteStartupError(MessageService.GetArgsString("mcp-error-server-failed-start", "message", ex.Message)); Environment.ExitCode = 1; } }); From 4fbbabf90eaea8e646e5f8b613423e9a7de5620d Mon Sep 17 00:00:00 2001 From: KhushviB Date: Wed, 15 Jul 2026 01:31:07 +0530 Subject: [PATCH 26/26] fix: harden machine-mode output end-to-end - Treat startup machine mode consistently for quiet/json/ndjson and suppress startup informational stdout (clear-history/theme warnings). - Route startup machine-mode errors through structured stderr JSON and return exit code 2 for invalid --mcp port argument. - In PrintState, bypass AnsiConsole markup rendering in machine mode so JSON/ text outputs always go through Console.Out. - Expand process integration coverage for implicit -c ndjson output, machine- mode clear-history behavior, and invalid --mcp port behavior. - Clarify README machine-mode wording to avoid over-promising stdout guarantees. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Integration/ShellProcessTests.cs | 71 ++++++++++++++++-- .../ShellInterpreter.cs | 11 ++- CosmosDBShell/Program.cs | 73 +++++++++++-------- README.md | 2 +- 4 files changed, 116 insertions(+), 41 deletions(-) diff --git a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs index e4eda1e4..50099740 100644 --- a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs +++ b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs @@ -6,6 +6,7 @@ namespace CosmosShell.Tests.Integration; using System.Diagnostics; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; using Azure.Data.Cosmos.Shell.Util; @@ -192,6 +193,55 @@ public async Task ConnectionFailure_WithOutputJson_ReturnsExitCode3_AndJsonOnStd Assert.Empty(result.StdOut.Trim()); Assert.Contains("\"status\":\"error\"", result.StdErr, StringComparison.OrdinalIgnoreCase); } + + [Fact] + public async Task ExecuteAndQuit_WithoutOutput_EmitsParseableNdjsonOnStdOut() + { + var result = await RunShellAsync( + stdinScript: null, + extraArgs: ["-c", "version"], + cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal(0, result.ExitCode); + Assert.Empty(result.StdErr.Trim()); + + var lines = result.StdOut + .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.NotEmpty(lines); + + foreach (var line in lines) + { + using var doc = JsonDocument.Parse(line); + Assert.True(doc.RootElement.ValueKind == JsonValueKind.Object || doc.RootElement.ValueKind == JsonValueKind.Array); + } + } + + [Fact] + public async Task InvalidMcpPort_WithOutputJson_ReturnsExitCode2_AndJsonOnStdErr() + { + var result = await RunShellAsync( + stdinScript: null, + extraArgs: ["--output", "json", "--mcp", "0"], + cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal(2, result.ExitCode); + Assert.Empty(result.StdOut.Trim()); + Assert.Contains("\"status\":\"error\"", result.StdErr, StringComparison.OrdinalIgnoreCase); + Assert.Contains("port", result.StdErr, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ClearHistory_WithOutputJson_DoesNotWriteInformationalStdOut() + { + var result = await RunShellAsync( + stdinScript: null, + extraArgs: ["--output", "json", "--clear-history"], + cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal(0, result.ExitCode); + Assert.Empty(result.StdOut.Trim()); + } + private static async Task RunShellAsync( string stdinScript, CancellationToken cancellationToken) @@ -204,6 +254,11 @@ private static async Task RunShellAsync( IEnumerable? extraArgs, CancellationToken cancellationToken) { + var argsList = extraArgs?.ToList(); + var requiresOwnedStdin = stdinScript != null + || (argsList?.Contains("-c") == true) + || (argsList?.Contains("-k") == true); + var shellDll = Path.Combine(AppContext.BaseDirectory, "CosmosDBShell.dll"); if (!File.Exists(shellDll)) { @@ -217,7 +272,7 @@ private static async Task RunShellAsync( FileName = dotnet, WorkingDirectory = AppContext.BaseDirectory, UseShellExecute = false, - RedirectStandardInput = stdinScript != null, + RedirectStandardInput = requiresOwnedStdin, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, @@ -226,9 +281,9 @@ private static async Task RunShellAsync( }; startInfo.ArgumentList.Add(shellDll); - if (extraArgs != null) + if (argsList != null) { - foreach (var a in extraArgs) + foreach (var a in argsList) { startInfo.ArgumentList.Add(a); } @@ -265,10 +320,14 @@ private static async Task RunShellAsync( process.BeginOutputReadLine(); process.BeginErrorReadLine(); - if (stdinScript != null) + if (requiresOwnedStdin) { - await process.StandardInput.WriteAsync(stdinScript); - await process.StandardInput.WriteLineAsync(); + if (stdinScript != null) + { + await process.StandardInput.WriteAsync(stdinScript); + await process.StandardInput.WriteLineAsync(); + } + process.StandardInput.Close(); } diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs index e594626a..6f8b601f 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ShellInterpreter.cs @@ -1351,6 +1351,9 @@ internal CommandState PrintState(CommandState state) try { string? output; + var inMachineMode = this.Options?.Quiet == true + || string.Equals(this.Options?.Output, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(this.Options?.Output, "ndjson", StringComparison.OrdinalIgnoreCase); if (state.Result?.DataType == Parser.DataType.Json) { @@ -1358,7 +1361,9 @@ internal CommandState PrintState(CommandState state) // syntax highlighting using the configured Spectre.Console theme. File // redirection still receives plain text so downstream tooling and tests // are unaffected. - if (state.OutputFormat == OutputFormat.JSon && string.IsNullOrEmpty(this.StdOutRedirect)) + if (!inMachineMode + && state.OutputFormat == OutputFormat.JSon + && string.IsNullOrEmpty(this.StdOutRedirect)) { var element = (JsonElement?)state.Result.ConvertShellObject(Parser.DataType.Json); if (element.HasValue) @@ -1378,7 +1383,9 @@ internal CommandState PrintState(CommandState state) // When a text result carries a highlighter (e.g. script bodies), apply it // when writing to the terminal. Redirection and piping still receive plain // text so downstream tooling and tests are unaffected. - if (output != null && string.IsNullOrEmpty(this.StdOutRedirect) + if (!inMachineMode + && output != null + && string.IsNullOrEmpty(this.StdOutRedirect) && state.Result is ShellText { Highlighter: { } highlighter }) { AnsiConsole.MarkupLine(highlighter(output)); diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index b737a704..829acfce 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -229,6 +229,27 @@ public static async Task Main(string[] args) Environment.SetEnvironmentVariable("COSMOSDB_SHELL_FORMAT", o.Output); } + var structuredOutputMode = + string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) + || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase); + var startupMachineMode = o.Quiet || structuredOutputMode; + + void WriteStartupError(string message) + { + if (startupMachineMode) + { + var errObj = new + { + status = "error", + error = message, + }; + Console.Error.WriteLine(System.Text.Json.JsonSerializer.Serialize(errObj)); + return; + } + + AnsiConsole.WriteLine(message); + } + if (o.ClearHistory) { if (File.Exists(ShellInterpreter.Instance.HistoryFile)) @@ -236,15 +257,13 @@ public static async Task Main(string[] args) File.Delete(ShellInterpreter.Instance.HistoryFile); } - ShellInterpreter.WriteLine(MessageService.GetString("shell-hisory_file_deleted")); + if (!startupMachineMode) + { + ShellInterpreter.WriteLine(MessageService.GetString("shell-hisory_file_deleted")); + } return; } - var structuredOutputMode = - string.Equals(o.Output, "json", StringComparison.OrdinalIgnoreCase) - || string.Equals(o.Output, "ndjson", StringComparison.OrdinalIgnoreCase); - var startupMachineMode = o.Quiet || structuredOutputMode; - var colorSystemVal = o.ColorSystem; if (startupMachineMode) { @@ -259,7 +278,7 @@ public static async Task Main(string[] args) _ => ColorSystem.NoColors, }; - ApplyTheme(o.Theme); + ApplyTheme(o.Theme, startupMachineMode); if (startupMachineMode) { @@ -275,22 +294,6 @@ public static async Task Main(string[] args) ShellInterpreter.Instance.Options = o; - void WriteStartupError(string message) - { - if (startupMachineMode) - { - var errObj = new - { - status = "error", - error = message, - }; - Console.Error.WriteLine(System.Text.Json.JsonSerializer.Serialize(errObj)); - return; - } - - AnsiConsole.WriteLine(message); - } - // Enable diagnostic logging before connecting so the startup --connect // event is captured in the log. if (o.EnableDiagnostics) @@ -371,7 +374,7 @@ await ShellInterpreter.Instance.ConnectAsync( if (mcpPort <= 0) { WriteStartupError(MessageService.GetString("mcp-error-invalid-port")); - Environment.ExitCode = 1; + Environment.ExitCode = 2; return; } @@ -802,7 +805,7 @@ private static string BuildHelpText() /// environment variable, then the built-in default. Unknown names emit a warning /// to standard output and fall back to the default profile. /// - private static void ApplyTheme(string? themeFromCli) + private static void ApplyTheme(string? themeFromCli, bool suppressStartupWarnings) { // Always scan the user themes directory so file-loaded themes are visible // to --theme, the COSMOSDB_SHELL_THEME env var, and the in-shell `theme` @@ -812,7 +815,10 @@ private static void ApplyTheme(string? themeFromCli) registry.LoadFromDirectory(ThemeFile.DefaultUserThemesDirectory()); foreach (var warning in registry.Warnings) { - ShellInterpreter.WriteLine(warning); + if (!suppressStartupWarnings) + { + ShellInterpreter.WriteLine(warning); + } } var requested = !string.IsNullOrWhiteSpace(themeFromCli) @@ -826,12 +832,15 @@ private static void ApplyTheme(string? themeFromCli) if (!ThemeProfiles.TryGet(requested, out var profile)) { - ShellInterpreter.WriteLine(MessageService.GetArgsString( - "warning-unknown-theme", - "name", - requested, - "themes", - string.Join(", ", registry.All.Keys))); + if (!suppressStartupWarnings) + { + ShellInterpreter.WriteLine(MessageService.GetArgsString( + "warning-unknown-theme", + "name", + requested, + "themes", + string.Join(", ", registry.All.Keys))); + } } Theme.Apply(profile); diff --git a/README.md b/README.md index 0bbc35bf..61f88c75 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ When running scripts or automation, Cosmos DB Shell maps execution failures to a - **`3`**: Auth / connection failure - **`4`**: Not found - **`5`**: Throttled (RU budget exceeded) -> **Machine Mode**: Using `--output json`, `--output ndjson`, or `--quiet` (or running `-c` without specifying `--output`) disables ANSI colors, suppresses connection/informational banners, and redirects early parser/connection exceptions to `STDERR` as structured JSON. For most data operations, this ensures that `STDOUT` contains only structured JSON and can be safely piped to downstream parsers (though some diagnostic or interactive commands may still emit plain text). +> **Machine Mode**: Using `--output json`, `--output ndjson`, or `--quiet` (or running `-c` without specifying `--output`) disables ANSI colors, suppresses connection/informational banners, and redirects early parser/connection exceptions to `STDERR` as structured JSON. Data-operation command output is intended to remain structured on `STDOUT` for parser pipelines; diagnostic or explicitly interactive commands may still emit plain text. ## Theming