【Swift Argument Parser入門】Swiftでコマンドラインツールを作る
2020年2月、AppleがSwift向けライブラリーArgumentParserをリリースしました。 これはSwiftでコマンドラインツールを作る際にコマンド引数を簡単に扱えるライブラリーです。 CarthageやXcodeGenなどSwift製のコマンドラインツールはたくさんありますが、いざ自分で作ろうと思ったときにこのライブラリーが役に立ちます。 この記事ではArgumentParserの使い方を解説します。 Swiftでコマンドラインツールを作成した方にぴったりの記事です。
2020年2月、AppleがSwift向けライブラリーArgumentParserをリリースしました。
これはSwiftでコマンドラインツールを作る際にコマンド引数を簡単に扱えるライブラリーです。
CarthageやXcodeGenなどSwift製のコマンドラインツールはたくさんありますが、いざ自分で作ろうと思ったときにこのライブラリーが役に立ちます。
この記事ではArgumentParserの使い方を解説します。
Swiftでコマンドラインツールを作成したい方にぴったりの記事です。
実行環境
- Xcode 11.5
ソース
https://github.com/SatoTakeshiX/swift-argument-parser-example-sounds
今回作るコマンドラインツール
今回はsoundsという動物の鳴き声を出力するコマンドラインツールを作ります。
$ sounds
cat: meow meow
プロジェクト作成
まずはプロジェクト作成です。Xcodeを立ち上げて File > New > Project ... で新しいプロジェクトを作成します。
選ぶテンプレートは macOS > Command Line Toolです。
プロジェクト名はsoundsとしておきます。
プロジェクトを開いてmain.swift
ファイルをみてみましょう。
テンプレートとしてprintが書かれているはずです。
// main.swift
import Foundation
print("Hello, World!")
プロジェクトが作成できたら一度⌘+Bしましょう。
XcodeのProductsグループの下にコンパイル済みのコマンドラインツールのバイナリファイルが出力されています。
ターミナルを起動してドロップ&ドラッグして実行しましょう。
Hello, World!
がターミナルに出力されるはずです。
Copy Files
開発をスムーズに進められるようにコマンドラインツールのバイナリファイルを/usr/local/bin
にコピーしましょう。
XcodeのプロジェクトファイルのBuild Phasesタブに移動します。
Copy Filesを選びます。+ ボタンをクリックしてsoundsファイルを選択します。
その後Pathのテキストフィールドに/usr/local/bin
を指定しましょう。
⌘+Bでビルドすればsoundsが/usr/local/bin
にコピーされているはずです。
$ ls -l /usr/local/bin/sounds
-rwxr-xr-x 1 satoutakeshi wheel 14164 6 3 23:22 /usr/local/bin/sounds
改めてターミナルでsoundsを実行してみましょう。
Hello, World!
が出力されるはずです。
$ sounds
Hello, World!
うまくコピーされない場合はShift+⌘+Kでクリーンビルドするとうまくいきます。
ArgumentParserをインストール
ArgumentParserをSwiftPMを使ってプロジェクトにインストールします。
XcodeのメニューからFile > Swift Packages > Add Package Dependencyを選びます。
Choose Package ReposigoryにArgumentParserのGitHub URLを入れます。
https://github.com/apple/swift-argument-parser
Choose Package Optionsではそのまま最新のバージョンを指定しましょう。
Nextボタンをクリックします。
しばらくするとXcodeがArgumentParserをプロジェクトにダウンロードします。
これでArgumentParserをプロジェクトで利用する準備が終了しました。
始めてのコマンドラインツール
ArgumentParser用にコードを書き換えます。
main.swift
を開きます。
import Foundation
import ArgumentParser
struct Sounder: ParsableCommand {
}
ArgumentParser
をインポートし、Sounder
という型を定義します。これがコマンドライン引数を扱う型になります。
そしてSounder
にParsableCommand
を準拠します。
configuration
プロパティを定義します。
コマンドラインツールのメタ情報を設定できます。
struct Sounder: ParsableCommand {
static var configuration = CommandConfiguration(
commandName: "sounds",
abstract: "Output some animal sounds",
discussion: """
Demonstrationg how the Swift Argument Parser works.
""",
version: "1.0.0",
shouldDisplay: true,
//subcommands: <#T##[ParsableCommand.Type]#>, // non use
//defaultSubcommand: <#T##ParsableCommand.Type?#>, // non use
helpNames: [.long, .short]
)
CommandConfiguration
のイニシャライザ引数は次の通りです。
コマンドラインのヘルプの表示を指定することができます。
- commandName:
String
コマンドラインの名前 - abstract:
String
コマンドラインの短い説明 - discussion:
String
コマンドラインの長い説明 - version:
String
バージョン - shouldDisplay:
Bool
コマンド引数のヘルプを表示するか - subcommands:
[ParsableCommand.Type]
サブコマンド(この記事では解説しません) - defaultSubcommand: ParsableCommand.Type? サブコマンド指定がない場合のデフォルトコマンド(この記事では解説しません)
- helpNames:
NameSpecification
ヘルプコマンド指定。デフォルトで--help
,-h
。[.long, .short]
を指定しても同様
続いてrun
メソッドを実装します。
func run() throws {
print("Hello, World!")
}
最後にグローバルなところでSounder.main()
を実行すれば完成です。
Sounder.main()
ここまでの全体コードはこちらです。
import Foundation
import ArgumentParser
struct Sounder: ParsableCommand {
static var configuration = CommandConfiguration(
commandName: "sounds",
abstract: "Output some animal sounds",
discussion: """
Demonstrationg how the Swift Argument Parser works.
""",
version: "1.0.0",
shouldDisplay: true,
//subcommands: <#T##[ParsableCommand.Type]#>, // non use
//defaultSubcommand: <#T##ParsableCommand.Type?#>, // non use
helpNames: [.long, .short]
)
func run() throws {
print("Hello, World!")
}
}
Sounder.main()
ビルドしてsoundsコマンドのヘルプを出力しましょう。
$ sounds --help
OVERVIEW: Output some animal sounds
Demonstrationg how the Swift Argument Parser works.
USAGE: sounds
OPTIONS:
--version Show the version.
-h, --help Show help information.
configuration
で指定したものが出力されていますね。ここまでくればOKです。
ArgumentParserで設定できる引数
ArgumentParserではコマンドライン引数を定義する方法としてProperty Wrapperの@Argument
と@Option
、そして@Flag
の3つが使用できます。
それぞれの特徴は次の通りです。
- Arguments: ユーザーから与えられる値で、前から順番に読み込まれます。例として次のコマンドラインは3つのファイル名をコマンド引数として与えられています。
% example file1.swift file2.swift file3.swift
- Option: Key-Valueペアを指定できます。Keyの前に
-
か--
をつけ、Valueの前にスペースか=
をつけて指定します。
% example --count=2 # countというKeyにValueが2。--と=で指定。
% example -count 2 # -とスペースでも指定できる。
- Flag: Valueが指定できないOptionみたいなものです。Keyのみを指定してそれが特定の値を表します。指定する場合は
--
をKeyの前につけます。たいてい与えられたKeyがture
の意味を表すときに使います。
% example --verbose # debugモードを表すときに使える
@Argument
と@Option
、@Flag
それぞれ実装方法とコマンドラインの指定がどうなるかをみていきます。
Flag
まずはFlagからです。
Flagはデフォルト値を持ち、コマンド引数が指定があれば特定の値に変化する引数です。
指定できる型はBool
, Int
, enum
の3つです。
Bool型のFlag
まずはBool
型のFlagを作ってみましょう。
verbose
というFlagを作って、コマンドラインの処理をステップ・バイ・ステップでユーザーにわかるようにします。
main.swift
でverbose
プロパティを定義します。
@Flag(help: "show detail logs")
var verbose: Bool
@Flag(help: "show detail logs")
でヘルプ出力した際の説明を指定します。
続いてrun
メソッドでverbose
を使ったコードを実装します。
func run() throws {
if verbose {
print("start sounds")
}
print("Meow Meow")
if verbose {
print("end sounds")
}
}
処理の最初と最後にログを出すだけです。ビルドしてコマンドを実行します。
最初は引数なしで実行してみます。
$ sounds
Meow Meow
続いて--verbose
引数を追加して実行します。
$ sounds --verbose
start sounds
Meow Meow
end sounds
見事にログが出力されました。
ついでにヘルプも出してみましょう。
$ sounds --help
OVERVIEW: Output some animal sounds
Demonstrationg how the Swift Argument Parser works.
USAGE: sounds [--verbose]
OPTIONS:
--verbose show detail logs
--version Show the version.
-h, --help Show help information.
きちんとOptionとして--verbose
引数とその使い方が表示されているのがわかります。
Int型のFlag
FlagはInt型でも定義できます。
さっきのverbose
をIntに直してみましょう。
Int型のFlagはコマンド引数指定がなければ0,指定があれば1の値になります。
@Flag(help: "show detail logs")
var verbose: Int
func run() throws {
if verbose == 1 {
print("start sounds")
}
print("Meow Meow")
if verbose == 1 {
print("end sounds")
}
}
--verbose
をつけてコマンドを実行するとさきほどと同じ出力になるはずです。
$ sounds --verbose
start sounds
Meow Meow
end sounds
Intが0か1かしか値を取らないのならBool型のほうを使ったほうがいいかもしれませんね。
enum型のFlag
最後にFlagはenum型も取ることができます。enum型にEnumerableFlag
プロトコルの準拠が必要です。
動物の鳴き声を指定するためにAnimalKind
というenumを作ります。
enum AnimalKind: EnumerableFlag {
case cat
case dog
case mouse
static func name(for value: Sounder.AnimalKind) -> NameSpecification {
switch value {
case .cat:
return [.customShort("c"), .long]
case .dog:
return [.customShort("d"), .long]
case .mouse:
return [.customShort("m"), .long]
}
}
}
EnumerableFlag
はstatic func name(for:)
メソッドを実装することができ、コマンド引数の指定方法を設定することができます。
例えばcat
の場合[.customShort("c"), .long]
を返しているので引数は-c
と--c
を指定できます。.customShort
は任意の文字列一文字をコマンド引数に指定する方法です。.long
はcase名がそのままコマンド引数名に指定する方法です。
続いてAnimalKind
プロパティとrun
メソッドを実装します。
@Flag(help: "specify the kind of animal")
var animalKind: AnimalKind
func run() throws {
let sounds: String
switch animalKind {
case .cat:
sounds = "Meow Meow"
case .dog:
sounds = "bow-wow bow-wow"
case .mouse:
sounds = "squeak squeak"
}
print(sounds)
}
animalKind
によって鳴き声を出し分けています。
ビルドしてまずはヘルプを出力させます。
$ sounds -h
OVERVIEW: Output some animal sounds
Demonstrationg how the Swift Argument Parser works.
USAGE: sounds [--verbose] --cat --dog --mouse
OPTIONS:
--verbose show detail logs
-c, --cat/-d, --dog/-m, --mouse
specify the kind of animal
--version Show the version.
-h, --help Show help information.
-c, --cat/-d, --dog/-m, --mouse
のコマンド説明が追加されています。
コマンドを実行してみます。
$ sounds --cat
Meow Meow
$ sounds -c
Meow Meow
$ sounds --dog
bow-wow bow-wow
$ sounds -d
bow-wow bow-wow
$ sounds --mouse
squeak squeak
$ sounds -m
squeak squeak
指定した動物の鳴き声が出力されます。
ちなみに、引数を間違うと自動的にエラーが出力されます。
$ sounds --dd # 存在しないFlagを指定した
Error: Missing one of: '--cat', '--dog', '--mouse'
Usage: sounds [--verbose] --cat --dog --mouse
また、現状animalKind
プロパティは非オプショナル型なので、コマンド引数が必須です。
引数なしでコマンドを実行してもエラーが表示されます。
$ sounds
Error: Missing one of: '--cat', '--dog', '--mouse'
OVERVIEW: Output some animal sounds
Demonstrationg how the Swift Argument Parser works.
USAGE: sounds [--verbose] --cat --dog --mouse
OPTIONS:
--verbose show detail logs
-c, --cat/-d, --dog/-m, --mouse
specify the kind of animal
--version Show the version.
-h, --help Show help information.**
デフォルト値の設定
コマンド引数が必須だと入力が面倒ですね。
デフォルト値を設定しましょう。
やり方は2つあります。プロパティをオプショナル型にするかPropertyWrapperのイニシャライザーを指定するかです。
まずはプロパティをオプショナル型にする方法です。
animalKind
プロパティをオプショナル型にしてデフォルト値をつけるように変更します。
@Flag(help: "specify the kind of animal")
var animalKind: AnimalKind? // オプショナル型へ変更
func run() throws {
let sounds: String
if let animalKind = animalKind {
switch animalKind {
case .cat:
sounds = "Meow Meow"
case .dog:
sounds = "bow-wow bow-wow"
case .mouse:
sounds = "squeak squeak"
}
} else {
sounds = "Meow Meow"
}
print(sounds)
}
ビルドをしてコマンドを実行すればデフォルト値としてネコが泣くようになります。
$ sounds
Meow Meow
続いて、PropertyWrapperのイニシャライザーで指定する方法です。
@Flag
, @Option
, @Argument
にはイニシャライザーの引数にデフォルト値を渡すことができます。
@Flag(default: .cat, help: "specify the kind of animal")
var animalKind: AnimalKind
オプショナルバインディングのコードを書かなくて済むようになるのでコードがスッキリします。
func run() throws {
let sounds: String
switch animalKind {
case .cat:
sounds = "Meow Meow"
case .dog:
sounds = "bow-wow bow-wow"
case .mouse:
sounds = "squeak squeak"
}
print(sounds)
}
Option
OptionはKey-Valueの値を指定できるコマンド引数を定義します。
使える型はExpressibleByArgument
プロトコルを準拠したものでVersion 1.0の現在String
, Int
, UInt
,Double
, Bool
などが使えます。
鳴き声の回数を指定するcounter
プロパティを定義します。
@Option(default: 2,
help: "the number of sounds")
var counter: Int
run
メソッドもcounter
プロパティによって鳴き声の回数を変えるように変更します。
func run() throws {
let sounds: String
switch animalKind {
case .cat:
sounds = "Meow"
case .dog:
sounds = "bow-wow"
case .mouse:
sounds = "squeak"
}
var outputs = ""
for _ in 0 ..< counter {
outputs += sounds + " "
}
print(outputs)
}
ビルドして--counter
引数をつけてsounds
を実行します。
$ sounds --counter=3
Meow Meow Meow
$ sounds --counter=5
Meow Meow Meow Meow Meow
$ sounds
Meow Meow
期待通りcounterの数だけ鳴き声が増えるようになりました。
また引数がない場合はデフォルトで2回出力されています。
Errorハンドリング
さて、counter
引数を定義しましたが、とくに数値の上限は現状設けていないため、どんな巨大な数字も受け入れてしまっています。
$ sounds --counter=100
Meow Meow Meow Meow Meow Meow Meow Meow Meow Meow Meow Meow Meow Meow Meow ...
このままでは動物たちも鳴くのに疲れてしまうでしょう。
またマイナス値が入力されてもエラーになります。
$ sounds --counter=-2
Fatal error: Can't form Range with upperBound < lowerBound: file /Library/Caches/com.apple.xbs/Binaries/SwiftPrebuiltSDKModules_macOS/install/TempContent/Objects/EmbeddedProjects/CrossTrain_macOS_SDK/macOS_SDK/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib/swift/Swift.swiftmodule/x86_64.swiftinterface, line 12682
Illegal instruction: 4
このエラーは直接的には次のコードが実行できないことを表すエラーです。
for _ in 0 ..< counter {
outputs += sounds + " "
}
しかしコマンドを使うユーザーにとっては有益なエラー内容ではありません。
どのような値を入力し直せばいいのかのフィードバックが必要です。
適切なエラーを出力されるように追加実装をしてみましょう。
RuntimeError
を定義します。
struct RuntimeError: Error, CustomStringConvertible {
var description: String
init(_ description: String) {
self.description = description
}
}
そして、run
メソッドでcounter
の値がマイナス値と10より上の値だったらエラーをthrowするように変更します。
func run() throws {
if counter < 0 {
throw(RuntimeError("Counter must be positive."))
}
if counter > 10 {
throw(RuntimeError("Too many counter. Max 10."))
}
...
}
ビルドすれば、--counter
がマイナス値と10より上の値でエラーを返すようになります。
$ sounds --counter=-5
Error: Counter must be positive.
$ sounds --counter=11
Error: Too many counter. Max 10.
これでユーザーにわかりやすいエラーを実装することができました。
Argument
続いて最後の引数の指定方法の@Argument
について解説します。
明示的なコマンド引数を表し、前から順番に読み込まれるものです。
鳴いている動物の名前を指定できるコマンド引数name
を追加でき定義します。
@Argument(help: "the name of sounds animal")
var name: String
run
メソッドでname
プロパティを出力するよう変更します。
func run() throws {
let sounds: String
switch animalKind {
case .cat:
sounds = "Meow"
case .dog:
sounds = "bow-wow"
case .mouse:
sounds = "squeak"
}
var outputs = ""
for _ in 0 ..< counter {
outputs += sounds + " "
}
print("\(name): \(outputs)")
}
ビルドしてsounds
コマンドを実行します。
$ sounds tama
tama: Meow Meow
無事にタマが鳴くようになりました。
ところで、引数を2つにするとどうなるでしょう?
$ sounds tama Éclair
Error: Unexpected argument 'Éclair'
Usage: sounds [--verbose] [--cat] [--dog] [--mouse] [--counter <counter>] <name>
See 'sounds --help' for more information.
エラーが出力されました。name
プロパティはString
型なので複数の値を受け付けていません。
複数の値を受け入れるようにプロパティを配列に変更します。
@Argument(help: "the names of sounds animal")
var names: [String]
run
メソッドもnames
プロパティごとに出力できるように変更します。
for name in names {
print("\(name): \(outputs)")
}
ビルドしてsounds
コマンドを複数引数で実行します。
$ sounds tama Éclair
tama: Meow Meow
Éclair: Meow Meow
期待通り、複数引数が名前として出力されました。
ArgumentArrayParsingStrategy
@Argument
が配列の場合、コマンド引数が@Option
と組み合わされた場合、どのようにパースするかを指定できます。その動作を表すのがArgumentArrayParsingStrategy
です。
ArgumentArrayParsingStrategy
は2つ種類があります。
- remaining: コマンド引数としてダッシュのプレフィックスを無視し、それ以外を引数とします。
@Argument
のデフォルトパース値に設定されています。
例えば次のように@Option
と@Argument
のプロパティがあるとします。
@Option(default: 2,
help: "the number of sounds")
var counter: Int
@Argument(
parsing: .remaining,
help: "the names of sounds animal")
var names: [String]
sounds
コマンドを実行するさい、Optionのcounter
とArgumentのnames
はどんな位置で入力しても同じように認識されます。
$ sounds --counter=3 tama eclair # counterが最初
> [counter: 3, names: [tama, eclair]
$ sounds tama --counter=3 eclair # counterが真ん中
> [counter: 3, names: [tama, eclair]
$ sounds tama eclair --counter=3 # counterが最後
> [counter: 3, names: [tama, eclair]
ただし、プロパティとして定義していないダッシュプレフィックスの引数が入力された場合はエラーが表示されます。
$ sounds tama eclair --counter=3 --other
Error: Unknown option '--other'
Usage: sounds [--verbose] [--cat] [--dog] [--mouse] [--counter <counter>] [<names> ...]
See 'sounds --help' for more information.
- unconditionalRemaining: ダッシュプレフィックスの値も引数として扱います。
remaining
とは異なり、unconditionalRemaining
はダッシュプレフィックスの引数をArgument
の値として認識します。
さきほどの例のnames
プロパティをunconditionalRemaining
でパースするように変更します。
@Option(default: 2,
help: "the number of sounds")
var counter: Int
@Argument(
parsing: .unconditionalRemaining,
help: "the names of sounds animal")
var names: [String]
sounds
コマンドを実行すると--counter
の値もnames
の値として認識されます。
$ sounds tama eclair --counter=3 --other
> [counter: 3, names: [tama, eclair, --counter=3, --other]]
--counter=3
がnamesの値として認識されていたり、Optionとして定義していない--other
もArgumentとして認識されています。
コマンドを使うユーザーにとって期待する動作ではないので、特に理由がなければremaining
を使いましょう。
まとめ
ArgumentParserをつかってSwiftでコマンドラインツールを作る方法を解説しました。
今回の記事ではプロジェクト作成から、@Flag
, @Option
, @Argument
の使い方、エラーハンドリングやArgumentArrayParsingStrategy
によるパース方法まで触れました。
ArgumentParserはコマンド引数のハンドリングを簡単にするツールです。
ぜひSwiftでコマンドラインツールを作るさいは利用してみてください。
今回の成果物はこちらのGitHubに置いています。
こちらもチェックお願いします。
https://github.com/SatoTakeshiX/swift-argument-parser-example-sounds
さらに詳しく
公式GitHubに詳しいドキュメントがあります。
もっとArgumentParserについて調べたいときはこちらを参考にしてください。
今回省いたサブコマンドなどの解説が書かれています。
swift-argument-parser/Documentation at master · apple/swift-argument-parser
参考
- Swift.org - Announcing ArgumentParser
- apple/swift-argument-parser: Straightforward, type-safe argument parsing for Swift
画像
Free stock photos from www.rupixen.comによるPixabayからの画像
宣伝
SwiftUIでアプリを作り方を解説した「1人でアプリを作る人を支えるSwiftUI開発レシピ」がBOOTHで発売中です。
SwiftUIでアプリを作りたい方、ぜひチェックしてください!
https://personal-factory.booth.pm/items/1920812