チュートリアル: System.CommandLine の概要

重要

System.CommandLine は現在プレビュー段階であり、このドキュメントはバージョン 2.0 beta 4 を対象としています。 一部の情報は、リリース前に大きく変更される可能性があるプレリリースされた製品に関するものです。 Microsoft は、ここに記載されている情報について、明示または黙示を問わず、一切保証しません。

このチュートリアルでは、System.CommandLine ライブラリを使う .NET コマンドライン アプリを作成する方法について説明します。 まず、オプションが 1 つの単純なルート コマンドを作成します。 次に、それをベースにして、各コマンドの複数のサブコマンドと異なるオプションを含む、より複雑なアプリを作成します。

このチュートリアルでは、次の作業を行う方法について説明します。

  • コマンド、オプション、引数を作成します。
  • オプションの既定値を指定します。
  • コマンドにオプションと引数を割り当てます。
  • あるコマンド以下のすべてのサブコマンドに再帰的にオプションを割り当てます。
  • 複数レベルの入れ子になったサブコマンドを操作します。
  • コマンドとオプションの別名を作成します。
  • stringstring[]intboolFileInfo、列挙のオプション型を操作します。
  • オプション値をコマンド ハンドラー コードにバインドします。
  • オプションの解析と検証にカスタム コードを使います。

前提条件

または

  • .NET デスクトップ開発ワークロードをインストールした Visual Studio 2022

アプリを作成する

"scl" という名前の .NET 6 コンソール アプリ プロジェクトを作成します。

  1. プロジェクト用に scl というフォルダーを作成し、新しいフォルダーでコマンド プロンプトを開きます。

  2. 次のコマンドを実行します。

    dotnet new console --framework net6.0
    

System.CommandLine パッケージのインストール

  • 次のコマンドを実行します。

    dotnet add package System.CommandLine --prerelease
    

    ライブラリはまだベータ版なので、--prerelease オプションが必要です。

  1. Program.cs の内容を次のコードで置き換えます。

    using System.CommandLine;
    
    namespace scl;
    
    class Program
    {
        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);
        }
    
        static void ReadFile(FileInfo file)
        {
            File.ReadLines(file.FullName).ToList()
                .ForEach(line => Console.WriteLine(line));
        }
    }
    

上記のコードでは次の操作が行われます。

  • FileInfo--file というオプションを作成し、ルート コマンドに割り当てます。

    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);
    
  • ルート コマンドが呼び出されたときに呼び出すメソッドとして ReadFile を指定します。

    rootCommand.SetHandler((file) => 
        { 
            ReadFile(file!); 
        },
        fileOption);
    
  • ルート コマンドが呼び出されたときに、指定したファイルの内容を表示します。

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

アプリをテストする

コマンドライン アプリの開発中に、次のいずれかの方法でテストを行うことができます。

  • dotnet build コマンドを実行し、scl/bin/Debug/net6.0 フォルダーでコマンド プロンプトを開き、実行可能ファイルを実行します。

    dotnet build
    cd bin/Debug/net6.0
    scl --file scl.runtimeconfig.json
    
  • dotnet run を使い、次の例のように -- の後にオプション値を含めることで、run コマンドではなくアプリにオプション値を渡します。

    dotnet run -- --file scl.runtimeconfig.json
    

    .NET 7.0.100 SDK プレビューでは、コマンド dotnet run --launch-profile <profilename> を実行することで、launchSettings.json ファイルの commandLineArgs を使用できます。

  • プロジェクトをフォルダーに発行し、そのフォルダーでコマンド プロンプトを開き、実行可能ファイルを実行します。

    dotnet publish -o publish
    cd ./publish
    scl --file scl.runtimeconfig.json
    
  • Visual Studio 2022 で、メニューから [デバッグ]>[デバッグ プロパティ] を選び、[コマンド ライン引数] ボックスにオプションと引数を入力します。 次に例を示します。

    Visual Studio 2022 のコマンド ライン引数

    その後、Ctrl + F5 キーを押すなどしてアプリを実行します。

