.NET Core 2.1から.NET Core Global Toolsというものが追加されました。
それによってNuGetを使ってコマンドラインツールを提供できるようになるみたいです。今回はコマンドラインの引数を簡単にパースしてくれるMcMaster.Extensions.CommandLineUtilsを使いたくなったので、簡単なコマンドラインツールを作ってみます。
また、今回作ったものはいつも通りGithubに公開しています。
プロジェクト構成
.NET Core Global Toolsということもあって、コンソールアプリケーションプロジェクトは.NET Coreである必要があります。また、今回使いたかったMcMaster.Extensions.CommandLineUtilsは.NET Standardにも対応している便利設計なため、コード本体は.NET Standardプロジェクトにすることにしました。
具体的には以下の構成です:
- PenguinCommand.NETCore
- PenguinCommand.NETStandardを参照
- PenguinCommand.NETStandard
あと、こないだTwitterで話題になったソリューション内のプロジェクトすべてのC# LangVersionを一括で変更できるDirectory.build.props
をソリューション直下に配置しています。
もちろん中身はこれです:
<Project> <PropertyGroup> <LangVersion>latest</LangVersion> </PropertyGroup> </Project>
C#erなら無条件で最新バージョン使うよなぁ?という感じにlatest指定。ちなみにVisual Studio 2019 previewでこの指定をしてもC# 8.0 previewにはならないみたいです。8.0
と明示しないといけないみたいですが、PCの容量不足でVisual Studio 2019 previewをインストールできてないので確かめてないです……
あと、このファイルをVisual Studio上から追加した場合はプロジェクト設定に反映させるためにソリューションを開きなおすなどして再読み込みしないといけませんので注意!
コマンドの用意
今回は実際にコマンドラインツール作成時に使いそうなMcMaster.Extensions.CommandLineUtilsのAttribute APIを使います。サンプルもそれなりに用意してくれてる親切ライブラリのようで、このサンプルに似た作りにします。
Attribute APIでは[HelpOption("--help|-h")]
のような属性を付けて実際のコマンドと結びつけます。全てのコマンドにHelpOptionを対応させたいので基底クラスを用意してあげます
[HelpOption("--help|-h")] public abstract class BaseCommand { protected virtual int OnExecute(CommandLineApplication application) { return 0; } }
基底クラスを用意したら、コマンドの引数がなかった時に呼ばれる?ルートコマンドを用意し、そこにサブコマンドを結び付けます
[Command] [Subcommand("show", typeof(ShowCommand))] [Subcommand("list", typeof(ListCommand))] public class Command : BaseCommand { protected override int OnExecute(CommandLineApplication application) { application.ShowHelp(); return base.OnExecute(application); } }
サブコマンドはこんな感じでいいでしょう:
[Command(Description = "Show the penguin.")] public class ShowCommand : BaseCommand { [Option("-l|--logo")] public bool IsLogo { get; } protected override int OnExecute(CommandLineApplication application) { string text = IsLogo ? Constant.PenguinLogo : Constant.Penguin; Console.WriteLine(text); return base.OnExecute(application); } }
詳しいAPIは公式ドキュメントを見ればだいたいわかるはずです:
https://natemcmaster.github.io/CommandLineUtils/index.html
コマンドを用意できたら.NET CoreプロジェクトでCommandLineApplicationを実行するだけです。
internal class Program { private static void Main(string[] args) => CommandLineApplication.Execute<Command>(args); }
デバッグ
コンソールアプリケーションを作りながらコマンドのデバッグをしたいときはVisual Studio上からはめんどくさいので、dotnet
コマンドを叩けるCLI上でデバッグしましょう。
Windows環境ならエクスプローラーで該当のプロジェクト(今回はPenguinCommand.NETCore)のディレクトリを開き、左上のファイルのとこからPowerShellを開けば該当プロジェクトがCurrentDirectoryなPowerShellが開けます。
引数有りのコマンドを動かすには、コンソールアプリケーションに引数を与えて実行すればいいのでdotnet run -- {コマンド 引数}
といった感じにコマンドを実行しましょう。
先ほど作ったshowコマンドならdotnet run -- show -l
みたいな感じになります。
.NET Core Global Toolsの作成
コンソールアプリケーション自体を作れたらそれを.NET Core Global Toolsに合わせた形式にします。公式ドキュメントはこちらです↓
公式ドキュメントの解説が丁寧すぎて言うことはあまりありませんが、コンソールアプリケーションのcsprojに<PackAsTool>
と<ToolCommandName>
を追加します
こんな感じ:
<PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp2.1</TargetFramework> <PackAsTool>true</PackAsTool> <ToolCommandName>penguin</ToolCommandName> </PropertyGroup>
公式ドキュメントでは<PackageOutputPath>
も記載されてますが、これは生成されるnupkgファイルの場所を変えるもので、今回はデフォルトのままにします。
NuGet packageの作成はコマンド上ではdotnet pack
とするだけで生成されます。デフォルトでは/bin/Debug/
に配置されるかと思います。
それをdotnet tool install --global
でインストールすることになるのですが、今回はNuGet公開しないローカルなものとしてインストールするので--add-source
オプションでNuGet packageが作成されたディレクトリを指定することにします。
作成したサンプルではこのようなコマンドです: dotnet tool install --global --add-source bin\Debug PenguinCommand.NETCore
無事インストールされたら以下のようなメッセージが出ます:
次のコマンドを使用してツールを呼び出せます。penguin ツール 'penguincommand.netcore' (バージョン '1.0.0') が正常にインストールされました。
今回はNuGet packageのバージョンを指定しなかったので、バージョン1.0.0となりましたが、実際に配布するものの場合はちゃんとバージョン指定しないといけませんね。
インストールできたら<ToolCommandName>
で指定したコマンド名を叩けば作成したコンソールアプリケーションが実行されます。
penguin show
とかpenguin --help
とかそんな感じで