Skip to content

Commit d9fccdb

Browse files
committed
Strip dotnet/app args before CAF parsing in dnx go
1 parent 8e662aa commit d9fccdb

6 files changed

Lines changed: 214 additions & 16 deletions

File tree

src/Tests/EndToEndTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,27 @@ public void Dev_second_run_hits_cache_and_edit_invalidates()
4848
}
4949
}
5050

51+
[Fact]
52+
public void Publish_passes_v_q_to_dotnet_and_forwards_app_args()
53+
{
54+
var app = CreateAppThatEchoesArgs();
55+
56+
try
57+
{
58+
RunGo("clean", app);
59+
60+
var (exit, output) = RunGo(app, "--r2r", "/v:q", "--", "hello", "world");
61+
Assert.Equal(0, exit);
62+
Assert.Contains("APP:hello|world", output);
63+
Assert.DoesNotContain("Build started", output);
64+
Assert.DoesNotContain("Determining projects to restore", output);
65+
}
66+
finally
67+
{
68+
CleanApp(app);
69+
}
70+
}
71+
5172
[Fact]
5273
public void Publish_r2r_second_run_hits_cache()
5374
{
@@ -153,6 +174,18 @@ static string CreateApp(string marker)
153174
return app;
154175
}
155176

177+
static string CreateAppThatEchoesArgs()
178+
{
179+
var dir = CreateTempDir();
180+
var app = Path.Combine(dir, "app.cs");
181+
File.WriteAllText(app, """
182+
#:property TargetFramework=net10.0
183+
184+
Console.WriteLine("APP:" + string.Join("|", args));
185+
""");
186+
return app;
187+
}
188+
156189
static string GetTempRoot()
157190
{
158191
var directory = OperatingSystem.IsWindows()

src/Tests/GoArgsTests.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,74 @@ public void Normalize_empty_and_null()
8686
Assert.Empty(GoArgs.Normalize([]));
8787
Assert.Empty(GoArgs.Normalize(null!));
8888
}
89+
90+
[Fact]
91+
public void PrepareCafArgs_strips_dotnet_args_and_separator_for_default_command()
92+
{
93+
var caf = GoArgs.PrepareCafArgs(["app.cs", "/p:MyProp=true", "--", "arg1", "arg2"]);
94+
95+
Assert.Equal(["app.cs"], caf);
96+
Assert.Equal(["/p:MyProp=true", "--", "arg1", "arg2"], GoArgs.ForwardArgs);
97+
98+
var (dotnet, app) = GoArgs.Split(GoArgs.ForwardArgs);
99+
Assert.Equal(["/p:MyProp=true"], dotnet);
100+
Assert.Equal(["arg1", "arg2"], app);
101+
}
102+
103+
[Fact]
104+
public void PrepareCafArgs_strips_msbuild_verbosity_before_separator()
105+
{
106+
var caf = GoArgs.PrepareCafArgs(["app.cs", "/v:q", "--", "hello", "world"]);
107+
108+
Assert.Equal(["app.cs"], caf);
109+
Assert.Equal(["/v:q", "--", "hello", "world"], GoArgs.ForwardArgs);
110+
111+
var (dotnet, app) = GoArgs.Split(GoArgs.ForwardArgs);
112+
Assert.Equal(["/v:q"], dotnet);
113+
Assert.Equal(["hello", "world"], app);
114+
}
115+
116+
[Fact]
117+
public void PrepareCafArgs_keeps_go_flags_and_forwards_rest_for_dev()
118+
{
119+
var caf = GoArgs.PrepareCafArgs(["dev", "app.cs", "--r2r", "/p:Configuration=Release", "--", "apparg"]);
120+
121+
Assert.Equal(["dev", "app.cs", "--r2r"], caf);
122+
Assert.Equal(["/p:Configuration=Release", "--", "apparg"], GoArgs.ForwardArgs);
123+
}
124+
125+
[Fact]
126+
public void PrepareCafArgs_forwards_positional_args_when_no_separator()
127+
{
128+
GoArgs.PrepareCafArgs(["app.cs", "arg1", "arg2"]);
129+
130+
Assert.Equal(["arg1", "arg2"], GoArgs.ForwardArgs);
131+
}
132+
133+
[Fact]
134+
public void PrepareCafArgs_passes_clean_args_through_unchanged()
135+
{
136+
var caf = GoArgs.PrepareCafArgs(["clean", "--all"]);
137+
138+
Assert.Equal(["clean", "--all"], caf);
139+
Assert.Empty(GoArgs.ForwardArgs);
140+
}
141+
142+
[Fact]
143+
public void PrepareCafArgs_passes_help_through_unchanged()
144+
{
145+
var caf = GoArgs.PrepareCafArgs(["--help"]);
146+
147+
Assert.Equal(["--help"], caf);
148+
Assert.Empty(GoArgs.ForwardArgs);
149+
}
150+
151+
[Fact]
152+
public void PrepareCafArgs_maps_debug_flag_to_caf_option_name()
153+
{
154+
var caf = GoArgs.PrepareCafArgs(["app.cs", "--debug", "/p:x=1", "--", "apparg"]);
155+
156+
Assert.Equal(["app.cs", "--gdbg"], caf);
157+
Assert.Equal(["/p:x=1", "--", "apparg"], GoArgs.ForwardArgs);
158+
}
89159
}

