目次

コンソールアプリの引数の扱い方

コンソールアプリを、簡単そうだから作ってみようと思うと、意外と引数の扱い方に苦悶します。
というわけで、簡単(というほど簡単ではなかった)に引数を扱う方法を紹介します。
参考文献はこれ
https://learn.microsoft.com/ja-jp/dotnet/standard/commandline/define-commands とりあえず、迷ったら参考文献のリンク含めて全部読んでください。

NuGetで取得する方法

まず、ここでつまづく。

NuGetでライブラリを取得するわだが、「system.commandLine」で検索するわけですが、必ず「プレスリリースを含める」にチェックしましょう。あたしゃこれだけで、1、2時間つぶしました。

用語を覚える

ルートコマンド・サブコマンド

どうやら、ルートコマンド・サブコマンドというものがあるらしいです。詳しくは知らん。

オプション

オプションというのは、いわゆるDOSでいうところの「/d」とか「/s /a」とかっていうやつ。基本的に、各オプションには「型」と「引数」というのがあって、文字列型オプション「-S」に「hogehoge」を渡す場合は「-s hogehoge」となる。
で!!「型」を「boolean」型にすると、「引数」を省略できる。つまり「-s」と書けば、「-s」スイッチが渡されたことになる。

オプションのバンドル

これは、上記参考文献の関連項目に記載されています。

POSIX では、1 文字のオプションの “バンドル” (“スタック” とも呼ばれます) をサポートすることが推奨されています。 バンドルされたオプションは、1 つのハイフン プレフィックスの後に、1 文字のオプション エイリアスをまとめて指定したものです。

つまりこれの「ltr」の部分。下の2つは等価であるということ。

ls -ltr
ls -l -t -r

引数

引数は、オプションの指定が必要ないもの。下の場合、42と「Hello world!」がアプリケーションに渡される。

myapp 42 "Hello world!"

実装してみる

基本的な構成

「ルートコマンド」と定義して、その下に「オプション」や「引数」をぶらさげていく。正直、参考文献見た方が早い。

var delayOption = new Option<int>
    (name: "--delay",
    description: "An option whose argument is parsed as an int.",
    getDefaultValue: () => 42);
var messageOption = new Option<string>
    ("--message", "An option whose argument is parsed as a string.");
 
var rootCommand = new RootCommand();
rootCommand.Add(delayOption);
rootCommand.Add(messageOption);
 
rootCommand.SetHandler((delayOptionValue, messageOptionValue) =>
    {
        Console.WriteLine($"--delay = {delayOptionValue}");
        Console.WriteLine($"--message = {messageOptionValue}");
    },
    delayOption, messageOption);

上の例では、delayOption オプションとmessageOption オプションを定義して、rootCommand にAddしている。Option型でnewすればオプションの定義、Argument型でnewすれば引数の定義となる。どちらも定義したあとに、rootComandにAddすれば良い。
newするときの「name」は、ユーザが渡すときの「名称」となるので、「–delay」スイッチを渡すことになる。
使用例は下。

myapp --delay 21 --message "Hello world!"

「description」は、helpを表示したときの説明となる。help表示に関しては、何も実装しなくてもコマンド概要が表示されるようになる。つまり、helpオプションを実装しなくても

myapp -h

としたり、オプションエラーは引数エラーとなったときに表示される内容が担保される。

SetHandlerの罠

さて、rootCommandにオプションや引数をAddしたら、最後にrootCommandのハンドラーをセットする。これは、正直意味不明だが、イメージとして、「アプリケーションに引数が渡ってきた!」という「イベント」の処理を定義すると思って良いと思います。
ただし、このSetHandlerメソッドは引数が8個までしか受け入れない。つまり、9個以上の引数が必要なときに困る。
9個以上のときは、2つに分けて、SetHandlerメソッドを2回呼べばいいじゃんと考えてはダメ。「アプリケーションに引数が渡ってきた!」という「イベント」の処理の定義なので、2行かいたら、後に書かれた行で上書きされる。というか、ハンドラーと名のつくものを2回も3回も呼び出すのは良くないと覚えておいたほうが良い。