このチュートリアルでは、これらのオプションのうち最初のものを使うことを想定しています。

アプリを実行すると、--file オプションで指定したファイルの内容が表示されます。

{
  "runtimeOptions": {
    "tfm": "net6.0",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "6.0.0"
    }
  }
}

ヘルプの出力

System.CommandLine を使うと、自動的にヘルプの出力が用意されます。

scl --help
Description:
  Sample app for System.CommandLine

Usage:
  scl [options]

Options:
  --file <file>   The file to read and display on the console.
  --version       Show version information
  -?, -h, --help  Show help and usage information

バージョンの出力

System.CommandLine を使うと、自動的にバージョンの出力が用意されます。

scl --version
1.0.0

サブコマンドとオプションを追加する

このセクションでは、次の作業を行います。

  • その他のオプションを作成します。
  • サブコマンドを作成します。
  • 新しいサブコマンドに新しいオプションを割り当てます。

新しいオプションを使うと、前景と背景のテキストの色、読み上げ速度を構成できます。 これらの機能は、テレプロンプター コンソール アプリのチュートリアルから取得した引用文のコレクションを読み取るために使われます。

  1. このサンプルの GitHub リポジトリから、sampleQuotes.txt ファイルをプロジェクト ディレクトリにコピーします。 ファイルのダウンロード方法については、サンプルとチュートリアルに関するページを参照してください。

  2. プロジェクト ファイルを開き、終了 </Project> タグの直前に <ItemGroup> 要素を追加します。

    <ItemGroup>
      <Content Include="sampleQuotes.txt">
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
      </Content>
    </ItemGroup>
    

    このマークアップを追加すると、アプリのビルド時にテキスト ファイルが bin/debug/net6.0 フォルダーにコピーされます。 そのため、そのフォルダー内の実行可能ファイルを実行するときに、フォルダーのパスを指定しなくても、名前を指定してファイルにアクセスできます。

  3. Program.cs で、--file オプションを作成するコードの後に、読み上げ速度とテキストの色を制御するオプションを作成します。

    var delayOption = new Option<int>(
        name: "--delay",
        description: "Delay between lines, specified as milliseconds per character in a line.",
        getDefaultValue: () => 42);
    
    var fgcolorOption = new Option<ConsoleColor>(
        name: "--fgcolor",
        description: "Foreground color of text displayed on the console.",
        getDefaultValue: () => ConsoleColor.White);
    
    var lightModeOption = new Option<bool>(
        name: "--light-mode",
        description: "Background color of text displayed on the console: default is black, light mode is white.");
    
  4. ルート コマンドを作成した行の後、--file オプションを追加した行を削除します。 ここで削除しているのは、新しいサブコマンドに追加するためです。

    var rootCommand = new RootCommand("Sample app for System.CommandLine");
    //rootCommand.AddOption(fileOption);
    
  5. ルート コマンドを作成した行の後に、read サブコマンドを作成します。 このサブコマンドにオプションを追加し、そのサブコマンドをルート コマンドに追加します。

    var readCommand = new Command("read", "Read and display the file.")
        {
            fileOption,
            delayOption,
            fgcolorOption,
            lightModeOption
        };
    rootCommand.AddCommand(readCommand);
    
  6. 新しいサブコマンドについて、SetHandler コードを次の SetHandler コードに置き換えます。

    readCommand.SetHandler(async (file, delay, fgcolor, lightMode) =>
        {
            await ReadFile(file!, delay, fgcolor, lightMode);
        },
        fileOption, delayOption, fgcolorOption, lightModeOption);
    

    ルート コマンドにハンドラーが不要になったため、ルート コマンドで SetHandler を呼び出す必要がなくなりました。 通常、コマンドにサブコマンドがある場合、コマンドライン アプリケーションを起動するときにサブコマンドのいずれかを指定する必要があります。

  7. ReadFile ハンドラー メソッドを次のコードに置き換えます。

    internal static async Task ReadFile(
            FileInfo file, int delay, ConsoleColor fgColor, bool lightMode)
    {
        Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black;
        Console.ForegroundColor = fgColor;
        List<string> lines = File.ReadLines(file.FullName).ToList();
        foreach (string line in lines)
        {
            Console.WriteLine(line);
            await Task.Delay(delay * line.Length);
        };
    }
    

