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