diff --git a/src/System.CommandLine.StaticCompletions/shells/FishShellProvider.cs b/src/System.CommandLine.StaticCompletions/shells/FishShellProvider.cs index b9555424d493..27b4fc71fa34 100644 --- a/src/System.CommandLine.StaticCompletions/shells/FishShellProvider.cs +++ b/src/System.CommandLine.StaticCompletions/shells/FishShellProvider.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.CodeDom.Compiler; +using System.CommandLine.Completions; using System.CommandLine.StaticCompletions.Resources; namespace System.CommandLine.StaticCompletions.Shells; @@ -16,13 +18,388 @@ public class FishShellProvider : IShellProvider // override the ToString method to return the argument name so that CLI help is cleaner for 'default' values public override string ToString() => ArgumentName; - private static readonly string _dynamicCompletionScript = - """ - # fish parameter completion for the dotnet CLI - # add the following to your config.fish to enable completions + public string GenerateCompletions(Command command) + { + var safeName = command.Name.MakeSafeFunctionName(); - complete -f -c dotnet -a "(dotnet complete (commandline -cp))" - """; + using var textWriter = new StringWriter { NewLine = "\n" }; + using var writer = new IndentedTextWriter(textWriter); - public string GenerateCompletions(System.CommandLine.Command command) => _dynamicCompletionScript; + writer.WriteLine($"# fish completions for {command.Name}"); + writer.WriteLine(); + + // Collect all commands in a flat list and assign numeric state IDs + var states = new List<(int id, Command cmd)>(); + CollectStates(command, states); + var stateIdByCommand = states.ToDictionary(s => s.cmd, s => s.id); + + // Write the main completion function + writer.WriteLine($"function _{safeName}"); + writer.Indent++; + + WriteTokenization(writer); + writer.WriteLine(); + WriteStateWalker(writer, states, stateIdByCommand); + writer.WriteLine(); + WriteOptionValueCompletions(writer, states); + WriteStateCompletions(writer, states); + + writer.Indent--; + writer.WriteLine("end"); + writer.WriteLine(); + + writer.WriteLine($"complete -c {command.Name} -f -a '(_{safeName})'"); + + writer.Flush(); + return textWriter.ToString(); + } + + /// + /// Recursively collect all visible commands into a flat list with numeric state IDs. + /// State 0 is the root command. + /// + private static void CollectStates(Command cmd, List<(int id, Command cmd)> states) + { + states.Add((states.Count, cmd)); + foreach (var sub in cmd.Subcommands.Where(c => !c.Hidden)) + { + CollectStates(sub, states); + } + } + + /// + /// Write the command line tokenization logic. + /// Uses fish's commandline builtin to get completed tokens up to the cursor. + /// + private static void WriteTokenization(IndentedTextWriter writer) + { + // -opc: tokenize, cut at cursor, only completed tokens (excludes current partial word) + writer.WriteLine("set -l tokens (commandline -opc)"); + } + + // Options with MaximumNumberOfValues at or above this threshold are treated as unbounded + // (i.e. consume tokens until an option-like token is encountered). + // System.CommandLine uses 100_000 as its internal sentinel for ZeroOrMore/OneOrMore. + private const int UnboundedArityThreshold = 100_000; + + /// + /// Generate the state machine that walks completed tokens to determine which subcommand context we're in. + /// For each state, we check if the current word matches a known subcommand (transitioning to that subcommand's state) + /// or a value-taking option (skipping tokens for the option's value(s), respecting the option's arity). + /// + private static void WriteStateWalker(IndentedTextWriter writer, List<(int id, Command cmd)> states, Dictionary stateIdByCommand) + { + writer.WriteLine("set -l state 0"); + writer.WriteLine("set -l i 2"); // start after the command name (fish arrays are 1-based) + writer.WriteLine("while test $i -le (count $tokens)"); + writer.Indent++; + writer.WriteLine("set -l word $tokens[$i]"); + + writer.WriteLine("switch $state"); + writer.Indent++; + + foreach (var (stateId, cmd) in states) + { + var visibleSubs = cmd.Subcommands.Where(c => !c.Hidden).ToArray(); + var valueOptions = cmd.HierarchicalOptions() + .Where(o => !o.Hidden && !o.IsFlag()) + .ToArray(); + + // Skip states that have no transitions to emit + if (visibleSubs.Length == 0 && valueOptions.Length == 0) + continue; + + writer.WriteLine($"case {stateId}"); + writer.Indent++; + writer.WriteLine("switch $word"); + writer.Indent++; + + // Subcommand transitions + foreach (var sub in visibleSubs) + { + var subStateId = stateIdByCommand[sub]; + var names = string.Join(" ", sub.Names()); + writer.WriteLine($"case {names}"); + writer.Indent++; + writer.WriteLine($"set state {subStateId}"); + writer.Indent--; + } + + // Single-value options (arity exactly 1): skip the next token + var singleValueNames = valueOptions + .Where(o => o.Arity.MaximumNumberOfValues == 1) + .SelectMany(o => o.Names()) + .ToArray(); + if (singleValueNames.Length > 0) + { + writer.WriteLine($"case {string.Join(" ", singleValueNames)}"); + writer.Indent++; + writer.WriteLine("set i (math $i + 1)"); + writer.Indent--; + } + + // Multi-value options (arity > 1): skip up to N tokens, stopping at option-like tokens. + // Group by arity so options with the same max can share a case branch. + var multiValueByArity = valueOptions + .Where(o => o.Arity.MaximumNumberOfValues > 1) + .GroupBy(o => o.Arity.MaximumNumberOfValues); + foreach (var group in multiValueByArity) + { + var names = string.Join(" ", group.SelectMany(o => o.Names())); + writer.WriteLine($"case {names}"); + writer.Indent++; + WriteMultiValueSkipLoop(writer, group.Key); + writer.Indent--; + } + + writer.Indent--; + writer.WriteLine("end"); + writer.Indent--; + } + + writer.Indent--; + writer.WriteLine("end"); + + writer.WriteLine("set i (math $i + 1)"); + writer.Indent--; + writer.WriteLine("end"); + } + + /// + /// Emit a fish loop that skips value tokens after a multi-value option. + /// Stops when it has consumed maxValues tokens, or encounters a token starting with '-', + /// whichever comes first. For unbounded arities, only the '-' check applies. + /// + private static void WriteMultiValueSkipLoop(IndentedTextWriter writer, int maxValues) + { + bool isBounded = maxValues < UnboundedArityThreshold; + + if (isBounded) + { + writer.WriteLine($"set -l skip_max {maxValues}"); + writer.WriteLine("set -l skipped 0"); + writer.WriteLine("while test $skipped -lt $skip_max -a (math $i + 1) -le (count $tokens)"); + } + else + { + writer.WriteLine("while test (math $i + 1) -le (count $tokens)"); + } + + writer.Indent++; + writer.WriteLine("set -l next $tokens[(math $i + 1)]"); + writer.WriteLine("if string match -q -- '-*' $next"); + writer.Indent++; + writer.WriteLine("break"); + writer.Indent--; + writer.WriteLine("end"); + writer.WriteLine("set i (math $i + 1)"); + if (isBounded) + { + writer.WriteLine("set skipped (math $skipped + 1)"); + } + writer.Indent--; + writer.WriteLine("end"); + } + + /// + /// Generate option value completions. + /// Scans backward through completed tokens to find the nearest option, then checks whether + /// we're still within that option's arity. This correctly handles both single-value and + /// multi-value options (e.g. --sources foo bar with arity 3 still offers completions + /// for the third value). + /// + private static void WriteOptionValueCompletions(IndentedTextWriter writer, List<(int id, Command cmd)> states) + { + var hasAnyValueOptions = states.Any(s => + s.cmd.HierarchicalOptions().Any(o => !o.Hidden && !o.IsFlag())); + + if (!hasAnyValueOptions) return; + + // Scan backward through tokens to find the nearest option (token starting with -) + writer.WriteLine("set -l opt_index 0"); + writer.WriteLine("if test (count $tokens) -ge 2"); + writer.Indent++; + writer.WriteLine("for j in (seq (count $tokens) -1 2)"); + writer.Indent++; + writer.WriteLine("if string match -q -- '-*' $tokens[$j]"); + writer.Indent++; + writer.WriteLine("set opt_index $j"); + writer.WriteLine("break"); + writer.Indent--; + writer.WriteLine("end"); + writer.Indent--; + writer.WriteLine("end"); + writer.Indent--; + writer.WriteLine("end"); + writer.WriteLine(); + + writer.WriteLine("if test $opt_index -gt 0"); + writer.Indent++; + writer.WriteLine("set -l opt $tokens[$opt_index]"); + // values_after = number of non-option tokens between the option and the cursor + writer.WriteLine("set -l values_after (math (count $tokens) - $opt_index)"); + writer.WriteLine("switch $state"); + writer.Indent++; + + foreach (var (stateId, cmd) in states) + { + var valueOptions = cmd.HierarchicalOptions() + .Where(o => !o.Hidden && !o.IsFlag()) + .ToArray(); + + if (valueOptions.Length == 0) + continue; + + writer.WriteLine($"case {stateId}"); + writer.Indent++; + writer.WriteLine("switch $opt"); + writer.Indent++; + + foreach (var option in valueOptions) + { + var names = string.Join(" ", option.Names()); + var maxValues = option.Arity.MaximumNumberOfValues; + bool isBounded = maxValues < UnboundedArityThreshold; + + writer.WriteLine($"case {names}"); + writer.Indent++; + + // For bounded options, check that we haven't exceeded the arity. + // For unbounded options, always offer completions (any number of values is valid). + if (isBounded) + { + writer.WriteLine($"if test $values_after -lt {maxValues}"); + writer.Indent++; + } + + if (option.IsDynamic) + { + writer.WriteLine(GenerateDynamicCall()); + } + else + { + var completions = option.GetCompletions(CompletionContext.Empty).ToArray(); + foreach (var c in completions) + { + WriteCompletionCandidate(writer, c); + } + } + writer.WriteLine("return"); + + if (isBounded) + { + writer.Indent--; + writer.WriteLine("end"); + } + + writer.Indent--; + } + + writer.Indent--; + writer.WriteLine("end"); + writer.Indent--; + } + + writer.Indent--; + writer.WriteLine("end"); + writer.Indent--; + writer.WriteLine("end"); + writer.WriteLine(); + } + + /// + /// Generate the main completion output for each state. + /// Emits subcommands, options, and positional argument completions for the current context. + /// + private static void WriteStateCompletions(IndentedTextWriter writer, List<(int id, Command cmd)> states) + { + writer.WriteLine("switch $state"); + writer.Indent++; + + foreach (var (stateId, cmd) in states) + { + writer.WriteLine($"case {stateId}"); + writer.Indent++; + + // Subcommand completions + foreach (var sub in cmd.Subcommands.Where(c => !c.Hidden)) + { + WriteCandidate(writer, sub.Name, SanitizeDescription(sub.Description)); + } + + // Option completions - emit all aliases so both -h and --help are completable + foreach (var option in cmd.HierarchicalOptions().Where(o => !o.Hidden)) + { + var desc = SanitizeDescription(option.Description); + foreach (var name in option.Names()) + { + WriteCandidate(writer, name, desc); + } + } + + // Positional argument completions + foreach (var arg in cmd.Arguments.Where(a => !a.Hidden)) + { + if (arg.IsDynamic) + { + writer.WriteLine(GenerateDynamicCall()); + } + else + { + var completions = arg.GetCompletions(CompletionContext.Empty).ToArray(); + foreach (var c in completions) + { + WriteCompletionCandidate(writer, c); + } + } + } + + writer.Indent--; + } + + writer.Indent--; + writer.WriteLine("end"); + } + + /// + /// Write a single completion candidate line with an optional description. + /// Fish uses tab-separated format: candidate\tdescription + /// + private static void WriteCompletionCandidate(IndentedTextWriter writer, CompletionItem completion) + { + var label = completion.InsertText ?? completion.Label; + var desc = completion.Documentation ?? completion.Detail; + WriteCandidate(writer, label, desc); + } + + private static void WriteCandidate(IndentedTextWriter writer, string label, string? description) + { + if (!string.IsNullOrEmpty(description)) + writer.WriteLine($"printf '%s\\t%s\\n' {FishEscape(label)} {FishEscape(description)}"); + else + writer.WriteLine($"printf '%s\\n' {FishEscape(label)}"); + } + + /// + /// Generate a dynamic completion call that invokes the binary's 'complete' subcommand. + /// Uses the actual command from the command line ($tokens[1]) so it works regardless of how the binary was invoked. + /// + /// TODO: this is currently bound to the .NET CLI's 'complete' command pattern - this should be definable/injectable per-host instead. + internal static string GenerateDynamicCall() + { + return "command $tokens[1] complete --position (commandline -C) (commandline -cp) 2>/dev/null"; + } + + /// + /// Escape a string for use in a fish single-quoted string. + /// Fish single-quoted strings support \' and \\ as escape sequences. + /// + internal static string FishEscape(string? s) + { + if (string.IsNullOrEmpty(s)) return "''"; + return "'" + s.Replace("\\", "\\\\").Replace("'", "\\'") + "'"; + } + + private static string SanitizeDescription(string? s) => + s?.Replace("\r\n", " ").Replace('\n', ' ').Replace('\r', ' ').Replace('\t', ' ') ?? ""; } diff --git a/test/System.CommandLine.StaticCompletions.Tests/FishShellProviderTests.cs b/test/System.CommandLine.StaticCompletions.Tests/FishShellProviderTests.cs new file mode 100644 index 000000000000..c8f385a5013b --- /dev/null +++ b/test/System.CommandLine.StaticCompletions.Tests/FishShellProviderTests.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace System.CommandLine.StaticCompletions.Tests; + +using System.CommandLine.StaticCompletions.Shells; + +public class FishShellProviderTests(ITestOutputHelper log) +{ + private IShellProvider provider = new FishShellProvider(); + + [Fact] + public async Task GenericCompletions() + { + await provider.Verify(new("mycommand"), log); + } + + [Fact] + public async Task SimpleOptionCompletion() + { + await provider.Verify(new("mycommand") { + new Option("--name") + }, log); + } + + [Fact] + public async Task SubcommandAndOptionInTopLevelList() + { + await provider.Verify(new("mycommand") { + new Option("--name"), + new Command("subcommand") + }, log); + } + + [Fact] + public async Task NestedSubcommandCompletion() + { + await provider.Verify(new("mycommand") { + new Command("subcommand") { + new Command("nested") + } + }, log); + } + + [Fact] + public async Task DynamicCompletionsGeneration() + { + var dynamicOption = new Option("--dynamic") + { + IsDynamic = true + }; + var dynamicArg = new Argument("values") + { + IsDynamic = true + }; + Command command = new Command("mycommand") + { + dynamicOption, + dynamicArg + }; + await provider.Verify(command, log); + } + + [Fact] + public async Task StaticOptionValues() + { + var staticOption = new Option("--verbosity"); + staticOption.AcceptOnlyFromAmong("quiet", "minimal", "normal", "detailed", "diagnostic"); + Command command = new Command("mycommand") + { + staticOption + }; + await provider.Verify(command, log); + } + + [Fact] + public async Task BoundedMultiValueOption() + { + var multiOption = new Option("--sources") + { + Arity = new ArgumentArity(1, 3) + }; + multiOption.AcceptOnlyFromAmong("src1", "src2", "src3", "src4"); + Command command = new Command("mycommand") + { + multiOption, + new Command("subcommand") + }; + await provider.Verify(command, log); + } + + [Fact] + public async Task UnboundedMultiValueOption() + { + var unboundedOption = new Option("--items") + { + Arity = ArgumentArity.ZeroOrMore + }; + unboundedOption.AcceptOnlyFromAmong("a", "b", "c"); + Command command = new Command("mycommand") + { + unboundedOption, + new Option("--name"), + new Command("subcommand") + }; + await provider.Verify(command, log); + } + + [Fact] + public async Task MixedArityOptions() + { + var singleOption = new Option("--config"); + singleOption.AcceptOnlyFromAmong("debug", "release"); + + var multiOption = new Option("--framework", "-f") + { + Arity = new ArgumentArity(1, 3) + }; + multiOption.AcceptOnlyFromAmong("net8.0", "net9.0", "net10.0"); + + var unboundedOption = new Option("--sources") + { + Arity = ArgumentArity.OneOrMore + }; + + Command command = new Command("mycommand") + { + singleOption, + multiOption, + unboundedOption, + new Command("build") + }; + await provider.Verify(command, log); + } +} diff --git a/test/System.CommandLine.StaticCompletions.Tests/VerifyConfiguration.cs b/test/System.CommandLine.StaticCompletions.Tests/VerifyConfiguration.cs index 4eeca67880c1..71dd56b1d276 100644 --- a/test/System.CommandLine.StaticCompletions.Tests/VerifyConfiguration.cs +++ b/test/System.CommandLine.StaticCompletions.Tests/VerifyConfiguration.cs @@ -25,5 +25,6 @@ public static void Initialize() EmptyFiles.FileExtensions.AddTextExtension("ps1"); EmptyFiles.FileExtensions.AddTextExtension("nu"); + EmptyFiles.FileExtensions.AddTextExtension("fish"); } } diff --git a/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.BoundedMultiValueOption.verified.fish b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.BoundedMultiValueOption.verified.fish new file mode 100644 index 000000000000..b33eebee909a --- /dev/null +++ b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.BoundedMultiValueOption.verified.fish @@ -0,0 +1,67 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case subcommand + set state 1 + case --sources + set -l skip_max 3 + set -l skipped 0 + while test $skipped -lt $skip_max -a (math $i + 1) -le (count $tokens) + set -l next $tokens[(math $i + 1)] + if string match -q -- '-*' $next + break + end + set i (math $i + 1) + set skipped (math $skipped + 1) + end + end + end + set i (math $i + 1) + end + + set -l opt_index 0 + if test (count $tokens) -ge 2 + for j in (seq (count $tokens) -1 2) + if string match -q -- '-*' $tokens[$j] + set opt_index $j + break + end + end + end + + if test $opt_index -gt 0 + set -l opt $tokens[$opt_index] + set -l values_after (math (count $tokens) - $opt_index) + switch $state + case 0 + switch $opt + case --sources + if test $values_after -lt 3 + printf '%s\n' 'src1' + printf '%s\n' 'src2' + printf '%s\n' 'src3' + printf '%s\n' 'src4' + return + end + end + end + end + + switch $state + case 0 + printf '%s\n' 'subcommand' + printf '%s\n' '--sources' + case 1 + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.DynamicCompletionsGeneration.verified.fish b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.DynamicCompletionsGeneration.verified.fish new file mode 100644 index 000000000000..f7f353523b26 --- /dev/null +++ b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.DynamicCompletionsGeneration.verified.fish @@ -0,0 +1,52 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case --dynamic + set i (math $i + 1) + end + end + set i (math $i + 1) + end + + set -l opt_index 0 + if test (count $tokens) -ge 2 + for j in (seq (count $tokens) -1 2) + if string match -q -- '-*' $tokens[$j] + set opt_index $j + break + end + end + end + + if test $opt_index -gt 0 + set -l opt $tokens[$opt_index] + set -l values_after (math (count $tokens) - $opt_index) + switch $state + case 0 + switch $opt + case --dynamic + if test $values_after -lt 1 + command $tokens[1] complete --position (commandline -C) (commandline -cp) 2>/dev/null + return + end + end + end + end + + switch $state + case 0 + printf '%s\n' '--dynamic' + command $tokens[1] complete --position (commandline -C) (commandline -cp) 2>/dev/null + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.GenericCompletions.verified.fish b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.GenericCompletions.verified.fish new file mode 100644 index 000000000000..c86386e997c8 --- /dev/null +++ b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.GenericCompletions.verified.fish @@ -0,0 +1,20 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + end + set i (math $i + 1) + end + + switch $state + case 0 + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.MixedArityOptions.verified.fish b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.MixedArityOptions.verified.fish new file mode 100644 index 000000000000..a5728a17ce22 --- /dev/null +++ b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.MixedArityOptions.verified.fish @@ -0,0 +1,87 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case build + set state 1 + case --config + set i (math $i + 1) + case --framework -f + set -l skip_max 3 + set -l skipped 0 + while test $skipped -lt $skip_max -a (math $i + 1) -le (count $tokens) + set -l next $tokens[(math $i + 1)] + if string match -q -- '-*' $next + break + end + set i (math $i + 1) + set skipped (math $skipped + 1) + end + case --sources + while test (math $i + 1) -le (count $tokens) + set -l next $tokens[(math $i + 1)] + if string match -q -- '-*' $next + break + end + set i (math $i + 1) + end + end + end + set i (math $i + 1) + end + + set -l opt_index 0 + if test (count $tokens) -ge 2 + for j in (seq (count $tokens) -1 2) + if string match -q -- '-*' $tokens[$j] + set opt_index $j + break + end + end + end + + if test $opt_index -gt 0 + set -l opt $tokens[$opt_index] + set -l values_after (math (count $tokens) - $opt_index) + switch $state + case 0 + switch $opt + case --config + if test $values_after -lt 1 + printf '%s\n' 'debug' + printf '%s\n' 'release' + return + end + case --framework -f + if test $values_after -lt 3 + printf '%s\n' 'net10.0' + printf '%s\n' 'net8.0' + printf '%s\n' 'net9.0' + return + end + case --sources + return + end + end + end + + switch $state + case 0 + printf '%s\n' 'build' + printf '%s\n' '--config' + printf '%s\n' '--framework' + printf '%s\n' '-f' + printf '%s\n' '--sources' + case 1 + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.NestedSubcommandCompletion.verified.fish b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.NestedSubcommandCompletion.verified.fish new file mode 100644 index 000000000000..400d43cf29c3 --- /dev/null +++ b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.NestedSubcommandCompletion.verified.fish @@ -0,0 +1,34 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case subcommand + set state 1 + end + case 1 + switch $word + case nested + set state 2 + end + end + set i (math $i + 1) + end + + switch $state + case 0 + printf '%s\n' 'subcommand' + case 1 + printf '%s\n' 'nested' + case 2 + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.SimpleOptionCompletion.verified.fish b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.SimpleOptionCompletion.verified.fish new file mode 100644 index 000000000000..68d8ab21e67b --- /dev/null +++ b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.SimpleOptionCompletion.verified.fish @@ -0,0 +1,50 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case --name + set i (math $i + 1) + end + end + set i (math $i + 1) + end + + set -l opt_index 0 + if test (count $tokens) -ge 2 + for j in (seq (count $tokens) -1 2) + if string match -q -- '-*' $tokens[$j] + set opt_index $j + break + end + end + end + + if test $opt_index -gt 0 + set -l opt $tokens[$opt_index] + set -l values_after (math (count $tokens) - $opt_index) + switch $state + case 0 + switch $opt + case --name + if test $values_after -lt 1 + return + end + end + end + end + + switch $state + case 0 + printf '%s\n' '--name' + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.StaticOptionValues.verified.fish b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.StaticOptionValues.verified.fish new file mode 100644 index 000000000000..98a413a4497f --- /dev/null +++ b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.StaticOptionValues.verified.fish @@ -0,0 +1,55 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case --verbosity + set i (math $i + 1) + end + end + set i (math $i + 1) + end + + set -l opt_index 0 + if test (count $tokens) -ge 2 + for j in (seq (count $tokens) -1 2) + if string match -q -- '-*' $tokens[$j] + set opt_index $j + break + end + end + end + + if test $opt_index -gt 0 + set -l opt $tokens[$opt_index] + set -l values_after (math (count $tokens) - $opt_index) + switch $state + case 0 + switch $opt + case --verbosity + if test $values_after -lt 1 + printf '%s\n' 'detailed' + printf '%s\n' 'diagnostic' + printf '%s\n' 'minimal' + printf '%s\n' 'normal' + printf '%s\n' 'quiet' + return + end + end + end + end + + switch $state + case 0 + printf '%s\n' '--verbosity' + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.SubcommandAndOptionInTopLevelList.verified.fish b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.SubcommandAndOptionInTopLevelList.verified.fish new file mode 100644 index 000000000000..0936b3b28cf8 --- /dev/null +++ b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.SubcommandAndOptionInTopLevelList.verified.fish @@ -0,0 +1,54 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case subcommand + set state 1 + case --name + set i (math $i + 1) + end + end + set i (math $i + 1) + end + + set -l opt_index 0 + if test (count $tokens) -ge 2 + for j in (seq (count $tokens) -1 2) + if string match -q -- '-*' $tokens[$j] + set opt_index $j + break + end + end + end + + if test $opt_index -gt 0 + set -l opt $tokens[$opt_index] + set -l values_after (math (count $tokens) - $opt_index) + switch $state + case 0 + switch $opt + case --name + if test $values_after -lt 1 + return + end + end + end + end + + switch $state + case 0 + printf '%s\n' 'subcommand' + printf '%s\n' '--name' + case 1 + end +end + +complete -c mycommand -f -a '(_mycommand)' diff --git a/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.UnboundedMultiValueOption.verified.fish b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.UnboundedMultiValueOption.verified.fish new file mode 100644 index 000000000000..92f17b003bcb --- /dev/null +++ b/test/System.CommandLine.StaticCompletions.Tests/snapshots/fish/FishShellProviderTests.UnboundedMultiValueOption.verified.fish @@ -0,0 +1,68 @@ +# fish completions for mycommand + +function _mycommand + set -l tokens (commandline -opc) + + set -l state 0 + set -l i 2 + while test $i -le (count $tokens) + set -l word $tokens[$i] + switch $state + case 0 + switch $word + case subcommand + set state 1 + case --name + set i (math $i + 1) + case --items + while test (math $i + 1) -le (count $tokens) + set -l next $tokens[(math $i + 1)] + if string match -q -- '-*' $next + break + end + set i (math $i + 1) + end + end + end + set i (math $i + 1) + end + + set -l opt_index 0 + if test (count $tokens) -ge 2 + for j in (seq (count $tokens) -1 2) + if string match -q -- '-*' $tokens[$j] + set opt_index $j + break + end + end + end + + if test $opt_index -gt 0 + set -l opt $tokens[$opt_index] + set -l values_after (math (count $tokens) - $opt_index) + switch $state + case 0 + switch $opt + case --items + printf '%s\n' 'a' + printf '%s\n' 'b' + printf '%s\n' 'c' + return + case --name + if test $values_after -lt 1 + return + end + end + end + end + + switch $state + case 0 + printf '%s\n' 'subcommand' + printf '%s\n' '--items' + printf '%s\n' '--name' + case 1 + end +end + +complete -c mycommand -f -a '(_mycommand)'