これで、アプリは次のようになります。

using System.CommandLine;

namespace scl;

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

        var delayOption = new Option<int>(
            name: "--delay",
            description: "Delay between lines, specified as milliseconds per character in a line.",
            getDefaultValue: () => 42);

        var fgcolorOption = new Option<ConsoleColor>(
            name: "--fgcolor",
            description: "Foreground color of text displayed on the console.",
            getDefaultValue: () => ConsoleColor.White);

        var lightModeOption = new Option<bool>(
            name: "--light-mode",
            description: "Background color of text displayed on the console: default is black, light mode is white.");

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

        var readCommand = new Command("read", "Read and display the file.")
            {
                fileOption,
                delayOption,
                fgcolorOption,
                lightModeOption
            };
        rootCommand.AddCommand(readCommand);

        readCommand.SetHandler(async (file, delay, fgcolor, lightMode) =>
            {
                await ReadFile(file!, delay, fgcolor, lightMode);
            },
            fileOption, delayOption, fgcolorOption, lightModeOption);

        return rootCommand.InvokeAsync(args).Result;
    }

    internal static async Task ReadFile(
            FileInfo file, int delay, ConsoleColor fgColor, bool lightMode)
    {
        Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black;
        Console.ForegroundColor = fgColor;
        List<string> lines = File.ReadLines(file.FullName).ToList();
        foreach (string line in lines)
        {
            Console.WriteLine(line);
            await Task.Delay(delay * line.Length);
        };
    }
}

新しいサブコマンドをテストする

サブコマンドを指定せずにアプリを実行しようとすると、エラー メッセージの後に、使用できるサブコマンドを指定するヘルプ メッセージが表示されるようになりました。

scl --file sampleQuotes.txt
'--file' was not matched. Did you mean one of the following?
--help
Required command was not provided.
Unrecognized command or argument '--file'.
Unrecognized command or argument 'sampleQuotes.txt'.

Description:
  Sample app for System.CommandLine

Usage:
  scl [command] [options]

Options:
  --version       Show version information
  -?, -h, --help  Show help and usage information

Commands:
  read  Read and display the file.

サブコマンド read のヘルプ テキストは、4 つのオプションを使用できることを示しています。 列挙型の有効な値が示されています。

scl read -h
Description:
  Read and display the file.

Usage:
  scl read [options]

Options:
  --file <file>                                               The file to read and display on the console.
  --delay <delay>                                             Delay between lines, specified as milliseconds per
                                                              character in a line. [default: 42]
  --fgcolor                                                   Foreground color of text displayed on the console.
  <Black|Blue|Cyan|DarkBlue|DarkCyan|DarkGray|DarkGreen|Dark  [default: White]
  Magenta|DarkRed|DarkYellow|Gray|Green|Magenta|Red|White|Ye
  llow>
  --light-mode                                                Background color of text displayed on the console:
                                                              default is black, light mode is white.
  -?, -h, --help                                              Show help and usage information

--file オプションのみを指定してサブコマンド read を実行すると、他の 3 つのオプションの既定値を取得できます。

scl read --file sampleQuotes.txt

1 文字あたり 42 ミリ秒の既定の遅延が生じるため、読み上げ速度は遅くなります。 --delay を低い数値に設定すると、速度を上げることができます。

scl read --file sampleQuotes.txt --delay 0

--fgcolor--light-mode でテキストの色を設定できます。

scl read --file sampleQuotes.txt --fgcolor red --light-mode

--delay に無効な値を指定すると、エラー メッセージを受け取ります。

scl read --file sampleQuotes.txt --delay forty-two
Cannot parse argument 'forty-two' for option '--int' as expected type 'System.Int32'.

