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