最近JWTを取り扱っているのですが、仕様上base64urlフォーマットを頻繁に利用します。
C# でbase64というと、Convert.FromBase64String
あたりですが、base64urlにしてくれるような気の利いた仕組みはなく、入力がbase64url仕様に沿ってないとすぐに例外を吐いて使いにくさが目立ちます。*1
個人的には、base64urlへの変換、base64とbase64urlの相互変換をしてくれれば十分で、フォーマットもstringで上出来です。そこで、自分のJWT操作用にbase64url / base64の対応をするnugetライブラリ、それとJWTをコンソール上で検証するために、CLIを .NET Core Global Toolとして作りました。
今回はその内容と、dotnet global tool (要はコンソールアプリ) でMicroBatchFrameworkを使えるようにフィードバックしてた話です。
TL;DR;
.NET CoreのCLIもMicroBatchFrameworkで書きやすくなったのでオススメです。
GitHub
ライブラリと .NET Core Global Toolはnugetに置いてあります。普段使っており、npmなどほか言語実装との挙動チェックはしているので問題ないですが、何かあればリポジトリまでお願いします。
今回、CLIを提供するにあたり.NET Core Global Toolを作りました。*2 当初自前コマンドライン処理で書いたのですが、MicroBatchFrameworkに改善フィードバックを送り続けた結果、CLIでも使いやすくなりMicroBatchFrameworkへ移行完了しました。
ゴールとなる使い心地
NuGet ライブラリ
文字列やバイト配列を受けて、UTF-8 *3 のbase64urlでエンコード/デコードします。また、base64urlとbase64の文字列をお互いに切り替えます。普通です。
CLI
次のコマンド入力を満たしつつ、MicroBatchFrameworkで提供することを目指します。
base64urls [-version] [-help] [encode|decode|escape|unescape] [args]
これはnpmで提供されているb64-cliやbase64-url-cliのbase64url [encode|decode|escape|unescape|binarydecode] [input]
がちょうどいい使い勝手のバランス、かつよく利用されているので、コマンド入力から想定されていない入力時のエンコード/デコードも含めてこの挙動に合わせました。*4
https://www.np.js .com/package/b64-cliwww.np.js .com
https://www.np.js .com/package/base64-url-cliwww.np.js .com
NuGet ライブラリ
.NET Standard 2.0で作っています。
仕様に沿ってもくもくと書くだけなので余り書くことがありません。ほとんどすべて1行の処理で、コメントを除くと全部で20行程度なことからも察しかと。
base64urlでは、エンコーディング時に文字列長が4の倍数となるようにパディング(=)を末尾追加するのですが、ショートハンドでこう書けます。*5
base64String.Length + (4 - base64String.Length % 4) % 4, '='
.NET Core Global Tool
.NET Core 2.2以上で作っています。実は .NET Core 2.1でもいいのですが、私はもう2.2未満は作らないので上げておきます。*6
さて、.NET Core Global Toolはただの.NET Coreなコンソールアプリで、配布、インストール、アップグレード、アンインストールをNuGet基盤を利用できます。
利用側は、.NET Core SDKがあればokすぐに使えます。
開発側は、.NET Core Consoleテンプレートで作ればokです。詳細は公式Docsで十分書かれているので参照してみるといいでしょう。
ポイントは、csprojにいれるPackAsTool
です。また、私はbase64urlsとパッケージの名称でC# プロジェクトをわざわざ切りましたが、ToolCommandName
を使うことでパッケージに別名を付けることができるので厳守したい方はこっちでどうぞ。*7
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp2.1</TargetFramework> <PackAsTool>true</PackAsTool> <ToolCommandName>botsay</ToolCommandName> <PackageOutputPath>./nupkg</PackageOutputPath> </PropertyGroup> </Project>
自前のコマンドパーサーで組んでみる
さて、 base64urlsとしてはじめにCLIを組んだときは自分で引数からコマンドまで書いていました。その時に書いていたコードを置いておきます。
ごく少量のコマンドをサクッと処理すればいいだけだったので、MonoOptionsなども使わずざくっとゴールと同じ挙動を作っています。引数を検査して、コマンドに導いて処理をするだけのよくある小規模なものです。
MicroBatchFrameworkをこの時使わなかったのは、バッチ用途に限定されており、CLIとして使うには違和感が大きかったためです。例えば次のことがはじめはできませんでした。
* `base64urls encode 入力` という、提供したいコマンド入力の提供はできない。 * BatchBaseを継承してメソッドをたたくのですが、`-p params` 形式で解釈されるため、一般的なCLIとして提供するのは苦しいです。 * メソッドごとにコマンドにマッピングして提供したいが、`Class.Method` が強制される。 * バッチとしてはいいのですが、CLIとしては困ります。 * helpとversionはGNUスタイルに沿うようにしたいが、任意のフォーマットでのパラメーター形式ができない * クロスプラットフォームで利用するので、GNU スタイルで、-h と -help、--help を提供したいもの対応できません。 * help はデフォルトの引数で、オーバーライドが許可されていない * どうしようもない * コマンド入力なしの時に help を出したいがhelp をオーバーライドできず、独自内容になる。 * バッチとしては理解して使う分にはいいのですが、CLIとしては初めて使う人にとってはヘルプになってないのでアウト * 想定されていないコマンド入力時にヘルプを出したいがエラーに回される。 * しょうがない
MicroBatchFramework で CLI を組む
CLIとしてはいまいち使いにくいことを作者にフィードバックしたところ、「メソッドに対してサブコマンドのマッピングが[Command("decode")]
のようなフォーマットで可能になった」「引数も([Option(0)]T param)
で可能になった」と連絡が来ました。
つまり、base64urls decode 値
を次のように表現できるようになったということです。
[Command("encode")] public void Encode([Option(0)]string input) => コマンド;
連絡を受けて、MicroBatchFrameworkに移行したのが次のPRです。
MicroBatchFramework 0.45-beta3 の状態で組んでみたところ、次のようになりました。
自作コマンドと比較してみると、処理に集中できているのが分かります。
一方で、次の課題が残っています。
* MicroBatchFramework のデフォルト引数である help の挙動をオーバーライドできない。 * 引数段階で`help` を`-help` に差し替えている * MicroBatchFramework のデフォルト引数である list の挙動をオーバーライドできない * 引数段階で`list` を`-help` に差し替えている * `[Command]` 属性が複数のコマンドを受け付けない * `-v`、`-version`、`--version` や -helpのメソッドを冗長に組んでいる * コマンドに引数を空で渡したときに、オーバーライドしたhelp が表示されない * 誤ったコマンドを渡したときに、オーバーライドしたhelpが表示されない
これらは、0.4.5-beta9 のリリースで次のように改善されました。
* MicroBatchFramework のデフォルト引数である help の挙動をオーバーライドできない。 * コマンドでオーバーライド可能になりました。 * MicroBatchFramework のデフォルト引数である list の挙動をオーバーライドできない * コマンドでオーバーライド可能になりました。 * `[Command]` 属性が複数のコマンドを受け付けない` * `[Command(new [] {"-v", "-version", "--version"})]` のようなフォーマットで指定できるようになりました。 * コマンドに引数を空で渡したときに、オーバーライドしたhelp が表示されない * 空の引数を渡したときにオーバーライドされたhelp が表示できるようになりました。
残りの課題は1つですが、これはすぐに対応はしないとのことだったので、受け付けるコマンドのホワイトリストを作って事前検査することで対応しました。
* 誤ったコマンドを渡したときに、オーバーライドしたhelpが表示されない
CLIのAPI、挙動的にはこの時点で自前コマンドからMicroBatchFrameworkで差し替え可能になりました。コードを比較するとかなりシンプルになっています。
残った課題は、0.4.5-beta10 で 対応されたので私がGolang や C# で書くCLI的には要件がすべて達成されています。各種処理がほぼ1行に収まりユニットテストと処理内容的にデバッガを挟む必要もないので Expression Methodに書き換えて、MicroBatchFramework への移行が完了しました。
コードを比較してみましょう。自前コマンドパーサーの時から見ると、引数の取り扱いやヘルプにどうやって回すかを考えることなく、やりたい処理だけに集中できるようになっていることが分かります。特に、Mainメソッドが1行になったことで見通しが良くなり、とっつきやすくなっています。
まとめ
MicroBatchFrameworkは、私も注目しているGenericHostの仕組みをうまく活用しつつ、.NET CoreでCLIを書くときも書きやすいレベルまで改善されたのでオススメです。*8
*1:NuGetを見ても使いやすいライブラリがなく、 Convert.FromBase64StringとConvert.FromBase64Stringしているだけのものだったりしてカオス
*2:いろいろあるのどうかと思いつつ、あってもいいし、配布機構を考えたくなかった
*3:base64はUTF-8を想定していますが、一応任意のエンコーディングも
*4:私も自作しても時にこれを使っているので同じ体験がうれしい。
*5:採用していませんが
*6:下げたのをあげるのは大変なので初めから上げておくスタイル
*7:私はこういうところを気にしないというか、あえて合わせに行く方が後日分かりやすいので別名を避けています
*8:NuGet上はPreleaseなので注意です