--file に無効な値を指定すると、例外を受け取ります。

scl read --file nofile
Unhandled exception: System.IO.FileNotFoundException:
Could not find file 'C:\bin\Debug\net6.0\nofile'.

サブコマンドとカスタム検証を追加する

このセクションでは、アプリの最終バージョンを作成します。 完成すると、アプリのコマンドとオプションは次のようになります。

  • --file というグローバル* オプションがあるルート コマンド
    • quotes コマンド
      • --delay--fgcolor--light-mode というオプションがある read コマンド
      • quotebyline という引数がある add コマンド
      • --search-terms というオプションがある delete コマンド

* グローバル オプションは、割り当てられたコマンドと、再帰的にそのサブコマンドすべてに使用できます。

使用できる各コマンドを、そのオプションと引数を指定して呼び出すコマンド ライン入力の例を次に示します。

scl quotes read --file sampleQuotes.txt --delay 40 --fgcolor red --light-mode
scl quotes add "Hello world!" "Nancy Davolio"
scl quotes delete --search-terms David "You can do" Antoine "Perfection is achieved"
  1. Program.cs で、--file オプションを作成するコードを次のコードに置き換えます。

    var fileOption = new Option<FileInfo?>(
        name: "--file",
        description: "An option whose argument is parsed as a FileInfo",
        isDefault: true,
        parseArgument: result =>
        {
            if (result.Tokens.Count == 0)
            {
                return new FileInfo("sampleQuotes.txt");
    
            }
            string? filePath = result.Tokens.Single().Value;
            if (!File.Exists(filePath))
            {
                result.ErrorMessage = "File does not exist";
                return null;
            }
            else
            {
                return new FileInfo(filePath);
            }
        });
    

    このコードでは、ParseArgument<T> を使ってカスタムの解析、検証、エラー処理を行います。

    このコードを使用しない場合、不足しているファイルと共に、例外、スタック トレースが報告されます。 このコードでは、指定されたエラー メッセージだけが表示されます。

    このコードでは、既定値も指定しています。そのため、isDefault の設定は true です。 isDefaulttrue に設定しない場合、--file に入力を指定していないときに parseArgument のデリゲートが呼び出されません。

  2. lightModeOption を作成するコードの後に、adddelete の各コマンドのオプションと引数を追加します。

    var searchTermsOption = new Option<string[]>(
        name: "--search-terms",
        description: "Strings to search for when deleting entries.")
        { IsRequired = true, AllowMultipleArgumentsPerToken = true };
    
    var quoteArgument = new Argument<string>(
        name: "quote",
        description: "Text of quote.");
    
    var bylineArgument = new Argument<string>(
        name: "byline",
        description: "Byline of quote.");
    

    AllowMultipleArgumentsPerToken の設定を指定すると、一覧の要素を指定するときに、1 つ目以外の --search-terms のオプション名を省略できます。 これにより、次のコマンドライン入力の例が同等になります。

    scl quotes delete --search-terms David "You can do"
    scl quotes delete --search-terms David --search-terms "You can do"
    
  3. ルート コマンドと read コマンドを作成するコードを次のコードに置き換えます。

    var rootCommand = new RootCommand("Sample app for System.CommandLine");
    rootCommand.AddGlobalOption(fileOption);
    
    var quotesCommand = new Command("quotes", "Work with a file that contains quotes.");
    rootCommand.AddCommand(quotesCommand);
    
    var readCommand = new Command("read", "Read and display the file.")
        {
            delayOption,
            fgcolorOption,
            lightModeOption
        };
    quotesCommand.AddCommand(readCommand);
    
    var deleteCommand = new Command("delete", "Delete lines from the file.");
    deleteCommand.AddOption(searchTermsOption);
    quotesCommand.AddCommand(deleteCommand);
    
    var addCommand = new Command("add", "Add an entry to the file.");
    addCommand.AddArgument(quoteArgument);
    addCommand.AddArgument(bylineArgument);
    addCommand.AddAlias("insert");
    quotesCommand.AddCommand(addCommand);
    

    このコードにより、次の変更が行われます。

    • read コマンドから --file オプションを削除します。

    • ルート コマンドにグローバル オプションとして --file オプションを追加します。

    • quotes コマンドを作成し、ルート コマンドに追加します。

    • read コマンドをルート コマンドにではなく quotes コマンドに追加します。

    • adddelete の各コマンドを作成し、quotes コマンドに追加します。

    その結果、次のようなコマンド階層になります。

    • ルート コマンド
      • quotes
        • read
        • add
        • delete

    親コマンド (quotes) で領域またはグループを指定し、その子コマンド (readadddelete) がアクションという推奨パターンをアプリに実装しました。

    グローバル オプションはコマンドに適用され、サブコマンドにも再帰的に適用されます。 --file はルート コマンド上にあるので、アプリのすべてのサブコマンドで自動的に使用できるようになります。

  4. SetHandler コードの後に、新しいサブコマンドのための新しい SetHandler のコードを追加します。

    deleteCommand.SetHandler((file, searchTerms) =>
        {
            DeleteFromFile(file!, searchTerms);
        },
        fileOption, searchTermsOption);
    
    addCommand.SetHandler((file, quote, byline) =>
        {
            AddToFile(file!, quote, byline);
        },
        fileOption, quoteArgument, bylineArgument);
    

    サブコマンド quotes はリーフ コマンドではないため、ハンドラーがありません。 サブコマンド readadddeletequotes 以下のリーフ コマンドであり、そのそれぞれに対して SetHandler が呼び出されます。

  5. adddelete のハンドラーを追加します。

    internal static void DeleteFromFile(FileInfo file, string[] searchTerms)
    {
        Console.WriteLine("Deleting from file");
        File.WriteAllLines(
            file.FullName, File.ReadLines(file.FullName)
                .Where(line => searchTerms.All(s => !line.Contains(s))).ToList());
    }
    internal static void AddToFile(FileInfo file, string quote, string byline)
    {
        Console.WriteLine("Adding to file");
        using StreamWriter? writer = file.AppendText();
        writer.WriteLine($"{Environment.NewLine}{Environment.NewLine}{quote}");
        writer.WriteLine($"{Environment.NewLine}-{byline}");
        writer.Flush();
    }
    

完成したアプリは次のようになります。

using System.CommandLine;

namespace scl;

class Program
{
    static async Task<int> Main(string[] args)
    {
        var fileOption = new Option<FileInfo?>(
            name: "--file",
            description: "An option whose argument is parsed as a FileInfo",
            isDefault: true,
            parseArgument: result =>
            {
                if (result.Tokens.Count == 0)
                {
                    return new FileInfo("sampleQuotes.txt");

                }
                string? filePath = result.Tokens.Single().Value;
                if (!File.Exists(filePath))
                {
                    result.ErrorMessage = "File does not exist";
                    return null;
                }
                else
                {
                    return new FileInfo(filePath);
                }
            });

        var delayOption = new Option<int>(
            name: "--delay",
            description: "Delay between lines, specified as milliseconds per character in a line.",
            getDefaultValue: () => 42);

        var fgcolorOption = new Option<ConsoleColor>(
            name: "--fgcolor",
            description: "Foreground color of text displayed on the console.",
            getDefaultValue: () => ConsoleColor.White);

        var lightModeOption = new Option<bool>(
            name: "--light-mode",
            description: "Background color of text displayed on the console: default is black, light mode is white.");

        var searchTermsOption = new Option<string[]>(
            name: "--search-terms",
            description: "Strings to search for when deleting entries.")
            { IsRequired = true, AllowMultipleArgumentsPerToken = true };

        var quoteArgument = new Argument<string>(
            name: "quote",
            description: "Text of quote.");

        var bylineArgument = new Argument<string>(
            name: "byline",
            description: "Byline of quote.");

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

        var quotesCommand = new Command("quotes", "Work with a file that contains quotes.");
        rootCommand.AddCommand(quotesCommand);

        var readCommand = new Command("read", "Read and display the file.")
            {
                delayOption,
                fgcolorOption,
                lightModeOption
            };
        quotesCommand.AddCommand(readCommand);

        var deleteCommand = new Command("delete", "Delete lines from the file.");
        deleteCommand.AddOption(searchTermsOption);
        quotesCommand.AddCommand(deleteCommand);

        var addCommand = new Command("add", "Add an entry to the file.");
        addCommand.AddArgument(quoteArgument);
        addCommand.AddArgument(bylineArgument);
        addCommand.AddAlias("insert");
        quotesCommand.AddCommand(addCommand);

        readCommand.SetHandler(async (file, delay, fgcolor, lightMode) =>
            {
                await ReadFile(file!, delay, fgcolor, lightMode);
            },
            fileOption, delayOption, fgcolorOption, lightModeOption);

        deleteCommand.SetHandler((file, searchTerms) =>
            {
                DeleteFromFile(file!, searchTerms);
            },
            fileOption, searchTermsOption);

        addCommand.SetHandler((file, quote, byline) =>
            {
                AddToFile(file!, quote, byline);
            },
            fileOption, quoteArgument, bylineArgument);

        return await rootCommand.InvokeAsync(args);
    }

