Skip to content

Commit 520efe0

Browse files
FrankRay78patriksvensson
authored andcommitted
Significant improvement to the command line parsing
1 parent c81bc5f commit 520efe0

File tree

2 files changed

+86
-14
lines changed

2 files changed

+86
-14
lines changed

src/Spectre.Console.Cli/CommandParseException.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,9 @@ internal static CommandParseException ValueIsNotInValidFormat(string value)
113113
var text = $"[red]Error:[/] The value '[white]{value}[/]' is not in a correct format";
114114
return new CommandParseException("Could not parse value", new Markup(text));
115115
}
116+
117+
internal static CommandParseException UnknownParsingError()
118+
{
119+
return new CommandParseException("An unknown error occured when parsing the arguments.");
120+
}
116121
}

src/Spectre.Console.Cli/Internal/CommandExecutor.cs

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using static Spectre.Console.Cli.CommandTreeTokenizer;
2+
13
namespace Spectre.Console.Cli;
24

35
internal sealed class CommandExecutor
@@ -101,34 +103,99 @@ public async Task<int> Execute(IConfiguration configuration, IEnumerable<string>
101103
}
102104
}
103105

106+
[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1513:Closing brace should be followed by blank line", Justification = "Improves code readability by grouping together related statements into a block")]
104107
private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IReadOnlyList<string> args)
105108
{
106-
var parser = new CommandTreeParser(model, settings.CaseSensitivity, settings.ParsingMode, settings.ConvertFlagsToRemainingArguments);
109+
CommandTreeParserResult? parsedResult = null;
110+
CommandTreeTokenizerResult tokenizerResult;
107111

108-
var parserContext = new CommandTreeParserContext(args, settings.ParsingMode);
109-
var tokenizerResult = CommandTreeTokenizer.Tokenize(args);
110-
var parsedResult = parser.Parse(parserContext, tokenizerResult);
112+
try
113+
{
114+
(parsedResult, tokenizerResult) = InternalParseCommandLineArguments(model, settings, args);
111115

112-
var lastParsedLeaf = parsedResult.Tree?.GetLeafCommand();
113-
var lastParsedCommand = lastParsedLeaf?.Command;
114-
if (lastParsedLeaf != null && lastParsedCommand != null &&
116+
var lastParsedLeaf = parsedResult.Tree?.GetLeafCommand();
117+
var lastParsedCommand = lastParsedLeaf?.Command;
118+
119+
if (lastParsedLeaf != null && lastParsedCommand != null &&
115120
lastParsedCommand.IsBranch && !lastParsedLeaf.ShowHelp &&
116121
lastParsedCommand.DefaultCommand != null)
122+
{
123+
// Adjust for any parsed remaining arguments by
124+
// inserting the the default command ahead of them.
125+
var position = tokenizerResult.Tokens.Position;
126+
foreach (var parsedRemaining in parsedResult.Remaining.Parsed)
127+
{
128+
position--;
129+
position -= parsedRemaining.Count(value => value != null);
130+
}
131+
position = position < 0 ? 0 : position;
132+
133+
// Insert this branch's default command into the command line
134+
// arguments and try again to see if it will parse.
135+
var argsWithDefaultCommand = new List<string>(args);
136+
argsWithDefaultCommand.Insert(position, lastParsedCommand.DefaultCommand.Name);
137+
138+
(parsedResult, tokenizerResult) = InternalParseCommandLineArguments(model, settings, argsWithDefaultCommand);
139+
}
140+
}
141+
catch (CommandParseException) when (parsedResult == null && settings.ParsingMode == ParsingMode.Strict)
117142
{
118-
// Insert this branch's default command into the command line
119-
// arguments and try again to see if it will parse.
120-
var argsWithDefaultCommand = new List<string>(args);
143+
// The parsing exception might be resolved by adding in the default command,
144+
// but we can't know for sure. Take a brute force approach and try this for
145+
// every position between the arguments.
146+
for (int i = 0; i < args.Count; i++)
147+
{
148+
var argsWithDefaultCommand = new List<string>(args);
149+
argsWithDefaultCommand.Insert(args.Count - i, "__default_command");
150+
151+
try
152+
{
153+
(parsedResult, tokenizerResult) = InternalParseCommandLineArguments(model, settings, argsWithDefaultCommand);
121154

122-
argsWithDefaultCommand.Insert(tokenizerResult.Tokens.Position, lastParsedCommand.DefaultCommand.Name);
155+
break;
156+
}
157+
catch (CommandParseException)
158+
{
159+
// Continue.
160+
}
161+
}
123162

124-
parserContext = new CommandTreeParserContext(argsWithDefaultCommand, settings.ParsingMode);
125-
tokenizerResult = CommandTreeTokenizer.Tokenize(argsWithDefaultCommand);
126-
parsedResult = parser.Parse(parserContext, tokenizerResult);
163+
if (parsedResult == null)
164+
{
165+
// Failed to parse having inserted the default command between each argument.
166+
// Repeat the parsing of the original arguments to throw the correct exception.
167+
InternalParseCommandLineArguments(model, settings, args);
168+
}
169+
}
170+
171+
if (parsedResult == null)
172+
{
173+
// The arguments failed to parse despite everything we tried above.
174+
// Exceptions should be thrown above before ever getting this far,
175+
// however the following is the ulimately backstop and avoids
176+
// the compiler from complaining about returning null.
177+
throw CommandParseException.UnknownParsingError();
127178
}
128179

129180
return parsedResult;
130181
}
131182

183+
/// <summary>
184+
/// Parse the command line arguments using the specified <see cref="CommandModel"/> and <see cref="CommandAppSettings"/>,
185+
/// returning the parser and tokenizer results.
186+
/// </summary>
187+
/// <returns>The parser and tokenizer results as a tuple.</returns>
188+
private (CommandTreeParserResult ParserResult, CommandTreeTokenizerResult TokenizerResult) InternalParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IReadOnlyList<string> args)
189+
{
190+
var parser = new CommandTreeParser(model, settings.CaseSensitivity, settings.ParsingMode, settings.ConvertFlagsToRemainingArguments);
191+
192+
var parserContext = new CommandTreeParserContext(args, settings.ParsingMode);
193+
var tokenizerResult = CommandTreeTokenizer.Tokenize(args);
194+
var parsedResult = parser.Parse(parserContext, tokenizerResult);
195+
196+
return (parsedResult, tokenizerResult);
197+
}
198+
132199
private static async Task<int> Execute(
133200
CommandTree leaf,
134201
CommandTree tree,

0 commit comments

Comments
 (0)