Skip to content

RootCommand.ExecutableName truncates dotted app names on Linux/macOS when targeting .NET 11 #2850

Description

@eerhardt

Summary

Starting with .NET 11, Environment.GetCommandLineArgs()[0] returns the name the apphost was invoked with instead of the managed .dll path (dotnet/runtime#131671). RootCommand.ExecutableName runs Path.GetFileNameWithoutExtension on that value. On Linux/macOS the apphost has no extension, so any app whose name contains a dot has its last segment treated as an "extension" and dropped. For example, Contoso.Tool becomes Contoso.

This affects the root command's Name, so it changes the Usage: line in help output. It also affects everything else derived from ExecutableName. Apps get this just by retargeting from net10.0 to net11.0, with no code changes.

Background: the .NET 11 runtime change

dotnet/runtime#131671, "Fix Environment.GetCommandLineArgs()[0] to report the host invocation name", merged 2026-08-10 and fixes dotnet/runtime#101837. It adds an ARGV0 host runtime property: hostpolicy captures the apphost's native argv[0], and CoreCLR uses it for element zero of Environment.GetCommandLineArgs(). From that PR:

Command line Environment.GetCommandLineArgs()
dotnet.exe foo.dll 1 2 3 foo.dll, 1, 2, 3 (unchanged)
foo 1 2 3 foo, 1, 2, 3 (previously the managed …/foo.dll path)
../bar/special_foo.exe 1 2 3 (symlink to foo.exe) ../bar/special_foo.exe, 1, 2, 3

Before this change, args[0] for an apphost launch always ended in .dll, so GetFileNameWithoutExtension happened to strip a real extension on every OS. Now it's the apphost name:

  • Windows: the apphost name ends in .exe, so stripping the extension is still correct.
  • Linux/macOS: the apphost has no extension, so a dotted name loses its last segment.

The System.CommandLine code involved

2.0.x servicing, e.g. 2.0.12 (dotnet/dotnet release/10.0.1xx, RootCommand.cs#L48-L54):

public static string ExecutableName
    => _executableName ??= Path.GetFileNameWithoutExtension(ExecutablePath).Replace(" ", "");

public static string ExecutablePath => _executablePath ??= Environment.GetCommandLineArgs()[0];

main (RootCommand.cs#L104-L137) has the same core logic: Path.GetFileNameWithoutExtension(path).Replace(" ", "") on L128, where path is Environment.GetCommandLineArgs()[0].

Repro

Contoso.Tool.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>net10.0;net11.0</TargetFrameworks>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="System.CommandLine" Version="2.0.12" />
  </ItemGroup>
</Project>

Program.cs:

using System.CommandLine;

Console.WriteLine($"GetCommandLineArgs()[0] = {Environment.GetCommandLineArgs()[0]}");
Console.WriteLine($"RootCommand.ExecutableName = {RootCommand.ExecutableName}");
return new RootCommand("Sample tool").Parse(args).Invoke();

Publish for linux-x64 (I used SDK 11.0.100-rc.1.26425.128), then run ./Contoso.Tool --help on Linux.

net10.0

GetCommandLineArgs()[0] = /…/pub/net10.0/Contoso.Tool.dll
RootCommand.ExecutableName = Contoso.Tool
Description:
  Sample tool

Usage:
  Contoso.Tool [options]

net11.0

GetCommandLineArgs()[0] = ./Contoso.Tool
RootCommand.ExecutableName = Contoso
Description:
  Sample tool

Usage:
  Contoso [options]

On Windows the same net11.0 app still reports Contoso.Tool, because args[0] is Contoso.Tool.exe. The result is the same app showing different names per OS.

Who is affected

Any System.CommandLine app that:

  • targets .NET 11 and is launched through its apphost rather than dotnet app.dll, and
  • runs on Linux or macOS, and
  • has a dot in its executable name. That's common, because the default AssemblyName is the project name, e.g. Contoso.Tool, MyCompany.Cli, or test projects like Foo.Tests.

Impacts:

  • Help output: Usage: shows the wrong command name.
  • Other ExecutableName users: code that uses RootCommand.ExecutableName directly, and features that depend on it, such as completion/dotnet-suggest registration, get the truncated name. A similar issue was fixed earlier for RegisterWithDotnetSuggest in RegisterWithDotnetSuggest should not remove "extension" on Linux #2316.
  • Tests: help-output snapshot tests become OS-dependent. We hit this in Aspire when retargeting our CLI tests to net11.0: help snapshots started failing only on Linux/macOS, where Aspire.Cli.Tests was rendered as Aspire.Cli (Update Aspire.Cli to target net11.0 microsoft/aspire#20436). Our shipped aspire binary isn't affected only because its name has no dot.

cc dotnet/runtime#131671, where I also asked whether the runtime change should be tagged as a breaking change: dotnet/runtime#131671 (comment)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions