|
10 | 10 | using MetaObjects.Codegen; |
11 | 11 | using MetaObjects.Config; |
12 | 12 |
|
13 | | -if (args.Length == 0) |
| 13 | +// `--version`, `-v` and `-V` print the bare version and exit 0 — the same three spellings |
| 14 | +// every port's CLI answers. Checked before anything else so a version probe never parses |
| 15 | +// a command. |
| 16 | +if (args.Length == 1 && args[0] is "--version" or "-v" or "-V") |
14 | 17 | { |
15 | | - Console.Error.WriteLine( |
| 18 | + Console.WriteLine(EjectCommand.ToolVersion()); |
| 19 | + return 0; |
| 20 | +} |
| 21 | + |
| 22 | +// `--help`/`-h` print the command reference on stdout and exit 0: asked-for help is |
| 23 | +// output, not an error. A bare `dotnet meta` is still a usage error (stderr, exit 2). |
| 24 | +bool helpRequested = args.Length > 0 && args[0] is "--help" or "-h"; |
| 25 | +if (args.Length == 0 || helpRequested) |
| 26 | +{ |
| 27 | + (helpRequested ? Console.Out : Console.Error).WriteLine( |
16 | 28 | "usage: dotnet meta <command> [options]\n" + |
17 | 29 | " commands:\n" + |
18 | | - " gen <metadataDir> --out <dir> --namespace <ns> [--emit-abstract-shapes]\n" + |
| 30 | + " gen <metadataDir> --out <dir> [--namespace <ns>] [--emit-abstract-shapes]\n" + |
19 | 31 | " [--generators <a,b,c>] [--template-root <dir>]\n" + |
20 | 32 | " generate EF Core code from metadata\n" + |
21 | 33 | " gen --list list available generators (stable names) and exit\n" + |
|
37 | 49 | " see `dotnet meta gen --list`\n" + |
38 | 50 | " fmt [<metadataDir>] [--check] rewrite metadata into canonical form (#304);\n" + |
39 | 51 | " --check lists drift, exits non-zero, changes nothing\n" + |
40 | | - " agent-docs see `npx meta agent-docs`"); |
41 | | - return 2; |
| 52 | + " agent-docs see `npx meta agent-docs`\n" + |
| 53 | + " --version, -v, -V print the version\n" + |
| 54 | + " <command> --help that command's usage"); |
| 55 | + return helpRequested ? 0 : 2; |
| 56 | +} |
| 57 | + |
| 58 | +// `<command> --help` answers with that command's usage on stdout, exit 0, before the |
| 59 | +// command parses anything — so help is never refused as an unknown flag. |
| 60 | +if (args.Length > 1 && CommandUsage.TryGetValue(args[0], out var usage) && args[1..].Any(a => a is "--help" or "-h")) |
| 61 | +{ |
| 62 | + Console.WriteLine(usage); |
| 63 | + return 0; |
42 | 64 | } |
43 | 65 |
|
44 | 66 | return args[0] switch |
@@ -77,6 +99,9 @@ static int RunGen(string[] rest) |
77 | 99 | else if (rest[i] == "--baseline" && i + 1 < rest.Length) baseline = rest[++i]; |
78 | 100 | else if (rest[i].StartsWith("--baseline=", StringComparison.Ordinal)) baseline = rest[i]["--baseline=".Length..]; |
79 | 101 | else if (!rest[i].StartsWith('-')) metadataDir ??= rest[i]; |
| 102 | + // An unrecognised flag used to be dropped here: `gen ... --bogus` generated and |
| 103 | + // exited 0, as if the flag had been honoured. |
| 104 | + else return RefuseFlag("gen", rest[i], GenValueFlags, GenBoolFlags); |
80 | 105 | } |
81 | 106 |
|
82 | 107 | // How a field with NO explicit `@column` becomes a physical column name. The |
@@ -229,6 +254,7 @@ static int RunDocs(string[] rest) |
229 | 254 | else if (rest[i] == "--project" && i + 1 < rest.Length) project = rest[++i]; |
230 | 255 | else if (rest[i] == "--model-base-url" && i + 1 < rest.Length) modelBaseUrl = rest[++i]; |
231 | 256 | else if (!rest[i].StartsWith('-')) metadataDir ??= rest[i]; |
| 257 | + else return RefuseFlag("docs", rest[i], DocsValueFlags, []); |
232 | 258 | } |
233 | 259 |
|
234 | 260 | // Usage-first — see the identical comment in RunGen above; a missing --out |
@@ -284,12 +310,7 @@ static int RunFmt(string[] rest) |
284 | 310 | foreach (var a in rest) |
285 | 311 | { |
286 | 312 | if (a == "--check") check = true; |
287 | | - else if (a.StartsWith('-')) |
288 | | - { |
289 | | - Console.Error.WriteLine($"dotnet meta fmt: unknown option \"{a}\""); |
290 | | - Console.Error.WriteLine("usage: dotnet meta fmt [<metadataDir>] [--check]"); |
291 | | - return 2; |
292 | | - } |
| 313 | + else if (a.StartsWith('-')) return RefuseFlag("fmt", a, [], ["--check"]); |
293 | 314 | else metadataDir ??= a; |
294 | 315 | } |
295 | 316 |
|
@@ -366,12 +387,7 @@ static int RunEject(string[] rest) |
366 | 387 | if (rest[i] == "--force") force = true; |
367 | 388 | else if (rest[i] == "--root" && i + 1 < rest.Length) root = rest[++i]; |
368 | 389 | else if (!rest[i].StartsWith('-')) names.Add(rest[i]); |
369 | | - else |
370 | | - { |
371 | | - Console.Error.WriteLine($"dotnet meta eject: unknown option \"{rest[i]}\""); |
372 | | - Console.Error.WriteLine("usage: dotnet meta eject <name>... [--force] [--root <dir>]"); |
373 | | - return 2; |
374 | | - } |
| 390 | + else return RefuseFlag("eject", rest[i], ["--root"], ["--force"]); |
375 | 391 | } |
376 | 392 |
|
377 | 393 | var result = EjectCommand.Run(names, root ?? Directory.GetCurrentDirectory(), force); |
@@ -430,6 +446,26 @@ static ResolvedMetadata ResolveMetadataDirOrExit(string? metadataDir) |
430 | 446 | } |
431 | 447 | } |
432 | 448 |
|
| 449 | +/// <summary> |
| 450 | +/// The one refusal for a flag a command does not accept, in every command — the same |
| 451 | +/// shape the Node <c>meta</c> CLI uses. A known value flag with its value missing says |
| 452 | +/// so; anything else is named as unknown and the command's valid flags are listed, so the |
| 453 | +/// refusal corrects itself in one step. Exit 2 (usage). |
| 454 | +/// </summary> |
| 455 | +static int RefuseFlag(string command, string flag, string[] valueFlags, string[] boolFlags) |
| 456 | +{ |
| 457 | + if (valueFlags.Contains(flag)) |
| 458 | + { |
| 459 | + Console.Error.WriteLine($"dotnet meta {command}: {flag} needs a value"); |
| 460 | + return 2; |
| 461 | + } |
| 462 | + var valid = valueFlags.Concat(boolFlags).Order(StringComparer.Ordinal); |
| 463 | + Console.Error.WriteLine( |
| 464 | + $"unknown flag {flag} for `dotnet meta {command}`. Valid flags: {string.Join(", ", valid)} " + |
| 465 | + "(also accepted everywhere: --help)"); |
| 466 | + return 2; |
| 467 | +} |
| 468 | + |
433 | 469 | static int Unknown(string cmd) |
434 | 470 | { |
435 | 471 | Console.Error.WriteLine($"dotnet meta: unknown command \"{cmd}\""); |
@@ -495,12 +531,7 @@ static int RunVerify(string[] rest) |
495 | 531 | // wrong strategy and every one reports spurious drift on an otherwise-clean |
496 | 532 | // project. |
497 | 533 | else if (a == "--column-naming" && i + 1 < rest.Length) columnNamingRaw = rest[++i]; |
498 | | - else if (a.StartsWith('-')) |
499 | | - { |
500 | | - Console.Error.WriteLine($"dotnet meta verify: unknown option \"{a}\""); |
501 | | - Console.Error.WriteLine("usage: dotnet meta verify <metadataDir> [--templates [--prompts <dir>]] [--codegen --out <dir> [--namespace <ns>] [--column-naming literal|snake_case|kebab-case]] [--db] [--lax] [--no-field-lint]"); |
502 | | - return 2; |
503 | | - } |
| 534 | + else if (a.StartsWith('-')) return RefuseFlag("verify", a, VerifyValueFlags, VerifyBoolFlags); |
504 | 535 | else if (metadataDir is null) metadataDir = a; |
505 | 536 | // A second positional is the templates root for a BARE verify |
506 | 537 | // (`verify <metadataDir> <templatesRoot>`) — keeps the historical default |
@@ -651,3 +682,27 @@ static int RunVerify(string[] rest) |
651 | 682 | return codegenHandedOff ? Math.Max(result.ExitCode, codegenHandoffExit) : result.ExitCode; |
652 | 683 | } |
653 | 684 |
|
| 685 | + |
| 686 | +/// <summary>The flags each command parses, for <see cref="RefuseFlag"/>. Kept beside the |
| 687 | +/// parsers' own branches; a flag added to one must be added here.</summary> |
| 688 | +partial class Program |
| 689 | +{ |
| 690 | + static readonly string[] GenValueFlags = |
| 691 | + ["--out", "--namespace", "--generators", "--template-root", "--template-spec", "--column-naming", "--baseline"]; |
| 692 | + static readonly string[] GenBoolFlags = ["--list", "--emit-abstract-shapes"]; |
| 693 | + static readonly string[] DocsValueFlags = ["--out", "--namespace", "--project", "--model-base-url"]; |
| 694 | + static readonly string[] VerifyValueFlags = |
| 695 | + ["--prompts", "--out", "--namespace", "--generators", "--template-root", "--column-naming"]; |
| 696 | + static readonly string[] VerifyBoolFlags = ["--templates", "--codegen", "--db", "--lax", "--no-field-lint"]; |
| 697 | + |
| 698 | + /// <summary>Each command's usage, printed by <c>dotnet meta <command> --help</c>.</summary> |
| 699 | + static readonly Dictionary<string, string> CommandUsage = new() |
| 700 | + { |
| 701 | + ["gen"] = "usage: dotnet meta gen <metadataDir> --out <dir> [--namespace <ns>] [--generators <a,b,c>] [--template-root <dir>] [--template-spec <json>] [--emit-abstract-shapes] [--column-naming literal|snake_case|kebab-case] [--baseline default|adopt]\n" + |
| 702 | + " dotnet meta gen --list", |
| 703 | + ["verify"] = "usage: dotnet meta verify <metadataDir> [--templates [--prompts <dir>]] [--codegen --out <dir> [--namespace <ns>] [--generators <a,b,c>] [--template-root <dir>] [--column-naming literal|snake_case|kebab-case]] [--db] [--lax] [--no-field-lint]", |
| 704 | + ["docs"] = "usage: dotnet meta docs <metadataDir> --out <dir> [--namespace <ns>] [--project <name>] [--model-base-url <url>]", |
| 705 | + ["fmt"] = "usage: dotnet meta fmt [<metadataDir>] [--check]", |
| 706 | + ["eject"] = "usage: dotnet meta eject <name>... [--force] [--root <dir>]", |
| 707 | + }; |
| 708 | +} |
0 commit comments