-
Notifications
You must be signed in to change notification settings - Fork 460
Expand file tree
/
Copy pathArgParser.cs
More file actions
321 lines (277 loc) · 14.1 KB
/
Copy pathArgParser.cs
File metadata and controls
321 lines (277 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
#nullable enable
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Help;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using BizHawk.Common;
using BizHawk.Common.CollectionExtensions;
using BizHawk.Common.StringExtensions;
namespace BizHawk.Client.Common
{
/// <summary>Parses command-line flags into a <see cref="ParsedCLIFlags"/> struct.</summary>
public static class ArgParser
{
private sealed class HelpSedAction(HelpAction stockAction) : SynchronousCommandLineAction
{
public override int Invoke(ParseResult parseResult)
{
var outerOutput = parseResult.InvocationConfiguration.Output;
using StringWriter innerOutput = new();
parseResult.InvocationConfiguration.Output = innerOutput;
var result = stockAction.Invoke(parseResult);
var dollarZero = OSTailoredCode.IsUnixHost
#if true
? "./EmuHawkMono.sh"
#else //TODO for .NET Core: see https://github.com/dotnet/runtime/issues/101837
? Environment.GetCommandLineArgs()[0].SubstringAfterLast('/')
#endif
: Environment.GetCommandLineArgs()[0].SubstringAfterLast('\\');
outerOutput.Write(innerOutput.ToString().Replace("EmuHawk", dollarZero)
.Replace("[<rom>...] [options]", "[option...] [rom]"));
return result;
}
}
private static readonly Argument<string[]> ArgumentRomFilePath = new("rom")
{
Description = "path; if specified, the file will be loaded the same way as it would be from `File` > `Open...`",
};
private static readonly Option<string?> OptionAVDumpAudioSync = new("--audiosync")
{
Description = "bool; `true` is the only truthy value, all else falsey; if not set, uses remembered state from config",
};
private static readonly Option<int?> OptionAVDumpEndAtFrame = new("--dump-length")
{
Description = "int; frame index at which to stop A/V dumping (encoding)",
};
private static readonly Option<string?> OptionAVDumpFrameList = new("--dump-frames"); // desc added in static ctor
private static readonly Option<string?> OptionAVDumpName = new("--dump-name"); // desc added in static ctor
private static readonly Option<bool> OptionAVDumpQuitWhenDone = new("--dump-close")
{
Description = "pass to quit completely after A/V dumping (encoding) finishes",
};
private static readonly Option<string?> OptionAVDumpType = new("--dump-type"); // desc added in static ctor
private static readonly Option<string?> OptionConfigFilePath = new("--config")
{
Description = "path of config file to use",
};
private static readonly Option<bool> OptionGDIPlus = new("--gdi")
{
Description = "use the GDI+ display method rather than whatever preference is set in the config file",
};
private static readonly Option<string?> OptionHTTPClientURIGET = new("--url-get", "--url_get")
{
Description = "string; URI to use for HTTP 'GET' IPC (Lua `comm.http*Get*`)",
};
private static readonly Option<string?> OptionHTTPClientURIPOST = new("--url-post", "--url_post")
{
Description = "string; URI to use for HTTP 'POST' IPC (Lua `comm.http*Post*`)",
};
private static readonly Option<bool> OptionLaunchChromeless = new("--chromeless")
{
Description = "never show the GUI (a.k.a. 'chrome'), not even in windowed mode",
};
private static readonly Option<bool> OptionLaunchFullscreen = new("--fullscreen")
{
Description = "launch in fullscreen",
};
private static readonly Option<int?> OptionLoadQuicksaveSlot = new("--load-slot")
{
Description = "int; quicksave slot which should be loaded on launch",
};
private static readonly Option<string?> OptionLoadSavestateFilePath = new("--load-state"); // desc added in static ctor
private static readonly Option<string?> OptionLuaFilePath = new("--lua"); // desc added in static ctor
private static readonly Option<string?> OptionMMFPath = new("--mmf")
{
Description = "path of file to use for 'memory-mapped file' IPC (Lua `comm.mmf*`)",
};
private static readonly Option<string?> OptionMovieFilePath = new("--movie")
{
Description = "path; input movie which should be loaded on launch",
};
private static readonly Option<string?> OptionOpenExternalTool = new("--open-ext-tool-dll")
{
Description = "the first ext. tool from ExternalToolManager.ToolStripMenu which satisfies both of these will be opened: 1) available (no load errors, correct system/rom, etc.) and 2) dll path matches given string; or dll filename matches given string with or without `.dll`",
};
private static readonly Option<bool> OptionOpenLuaConsole = new("--luaconsole")
{
Description = "open the Lua Console, even if not loading a script",
};
private static readonly Option<bool> OptionQueryAppVersion = new("--version")
{
Description = "print version information and immediately exit",
};
private static readonly Option<string?> OptionSocketServerIP = new("--socket-ip", "--socket_ip"); // desc added in static ctor
private static readonly Option<ushort?> OptionSocketServerPort = new("--socket-port", "--socket_port"); // desc added in static ctor
private static readonly Option<bool> OptionSocketServerUseUDP = new("--socket-udp", "--socket_udp"); // desc added in static ctor
private static readonly Option<string?> OptionUserdataUnparsedPairs = new("--userdata")
{
Description = "pairs in the format `k1:v1;k2:v2` (mind your shell escape sequences); if the value is `true`/`false` it's interpreted as a boolean, if it's a valid 32-bit signed integer e.g. `-1234` it's interpreted as such, if it's a valid 32-bit float e.g. `12.34` it's interpreted as such, else it's interpreted as a string",
};
static ArgParser()
{
OptionAVDumpFrameList.Description = $"comma-separated list of integers, indices of frames which should be included in the A/V dump (encoding); implies `{OptionAVDumpEndAtFrame.Name}=<end>` where `<end>` is the highest frame listed";
OptionAVDumpName.Description = $"ignored unless `{OptionAVDumpType.Name}` also passed";
OptionAVDumpType.Description = $"ignored unless `{OptionAVDumpName.Name}` also passed";
OptionLoadSavestateFilePath.Description = $"path; savestate which should be loaded on launch; this takes precedence over `{OptionLoadQuicksaveSlot.Name}`";
OptionLuaFilePath.Description = $"path; Lua script or Console session to load; implies `{OptionOpenLuaConsole.Name}`";
OptionSocketServerIP.Description = $"string; IP address for Unix socket IPC (Lua `comm.socket*`); must be paired with `{OptionSocketServerPort.Name}`";
OptionSocketServerPort.Description = $"int; port for Unix socket IPC (Lua `comm.socket*`); must be paired with `{OptionSocketServerIP.Name}`";
OptionSocketServerUseUDP.Description = $"pass to use UDP instead of TCP for Unix socket IPC (Lua `comm.socket*`); ignored unless `{OptionSocketServerIP.Name} {OptionSocketServerPort.Name}` also passed";
}
private static RootCommand GetRootCommand()
{
RootCommand root = new($"{
(string.IsNullOrEmpty(VersionInfo.CustomBuildString) ? "EmuHawk" : VersionInfo.CustomBuildString)
}, a multi-system emulator frontend\n{VersionInfo.GetEmuVersion()}");
root.Options.RemoveAll(option => option is VersionOption); // we have our own version command
var helpOption = root.Options.OfType<HelpOption>().First();
helpOption.Action = new HelpSedAction((HelpAction) helpOption.Action!);
// `--help` uses this order, so keep alphabetised by flag
root.Add(/* --audiosync */ OptionAVDumpAudioSync);
root.Add(/* --chromeless */ OptionLaunchChromeless);
root.Add(/* --config */ OptionConfigFilePath);
root.Add(/* --dump-close */ OptionAVDumpQuitWhenDone);
root.Add(/* --dump-frames */ OptionAVDumpFrameList);
root.Add(/* --dump-length */ OptionAVDumpEndAtFrame);
root.Add(/* --dump-name */ OptionAVDumpName);
root.Add(/* --dump-type */ OptionAVDumpType);
root.Add(/* --fullscreen */ OptionLaunchFullscreen);
root.Add(/* --gdi */ OptionGDIPlus);
root.Add(/* --load-slot */ OptionLoadQuicksaveSlot);
root.Add(/* --load-state */ OptionLoadSavestateFilePath);
root.Add(/* --lua */ OptionLuaFilePath);
root.Add(/* --luaconsole */ OptionOpenLuaConsole);
root.Add(/* --mmf */ OptionMMFPath);
root.Add(/* --movie */ OptionMovieFilePath);
root.Add(/* --open-ext-tool-dll */ OptionOpenExternalTool);
root.Add(/* --socket-ip */ OptionSocketServerIP);
root.Add(/* --socket-port */ OptionSocketServerPort);
root.Add(/* --socket-udp */ OptionSocketServerUseUDP);
root.Add(/* --url-get */ OptionHTTPClientURIGET);
root.Add(/* --url-post */ OptionHTTPClientURIPOST);
root.Add(/* --userdata */ OptionUserdataUnparsedPairs);
root.Add(/* --version */ OptionQueryAppVersion);
root.Add(ArgumentRomFilePath);
return root;
}
private static void EnsureConsole()
{
if (!OSTailoredCode.IsUnixHost)
{
// the behavior of this kinda sucks, but it's better than nothing I think
Win32Imports.AttachConsole(Win32Imports.ATTACH_PARENT_PROCESS);
}
}
/// <return>exit code, or <see langword="null"/> if should not exit</return>
/// <exception cref="ArgParserException">parsing failure, or invariant broken</exception>
public static int? ParseArguments(out ParsedCLIFlags parsed, string[] args, bool fromUnitTest = false)
{
parsed = default;
if (!fromUnitTest && args.Length is not 0) Console.Error.WriteLine($"parsing command-line flags: {string.Join(" ", args)}");
var rootCommand = GetRootCommand();
var result = CommandLineParser.Parse(rootCommand, args);
if (result.Errors.Count is not 0)
{
// generate useful commandline error output
EnsureConsole();
if (!fromUnitTest) result.Invoke();
// show first error in modal dialog (done in `catch` block in `Program`)
throw new ArgParserException($"failed to parse command-line arguments: {result.Errors[0].Message}");
}
if (result.Action is not null)
{
// means e.g. `./EmuHawkMono.sh --help` was passed, run whatever behaviour it normally has
EnsureConsole();
return fromUnitTest ? 0 : result.Invoke();
}
if (result.GetValue(OptionQueryAppVersion))
{
// means e.g. `./EmuHawkMono.sh --version` was passed, so print that and exit immediately
EnsureConsole();
if (!fromUnitTest) Console.WriteLine(VersionInfo.GetEmuVersion());
return 0;
}
var unmatchedArguments = result.GetValue(ArgumentRomFilePath) ?? [ ];
if (unmatchedArguments.Length >= 2)
{
var foundFlagLike = unmatchedArguments.FirstOrDefault(static s => s.StartsWith("--"));
throw new ArgParserException(foundFlagLike is null
? "Multiple rom paths provided. (Did you delete half of a flag? Or forget the \"--\" of one?)"
: $"Unrecognised flag(s): \"{foundFlagLike}\"");
}
var autoDumpLength = result.GetValue(OptionAVDumpEndAtFrame);
HashSet<int>? currAviWriterFrameList = null;
if (result.GetValue(OptionAVDumpFrameList) is string list)
{
currAviWriterFrameList = new();
currAviWriterFrameList.AddRange(list.Split(',').Select(int.Parse));
// automatically set dump length to maximum frame
autoDumpLength ??= currAviWriterFrameList.Max();
}
var luaScript = result.GetValue(OptionLuaFilePath);
var luaConsole = luaScript is not null || result.GetValue(OptionOpenLuaConsole);
var socketIP = result.GetValue(OptionSocketServerIP);
var socketPort = result.GetValue(OptionSocketServerPort);
var socketAddress = socketIP is null && socketPort is null
? ((string, ushort)?) null // don't bother
: socketIP is not null && socketPort is not null
? (socketIP, socketPort.Value)
: throw new ArgParserException("Socket server needs both --socket_ip and --socket_port. Socket server was not started");
var httpClientURIGET = result.GetValue(OptionHTTPClientURIGET);
var httpClientURIPOST = result.GetValue(OptionHTTPClientURIPOST);
var httpAddresses = httpClientURIGET is null && httpClientURIPOST is null
? ((string?, string?)?) null // don't bother
: (httpClientURIGET, httpClientURIPOST);
var audiosync = result.GetValue(OptionAVDumpAudioSync)?.EqualsIgnoreCase("true");
List<(string Key, string Value)>? userdataUnparsedPairs = null;
if (result.GetValue(OptionUserdataUnparsedPairs) is string list1)
{
userdataUnparsedPairs = new();
foreach (var s in list1.Split(';'))
{
var iColon = s.IndexOf(':');
if (iColon is -1) throw new ArgParserException("malformed userdata (';' without ':')");
userdataUnparsedPairs.Add((s.Substring(startIndex: 0, length: iColon), s.Substring(iColon + 1)));
}
}
parsed = new(
cmdLoadSlot: result.GetValue(OptionLoadQuicksaveSlot),
cmdLoadState: result.GetValue(OptionLoadSavestateFilePath),
cmdConfigFile: result.GetValue(OptionConfigFilePath),
cmdMovie: result.GetValue(OptionMovieFilePath),
cmdDumpType: result.GetValue(OptionAVDumpType),
currAviWriterFrameList: currAviWriterFrameList,
autoDumpLength: autoDumpLength ?? 0,
cmdDumpName: result.GetValue(OptionAVDumpName),
autoCloseOnDump: result.GetValue(OptionAVDumpQuitWhenDone),
chromeless: result.GetValue(OptionLaunchChromeless),
startFullscreen: result.GetValue(OptionLaunchFullscreen),
gdiPlusRequested: result.GetValue(OptionGDIPlus),
luaScript: luaScript,
luaConsole: luaConsole,
socketAddress: socketAddress,
mmfFilename: result.GetValue(OptionMMFPath),
httpAddresses: httpAddresses,
audiosync: audiosync,
openExtToolDll: result.GetValue(OptionOpenExternalTool),
socketProtocol: result.GetValue(OptionSocketServerUseUDP) ? ProtocolType.Udp : ProtocolType.Tcp,
userdataUnparsedPairs: userdataUnparsedPairs,
cmdRom: unmatchedArguments.LastOrDefault()); // `unmatchedArguments.Length` must be 0 or 1 at this point, but in case we change how that's handled, 'last' here preserves the behaviour from the old hand-rolled parser
return null;
}
internal static void RunHelpActionForUnitTest(TextWriter output)
{
var result = CommandLineParser.Parse(GetRootCommand(), [ "--help" ]);
result.InvocationConfiguration.Output = output;
result.Invoke();
}
public sealed class ArgParserException : Exception
{
public ArgParserException(string message) : base(message) {}
}
}
}