    internal static async Task ReadFile(
                FileInfo file, int delay, ConsoleColor fgColor, bool lightMode)
    {
        Console.BackgroundColor = lightMode ? ConsoleColor.White : ConsoleColor.Black;
        Console.ForegroundColor = fgColor;
        var lines = File.ReadLines(file.FullName).ToList();
        foreach (string line in lines)
        {
            Console.WriteLine(line);
            await Task.Delay(delay * line.Length);
        };

    }
    internal static void DeleteFromFile(FileInfo file, string[] searchTerms)
    {
        Console.WriteLine("Deleting from file");
        File.WriteAllLines(
            file.FullName, File.ReadLines(file.FullName)
                .Where(line => searchTerms.All(s => !line.Contains(s))).ToList());
    }
    internal static void AddToFile(FileInfo file, string quote, string byline)
    {
        Console.WriteLine("Adding to file");
        using StreamWriter? writer = file.AppendText();
        writer.WriteLine($"{Environment.NewLine}{Environment.NewLine}{quote}");
        writer.WriteLine($"{Environment.NewLine}-{byline}");
        writer.Flush();
    }
}

プロジェクトをビルドして、次のコマンドを試してみてください。

read コマンドを使って存在しないファイルを --file に送信すると、例外とスタック トレースではなく、エラー メッセージを受け取ります。

scl quotes read --file nofile
File does not exist

サブコマンド quotes を実行しようとすると、readadd、または deleteを使うように指示するメッセージを受け取ります。

scl quotes
Required command was not provided.

Description:
  Work with a file that contains quotes.

Usage:
  scl quotes [command] [options]

Options:
  --file <file>   An option whose argument is parsed as a FileInfo [default: sampleQuotes.txt]
  -?, -h, --help  Show help and usage information

Commands:
  read                          Read and display the file.
  delete                        Delete lines from the file.
  add, insert <quote> <byline>  Add an entry to the file.

サブコマンド add を実行し、テキスト ファイルの末尾に追加されたテキストを確認します。

scl quotes add "Hello world!" "Nancy Davolio"

ファイルの先頭から検索文字列を指定してサブコマンド delete を実行し、テキスト ファイルの先頭を見て、テキストが削除された場所を確認します。

scl quotes delete --search-terms David "You can do" Antoine "Perfection is achieved"

注意

bin/debug/net6.0 フォルダーで実行している場合、そのフォルダーには adddelete の各コマンドによる変更が加えられたファイルがあります。 プロジェクト フォルダー内のファイルのコピーは変更されません。

次の手順

このチュートリアルでは、System.CommandLine を使う簡単なコマンドライン アプリを作成しました。 ライブラリの詳細については、System.CommandLine の概要に関するページを参照してください。