Skip to content

Commit 9661de9

Browse files
committed
Fix option precedence over positional arguments
1 parent 276215d commit 9661de9

File tree

2 files changed

+134
-56
lines changed

2 files changed

+134
-56
lines changed

src/ConsoleAppFramework/Emitter.cs

Lines changed: 80 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -136,88 +136,112 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy
136136

137137
using (command.HasFilter ? sb.Nop : sb.BeginBlock("try"))
138138
{
139+
if (hasArgument)
140+
{
141+
sb.AppendLine("var argumentPosition = 0;");
142+
}
143+
139144
using (sb.BeginBlock("for (int i = 0; i < commandArgs.Length; i++)"))
140145
{
141-
// parse indexed argument([Argument] parameter)
142-
if (hasArgument)
146+
sb.AppendLine("var name = commandArgs[i];");
147+
sb.AppendLine("var optionMatched = false;");
148+
sb.AppendLine("var optionCandidate = name.Length > 1 && name[0] == '-' && !char.IsDigit(name[1]);");
149+
sb.AppendLine();
150+
151+
if (!command.Parameters.All(p => !p.IsParsable || p.IsArgument))
143152
{
144-
for (int i = 0; i < command.Parameters.Length; i++)
153+
using (sb.BeginBlock("if (optionCandidate)"))
145154
{
146-
var parameter = command.Parameters[i];
147-
if (!parameter.IsArgument) continue;
148-
149-
sb.AppendLine($"if (i == {parameter.ArgumentIndex})");
150-
using (sb.BeginBlock())
155+
using (sb.BeginBlock("switch (name)"))
151156
{
152-
sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, increment: false)}");
153-
if (parameter.RequireCheckArgumentParsed)
157+
// parse argument(fast, switch directly)
158+
for (int i = 0; i < command.Parameters.Length; i++)
154159
{
155-
sb.AppendLine($"arg{i}Parsed = true;");
160+
var parameter = command.Parameters[i];
161+
if (!parameter.IsParsable) continue;
162+
if (parameter.IsArgument) continue;
163+
164+
sb.AppendLine($"case \"--{parameter.Name}\":");
165+
foreach (var alias in parameter.Aliases)
166+
{
167+
sb.AppendLine($"case \"{alias}\":");
168+
}
169+
using (sb.BeginBlock())
170+
{
171+
sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, increment: true)}");
172+
if (parameter.RequireCheckArgumentParsed)
173+
{
174+
sb.AppendLine($"arg{i}Parsed = true;");
175+
}
176+
sb.AppendLine("optionMatched = true;");
177+
sb.AppendLine("break;");
178+
}
179+
}
180+
181+
using (sb.BeginIndent("default:"))
182+
{
183+
// parse argument(slow, ignorecase)
184+
for (int i = 0; i < command.Parameters.Length; i++)
185+
{
186+
var parameter = command.Parameters[i];
187+
if (!parameter.IsParsable) continue;
188+
if (parameter.IsArgument) continue;
189+
190+
sb.AppendLine($"if (string.Equals(name, \"--{parameter.Name}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == 0 ? ")" : "")}");
191+
for (int j = 0; j < parameter.Aliases.Length; j++)
192+
{
193+
var alias = parameter.Aliases[j];
194+
sb.AppendLine($" || string.Equals(name, \"{alias}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == j + 1 ? ")" : "")}");
195+
}
196+
using (sb.BeginBlock())
197+
{
198+
sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, increment: true)}");
199+
if (parameter.RequireCheckArgumentParsed)
200+
{
201+
sb.AppendLine($"arg{i}Parsed = true;");
202+
}
203+
sb.AppendLine("optionMatched = true;");
204+
sb.AppendLine($"break;");
205+
}
206+
}
207+
208+
sb.AppendLine("ThrowArgumentNameNotFound(name);");
209+
sb.AppendLine("break;");
156210
}
211+
}
212+
213+
sb.AppendLine("if (optionMatched)");
214+
using (sb.BeginBlock())
215+
{
157216
sb.AppendLine("continue;");
158217
}
159218
}
160-
sb.AppendLine();
161219
}
162220

