Skip to content

Commit 6e344c9

Browse files
authored
Merge pull request #197 from dusrdev/master
Fix option precedence over positional arguments
2 parents 281ab69 + e38a86d commit 6e344c9

File tree

2 files changed

+149
-56
lines changed

2 files changed

+149
-56
lines changed

src/ConsoleAppFramework/Emitter.cs

Lines changed: 80 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -136,87 +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)
146+
sb.AppendLine("var name = commandArgs[i];");
142147
if (hasArgument)
143148
{
144-
for (int i = 0; i < command.Parameters.Length; i++)
145-
{
146-
var parameter = command.Parameters[i];
147-
if (!parameter.IsArgument) continue;
149+
sb.AppendLine("var optionCandidate = name.Length > 1 && name[0] == '-' && !char.IsDigit(name[1]);");
150+
}
151+
else
152+
{
153+
sb.AppendLine("var optionCandidate = name.Length > 1 && name[0] == '-';");
154+
}
155+
sb.AppendLine();
148156

149-
sb.AppendLine($"if (i == {parameter.ArgumentIndex})");
150-
using (sb.BeginBlock())
157+
if (!command.Parameters.All(p => !p.IsParsable || p.IsArgument))
158+
{
159+
using (sb.BeginBlock("if (optionCandidate)"))
160+
{
161+
using (sb.BeginBlock("switch (name)"))
151162
{
152-
sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, increment: false)}");
153-
if (parameter.RequireCheckArgumentParsed)
163+
// parse argument(fast, switch directly)
164+
for (int i = 0; i < command.Parameters.Length; i++)
154165
{
155-
sb.AppendLine($"arg{i}Parsed = true;");
166+
var parameter = command.Parameters[i];
167+
if (!parameter.IsParsable) continue;
168+
if (parameter.IsArgument) continue;
169+
170+
sb.AppendLine($"case \"--{parameter.Name}\":");
171+
foreach (var alias in parameter.Aliases)
172+
{
173+
sb.AppendLine($"case \"{alias}\":");
174+
}
175+
using (sb.BeginBlock())
176+
{
177+
sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, increment: true)}");
178+
if (parameter.RequireCheckArgumentParsed)
179+
{
180+
sb.AppendLine($"arg{i}Parsed = true;");
181+
}
182+
sb.AppendLine("continue;");
183+
}
184+
}
185+
186+
using (sb.BeginIndent("default:"))
187+
{
188+
// parse argument(slow, ignorecase)
189+
for (int i = 0; i < command.Parameters.Length; i++)
190+
{
191+
var parameter = command.Parameters[i];
192+
if (!parameter.IsParsable) continue;
193+
if (parameter.IsArgument) continue;
194+
195+
sb.AppendLine($"if (string.Equals(name, \"--{parameter.Name}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == 0 ? ")" : "")}");
196+
for (int j = 0; j < parameter.Aliases.Length; j++)
197+
{
198+
var alias = parameter.Aliases[j];
199+
sb.AppendLine($" || string.Equals(name, \"{alias}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == j + 1 ? ")" : "")}");
200+
}
201+
using (sb.BeginBlock())
202+
{
203+
sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, increment: true)}");
204+
if (parameter.RequireCheckArgumentParsed)
205+
{
206+
sb.AppendLine($"arg{i}Parsed = true;");
207+
}
208+
sb.AppendLine("continue;");
209+
}
210+
}
211+
212+
sb.AppendLine("ThrowArgumentNameNotFound(name);");
213+
sb.AppendLine("break;");
156214
}
157-
sb.AppendLine("continue;");
158215
}
159216
}
160-
sb.AppendLine();
161217
}
162218

163-
sb.AppendLine("var name = commandArgs[i];");
164-
sb.AppendLine();
165-
166-
using (sb.BeginBlock("switch (name)"))
219+
// parse indexed argument([Argument] parameter)
220+
if (hasArgument)
167221
{
168-
// parse argument(fast, switch directly)
169222
for (int i = 0; i < command.Parameters.Length; i++)
170223
{
171224
var parameter = command.Parameters[i];
172-
if (!parameter.IsParsable) continue;
173-
if (parameter.IsArgument) continue;
225+
if (!parameter.IsArgument) continue;
174226

175-
sb.AppendLine($"case \"--{parameter.Name}\":");
176-
foreach (var alias in parameter.Aliases)
177-
{
178-
sb.AppendLine($"case \"{alias}\":");
179-
}
227+
sb.AppendLine($"if (argumentPosition == {parameter.ArgumentIndex})");
180228
using (sb.BeginBlock())
181229
{
182-
sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, increment: true)}");
230+
sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, increment: false)}");
183231
if (parameter.RequireCheckArgumentParsed)
184232
{
185233
sb.AppendLine($"arg{i}Parsed = true;");
186234
}
187-
sb.AppendLine("break;");
235+
sb.AppendLine("argumentPosition++;");
236+
sb.AppendLine("continue;");
188237
}
189238
}
239+
sb.AppendLine();
240+
}
190241

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-
}
215-
}
216-
217-
sb.AppendLine("ThrowArgumentNameNotFound(name);");
218-
sb.AppendLine("break;");
219-
}
242+
if (hasArgument)
243+
{
244+
sb.AppendLine("ThrowArgumentNameNotFound(name);");
220245
}
221246
}
222247

tests/ConsoleAppFramework.GeneratorTests/RunTest.cs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,74 @@ 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+
69+
[Fact]
70+
public void ArgumentAllowsLeadingDashValue()
71+
{
72+
var code = """
73+
ConsoleApp.Run(args, ([Argument] int count, bool dryRun) =>
74+
{
75+
Console.Write((count, dryRun).ToString());
76+
});
77+
""";
78+
79+
verifier.Execute(code, "-5 --dry-run", "(-5, True)");
80+
verifier.Execute(code, "-5", "(-5, False)");
81+
}
82+
1583
[Fact]
1684
public void SyncRunShouldFailed()
1785
{
@@ -21,7 +89,7 @@ public void SyncRunShouldFailed()
2189
[Fact]
2290
public void MissingArgument()
2391
{
24-
verifier.Error("ConsoleApp.Run(args, (int x, int y) => { Console.Write((x + y)); });", "--x 10 y 20").ShouldContain("Argument 'y' is not recognized.");
92+
verifier.Error("ConsoleApp.Run(args, (int x, int y) => { Console.Write((x + y)); });", "--x 10 y 20").ShouldContain("Required argument 'y' was not specified.");
2593

2694
Environment.ExitCode.ShouldBe(1);
2795
Environment.ExitCode = 0;

0 commit comments

Comments
 (0)