で、そうするの?ってところだけども、オプション用や引数用のクラスを定義するのが良い。というより、これしか方法がない。
ここからtouchコマンドの実装を一部公開していく。(公開っていうか、まぁ、全部見たかったら逆コンパイルでもして)。

        public class CommandOptions
        {
            public string timeStamp = "", timeStamp2 = "", r = "";
            public bool c = false, D = false, n = false, s = false, v = false, w=false,ig=false, eos = false;
        }
 
        public class optionBinder : BinderBase<CommandOptions>
        {
            private readonly Option<string> _timeStampOption, _timeStampOption2, _readFileOption;
            private readonly Option<bool> _createArgument, _directroyArgument, _fileNodeOption, _subDirectoryOption, _viewMoreOption, _warningOption, _ignoreOption, _cultureOption;
 
            public optionBinder(Option<string> timeStampOption, Option<string> timeStampOption2, Option<string> readFileOption, Option<bool> createArgument, Option<bool> directroyArgument, Option<bool> fileNodeOption, Option<bool> subDirectoryOption, Option<bool> viewMoreOption, Option<bool> warningOption, Option<bool> ignoreOption, Option<bool> cultureOption)
            {
                _timeStampOption = timeStampOption;
                _timeStampOption2 = timeStampOption2;
                _readFileOption = readFileOption;
                _createArgument = createArgument;
                _directroyArgument = directroyArgument;
                _fileNodeOption = fileNodeOption;
                _subDirectoryOption = subDirectoryOption;
                _viewMoreOption = viewMoreOption;
                _warningOption = warningOption;
                _ignoreOption = ignoreOption;
                _cultureOption = cultureOption;
            }
 
            protected override CommandOptions GetBoundValue(BindingContext bindingContext) =>
                new CommandOptions
                {
                    timeStamp = bindingContext.ParseResult.GetValueForOption(_timeStampOption),
                    timeStamp2 = bindingContext.ParseResult.GetValueForOption(_timeStampOption2),
                    r = bindingContext.ParseResult.GetValueForOption(_readFileOption),
                    c = bindingContext.ParseResult.GetValueForOption(_createArgument),
                    D = bindingContext.ParseResult.GetValueForOption(_directroyArgument),
                    n = bindingContext.ParseResult.GetValueForOption(_fileNodeOption),
                    s = bindingContext.ParseResult.GetValueForOption(_subDirectoryOption),
                    v = bindingContext.ParseResult.GetValueForOption(_viewMoreOption),
                    w = bindingContext.ParseResult.GetValueForOption(_warningOption),
                    eos = bindingContext.ParseResult.GetValueForOption(_cultureOption),
                    ig = bindingContext.ParseResult.GetValueForOption(_ignoreOption),
 
                };
        }

まず、全てのオプション、引数を格納するためだけのクラス「CommandOptions」を定義する。その後、「BinderBase」から継承するクラス「optionBinder」を作成する。「optionBinder」のコンストラクタで、メンバー変数にアプリに渡ってきたオプションや引数を格納する。正直、きまったことを書くだけの作業となる。 最後に、GetBoundValueをオーバーライドして、CommandOptionsクラスを参照さえすれば、引数を受け取れる状態にする。
メイン処理では、このように実装する。

            var rootCommand = new RootCommand("Developed by https://it01.hooray-eri.com/");
            rootCommand.Add(createArgument);
            rootCommand.Add(directroyArgument);
            rootCommand.Add(timeStampOption);
            rootCommand.Add(timeStampOption2);
            rootCommand.Add(fileNodeOption);
            rootCommand.Add(readFileOption);
            rootCommand.Add(subDirectoryOption);
            rootCommand.Add(viewMoreOption);
            rootCommand.Add(warningOption);
            rootCommand.Add(ignoreOption);
            rootCommand.Add(cultureOption);
            rootCommand.Add(fileName);
 
            CommandOptions CO = new CommandOptions();
 
            rootCommand.SetHandler((CommandOptionsV, filePtnV) =>
            {
                CO = CommandOptionsV;
                fileNamePtn = filePtnV;
            },
                new optionBinder(timeStampOption,timeStampOption2,readFileOption,createArgument,directroyArgument,fileNodeOption,subDirectoryOption,viewMoreOption,warningOption,ignoreOption,cultureOption)
                ,fileName
                );

今回の例では、11個のオプションは別のクラスにして、1個の引数(fileName)はクラスを使うこと無く使用することにした。オプションは「CO」クラスメンバーを参照すれば、引数で渡ってきた値がわかるようになる、という仕掛けである。

if (reg1.IsMatch(CO.timeStamp))
{
・・・
}

例えば、timeStampオプションであれば、上記のように使用することができるということ。