163-
sb.AppendLine("var name = commandArgs[i];");
164-
sb.AppendLine();
165-
166-
using (sb.BeginBlock("switch (name)"))
221+
// parse indexed argument([Argument] parameter)
222+
if (hasArgument)
167223
{
168-
// parse argument(fast, switch directly)
169224
for (int i = 0; i < command.Parameters.Length; i++)
170225
{
171226
var parameter = command.Parameters[i];
172-
if (!parameter.IsParsable) continue;
173-
if (parameter.IsArgument) continue;
227+
if (!parameter.IsArgument) continue;
174228

175-
sb.AppendLine($"case \"--{parameter.Name}\":");
176-
foreach (var alias in parameter.Aliases)
177-
{
178-
sb.AppendLine($"case \"{alias}\":");
179-
}
229+
sb.AppendLine($"if (argumentPosition == {parameter.ArgumentIndex})");
180230
using (sb.BeginBlock())
181231
{
182-
sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, increment: true)}");
232+
sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, increment: false)}");
183233
if (parameter.RequireCheckArgumentParsed)
184234
{
185235
sb.AppendLine($"arg{i}Parsed = true;");
186236
}
187-
sb.AppendLine("break;");
188-
}
189-
}
190-
191-
using (sb.BeginIndent("default:"))
192-
{
193-
// parse argument(slow, ignorecase)
194-
for (int i = 0; i < command.Parameters.Length; i++)
195-
{
196-
var parameter = command.Parameters[i];
197-
if (!parameter.IsParsable) continue;
198-
if (parameter.IsArgument) continue;
199-
200-
sb.AppendLine($"if (string.Equals(name, \"--{parameter.Name}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == 0 ? ")" : "")}");
201-
for (int j = 0; j < parameter.Aliases.Length; j++)
202-
{
203-
var alias = parameter.Aliases[j];
204-
sb.AppendLine($" || string.Equals(name, \"{alias}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == j + 1 ? ")" : "")}");
205-
}
206-
using (sb.BeginBlock())
207-
{
208-
sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, increment: true)}");
209-
if (parameter.RequireCheckArgumentParsed)
210-
{
211-
sb.AppendLine($"arg{i}Parsed = true;");
212-
}
213-
sb.AppendLine($"break;");
214-
}
237+
sb.AppendLine("argumentPosition++;");
238+
sb.AppendLine("continue;");
215239
}
216-
217-
sb.AppendLine("ThrowArgumentNameNotFound(name);");
218-
sb.AppendLine("break;");
219240
}
241+
sb.AppendLine();
220242
}
243+
244+
sb.AppendLine("ThrowArgumentNameNotFound(name);");
221245
}
222246

223247
// validate parsed

tests/ConsoleAppFramework.GeneratorTests/RunTest.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,60 @@ public void SyncRun()
1212
verifier.Execute("ConsoleApp.Run(args, (int x, int y) => { Console.Write((x + y)); });", "--x 10 --y 20", "30");
1313
}
1414

15+
[Fact]
16+
public void OptionTokenShouldNotFillArgumentSlot()
17+
{
18+
var code = """
19+
ConsoleApp.Run(args, ([Argument] string path, bool dryRun) =>
20+
{
21+
Console.Write((dryRun, path).ToString());
22+
});
23+
""";
24+
25+
verifier.Error(code, "--dry-run").ShouldContain("Required argument 'path' was not specified.");
26+
verifier.Execute(code, "--dry-run sample.txt", "(True, sample.txt)");
27+
}
28+
29+
[Fact]
30+
public void OptionTokenAllowsMultipleArguments()
31+
{
32+
var code = """
33+
ConsoleApp.Run(args, ([Argument] string source, [Argument] string destination, bool dryRun) =>
34+
{
35+
Console.Write((dryRun, source, destination).ToString());
36+
});
37+
""";
38+
39+
verifier.Execute(code, "--dry-run input.json output.json", "(True, input.json, output.json)");
40+
}
41+
42+
[Fact]
43+
public void OptionTokenRespectsArgumentDefaultValue()
44+
{
45+
var code = """
46+
ConsoleApp.Run(args, ([Argument] string path = "default-path", bool dryRun = false) =>
47+
{
48+
Console.Write((dryRun, path).ToString());
49+
});
50+
""";
51+
52+
verifier.Execute(code, "--dry-run", "(True, default-path)");
53+
}
54+
55+
[Fact]
56+
public void OptionTokenHandlesParamsArguments()
57+
{
58+
var code = """
59+
ConsoleApp.Run(args, ([Argument] string path, bool dryRun, params string[] extras) =>
60+
{
61+
Console.Write($"{dryRun}:{path}:{string.Join("|", extras)}");
62+
});
63+
""";
64+
65+
verifier.Execute(code, "--dry-run path.txt --extras src.txt dst.txt", "True:path.txt:src.txt|dst.txt");
66+
verifier.Execute(code, "--dry-run path.txt", "True:path.txt:");
67+
}
68+
1569
[Fact]
1670
public void SyncRunShouldFailed()
1771
{

0 commit comments

Comments
 (0)