コマンドラインツールを作っているとそのツールのバージョン番号を出力したい事があります。ここでは引数で渡した文字列をそのまま標準出力に出力する myEcho
というコマンドを作るというのを例にしてみます。
完成イメージはこんな感じです。
% myEcho Hello
Hello
% myEcho --version
1.0.0
引数の文字をそのまま Print し、--version
フラグが立っているときはバージョン番号を出力するだけのコマンドです。
コード例その1
import ArgumentParser
struct MyEcho: ParsableCommand {
static var configuration = CommandConfiguration(commandName: "myEcho")
@Flag()
var version: Bool
@Argument()
var text: String
func run() throws {
if version {
print("1.0.0")
return
}
print(text)
}
}
MyEcho.main()
素直に今回の仕様を ArgumentParser を使って実装すると上記のような感じで書けると思います。
ヘルプを出力してみるとこんな感じ
% myEcho --help
USAGE: myEcho [--version] <text>
ARGUMENTS:
<text>
OPTIONS:
--version
-h, --help Show help information.
いけてそうです。
引数を渡してみます。
% myEcho Hello
Hello
動いてますね。バージョン番号を出力させてみます。
% myEcho --version
Error: Missing expected argument '<text>'
Usage: myEcho [--version] <text>
エラーが出ました。必須引数がないってことで怒られています。とはいえよくあるコマンドラインツールはそのツールが引数を必須としていても --version
フラグが有効な場合は必須として扱いません。
直します。
コード例その2
import ArgumentParser
struct MyEcho: ParsableCommand {
static var configuration = CommandConfiguration(commandName: "myEcho")
@Flag()
var version: Bool
@Argument()
var text: String?
func run() throws {
if version {
print("1.0.0")
return
}
guard let text = text else {
throw ValidationError("need argument")
}
print(text)
}
}
MyEcho.main()
@Argument()
にしている text プロパティをオプショナルにして、実行時に version フラグが立っていなければ text プロパティをチェックしてエラー処理したうえで出力します。
実際動かしてみると echo 部分も --version
も期待通りに動きます。引数なしで実行すると以下のような感じに怒られます。
% myEcho
Error: need argument
Usage: myEcho [--version] [<text>]
補足すると ValidateionError
は ArgumentParser 側で v0.0.1 のころから定義されているエラーです。このエラーを throw するとエラーコードを履きながらエラーメッセージを表示しつつ Usage を出力してくれます。便利ですね。
とはいえ個人的には
-
func run() throws
実行時には引数関連のエラー処理は終わった状態であってほしい - Unwrap を後続の処理の中であまりやりたくない
という思いもありモヤモヤします。
このモヤモヤを解決するために issue で良い方法がないか聞いてみた回答が次のコードです。
完成形
import ArgumentParser
struct MyEcho: ParsableCommand {
static var configuration = CommandConfiguration(commandName: "myEcho")
struct Version: ParsableArguments {
@Flag()
var version: Bool
func validate() throws {
if version { throw CleanExit.message("1.0.0") }
}
}
@OptionGroup()
var version: Version
@Argument()
var text: String
func run() throws {
print(text)
}
}
MyEcho.main()
変更点は地味に多いですが以下の通り
- version: Bool を Version 型 に変更
- struct Version 内で
- Bool フラグを持たせる
-
ParsableArguments
プロトコルのfunc validate() throws
を実装 -
func validate() throws
内でフラグが立っていればバージョン番号を出力- 出力時には正常終了するために ArgumentParser が定義する
CleanExit
型のエラーをスローする
- 出力時には正常終了するために ArgumentParser が定義する
- text プロパティはオプショナルをやめる
-
func run() throws
ではただ text プロパティを出力するのみ
これが現状のスッキリ終わらせるための Workaround です。
func validate() throws
が run よりも先に実行されているので func run() throws
内では version のことは気にせずに済むようになりました。
コード量は増えましたが僕が元々感じていたモヤモヤはすべて解消されています。また、バージョン番号を出力する処理が Version 型にまとまっています。今回は MyEcho.Version とネストさせて定義していますが、ネストさせる必要性は特にないのでので単体のファイルとして定義していれば使い回しもしやすいです。
まとめ
この記事を読んで気付いた方がいるかも知れませんが、ArgumentParser ではバージョン番号出力はデフォルトで提供されていません。Issue 内でも提供して欲しいと他の方がリクエストしていました。将来的には提供されるようになるかも知れませんね。