From 75309b02dd869874d2f38dd3407e404cd18211e4 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Mon, 21 Sep 2026 16:32:05 -0300 Subject: [PATCH] Forward a bare --version after the package to the tool --- src/Tests/ArgParserTests.cs | 94 +++++++++++++++++++++++++++++++++++++ src/ndx/ArgParser.cs | 22 +++++++-- 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/src/Tests/ArgParserTests.cs b/src/Tests/ArgParserTests.cs index fbdb1d7..59fe54f 100644 --- a/src/Tests/ArgParserTests.cs +++ b/src/Tests/ArgParserTests.cs @@ -141,6 +141,100 @@ public void Version_with_a_value_and_no_package_is_still_an_error() Assert.Contains("PACKAGE_NAME", parsed.Error); } + [Theory] + [InlineData("stop", "--version")] + [InlineData("stop", "--VERSION")] + public void Bare_version_after_a_package_is_forwarded_to_the_tool(params string[] args) + { + var parsed = ArgParser.Parse(args); + var viaSeparator = ArgParser.Parse(args[0], "--", args[1]); + + Assert.True(parsed.Success); + Assert.False(parsed.ShowVersion); + Assert.Equal("stop", parsed.PackageId); + Assert.Null(parsed.Version); + Assert.Equal(viaSeparator.ForwardedArguments, parsed.ForwardedArguments); + Assert.Equal([args[1]], parsed.ForwardedArguments); + } + + [Fact] + public void Bare_version_after_a_pinned_package_keeps_the_pin_and_forwards() + { + var parsed = ArgParser.Parse("stop@1.2.3", "--version"); + + Assert.True(parsed.Success); + Assert.Equal("stop", parsed.PackageId); + Assert.Equal("1.2.3", parsed.Version); + Assert.Equal(["--version"], parsed.ForwardedArguments); + } + + [Fact] + public void Bare_version_after_other_options_still_forwards_only_the_flag() + { + var parsed = ArgParser.Parse( + "stop", "--prerelease", "--source", "https://feed.example", "--version"); + + Assert.True(parsed.Success); + Assert.False(parsed.ShowVersion); + Assert.Equal("stop", parsed.PackageId); + Assert.Null(parsed.Version); + Assert.True(parsed.Prerelease); + Assert.Equal(["https://feed.example"], parsed.Sources); + Assert.Equal(["--version"], parsed.ForwardedArguments); + } + + [Fact] + public void Version_value_after_a_package_still_selects_that_version() + { + var parsed = ArgParser.Parse("stop", "--version", "1.2.3", "--", "--version"); + + Assert.True(parsed.Success); + Assert.Equal("stop", parsed.PackageId); + Assert.Equal("1.2.3", parsed.Version); + Assert.Equal(["--version"], parsed.ForwardedArguments); + } + + [Fact] + public void Second_bare_version_is_forwarded_when_the_first_already_has_a_value() + { + var parsed = ArgParser.Parse("stop", "--version", "1.2.3", "--version"); + + Assert.True(parsed.Success); + Assert.Equal("1.2.3", parsed.Version); + Assert.Equal(["--version"], parsed.ForwardedArguments); + } + + [Theory] + [InlineData("--yes", "--version")] + [InlineData("--update", "--version")] + [InlineData("--update", "1.2.3", "--version")] + public void Bare_version_without_a_package_still_requires_a_value(params string[] args) + { + var parsed = ArgParser.Parse(args); + + Assert.False(parsed.Success); + Assert.False(parsed.ShowVersion); + Assert.Contains("Missing value for --version", parsed.Error); + } + + [Fact] + public void Version_followed_by_another_token_is_still_that_tokens_value() + { + var parsed = ArgParser.Parse("stop", "--version", "--help"); + + Assert.False(parsed.Success); + Assert.Contains("Invalid version '--help'", parsed.Error); + } + + [Fact] + public void Empty_version_assignment_is_still_missing_a_value() + { + var parsed = ArgParser.Parse("stop", "--version="); + + Assert.False(parsed.Success); + Assert.Contains("Missing value for --version", parsed.Error); + } + [Fact] public void Update_alone_is_a_self_update_to_latest() { diff --git a/src/ndx/ArgParser.cs b/src/ndx/ArgParser.cs index 7397907..851732a 100644 --- a/src/ndx/ArgParser.cs +++ b/src/ndx/ArgParser.cs @@ -4,7 +4,9 @@ namespace ndx; /// dnx.cmd-compatible argv split: first operand is PACKAGE[@VERSION], listed /// flags are consumed by ndx, everything else (including tokens after --) is /// forwarded to the child. --update [VERSION] is a standalone self-update. -/// A lone --version prints the ndx version. +/// A lone --version prints the ndx version. After a package id, a +/// --version with no value is forwarded to the tool, as in +/// ndx stop -- --version. /// public static class ArgParser { @@ -106,7 +108,18 @@ public static Invocation Parse(IReadOnlyList args) if (value is null) { if (i + 1 >= args.Count) + { + // `ndx stop --version` names the tool, not a package version. + // A following token is still the value (`--version 1.2.3`, + // or an invalid one such as `--version --help`). + if (IsVersionOption(option) && packageId is not null && !update) + { + forwarded.Add(arg); + continue; + } + return Invocation.Failed($"Missing value for {option}."); + } value = args[++i]; } @@ -332,11 +345,12 @@ static bool TryParseIdentity(string token, out string? packageId, out string? ve static bool IsBareVersionFlag(string token) { var (option, inline) = SplitOption(token); - return option is not null - && option.Equals("--version", StringComparison.OrdinalIgnoreCase) - && inline is null; + return IsVersionOption(option) && inline is null; } + static bool IsVersionOption(string? option) + => option is not null && option.Equals("--version", StringComparison.OrdinalIgnoreCase); + static (string? Name, string? InlineValue) SplitOption(string token) { if (token.Length < 2 || token[0] != '-')