command-line-api icon indicating copy to clipboard operation
command-line-api copied to clipboard

Customize --version output

Open KalleOlaviNiemitalo opened this issue 2 years ago • 10 comments

I'd like to have --version output some additional information, like in these:

PS C:\> MSBuild.exe /version
Microsoft (R) Build Engine version 15.9.21+g9802d43bc3 for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.

15.9.21.664
$ gcc --version
gcc.exe (x86_64-posix-sjlj, built by strawberryperl.com project) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

That is however somewhat cumbersome to implement on top of System.Commandline, because then the application:

  • Cannot call CommandLineBuilderExtensions.UseDefaults, which would add another --version option and cause an exception. Must instead call UseEnvironmentVariableDirective etc. one by one.
  • Must hardcode the value of MiddlewareOrderInternal.VersionOption = -1200.
  • Must reimplement the VersionOptionCannotBeCombinedWithOtherArguments validation.

Can we have API like this:

 namespace System.CommandLine.Builder
 {
     partial class CommandLineBuilderExtensions
     {
         public static CommandLineBuilder UseVersionOption(this CommandLineBuilder builder);
         public static CommandLineBuilder UseVersionOption(this CommandLineBuilder builder, params string[] aliases);
+        public static CommandLineBuilder UseVersionOption(this CommandLineBuilder builder, Action<InvocationContext> handler);
+        public static CommandLineBuilder UseVersionOption(this CommandLineBuilder builder, Action<InvocationContext> handler, params string[] aliases);
     }
 }

The provided handler would then be responsible of all output, and could set InvocationContext.ExitCode if desired.

Overloads with Func<InvocationContext, Task> could be added later if needed, but I don't think they would be needed.

KalleOlaviNiemitalo avatar Jan 02 '23 12:01 KalleOlaviNiemitalo

The middleware would call handler(context) instead of doing this write: https://github.com/dotnet/command-line-api/blob/209b724a3c843253d3071e8348c353b297b0b8b5/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs#L625

If the handler wanted to display AssemblyInformationalVersionAttribute.InformationalVersion, it would have to locate the attribute on its own.

KalleOlaviNiemitalo avatar Jan 02 '23 13:01 KalleOlaviNiemitalo

Another difficulty here is that, because CommandLineBuilder.LocalizationResources is not public, an external definition of VersionOption cannot so easily get the localized VersionOptionDescription: https://github.com/dotnet/command-line-api/blob/209b724a3c843253d3071e8348c353b297b0b8b5/src/System.CommandLine/Help/VersionOption.cs#L56-L60

It is possible to call the public API _builder.Build().Configuration.LocalizationResources.VersionOptionDescription(), though.

KalleOlaviNiemitalo avatar Jan 02 '23 14:01 KalleOlaviNiemitalo

I hope https://github.com/dotnet/command-line-api/pull/1969 won't make an external implementation more difficult.

KalleOlaviNiemitalo avatar Feb 02 '23 18:02 KalleOlaviNiemitalo

We're planning to move many features out of middleware in order to make things more flexible while also reducing the performance footprint of the built-in features that happen to be implemented as middleware today. Making it easier to bring your own external implementations of --help and --version is a core goal.

jonsequitur avatar Feb 02 '23 20:02 jonsequitur

With System.CommandLine 2.0.0-beta4.23178.3, I can apparently do this:

VersionOption versionOption = rootCommand.Options.OfType<VersionOption>().Single();
versionOption.Action = new AugmentedVersionOptionAction(versionOption.Action);

and then:

private class AugmentedVersionOptionAction : CliAction
{
    private readonly CliAction inner;

    public AugmentedVersionOptionAction(CliAction inner)
    {
        this.inner = inner;
    }

    public override int Invoke(ParseResult parseResult)
    {
        int exitCode = this.inner.Invoke(parseResult);
        this.OutputExtraText(parseResult);
        return exitCode;
    }

    public override async Task<int> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default)
    {
        int exitCode = await this.inner.InvokeAsync(parseResult, cancellationToken).ConfigureAwait(false);
        this.OutputExtraText(parseResult);
        return exitCode;
    }

    private void OutputExtraText(ParseResult parseResult)
    {
        parseResult.Configuration.Output.WriteLine("Company Confidential");
    }
}

It is unfortunate that this has to override both Invoke and InvokeAsync, though.

KalleOlaviNiemitalo avatar Mar 29 '23 08:03 KalleOlaviNiemitalo

What is the state of easier ways to extend / replace default help and version commands?
Is this the best option we have, or is there something in the works?

ptr727 avatar Apr 29 '25 04:04 ptr727

Also found that calling Invoke on ParseResult does not call help or version, and reports --help and --version as errors. This is inconsistent and means I can't check if Help or Version will be invoked as they seem to be hardcoded outside of Parse.

ptr727 avatar May 03 '25 23:05 ptr727

Also found that calling Invoke on ParseResult does not call help or version, and reports --help and --version as errors. This is inconsistent and means I can't check if Help or Version will be invoked as they seem to be hardcoded outside of Parse.

Which version are you using?

jonsequitur avatar May 07 '25 20:05 jonsequitur

Which version are you using?

Latest released on nuget, beta 4.

ptr727 avatar May 07 '25 21:05 ptr727

This (Copilot GPT5 provided) code seems to work with the latest RC:

void Main(string[] args)
{
	var root = new RootCommand("Demo with custom --version");

	// Replace the Action on the built-in VersionOption
	var versionOpt = root.Options.OfType<VersionOption>().Single();
	versionOpt.Action = new CustomVersionAction();

	// set up other Options...

	var exitCode = root.Parse(args ?? new string[0]).Invoke();
}

// A synchronous action that runs when --version is present.
// Actions are terminating in the invocation pipeline by default, so this will short-circuit normal execution.
sealed class CustomVersionAction : SynchronousCommandLineAction
{
	public override int Invoke(ParseResult parseResult)
	{
		parseResult.InvocationConfiguration.Output.WriteLine("MyVersion");
		return 0;
	}
	public override bool ClearsParseErrors => true;
}

Gerboa avatar Sep 12 '25 09:09 Gerboa

@Gerboa, CustomVersionAction should also override ClearsParseErrors like the built-in action does here: https://github.com/dotnet/command-line-api/blob/e292617c0f829bfe777c7ad51467c6a509a9aff8/src/System.CommandLine/VersionOption.cs#L72

KalleOlaviNiemitalo avatar Sep 12 '25 10:09 KalleOlaviNiemitalo

I'll consider this fixed in v2.0.0-rc.1.25451.107, even though the API is not what I proposed.

KalleOlaviNiemitalo avatar Sep 12 '25 10:09 KalleOlaviNiemitalo

Updated my code example above with suggested ClearsParseErrors override.

Gerboa avatar Sep 12 '25 14:09 Gerboa