src/go/GoArgs.cs

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ public static class GoArgs
44
{
55
public static readonly string[] ReadyToRunPublishArgs = ["/p:PublishAot=false", "/p:PublishReadyToRun=true"];
66

7-
static readonly string[] GoSwitchNames = ["debug", "r2r"];
7+
static readonly string[] GoSwitchNames = ["debug", "r2r", "gdbg"];
8+
static readonly HashSet<string> Subcommands = new(StringComparer.OrdinalIgnoreCase) { "dev", "clean" };
9+
10+
static string[]? forwardArgs;
11+
12+
/// <summary>Arguments stripped before CAF parsing; forward to dotnet and/or the target app via <see cref="Split"/>.</summary>
13+
internal static string[] ForwardArgs => forwardArgs ?? [];
814

915
/// <summary>
1016
/// Normalizes go-specific switches so both prefix-less and --go- forms
@@ -41,6 +47,101 @@ public static string[] Normalize(string[] args)
4147
return [.. result];
4248
}
4349

50+
/// <summary>
51+
/// Splits raw invocation args into a CAF-safe slice (subcommand, input, go flags only)
52+
/// and pass-through args for dotnet publish/run and the target app.
53+
/// <para>
54+
/// Under <c>dnx go</c>, the first <c>--</c> is go's dotnet/app separator — not a CAF escape.
55+
/// Dotnet options like <c>/p:MyProp=true</c> or <c>-v:q</c> must not reach CAF or they are
56+
/// rejected as unrecognized options.
57+
/// </para>
58+
/// </summary>
59+
internal static string[] PrepareCafArgs(string[] args)
60+
{
61+
args = Normalize(args);
62+
if (args.Length == 0)
63+
{
64+
forwardArgs = [];
65+
return args;
66+
}
67+
68+
if (args.Any(static a => a is "-h" or "--help" or "--version"))
69+
{
70+
forwardArgs = [];
71+
return args;
72+
}
73+
74+
var index = 0;
75+
string? subcommand = null;
76+
if (Subcommands.Contains(args[0]))
77+
{
78+
subcommand = args[0].Equals("dev", StringComparison.OrdinalIgnoreCase) ? "dev" : "clean";
79+
index = 1;
80+
}
81+
82+
if (subcommand == "clean")
83+
{
84+
forwardArgs = [];
85+
return args;
86+
}
87+
88+
var caf = new List<string>();
89+
if (subcommand == "dev")
90+
caf.Add("dev");
91+
92+
string? input = null;
93+
var forward = new List<string>();
94+
var goFlags = new List<string>();
95+
96+
for (var i = index; i < args.Length; i++)
97+
{
98+
var arg = args[i];
99+
if (TryMatchGoFlag(arg, out var cafFlag))
100+
{
101+
goFlags.Add(cafFlag);
102+
continue;
103+
}
104+
105+
if (input is null)
106+
{
107+
input = arg;
108+
continue;
109+
}
110+
111+
forward.Add(arg);
112+
}
113+
114+
if (input is not null)
115+
caf.Add(input);
116+
117+
caf.AddRange(goFlags);
118+
119+
forwardArgs = forward.Count == 0 ? [] : [.. forward];
120+
return [.. caf];
121+
}
122+
123+
static bool TryMatchGoFlag(string arg, out string cafFlag)
124+
{
125+
cafFlag = "";
126+
if (!arg.StartsWith("--", StringComparison.Ordinal))
127+
return false;
128+
129+
var name = arg[2..];
130+
if (name.Equals("r2r", StringComparison.OrdinalIgnoreCase))
131+
{
132+
cafFlag = "--r2r";
133+
return true;
134+
}
135+
136+
if (name is "debug" or "gdbg")
137+
{
138+
cafFlag = "--gdbg";
139+
return true;
140+
}
141+
142+
return false;
143+
}
144+
44145
public static (string[] Dotnet, string[] App) Split(string[] extraArgs)
45146
{
46147
var separator = Array.IndexOf(extraArgs, "--");

src/go/Program.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,13 @@
77
app.Add("dev", DevAsync);
88
app.Add("clean", CleanAsync);
99
app.Add<CleanupCommands>();
10-
await app.RunAsync(GoArgs.Normalize(args));
10+
await app.RunAsync(GoArgs.PrepareCafArgs(args));
1111

1212
/// <summary>Runs a file-based .NET app from a .cs entrypoint.</summary>
1313
/// <param name="input">Path to an existing .cs file or remote ref (owner/repo[@ref][:path]).</param>
1414
/// <param name="r2r">Publish with ReadyToRun instead of native AOT; supports more dynamic .NET features while keeping most publish optimizations. </param>
1515
/// <param name="gdbg">Launch debugger before executing.</param>
16-
/// <param name="extraArgs">Arguments before '--' are passed to 'dotnet publish'; arguments after '--' are forwarded to the published app. Without '--', all extra arguments are forwarded to the published app.
17-
/// </param>
18-
static async Task<int> RunAsync([Argument] string input, bool r2r = false, [Hidden] bool gdbg = false, [Argument] params string[] extraArgs)
16+
static async Task<int> RunAsync([Argument] string input, bool r2r = false, [Hidden] bool gdbg = false)
1917
{
2018
if (gdbg)
2119
System.Diagnostics.Debugger.Launch();
@@ -24,7 +22,7 @@ static async Task<int> RunAsync([Argument] string input, bool r2r = false, [Hidd
2422
if (source is null)
2523
return 1;
2624

27-
var context = Prepare(source, extraArgs, r2r);
25+
var context = Prepare(source, GoArgs.ForwardArgs, r2r);
2826
if (context is null)
2927
return 1;
3028

@@ -144,9 +142,7 @@ static int CleanAsync([Argument] string? input = null, bool all = false, [Hidden
144142
/// <param name="input">Path to an existing .cs file or remote ref (owner/repo[@ref][:path]).</param>
145143
/// <param name="r2r">Accepted for consistency (ignored for dev which uses dotnet run).</param>
146144
/// <param name="gdbg">Launch debugger before executing.</param>
147-
/// <param name="extraArgs">Arguments before '--' are passed to 'dotnet run'; arguments after '--' are forwarded to the app. Without '--', all extra arguments are forwarded to the app.
148-
/// </param>
149-
static async Task<int> DevAsync([Argument] string input, [Hidden] bool r2r = false, [Hidden] bool gdbg = false, [Argument] params string[] extraArgs)
145+
static async Task<int> DevAsync([Argument] string input, [Hidden] bool r2r = false, [Hidden] bool gdbg = false)
150146
{
151147
if (gdbg)
152148
System.Diagnostics.Debugger.Launch();
@@ -155,7 +151,7 @@ static async Task<int> DevAsync([Argument] string input, [Hidden] bool r2r = fal
155151
if (source is null)
156152
return 1;
157153

158-
var context = Prepare(source, extraArgs);
154+
var context = Prepare(source, GoArgs.ForwardArgs);
159155
if (context is null)
160156
return 1;
161157

src/go/Properties/launchSettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"profiles": {
33
"go": {
44
"commandName": "Project",
5-
"commandLineArgs": "kzu/runfile@v1:run.cs -gdbg"
5+
"commandLineArgs": "kzu/runfile@v1:run.cs -v:q --r2r -- hello world"
66
}
77
}
88
}

src/go/help.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@ Usage: [command] [arguments...] [options...] [-h|--help] [--version]
44
Runs a file-based .NET app from a .cs entrypoint.
55

66
Arguments:
7-
[0] <string> Path to an existing .cs file or remote ref (owner/repo[@ref][:path]).
8-
[1] <string[]> Arguments before '--' are passed to 'dotnet publish'; arguments after '--' are forwarded to the published app. Without '--', all extra arguments are forwarded to the published app.
7+
[0] <string> Path to an existing .cs file or remote ref (owner/repo[@ref][:path]).
98

109
Options:
11-
--r2r Publish with ReadyToRun instead of native AOT; supports more dynamic .NET features while keeping most publish optimizations. Alias: --go-r2r.
12-
--debug Launch debugger before executing. Alias: --go-debug.
10+
--r2r Publish with ReadyToRun instead of native AOT; supports more dynamic .NET features while keeping most publish optimizations.
1311

1412
Commands:
15-
clean Deletes cached publish artifacts for a file-based .NET app, or for a remote ref: deletes the bundle and also its associated publish artifacts for every previously-used path recorded in the bundle's ETags/Entry (:path on the ref is ignored).
13+
clean Deletes cached publish artifacts for a file-based .NET app, or for a remote ref.
1614
dev Runs a file-based .NET app from a .cs entrypoint using dotnet run for fast iteration.
1715
```

0 commit comments

Comments
 (0)