コンソールアプリの引数の扱い方
コンソールアプリを、簡単そうだから作ってみようと思うと、意外と引数の扱い方に苦悶します。
というわけで、簡単(というほど簡単ではなかった)に引数を扱う方法を紹介します。
参考文献はこれ
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オプションであれば、上記のように使用することができるということ。