Skip to content

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

Closed

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)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions