diff --git a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs index 2a76b5f..c073ee4 100644 --- a/CosmosDBShell.Tests/Integration/ShellProcessTests.cs +++ b/CosmosDBShell.Tests/Integration/ShellProcessTests.cs @@ -123,6 +123,31 @@ public async Task HelpOption_PrintsHelpAndExitsZero() Assert.Contains("--version", result.StdOut, StringComparison.OrdinalIgnoreCase); } + [Fact] + public async Task UnknownRootArgument_ReturnsUnknownArgumentError() + { + var result = await RunShellAsync( + stdinScript: null, + extraArgs: ["not-a-root-option"], + cancellationToken: TestContext.Current.CancellationToken); + + Assert.NotEqual(0, result.ExitCode); + Assert.Contains("Unrecognized argument 'not-a-root-option'", result.StdOut); + Assert.DoesNotContain("Option '--connect-mode' is defined with a bad format", result.StdOut); + } + + [Fact] + public async Task MissingConnectValue_ReportsUserFacingOptionAlias() + { + var result = await RunShellAsync( + stdinScript: null, + extraArgs: ["--connect"], + cancellationToken: TestContext.Current.CancellationToken); + + Assert.NotEqual(0, result.ExitCode); + Assert.Contains("Required option '--connect' is missing", result.StdOut); + } + [Fact] [Trait("Category", "Emulator")] public async Task ConnectOption_WithExecuteAndQuit_RunsCommandAgainstEmulator() diff --git a/CosmosDBShell.Tests/UtilTest/NormalizeArgumentsTests.cs b/CosmosDBShell.Tests/UtilTest/NormalizeArgumentsTests.cs new file mode 100644 index 0000000..a69228d --- /dev/null +++ b/CosmosDBShell.Tests/UtilTest/NormalizeArgumentsTests.cs @@ -0,0 +1,113 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------ + +namespace CosmosShell.Tests.UtilTest; + +public class NormalizeArgumentsTests +{ + [Fact] + public void EmptyArgs_ReturnsEmpty() + { + Assert.Empty(Program.NormalizeArguments([])); + } + + [Fact] + public void NonCommandArgs_PassThroughUnchanged() + { + var input = new[] { "--connect", "endpoint", "--verbose" }; + Assert.Equal(input, Program.NormalizeArguments(input)); + } + + [Fact] + public void DashC_ConsumesRemainingTokensAsSingleString() + { + var result = Program.NormalizeArguments(["-c", "help", "mkitem"]); + Assert.Equal(["-c", "help mkitem"], result); + } + + [Fact] + public void DashK_ConsumesRemainingTokensAsSingleString() + { + var result = Program.NormalizeArguments(["-k", "help", "mkitem"]); + Assert.Equal(["-k", "help mkitem"], result); + } + + [Theory] + [InlineData("/c")] + [InlineData("/C")] + public void SlashC_IsTranslatedToDashC(string token) + { + var result = Program.NormalizeArguments([token, "help", "mkitem"]); + Assert.Equal(["-c", "help mkitem"], result); + } + + [Theory] + [InlineData("/k")] + [InlineData("/K")] + public void SlashK_IsTranslatedToDashK(string token) + { + var result = Program.NormalizeArguments([token, "help", "mkitem"]); + Assert.Equal(["-k", "help mkitem"], result); + } + + [Fact] + public void AppOptionsBeforeDashC_ArePreserved() + { + var result = Program.NormalizeArguments( + ["--connect", "endpoint", "-c", "help", "mkitem"]); + Assert.Equal(["--connect", "endpoint", "-c", "help mkitem"], result); + } + + [Fact] + public void DashCWithoutTail_LeavesDashCAlone() + { + var result = Program.NormalizeArguments(["-c"]); + Assert.Equal(["-c"], result); + } + + [Fact] + public void DashCWithQuotedSingleToken_StaysSingleToken() + { + var result = Program.NormalizeArguments(["-c", "help mkitem"]); + Assert.Equal(["-c", "help mkitem"], result); + } + + [Fact] + public void TokensThatLookLikeOptions_AfterDashC_AreAbsorbed() + { + var result = Program.NormalizeArguments( + ["-c", "seed.csh", "--connect", "xyz"]); + Assert.Equal(["-c", "seed.csh --connect xyz"], result); + } + + [Fact] + public void TakePreCommandArgs_ReturnsEverythingBeforeDashC() + { + var result = Program.TakePreCommandArgs( + ["--verbose", "-c", "help"]); + Assert.Equal(["--verbose"], result); + } + + [Fact] + public void TakePreCommandArgs_ReturnsEverythingBeforeDashK() + { + var result = Program.TakePreCommandArgs( + ["--connect", "ep", "-k", "help"]); + Assert.Equal(["--connect", "ep"], result); + } + + [Fact] + public void TakePreCommandArgs_NoCommandMarker_ReturnsAll() + { + var input = new[] { "--connect", "endpoint", "--verbose" }; + Assert.Equal(input, Program.TakePreCommandArgs(input)); + } + + [Fact] + public void TakePreCommandArgs_DashCFirst_ReturnsEmpty() + { + var result = Program.TakePreCommandArgs(["-c", "--help"]); + Assert.Empty(result); + } +} diff --git a/CosmosDBShell.Tests/UtilTest/ParseDocDBConnectionTests.cs b/CosmosDBShell.Tests/UtilTest/ParseDocDBConnectionTests.cs index 6b47104..61ad77c 100644 --- a/CosmosDBShell.Tests/UtilTest/ParseDocDBConnectionTests.cs +++ b/CosmosDBShell.Tests/UtilTest/ParseDocDBConnectionTests.cs @@ -2,13 +2,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // ------------------------------------------------------------ -using CommandLine; using Azure.Data.Cosmos.Shell.Util; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace CosmosShell.Tests.UtilTest; diff --git a/CosmosDBShell.Tests/UtilTest/SentenceBuilderTests.cs b/CosmosDBShell.Tests/UtilTest/SentenceBuilderTests.cs index 109d9f4..16cf39b 100644 --- a/CosmosDBShell.Tests/UtilTest/SentenceBuilderTests.cs +++ b/CosmosDBShell.Tests/UtilTest/SentenceBuilderTests.cs @@ -2,13 +2,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // ------------------------------------------------------------ -using CommandLine; using Azure.Data.Cosmos.Shell.Util; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace CosmosShell.Tests.UtilTest; @@ -24,18 +18,4 @@ public void TestCommandDescriptions() Assert.NotNull(LocalizableSentenceBuilder.ConnectionString); Assert.NotNull(LocalizableSentenceBuilder.Command); } - - [Fact] - public void TestLocalizableSentenceBuilder() - { - var builder = new LocalizableSentenceBuilder(); - Assert.NotNull(builder.RequiredWord()); - Assert.NotNull(builder.ErrorsHeadingText()); - Assert.NotNull(builder.UsageHeadingText()); - Assert.NotNull(builder.OptionGroupWord()); - Assert.NotNull(builder.HelpCommandText(true)); - Assert.NotNull(builder.HelpCommandText(false)); - Assert.NotNull(builder.VersionCommandText(true)); - Assert.NotNull(builder.VersionCommandText(false)); - } } diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/HelpCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/HelpCommand.cs index 4dd6d59..a1a563b 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/HelpCommand.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/HelpCommand.cs @@ -10,7 +10,6 @@ namespace Azure.Data.Cosmos.Shell.Commands; using Azure.Data.Cosmos.Shell.Core; using Azure.Data.Cosmos.Shell.Parser; using Azure.Data.Cosmos.Shell.Util; -using CommandLine.Text; using RadLine; using Spectre.Console; using static System.Net.Mime.MediaTypeNames; diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Util/LocalizableSentenceBuilder.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Util/LocalizableSentenceBuilder.cs index cdccd53..cae8be3 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Util/LocalizableSentenceBuilder.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Util/LocalizableSentenceBuilder.cs @@ -4,12 +4,13 @@ namespace Azure.Data.Cosmos.Shell.Util; -using global::CommandLine; -using global::CommandLine.Text; - -using Spectre.Console; - -public class LocalizableSentenceBuilder : SentenceBuilder +/// +/// Static accessors for localized CLI option descriptions and usage strings. +/// Previously implemented CommandLine.Text.SentenceBuilder; after the +/// migration to System.CommandLine these strings are pulled directly +/// by the option builder in Program.cs. +/// +public static class LocalizableSentenceBuilder { public static string ExecuteAndContinue => MessageService.GetString("help-ExecuteAndContinue"); @@ -43,111 +44,5 @@ public class LocalizableSentenceBuilder : SentenceBuilder public static string Verbose => MessageService.GetString("help-Verbose"); - public override Func RequiredWord => () => MessageService.GetString("help-RequiredWord"); - - public override Func ErrorsHeadingText => () => MessageService.GetString("help-ErrorsHeadingText"); - - public override Func UsageHeadingText => () => MessageService.GetString("help-UsageHeadingText"); - - public override Func OptionGroupWord => () => MessageService.GetString("help-OptionGroupWord"); - - public override Func HelpCommandText - { - get - { - return isOption => isOption - ? MessageService.GetString("help-HelpCommandScreenText") - : MessageService.GetString("help-HelpCommandMoreText"); - } - } - - public override Func VersionCommandText => (b) => MessageService.GetString("help-VersionCommandText"); - - public override Func FormatError - { - get - { - return error => - { - switch (error.Tag) - { - case ErrorType.BadFormatTokenError: - return MessageService.GetString("help-error-BadFormatTokenError", new Dictionary { { "token", ((BadFormatTokenError)error).Token } }); - case ErrorType.MissingValueOptionError: - return MessageService.GetString("help-error-MissingValueOptionError", new Dictionary { { "option", ((MissingValueOptionError)error).NameInfo.NameText } }); - case ErrorType.UnknownOptionError: - return MessageService.GetString("help-error-UnknownOptionError", new Dictionary { { "option", ((UnknownOptionError)error).Token } }); - case ErrorType.MissingRequiredOptionError: - var errMisssing = (MissingRequiredOptionError)error; - return errMisssing.NameInfo.Equals(NameInfo.EmptyName) - ? MessageService.GetString("help-error-MissingRequiredOptionError1") - : MessageService.GetString("help-error-MissingRequiredOptionError2", new Dictionary { { "option", errMisssing.NameInfo.NameText } }); - case ErrorType.BadFormatConversionError: - var badFormat = (BadFormatConversionError)error; - return badFormat.NameInfo.Equals(NameInfo.EmptyName) - ? MessageService.GetString("help-error-BadFormatConversionError1") - : MessageService.GetString("help-error-BadFormatConversionError2", new Dictionary { { "option", badFormat.NameInfo.NameText } }); - case ErrorType.SequenceOutOfRangeError: - var seqOutRange = (SequenceOutOfRangeError)error; - return seqOutRange.NameInfo.Equals(NameInfo.EmptyName) - ? MessageService.GetString("help-error-SequenceOutOfRangeError1") - : MessageService.GetString("help-error-SequenceOutOfRangeError2", new Dictionary { { "option", seqOutRange.NameInfo.NameText } }); - case ErrorType.BadVerbSelectedError: - return MessageService.GetString("help-error-BadVerbSelectedError", new Dictionary { { "token", ((BadVerbSelectedError)error).Token } }); - case ErrorType.NoVerbSelectedError: - return MessageService.GetString("help-error-NoVerbSelectedError"); - case ErrorType.RepeatedOptionError: - return MessageService.GetString("help-error-RepeatedOptionError", new Dictionary { { "option", ((RepeatedOptionError)error).NameInfo.NameText } }); - case ErrorType.SetValueExceptionError: - var setValueError = (SetValueExceptionError)error; - return MessageService.GetString("help-error-SetValueExceptionError", new Dictionary - { - { "option", setValueError.NameInfo.NameText }, - { "message", setValueError.Exception.Message }, - }); - case ErrorType.MissingGroupOptionError: - var missingGroupOptionError = (MissingGroupOptionError)error; - return MessageService.GetString("help-error-MissingGroupOptionError", new Dictionary - { - { "option", missingGroupOptionError.Group }, - { "req_options", string.Join(", ", missingGroupOptionError.Names.Select(n => n.NameText)) }, - }); - case ErrorType.GroupOptionAmbiguityError: - var groupOptionAmbiguityError = (GroupOptionAmbiguityError)error; - return MessageService.GetString("help-error-GroupOptionAmbiguityError", new Dictionary { { "option", groupOptionAmbiguityError.Option.NameText } }); - case ErrorType.MultipleDefaultVerbsError: - return MultipleDefaultVerbsError.ErrorMessage; - } - - throw new InvalidOperationException(); - }; - } - } - - public override Func, string> FormatMutuallyExclusiveSetErrors => errors => - { - var bySet = from e in errors - group e by e.SetName into g - select new { SetName = g.Key, Errors = g.ToList() }; - - var msgs = bySet.Select( - set => - { - var names = string.Join(string.Empty, from e in set.Errors select "'" + e.NameInfo.NameText + "', "); - var namesCount = set.Errors.Count; - - var incompat = string.Join( - string.Empty, - (from x in (from s in bySet where !s.SetName.Equals(set.SetName) from e in s.Errors select e).Distinct() - select "'" + x.NameInfo.NameText + "', ").ToArray()); - - return MessageService.GetString("help-error-FormatMutuallyExclusiveSetErrors", new Dictionary - { - { "count", namesCount }, - { "option", names[..^2] }, - { "incompat", incompat[..^2] }, - }); - }).ToArray(); - return string.Join(Environment.NewLine, msgs); - }; -} \ No newline at end of file + public static string UsageHeadingText => MessageService.GetString("help-UsageHeadingText"); +} diff --git a/CosmosDBShell/CosmosDBShell.csproj b/CosmosDBShell/CosmosDBShell.csproj index a4e4e21..a874d2c 100644 --- a/CosmosDBShell/CosmosDBShell.csproj +++ b/CosmosDBShell/CosmosDBShell.csproj @@ -86,7 +86,7 @@ - + diff --git a/CosmosDBShell/Program.cs b/CosmosDBShell/Program.cs index e2d2132..2ae8324 100644 --- a/CosmosDBShell/Program.cs +++ b/CosmosDBShell/Program.cs @@ -2,22 +2,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // ------------------------------------------------------------ +using System.CommandLine; +using System.CommandLine.Parsing; using System.Reflection; using Azure.Data.Cosmos.Shell.Commands; using Azure.Data.Cosmos.Shell.Core; using Azure.Data.Cosmos.Shell.Lsp; using Azure.Data.Cosmos.Shell.Mcp; using Azure.Data.Cosmos.Shell.Util; -using CommandLine; -using CommandLine.Text; using Microsoft.Azure.Cosmos; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; -using OmniSharp.Extensions.LanguageServer.Protocol.Document; -using OmniSharp.Extensions.LanguageServer.Protocol.Window; -using OmniSharp.Extensions.LanguageServer.Server; using Spectre.Console; internal class Program @@ -26,9 +20,18 @@ internal class Program public static async Task Main(string[] args) { + // Normalize argv first so that any --lsp/--stdio token that is part + // of a -c/-k command tail is absorbed as command text and does not + // accidentally trigger LSP mode. + args = NormalizeArguments(args); + // Handle LSP mode early, before any other code can write to stdout. - // The LSP protocol requires exclusive access to stdin/stdout. - if (args.Contains("--lsp") || args.Contains("--stdio")) + // The LSP protocol requires exclusive access to stdin/stdout. Only + // inspect the prefix before -c / -k so that a command tail of literally + // "--lsp" or "--stdio" is forwarded to the shell command rather than + // accidentally starting the LSP server. + var preCommandLsp = TakePreCommandArgs(args); + if (preCommandLsp.Any(a => a is "--lsp" or "--stdio")) { var server = await LspServer.CreateLanguageServerAsync(); await server.WaitForExit; @@ -38,234 +41,238 @@ public static async Task Main(string[] args) IHost? host = null; try { - args = NormalizeArguments(args); - SentenceBuilder.Factory = () => new LocalizableSentenceBuilder(); - - // Use a custom parser so we can render our own heading for --version - // (CommandLineParser's default output is " ", - // which prints the build-metadata SHA inline and duplicated when both - // /p:InformationalVersion and SourceLink's SourceRevisionId contribute - // metadata). HelpWriter=null disables the built-in help/version writer; - // we delegate back to HelpText.AutoBuild for --help and parse errors. - using var parser = new Parser(settings => + // --help / --version handled manually so we can render our own + // localized usage and version heading. Only inspect the prefix + // before -c / -k so that a command tail of literally "--help" or + // "--version" (e.g. `-c --help`) is forwarded to the shell command + // instead of intercepted here. + var preCommandArgs = TakePreCommandArgs(args); + if (preCommandArgs.Any(a => a is "--help" or "-h" or "-?" or "/?" or "/h")) + { + ShellInterpreter.WriteLine(BuildHelpText()); + return; + } + + if (preCommandArgs.Any(a => a is "--version")) { - settings.HelpWriter = null; - }); - var parseResult = parser.ParseArguments(args); + WriteVersionHeading(); + return; + } + + var (rootCommand, optionMap) = BuildRootCommand(); + var configuration = new System.CommandLine.CommandLineConfiguration( + rootCommand, + resources: new LocalizedCliResources()); + var parser = new System.CommandLine.Parsing.Parser(configuration); + var parseResult = parser.Parse(args); - // Handle parse errors - parseResult.WithNotParsed(errors => + if (parseResult.Errors.Count > 0) { - if (errors.IsVersion()) + foreach (var error in parseResult.Errors) { - WriteVersionHeading(); - return; + ShellInterpreter.WriteLine(error.Message); } - var helpText = errors.IsHelp() - ? HelpText.AutoBuild(parseResult) - : HelpText.AutoBuild(parseResult, h => HelpText.DefaultParsingErrorsHandler(parseResult, h), e => e); - ShellInterpreter.WriteLine(helpText.ToString()); + ShellInterpreter.WriteLine(BuildHelpText()); + Environment.ExitCode = 1; + return; + } - if (!errors.IsHelp()) + var o = new CosmosShellOptions + { + ColorSystem = parseResult.GetValueForOption(optionMap.ColorSystem), + ExecuteAndQuit = parseResult.GetValueForOption(optionMap.ExecuteAndQuit), + ExecuteAndContinue = parseResult.GetValueForOption(optionMap.ExecuteAndContinue), + ClearHistory = parseResult.GetValueForOption(optionMap.ClearHistory), + ConnectionString = parseResult.GetValueForOption(optionMap.ConnectionString), + ConnectionMode = parseResult.GetValueForOption(optionMap.ConnectionMode), + ConnectTenant = parseResult.GetValueForOption(optionMap.ConnectTenant), + ConnectHint = parseResult.GetValueForOption(optionMap.ConnectHint), + ConnectAuthorityHost = parseResult.GetValueForOption(optionMap.ConnectAuthorityHost), + ConnectManagedIdentity = parseResult.GetValueForOption(optionMap.ConnectManagedIdentity), + ConnectVSCodeCredential = parseResult.GetValueForOption(optionMap.ConnectVSCodeCredential), + StartLspServer = parseResult.GetValueForOption(optionMap.StartLspServer), + LspStdio = parseResult.GetValueForOption(optionMap.LspStdio), + Verbose = parseResult.GetValueForOption(optionMap.Verbose), + }; + + // --mcp supports an optional value: when the option is present without + // an integer, fall back to the default port. + var mcpResult = parseResult.FindResultFor(optionMap.McpPort); + if (mcpResult is not null) + { + var mcpValue = parseResult.GetValueForOption(optionMap.McpPort); + o.McpPort = mcpValue ?? DefaultMcpPort; + } + + if (o.StartLspServer) + { + // Already handled above, but keep for completeness + var server = await LspServer.CreateLanguageServerAsync(); + await server.WaitForExit; + return; + } + + if (!string.IsNullOrWhiteSpace(o.ExecuteAndQuit) && !string.IsNullOrWhiteSpace(o.ExecuteAndContinue)) + { + Environment.ExitCode = 1; + ShellInterpreter.WriteLine(MessageService.GetString("error-mutually-exclusive-options")); + return; + } + + var executeAndQuitCommand = string.IsNullOrWhiteSpace(o.ExecuteAndQuit) ? null : o.ExecuteAndQuit; + var executeAndContinueCommand = string.IsNullOrWhiteSpace(o.ExecuteAndContinue) ? null : o.ExecuteAndContinue; + var explicitCommand = executeAndContinueCommand ?? executeAndQuitCommand; + + if (o.ClearHistory) + { + if (File.Exists(ShellInterpreter.Instance.HistoryFile)) { - Environment.ExitCode = 1; + File.Delete(ShellInterpreter.Instance.HistoryFile); } - }); - _ = await parseResult.WithParsedAsync(async o => + ShellInterpreter.WriteLine(MessageService.GetString("shell-hisory_file_deleted")); + return; + } + + AnsiConsole.Profile.Capabilities.ColorSystem = o.ColorSystem switch + { + 1 => ColorSystem.Standard, + 2 => ColorSystem.TrueColor, + _ => ColorSystem.NoColors, + }; + ShellInterpreter.Instance.Options = o; + + if (o.ConnectionString != null) { - if (o.StartLspServer) + using var connectTokenSource = ShellInterpreter.UserCancellationTokenSource; + var connectToken = connectTokenSource.Token; + try { - // Already handled above, but keep for completeness - var server = await LspServer.CreateLanguageServerAsync(); - await server.WaitForExit; - return; + await ShellInterpreter.Instance.ConnectAsync( + o.ConnectionString, + o.ConnectHint, + o.ConnectionMode, + tenantId: o.ConnectTenant, + authorityHost: o.ConnectAuthorityHost, + managedIdentityClientId: o.ConnectManagedIdentity, + useVSCodeCredential: o.ConnectVSCodeCredential, + token: connectToken); } - - if (!string.IsNullOrWhiteSpace(o.ExecuteAndQuit) && !string.IsNullOrWhiteSpace(o.ExecuteAndContinue)) + catch (OperationCanceledException) when (connectToken.IsCancellationRequested) { - Environment.ExitCode = 1; - ShellInterpreter.WriteLine(MessageService.GetString("error-mutually-exclusive-options")); return; } - - var executeAndQuitCommand = string.IsNullOrWhiteSpace(o.ExecuteAndQuit) ? null : o.ExecuteAndQuit; - var executeAndContinueCommand = string.IsNullOrWhiteSpace(o.ExecuteAndContinue) ? null : o.ExecuteAndContinue; - var explicitCommand = executeAndContinueCommand ?? executeAndQuitCommand; - - if (o.ClearHistory) + catch (Exception ex) { - if (File.Exists(ShellInterpreter.Instance.HistoryFile)) + Environment.ExitCode = 1; + if (ConnectCommand.TryGetPrincipalIdFromRbacException(ex, out var id, out var permission)) { - File.Delete(ShellInterpreter.Instance.HistoryFile); + ConnectCommand.AskForRBacPermissions(id ?? string.Empty, permission ?? string.Empty); + return; } - ShellInterpreter.WriteLine(MessageService.GetString("shell-hisory_file_deleted")); + ShellInterpreter.WriteLine(ex.Message); return; } + } - AnsiConsole.Profile.Capabilities.ColorSystem = o.ColorSystem switch - { - 1 => ColorSystem.Standard, - 2 => ColorSystem.TrueColor, - _ => ColorSystem.NoColors, - }; - ShellInterpreter.Instance.Options = o; + // Start MCP server if requested + Task? hostTask = null; - if (o.ConnectionString != null) + if (o.McpPort is int mcpPort) + { + if (mcpPort <= 0) { - using var connectTokenSource = ShellInterpreter.UserCancellationTokenSource; - var connectToken = connectTokenSource.Token; - try - { - await ShellInterpreter.Instance.ConnectAsync( - o.ConnectionString, - o.ConnectHint, - o.ConnectionMode, - tenantId: o.ConnectTenant, - authorityHost: o.ConnectAuthorityHost, - managedIdentityClientId: o.ConnectManagedIdentity, - useVSCodeCredential: o.ConnectVSCodeCredential, - token: connectToken); - } - catch (OperationCanceledException) when (connectToken.IsCancellationRequested) - { - return; - } - catch (Exception ex) - { - Environment.ExitCode = 1; - if (ConnectCommand.TryGetPrincipalIdFromRbacException(ex, out var id, out var permission)) - { - ConnectCommand.AskForRBacPermissions(id ?? string.Empty, permission ?? string.Empty); - return; - } - - ShellInterpreter.WriteLine(ex.Message); - return; - } + AnsiConsole.WriteLine(MessageService.GetString("mcp-error-invalid-port")); + Environment.ExitCode = 1; + return; } - // Start MCP server if requested - Task? hostTask = null; - - if (o.McpPort is int mcpPort) + try { - if (mcpPort <= 0) - { - AnsiConsole.WriteLine(MessageService.GetString("mcp-error-invalid-port")); - Environment.ExitCode = 1; - return; - } - - try - { - host = McpServer.CreateHost(o); - } - catch (Exception ex) - { - AnsiConsole.WriteLine(MessageService.GetArgsString("mcp-error-creating-server", "message", Markup.Escape(ex.Message))); - Environment.ExitCode = 1; - return; - } - - if (host != null) - { - ShellInterpreter.Instance.McpPort = mcpPort; - hostTask = Task.Run(async () => - { - try - { - var token = CancellationToken.None; - await host.StartAsync(token); - await host.WaitForShutdownAsync(token); - } - catch (Exception ex) - { - AnsiConsole.WriteLine(MessageService.GetArgsString("mcp-error-server-failed-start", "message", Markup.Escape(ex.Message))); - Environment.ExitCode = 1; - } - }); - } + host = McpServer.CreateHost(o); + } + catch (Exception ex) + { + AnsiConsole.WriteLine(MessageService.GetArgsString("mcp-error-creating-server", "message", Markup.Escape(ex.Message))); + Environment.ExitCode = 1; + return; } - if (Console.IsInputRedirected) + if (host != null) { - // Read entire stdin (script) - var script = await Console.In.ReadToEndAsync() + Environment.NewLine; - if (!string.IsNullOrWhiteSpace(script)) + ShellInterpreter.Instance.McpPort = mcpPort; + hostTask = Task.Run(async () => { - var state = await ShellInterpreter.Instance.ExecuteCommandAsync(script, default); - if (state.IsError) + try { - Environment.ExitCode = 1; - if (executeAndContinueCommand is null) - { - return; - } + var token = CancellationToken.None; + await host.StartAsync(token); + await host.WaitForShutdownAsync(token); } - - // If user only wants to execute piped script then quit (unless -k / ExecuteAndContinue) - if (executeAndContinueCommand is null && executeAndQuitCommand is null) + catch (Exception ex) { - // Stop host gracefully before returning - if (host != null) - { - await host.StopAsync(); - } - - return; + AnsiConsole.WriteLine(MessageService.GetArgsString("mcp-error-server-failed-start", "message", Markup.Escape(ex.Message))); + Environment.ExitCode = 1; } - } + }); } + } - if (explicitCommand is not null) + if (Console.IsInputRedirected) + { + // Read entire stdin (script) + var script = await Console.In.ReadToEndAsync() + Environment.NewLine; + if (!string.IsNullOrWhiteSpace(script)) { - var state = await ShellInterpreter.Instance.ExecuteCommandAsync(explicitCommand, default); + var state = await ShellInterpreter.Instance.ExecuteCommandAsync(script, default); if (state.IsError) { Environment.ExitCode = 1; if (executeAndContinueCommand is null) { - // Stop host gracefully before returning - if (host != null) - { - await host.StopAsync(); - } - - // Wait for the host task to complete - if (hostTask != null) - { - await hostTask; - } + await StopHostAsync(host, hostTask); return; } } - if (executeAndContinueCommand is not null) + // If user only wants to execute piped script then quit (unless -k / ExecuteAndContinue) + if (executeAndContinueCommand is null && executeAndQuitCommand is null) { - await ShellInterpreter.Instance.RunAsync(); + await StopHostAsync(host, hostTask); + + return; } } - else - { - await ShellInterpreter.Instance.RunAsync(); - } + } - // Stop the host gracefully before the task completes - if (host != null) + if (explicitCommand is not null) + { + var state = await ShellInterpreter.Instance.ExecuteCommandAsync(explicitCommand, default); + if (state.IsError) { - await host.StopAsync(); + Environment.ExitCode = 1; + if (executeAndContinueCommand is null) + { + await StopHostAsync(host, hostTask); + + return; + } } - // Wait for the host task to complete - if (hostTask != null) + if (executeAndContinueCommand is not null) { - await hostTask; + await ShellInterpreter.Instance.RunAsync(); } - }); + } + else + { + await ShellInterpreter.Instance.RunAsync(); + } + + await StopHostAsync(host, hostTask); } finally { @@ -286,82 +293,340 @@ private static void WriteVersionHeading() var version = ShellInterpreter.GetDisplayVersion(assembly); var commit = ShellInterpreter.GetDisplayCommit(assembly); var heading = string.IsNullOrEmpty(commit) - ? new HeadingInfo(product, version) - : new HeadingInfo(product, $"{version} ({commit})"); - ShellInterpreter.WriteLine(heading.ToString()); + ? $"{product} {version}" + : $"{product} {version} ({commit})"; + ShellInterpreter.WriteLine(heading); + } + + private static async Task StopHostAsync(IHost? host, Task? hostTask) + { + if (host != null) + { + await host.StopAsync(); + } + + if (hostTask != null) + { +#pragma warning disable VSTHRD003 // hostTask is created by this process via Task.Run for the MCP host loop. + await hostTask; +#pragma warning restore VSTHRD003 + } } - private static string[] NormalizeArguments(string[] args) + /// + /// Pre-processes argv before sees it: + /// 1. Translates Windows-style /c//k switches into their + /// POSIX equivalents (-c/-k). + /// 2. Once -c or -k is encountered, the rest of the + /// command line is collapsed into a single string value, so users + /// don't have to quote multi-word commands: + /// CosmosDBShell -c help mkitem + /// becomes + /// CosmosDBShell -c "help mkitem" + /// App-level options must therefore come before -c/-k. + /// + internal static string[] NormalizeArguments(string[] args) { - var normalizedArguments = new List(args.Length); + var result = new List(args.Length); for (int index = 0; index < args.Length; index++) { var argument = args[index]; - if (argument == "--mcp") + + // Translate /c, /k, /C, /K to -c / -k early so all later checks + // can treat them uniformly. + if (argument is "/c" or "/C") + { + argument = "-c"; + } + else if (argument is "/k" or "/K") + { + argument = "-k"; + } + + if (argument is "-c" or "-k") + { + result.Add(argument); + if (index + 1 < args.Length) + { + result.Add(string.Join(' ', args.Skip(index + 1))); + } + + return [.. result]; + } + + result.Add(argument); + } + + return [.. result]; + } + + /// + /// Returns the argv prefix that precedes the first -c / -k + /// (already normalized from /c, /k). Anything from the + /// command marker onward is the consume-rest command tail and must not + /// be inspected for app-level flags such as --help, --version, + /// --lsp, or --stdio. + /// + internal static string[] TakePreCommandArgs(string[] args) + { + for (int index = 0; index < args.Length; index++) + { + if (args[index] is "-c" or "-k") + { + return args.Take(index).ToArray(); + } + } + + return args; + } + + private static (RootCommand Command, OptionMap Map) BuildRootCommand() + { + var colorSystem = new Option( + aliases: ["--color-system", "--cs"], + getDefaultValue: () => 2, + description: MessageService.GetString("help-ColorSystem")); + + var executeAndQuit = new Option("-c", MessageService.GetString("help-ExecuteAndQuit")); + var executeAndContinue = new Option("-k", MessageService.GetString("help-ExecuteAndContinue")); + + var clearHistory = new Option( + aliases: ["--clear-history", "--clearhistory"], + description: MessageService.GetString("help-ClearHistory")); + var connectionString = new Option("--connect", MessageService.GetString("help-ConnectionString")); + + var connectionMode = new Option( + "--connect-mode", + parseArgument: argResult => { - if (index + 1 < args.Length && !args[index + 1].StartsWith("-", StringComparison.Ordinal)) + if (argResult.Tokens.Count == 0) { - normalizedArguments.Add(argument); - normalizedArguments.Add(args[++index]); - continue; + return null; } - normalizedArguments.Add($"--mcp={DefaultMcpPort}"); + var token = argResult.Tokens[0].Value; + if (Enum.TryParse(token, ignoreCase: true, out var mode)) + { + return mode; + } + + argResult.ErrorMessage = MessageService.GetArgsString( + "help-error-BadFormatConversionError2", + "option", + "--connect-mode"); + return null; + }, + description: MessageService.GetString("help-ConnectionMode")); + + var connectTenant = new Option("--connect-tenant", MessageService.GetString("help-ConnectTenant")); + var connectHint = new Option("--connect-hint", MessageService.GetString("help-ConnectHint")); + var connectAuthorityHost = new Option("--connect-authority-host", MessageService.GetString("help-ConnectAuthorityHost")); + var connectManagedIdentity = new Option("--connect-managed-identity", MessageService.GetString("help-ConnectManagedIdentity")); + var connectVSCodeCredential = new Option("--connect-vscode-credential", MessageService.GetString("help-ConnectVSCodeCredential")) + { + IsHidden = true, + }; + + var mcpPort = new Option("--mcp", MessageService.GetString("help-McpPort")) + { + Arity = ArgumentArity.ZeroOrOne, + }; + + var startLspServer = new Option("--lsp", MessageService.GetString("help-EnableLspServer")); + var lspStdio = new Option("--stdio", MessageService.GetString("help-EnableLspServer")) + { + IsHidden = true, + }; + var verbose = new Option("--verbose", MessageService.GetString("help-Verbose")); + + var root = new RootCommand("Cosmos DB Shell") + { + colorSystem, + executeAndQuit, + executeAndContinue, + clearHistory, + connectionString, + connectionMode, + connectTenant, + connectHint, + connectAuthorityHost, + connectManagedIdentity, + connectVSCodeCredential, + mcpPort, + startLspServer, + lspStdio, + verbose, + }; + + var map = new OptionMap( + colorSystem, + executeAndQuit, + executeAndContinue, + clearHistory, + connectionString, + connectionMode, + connectTenant, + connectHint, + connectAuthorityHost, + connectManagedIdentity, + connectVSCodeCredential, + mcpPort, + startLspServer, + lspStdio, + verbose); + + return (root, map); + } + + private static string BuildHelpText() + { + var (rootCommand, map) = BuildRootCommand(); + var product = (typeof(Program).Assembly.GetName().Name ?? "CosmosDBShell").ToLowerInvariant(); + + // Per-option value placeholder shown after the alias list. + var placeholders = new Dictionary + { + [map.ExecuteAndQuit] = "...", + [map.ExecuteAndContinue] = "...", + [map.ColorSystem] = "", + [map.ConnectionString] = "", + [map.ConnectionMode] = "", + [map.ConnectTenant] = "", + [map.ConnectHint] = "", + [map.ConnectAuthorityHost] = "", + [map.ConnectManagedIdentity] = "", + [map.McpPort] = "[]", + }; + + var rows = new List<(string Label, string? Description)>(); + foreach (var option in rootCommand.Options) + { + if (option.IsHidden) + { continue; } - normalizedArguments.Add(argument); + var label = string.Join(", ", option.Aliases); + if (placeholders.TryGetValue((System.CommandLine.Option)option, out var placeholder)) + { + label = $"{label} {placeholder}"; + } + + rows.Add((label, option.Description)); } - return [.. normalizedArguments]; + // --help / --version are intercepted before parsing, so they are not + // declared as Option. Surface them in the rendered help so users + // can discover them. + rows.Add(("--help, -h, -?", MessageService.GetString("help-HelpOptionDescription"))); + rows.Add(("--version", MessageService.GetString("help-VersionOptionDescription"))); + + var width = rows.Max(r => r.Label.Length); + + var builder = new System.Text.StringBuilder(); + builder.AppendLine(MessageService.GetString("help-UsageHeadingText")); + builder.AppendLine(" " + MessageService.GetArgsString("help-UsageSynopsis", "command", product)); + builder.AppendLine(); + builder.AppendLine(MessageService.GetString("help-OptionsHeadingText")); + foreach (var (label, description) in rows) + { + builder.AppendLine($" {label.PadRight(width)} {description}"); + } + + builder.AppendLine(); + builder.AppendLine(MessageService.GetString("help-NotesHeadingText")); + builder.AppendLine(" " + MessageService.GetString("help-CommandTailNote")); + + return builder.ToString(); + } + + private sealed record OptionMap( + Option ColorSystem, + Option ExecuteAndQuit, + Option ExecuteAndContinue, + Option ClearHistory, + Option ConnectionString, + Option ConnectionMode, + Option ConnectTenant, + Option ConnectHint, + Option ConnectAuthorityHost, + Option ConnectManagedIdentity, + Option ConnectVSCodeCredential, + Option McpPort, + Option StartLspServer, + Option LspStdio, + Option Verbose); + + /// + /// Maps the most common System.CommandLine parse error messages + /// to the existing help-error-* entries in en.ftl so the + /// localized help/error strings authored for the previous parser are not + /// silently lost. Anything not overridden falls back to the default + /// English text from . + /// + private sealed class LocalizedCliResources : System.CommandLine.LocalizationResources + { + public override string UnrecognizedCommandOrArgument(string arg) => + arg.StartsWith('-') + ? MessageService.GetArgsString("help-error-UnknownOptionError", "option", arg) + : MessageService.GetArgsString("help-error-UnknownArgumentError", "argument", arg); + + public override string UnrecognizedArgument(string unrecognizedArg, IReadOnlyCollection allowedValues) => + unrecognizedArg.StartsWith('-') + ? MessageService.GetArgsString("help-error-UnknownOptionError", "option", unrecognizedArg) + : MessageService.GetArgsString("help-error-UnknownArgumentError", "argument", unrecognizedArg); + + public override string ExpectsOneArgument(System.CommandLine.Parsing.SymbolResult symbolResult) => + MessageService.GetArgsString("help-error-MissingValueOptionError", "option", GetDisplayName(symbolResult)); + + public override string NoArgumentProvided(System.CommandLine.Parsing.SymbolResult symbolResult) => + MessageService.GetArgsString("help-error-MissingValueOptionError", "option", GetDisplayName(symbolResult)); + + public override string RequiredArgumentMissing(System.CommandLine.Parsing.SymbolResult symbolResult) => + MessageService.GetArgsString("help-error-MissingRequiredOptionError2", "option", GetDisplayName(symbolResult)); + + public override string ArgumentConversionCannotParseForOption(string value, string optionName, Type expectedType) => + MessageService.GetArgsString("help-error-BadFormatConversionError2", "option", optionName); + + private static string GetDisplayName(System.CommandLine.Parsing.SymbolResult symbolResult) + { + return symbolResult.Symbol is System.CommandLine.Option option && option.Aliases.Count > 0 + ? option.Aliases.First() + : symbolResult.Symbol.Name; + } } public class CosmosShellOptions { - [Option("cs", Required = false, HelpText = "ColorSystem", ResourceType = typeof(LocalizableSentenceBuilder))] public int ColorSystem { get; set; } = 2; - [Option('c', Required = false, HelpText = "ExecuteAndQuit", ResourceType = typeof(LocalizableSentenceBuilder))] public string? ExecuteAndQuit { get; set; } - [Option('k', Required = false, HelpText = "ExecuteAndContinue", ResourceType = typeof(LocalizableSentenceBuilder))] public string? ExecuteAndContinue { get; set; } - [Option("clearhistory", Required = false, HelpText = "ClearHistory", ResourceType = typeof(LocalizableSentenceBuilder))] public bool ClearHistory { get; set; } - [Option("connect", Required = false, HelpText = "ConnectionString", ResourceType = typeof(LocalizableSentenceBuilder))] public string? ConnectionString { get; set; } - [Option("connect-mode", Required = false, HelpText = "ConnectionMode", ResourceType = typeof(LocalizableSentenceBuilder))] public ConnectionMode? ConnectionMode { get; set; } - [Option("connect-tenant", Required = false, HelpText = "ConnectTenant", ResourceType = typeof(LocalizableSentenceBuilder))] public string? ConnectTenant { get; set; } - [Option("connect-hint", Required = false, HelpText = "ConnectHint", ResourceType = typeof(LocalizableSentenceBuilder))] public string? ConnectHint { get; set; } - [Option("connect-authority-host", Required = false, HelpText = "ConnectAuthorityHost", ResourceType = typeof(LocalizableSentenceBuilder))] public string? ConnectAuthorityHost { get; set; } - [Option("connect-managed-identity", Required = false, HelpText = "ConnectManagedIdentity", ResourceType = typeof(LocalizableSentenceBuilder))] public string? ConnectManagedIdentity { get; set; } - [Option("connect-vscode-credential", Required = false, HelpText = "ConnectVSCodeCredential", ResourceType = typeof(LocalizableSentenceBuilder), Hidden = true)] public bool ConnectVSCodeCredential { get; set; } - [Option("mcp", Required = false, HelpText = "McpPort", ResourceType = typeof(LocalizableSentenceBuilder))] public int? McpPort { get; set; } - [Option("lsp", Required = false, HelpText = "EnableLspServer", ResourceType = typeof(LocalizableSentenceBuilder))] public bool StartLspServer { get; set; } - [Option("stdio", Required = false, HelpText = "EnableLspServer", ResourceType = typeof(LocalizableSentenceBuilder), Hidden = true)] public bool LspStdio { get; set; } - [Option("verbose", Required = false, HelpText = "Verbose", ResourceType = typeof(LocalizableSentenceBuilder))] public bool Verbose { get; set; } } -} \ No newline at end of file +} diff --git a/CosmosDBShell/lang/en.ftl b/CosmosDBShell/lang/en.ftl index 50c60b8..3277e8a 100644 --- a/CosmosDBShell/lang/en.ftl +++ b/CosmosDBShell/lang/en.ftl @@ -436,6 +436,12 @@ command-version-repo = Report issues at [link={ $url }]{ $url }[/] help-RequiredWord = Required. help-ErrorsHeadingText = ERROR(S): help-UsageHeadingText = USAGE: +help-UsageSynopsis = { $command } [options] [-c|-k ...] +help-CommandTailNote = Everything after -c / -k (or /c, /k) is taken as the command (no quoting needed). App-level options must come before -c / -k. +help-OptionsHeadingText = OPTIONS: +help-NotesHeadingText = NOTES: +help-HelpOptionDescription = Show this help text and exit. +help-VersionOptionDescription = Show product version and exit. help-OptionGroupWord = Group help-HelpCommandScreenText = Display this help screen. help-HelpCommandMoreText = Display more information on a specific command. @@ -450,6 +456,7 @@ help-SentenceMutuallyExclusiveSetErrors = help-error-BadFormatTokenError = Token '{ $token }' is not recognized. help-error-MissingValueOptionError = Option '{ $option }' has no value. help-error-UnknownOptionError = Option '{ $option }' is unknown. +help-error-UnknownArgumentError = Unrecognized argument '{ $argument }'. help-error-MissingRequiredOptionError1 = A required value not bound to option name is missing. help-error-MissingRequiredOptionError2 = Required option '{ $option }' is missing. help-error-BadFormatConversionError1 = A value not bound to option name is defined with a bad format. @@ -463,9 +470,9 @@ help-error-SetValueExceptionError = Error setting value to option '{ $option }': help-error-MissingGroupOptionError = At least one option from group '{ $option }"' ({ $req_options }) is required. help-error-GroupOptionAmbiguityError= Both SetName and Group are not allowed in option: ({ $option }) -help-ExecuteAndContinue = Executes the specified command and keeps the shell running (for example: /k "help"). -help-ExecuteAndQuit = Executes the specified command and exits the shell (for example: /c "help"). -help-ColorSystem = ColorSystem to use.(0=off, 1=standard, 2=true color) +help-ExecuteAndContinue = Execute the specified command, then keep the shell running. +help-ExecuteAndQuit = Execute the specified command, then exit. +help-ColorSystem = Color system: 0=off, 1=standard, 2=true color (default: 2). help-ClearHistory = Clears command history and exits. help-ConnectionString = The endpoint URL or connection string to connect to. help-ConnectionMode = Connection mode: 'direct' (default) or 'gateway' diff --git a/Directory.Packages.props b/Directory.Packages.props index 38f24b1..20c4575 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ - + diff --git a/README.md b/README.md index 2aeccfe..31ed833 100644 --- a/README.md +++ b/README.md @@ -123,14 +123,18 @@ Packaging runs produce preview versions in the form `1.0.-preview.` | `--connect-managed-identity ` | Use a user-assigned managed identity | | `--mcp [port]` | Enable MCP server on the given port, or `6128` by default | | `--verbose` | Print full exception details | -| `--cs ` | Colors: 0=off, 1=standard, 2=truecolor | +| `--color-system ` | Colors: 0=off, 1=standard, 2=truecolor (alias: `--cs`) | | `--help` | Show help | Examples: ```bash # Run a script and exit. Script arguments become $1, $2, ... inside the script. -cosmosdbshell -c "seed.csh mydb mycontainer" --connect "AccountEndpoint=...;AccountKey=..." +cosmosdbshell --connect "AccountEndpoint=...;AccountKey=..." -c "seed.csh mydb mycontainer" + +# -c also accepts an unquoted command tail; everything after -c becomes the +# command, so app-level options (like --connect) must come BEFORE -c. +cosmosdbshell --connect "AccountEndpoint=...;AccountKey=..." -c seed.csh mydb mycontainer # Run a script from piped command text. echo "seed.csh mydb mycontainer" | cosmosdbshell --connect "AccountEndpoint=...;AccountKey=..." diff --git a/docs/navigation.md b/docs/navigation.md index 59c4976..e3d2126 100644 --- a/docs/navigation.md +++ b/docs/navigation.md @@ -207,8 +207,8 @@ Start the shell with options to customize behavior: | Option | Description | | ------ | ----------- | -| `-c ` | Execute command and exit | -| `-k ` | Execute command and stay in shell | +| `-c ` | Execute command and exit. Everything after `-c` is taken as the command, so app-level options must come before `-c`. Windows-style `/c` is also accepted. | +| `-k ` | Execute command and stay in shell. Everything after `-k` is taken as the command, so app-level options must come before `-k`. Windows-style `/k` is also accepted. | | `--connect ` | Connect with this connection string or endpoint on startup | | `--connect-mode ` | Connection mode at startup: 'direct' or 'gateway' | | `--connect-tenant ` | Entra ID tenant ID at startup | @@ -216,8 +216,8 @@ Start the shell with options to customize behavior: | `--connect-authority-host ` | Authority host URL at startup | | `--connect-managed-identity ` | User-assigned managed identity client ID at startup | | `--mcp [port]` | Enable MCP (Model Context Protocol) server on the given port, or `6128` by default | -| `--cs ` | Color scheme: 0=off, 1=standard, 2=truecolor | -| `--clearhistory` | Clear command history on start | +| `--color-system ` | Color scheme: 0=off, 1=standard, 2=truecolor (alias: `--cs`) | +| `--clear-history` | Clear command history on start | | `--help` | Show usage information | | `--version` | Show version | diff --git a/docs/programming.md b/docs/programming.md index 59e88c8..a18da87 100644 --- a/docs/programming.md +++ b/docs/programming.md @@ -115,13 +115,19 @@ Use `-k` to run a command or script and then stay in the interactive shell: cosmosdbshell -k "seed.csh \"AccountEndpoint=...;AccountKey=...\" mydb mycontainer" ``` -Startup connection options still belong to the shell process, not to the script: +Startup connection options still belong to the shell process, not to the script. Because everything after `-c` / `-k` is captured as the command, place app-level options before `-c` / `-k`: ```bash -cosmosdbshell -c "seed.csh mydb mycontainer" --connect "AccountEndpoint=...;AccountKey=..." +cosmosdbshell --connect "AccountEndpoint=...;AccountKey=..." -c "seed.csh mydb mycontainer" ``` -If you want a value such as `--connect` to be passed to the script, put it inside the `-c` or `-k` command text: +Quotes around the command are optional — the shell joins all remaining tokens after `-c` / `-k` into a single command string: + +```bash +cosmosdbshell --connect "AccountEndpoint=...;AccountKey=..." -c seed.csh mydb mycontainer +``` + +If you want a value such as `--connect` to be passed to the script, put it inside the `-c` or `-k` command text (after `-c` everything goes to the script anyway): ```bash cosmosdbshell -c "seed.csh --connect xyz"