dotnet / command-line-api

Command line parsing, invocation, and rendering of terminal output.
https://github.com/dotnet/command-line-api/wiki
MIT License
3.34k stars 375 forks source link

Getting started tutorial does not work with `dotnet run` using .NET 8.0 #2427

Open frankbuckley opened 1 month ago

frankbuckley commented 1 month ago

Following the getting started tutorial:

using System.CommandLine;

namespace CommandLineTest;

internal class Program
{
    private static async Task<int> Main(string[] args)
    {
        var fileOption = new Option<FileInfo?>(
            name: "--file",
            description: "The file to read and display on the console.");

        var rootCommand = new RootCommand("Sample app for System.CommandLine");
        rootCommand.AddOption(fileOption);

        rootCommand.SetHandler((file) => ReadFile(file!),
            fileOption);

        return await rootCommand.InvokeAsync(args);
    }

    private static void ReadFile(FileInfo file)
    {
        File.ReadLines(file.FullName)
            .ToList()
            .ForEach(Console.WriteLine);
    }
}

and with a launchSettings.json:

{
  "profiles": {
    "CommandLineTest": {
      "commandName": "Project",
      "commandLineArgs": "--file CommandLineTest.runtimeconfig.json"
    }
  }
}

If you start the project from Visual Studio, then it works as expected and writes the contents of the file to the console.

If you **start the built project from the bin\Debug\net8.0 directory***, then it works as expected and writes the contents of the file to the console.

If you start the project using dotnet run -- --file CommandLineTest.runtimeconfig.json, then you get an exception:

Unhandled exception: System.IO.FileNotFoundException: Could not find file 'V:\dev\testing\CommandLineTest\CommandLineTest\CommandLineTest.runtimeconfig.json'.
File name: 'V:\dev\testing\CommandLineTest\CommandLineTest\CommandLineTest.runtimeconfig.json'
   at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
   at System.IO.File.ReadLines(String path)
   at CommandLineTest.Program.ReadFile(FileInfo file) in V:\dev\testing\CommandLineTest\CommandLineTest\Program.cs:line 24
   at CommandLineTest.Program.<>c.<Main>b__0_0(FileInfo file) in V:\dev\testing\CommandLineTest\CommandLineTest\Program.cs:line 16
   at System.CommandLine.Handler.<>c__DisplayClass2_0`1.<SetHandler>b__0(InvocationContext context)
   at System.CommandLine.Invocation.AnonymousCommandHandler.Invoke(InvocationContext context)
   at System.CommandLine.Invocation.AnonymousCommandHandler.InvokeAsync(InvocationContext context)
   at System.CommandLine.Invocation.InvocationPipeline.<>c__DisplayClass4_0.<<BuildInvocationChain>b__0>d.MoveNext()

This is due to difference in how current directory is set when running from Visual Studio vs. CLI.

FileInfo (and DirectoryInfo) end up using current directory to normalize relative paths.

If you add the following lines to Main() and run from CLI and Visual Studio you can observe the differences:

        Console.WriteLine($"BaseDirectory: {AppContext.BaseDirectory}");
        Console.WriteLine($"CurrentDirectory: {Environment.CurrentDirectory}");
        Console.WriteLine($"Path.GetFullPath(\"./\"): {Path.GetFullPath("./")}");

You might consider updating the getting started docs to note the problem and/or update the path used for the dotnet run example - for example: dotnet run -- --file .\bin\Debug\net8.0\CommandLineTest.runtimeconfig.json

Related:

frankbuckley commented 1 month ago

Workaround to get Visual Studio to behave the same as dotnet run from the project root is to add "workingDirectory": "./" to launchSettings.json:

{
  "profiles": {
    "CommandLineTest": {
      "commandName": "Project",
      "commandLineArgs": "--file ./bin/Debug/net8.0/CommandLineTest.runtimeconfig.json",
      "workingDirectory": "./"
    }
  }
}

This works the same in Visual Studio as:

dotnet run -- --file ./bin/Debug/net8.0/CommandLineTest.runtimeconfig.json