Swift.org - Announcing ArgumentParser
2月末に Swift.org でアナウンスされた ArgumentParser をいろいろと触ってみたので紹介的なものを書いてみようと思います。
基本的にはわかりやすい英語とサンプルコードで書かれている 本家 Documentation が一次情報なのでそちらを読んでください。リポジトリには 動作するサンプルコード もあるのでそちらを動かすのもおすすめです。
この記事の執筆時点では ver 0.0.2 がリリースされていますが僕が試したのは v0.0.1 なのでご了承ください。
はじめに
3rd Party 製の Command Line Tool 向けの Argument Parser だったりフレームワークのようなものは以前からありました。Apple 自身も Swift Package Manager 内部で使われていた ArgumentParser (名前が同じでややこしい)は存在しており、apple/swift-tools-support-core (以下 TSC)として切り出されていました。今回アナウンスされたものは TSC の ArgumentParser とも別物の単体のオープンソースライブラリとしてリリースされました。
特徴としては Swift 5.1 で追加された Property Wrapper を使ってコマンドのパラメータとプロパティの対応が宣言的に書けるようなライブラリになっています。ある種のフレームワーク的なものとしても捉えられ、仕組みに沿ってコードを書いていけば簡単にコマンドラインツールが書けるようになっています。おかげで本質的な処理部分に時間をかけられるのはうれしいところです。
Apple が作っているいくつかのオープンソースプロダクトは TSC から ArgumentParser に乗り換える方向のようです。(indexstore-db
と swift-format
で PR が出されていてまだマージはされていない)
今回は僕がドキュメントを読んだり少し触った範囲で気になったことを書いていきます。
僕の背景として、公開はしていませんが5個ほど TSC を使ったコマンドラインツールを個人的に書いたことがあります。他の言語だと Go と Node.js で少しコマンドラインツールを使ったことがあります。
できること
- よくあるパターンの引数のパース処理を Property Wrapper を使って簡単に書ける
- フラグ
--verbose
みたいなただのフラグ・スイッチ的なオプションの指定方法
- オプション
swift build --configuration debug
の--configuration debug
のように名前付きの引数みたいなオプションの指定方法
- 引数
ls ~/
みたいに何もつかない引数
- フラグ
- help もデフォで最低限は生成される
-h
--help
がデフォルトで実装される
- エラーメッセージもデフォで最低限を返してくれる
- 型チェックとか必須パラメータの不足など
- ParsableCommand に準拠した型でコマンドを定義する
func run() throws
メソッド内にコマンドの処理を書く
- 実行自体は static メソッド
main
を呼ぶだけで、引数のパースや ParsableCommand の run をよしなに呼んでくれる - オプションのショートネームも定義できる
- Swift で一般的な Lower camel case は自動的に Kebab case に変換されたオプション名になる
@Flag() var printSupportedFile: Bool
は--print-supported-file
@Flag(name: .shortAndLong) var printSupportedFile: Bool
とすれば自動的に頭文字を使ったショートネームが追加されて-p
,--print-supported-file
が使えるようになる@Flag(name: [.customShort("s") .customLong("support-file")]) var printSupportedFile: Bool
とすれば自動生成じゃなく自分で任意のパラメータ名にできる。この場合は-s
,--support-file
が使えるようになる
- Swift で一般的な Lower camel case は自動的に Kebab case に変換されたオプション名になる
- long オプションは基本的に
--
だけどwithSingleDash: true
すれば-
形式にできるけど推奨はされていない - 独自の型もパースできる(してもらえる)
ExpressibleByArgument
に準拠してinit?(argument:)
を実装する- enum も RawRepresentable に対応してるような奴は
ExpressibleByArgument
に準拠してると宣言するだけで済む。 - transform 関数を実装していればパースできたりもする
- フラグの反転も指定できる
@Flag(inversion: .prefixedNo) var index: Bool
だと--index
と--no-index
が定義される@Flag(inversion: .prefixedEnableDisable)
だと--enable-hogehoge
と--disable-hogehoge
- CaseIterable と String の RawRepresentable に対応した enum を使えばフラグをまとめたりとかもできる
- Flag オプションを Int 型に適用すると呼び出された回数が入ってくる
-vvvv
とかしたら verbose プロパティの値は 4 になってるみたいなことができる
- 引数のパース方法は選べるけどほとんどの場合ややこしいことになる
@Option(parsing: . upToNextOption) var file: [String]
のケースだけはちょっと便利かもしれない。毎回 --file hoge ってしなくても連続して --file のオプションを渡せるから。
- サブコマンドの実装ができる
- ツリー上にパースされていく
- コマンド名は基本的に型名が使われるが以下のように明示もできる
swift static var configuration = CommandConfiguration( commandName: "stdev", abstract: "Print the standard deviation of the values.")
- サブコマンドのサブコマンドも作れる
- 共通のオプション(設定)は
ParsableArguments
に準拠した Struct とかを用意して、@OptionGroup() var options: Math.Options
と書くことでまとめられる- ちょっとオプションの多いコマンドを作ったときにはオプションのカテゴライズをしてまとめられてコードが見やすくなるし、排他処理も書きやすくなる
- ヘルプのカスタマイズ
- @Flagとかの help 引数に文字列を明示的にあたえることでコメントを追加できる
- より細かいカスタムは上記で渡していた文字列の代わりに
ArgumentHelp
のインスタンスを渡すことで可能 - コマンドやサブコマンドのヘルプは
static var configuration = CommandConfiguration( XXXX )
でできる - デフォルトの
-h
,--help
のパラメータ名はカスタム可能。Root レベルのコマンドに対する CommandConfiguration で helpName: で指定可能 - ヘルプに出力したくない場合には
@Flag(help: .hidden)
とかすれば、試験的に追加した機能とかをヘルプに出力せずにすませられる
- バリデーション
ParsableCommand
とParsableArguments
にvalidate()
を実装することでバリデーションできる- throw すればそのメッセージが標準エラーに出力されてエラーコードで終了する
- runメソッド内でも実行時エラーを throw できる
- v0.0.2 では
ExitCode
という Error が用意されたので自前でエラーメッセージを出力してthrow ExitCode.failure
することで綺麗にエラーからの終了が実装できるようになった
- v0.0.2 では
- マニュアルでパース
できないこと(提供されてないこと)
- コマンドラインツール的に便利なパスの処理
- 外部のコマンドの呼び出しのサポート
これらはライブラリの名前から連想する対応範囲を超えているように思えるので別のライブラリなどを使った方が良いかもしれないです。僕は個人的にパス周りは Path.swift, パスまわりや外部のコマンド呼び出しは前述の TSC を使ったりしています。
それ以外は特別困っていません。
まとめ
コマンドラインツールはいろんな宗派があると思うので好みが分かれると思います。細かいことを言えば切りがないかも知れません。とはいえ
Swift でコマンドラインツールを書く場合、これらは利点だと思うのでぜひ皆さんも使って見たらいいと思います。