Swift ArgumentParser におけるプロパティの定義順の重要性

今回はタイトル通り ArgumentParser を使っている時のプロパティの定義順の重要性について書きます。v0.0.2 で実際に経験していた範囲ですが、まだ ArgumentParser のソースコードを読んだ内容ではないこと、今後のバージョンで変わりうることをご了承ください。

前回のエントリのつづき

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()

上記サンプルコードは前回のエントリで紹介した、必須引数とバージョンフラグの両立をするためのコード例です。詳しい内容は前回のエントリを参照ください。

実はこの上記のコードでも前回解決した問題を再び発生させることが出来ます。

問題再燃サンプル

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") }
        }
    }

    @Argument()
    var text: String

    @OptionGroup()
    var version: Version

    func run() throws {
        print(text)
    }
}

MyEcho.main()

何が変わったかわかりますか?

text プロパティと version プロパティの定義順を変更して text プロパティを先に持ってきました。これだけで

% myEcho --version
Error: Missing expected argument '<text>'
Usage: myEcho <text> [--version]

というエラーになります。

結論(経験上)

プロパティの定義順に処理が進むので先に解決して欲しいものは先に書くべし。

もしくは @Argument() は必ず最後に書くべしとも言えます。

@OptionGroup() の Property Wrapper の init 時に ParsableArgumentsfunc validate() throws が実行されます。上からプロパティを埋めていくように ArgumentParser は動作しているようなので、引数を処理している最中に struct Version のイニシャライズの段階でバリデーション処理でこける(バージョン番号を出力して正常扱いで終了)ために、@Argument() var text: String に到達しない。これが、期待する動作を手に入れていたころのコードでした。

プロパティの定義順を text を先にすると、ArgumentParser が引数の処理をしているときに text プロパティに代入するべき値が存在しないため version プロパティの処理をする前に必須項目が欠如しているとしてエラーになります。

Help や Usage にも影響あり

% myEcho --version
Error: Missing expected argument '<text>'
Usage: myEcho <text> [--version]

このエラー時の Usage の出力をみると引数 text の後ろに version フラグが書かれています。

ためしに前回のエントリの完成形コードを引数もフラグもなしで実行すると

Error: Missing expected argument '<text>'
Usage: myEcho [--version] <text>

このように version のあとに text が来ています。

プロパティの定義順によって出力される説明の順番にも影響を与えています。

さらに僕がハマった罠

僕が実際に書いていたコードで、外から引数として与えられはしない固定値としてプロパティに保持したいものがありました。今手元で再現できないのですが、たしか Decodable に対応してないというコンパイルエラーが出ていました。ParsableCommandParsableArguments に準拠しています。さらに ParsableArgumentsDecodable に準拠しています。よって、 ParsableCommandDecodable である必要があるのです。ただし、別に Decodable である必要性がなかったプロパティだったので CodingKey を定義して除外できるようにしました。

するとさっきまで動いていたバージョン番号の出力が引数がないと怒られるようになりました。

ずっと謎だったのですが、最終的に分かったことは CodingKey に列挙している case がプロパティの定義順通りになっていなかったからでした。

このせいで結構な時間が溶けていました。

まとめ

プロパティの定義順は

  • パース処理の順番
  • Usage や Help の出力の順番
  • CodingKey を定義する場合はプロパティの定義順に揃える

ということでした。

常に順番を意識してコードを書きましょう!

Swift ArgumentParser でバージョン番号を出力する方法

コマンドラインツールを作っているとそのツールのバージョン番号を出力したい事があります。ここでは引数で渡した文字列をそのまま標準出力に出力する 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 型のエラーをスローする
  • text プロパティはオプショナルをやめる
  • func run() throws ではただ text プロパティを出力するのみ

これが現状のスッキリ終わらせるための Workaround です。

func validate() throws が run よりも先に実行されているので func run() throws 内では version のことは気にせずに済むようになりました。

コード量は増えましたが僕が元々感じていたモヤモヤはすべて解消されています。また、バージョン番号を出力する処理が Version 型にまとまっています。今回は MyEcho.Version とネストさせて定義していますが、ネストさせる必要性は特にないのでので単体のファイルとして定義していれば使い回しもしやすいです。

まとめ

この記事を読んで気付いた方がいるかも知れませんが、ArgumentParser ではバージョン番号出力はデフォルトで提供されていません。Issue 内でも提供して欲しいと他の方がリクエストしていました。将来的には提供されるようになるかも知れませんね。

Apple のオープンソースライブラリ ArgumentParser

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-dbswift-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 が使えるようになる
  • 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) とかすれば、試験的に追加した機能とかをヘルプに出力せずにすませられる
  • バリデーション
    • ParsableCommandParsableArgumentsvalidate() を実装することでバリデーションできる
    • throw すればそのメッセージが標準エラーに出力されてエラーコードで終了する
    • runメソッド内でも実行時エラーを throw できる
      • v0.0.2 では ExitCode という Error が用意されたので自前でエラーメッセージを出力して throw ExitCode.failure することで綺麗にエラーからの終了が実装できるようになった
  • マニュアルでパース
    • きっちりとコマンドラインツールとして作るのではなくスクリプトっぽく書きたいとき
      • ParsableArguments に準拠した struct にこれまど同様にオプションを書いておく
      • static method の parseOrExit() を呼べば、成功してればインスタンスが返ってくるし失敗したら終了してくれる
      • parse メソッドもあってこちらは throw されるので自分でエラーハンドリングができる。
    • コマンドやサブコマンドもマニュアルでパースできるけどややめんどくさい parseAsRoot() とかつかえばいい

できないこと(提供されてないこと)

  • コマンドラインツール的に便利なパスの処理
  • 外部のコマンドの呼び出しのサポート

これらはライブラリの名前から連想する対応範囲を超えているように思えるので別のライブラリなどを使った方が良いかもしれないです。僕は個人的にパス周りは Path.swift, パスまわりや外部のコマンド呼び出しは前述の TSC を使ったりしています。

それ以外は特別困っていません。

まとめ

コマンドラインツールはいろんな宗派があると思うので好みが分かれると思います。細かいことを言えば切りがないかも知れません。とはいえ

  • Apple 純正
  • サブコマンドも含めてコマンドラインツール作成における大枠の仕組み、パース処理が提供されている
  • 独特な実装ではなく Swift らしい素直な実装

Swift でコマンドラインツールを書く場合、これらは利点だと思うのでぜひ皆さんも使って見たらいいと思います。

iPhone 11 Pro 予約記録

予約日から数日経ちましたが 2019/09/13(金) の様子を記録として残しておきます。

今回は予約開始が 21:00 からというのが去年と大きく違っています。各キャリアともネット予約に関しては 21:00 から受付ですが、店舗受付に関しては店舗によっては翌日の朝からの対応という情報が流れていました。ヨドバシカメラ梅田は店舗での受け付けも 21:00 から対応してくれるということだったので現地向かいました。


当日持っていったもの

  • 寒さ対策として
  • 軽食と飲み物
  • 座る用に
    • ウレタンの座布団
    • スリーピングマットと座椅子アタッチメント

結果として終始Tシャツのままだったし、同僚とずっとしゃべっていたので座ることもなかった。とはいえ備えあれば憂いなし。

来年に向けて

  • 予約時には各キャリア前に並ぶで正解では?
  • 最近は並ぶ人も少ないので1〜2時間前でも十分かも
    • 店内カウンターで待たせてもらえる可能性大

Sketch で Symbol 新規作成時の名前入力で入力補完してくれる Plugin 「SymbolNameAutocomplete」を作ってみた

昨晩、Sketch Plugin 「SymbolNameAutocomplete」を公開しました。

できること

Symbol の新規作成時に Symbol 名を入力する時に入力補完してくれます。

より具体的には以下のような感じに振る舞います。

  • 既に存在している Local Symbol を対象とする
  • / による階層分け毎にマッチングさせていく
  • 補完決定時に下位階層がある場合には / まで補完
  • / の前後に半角スペースなどを入れている場合には <半角スペース>/<半角スペース> まで補完

/ の前後のスペースを考慮して補完するあたりはこだわりました。候補の選択にカーソルキーが使えるのは当然のこと、Ctrl+N, Ctrl+P での移動にもちゃんと対応させています。

背景

  1. 2018年2月28日 『try! macOS meet-up』で「Symbol 新規作成時の名前入力で Safari のアドレス補完っぽく入力補完したい」と @usagimaru 氏, @dy4_268 氏 に相談するも具体的な方法まで至らず
    • その筋の人達でも即答即解決とはいかず
  2. 2018年3月頃にあるデザイナーが「命名するときに入力補完されたらええのにな!」という。
    • 需要を認識
  3. 2018年7月10日 zeplin/emoji-autocomplete-sketch-plugin: Type “:” followed by the first few letters to autocomplete emojis. 🍒 zeplin が emoji の Autocomplete プラグインをおもむろにリリース
    • 大いに参考になるとコードを読んでみる
  4. 2018年9月29日 ずむ on Twitter: "WWDC 2010 Session 145 "Key Event Handling in Cocoa Applications" を見たら入力フィールドの下に出てくる候補表示の作り方があった! 今のところ Archive のページから入手可能。 https://t.co/rhyw9DnPAE" つぶやき発見
    • そのものズバリの情報。
    • サンプルコードもある!CustomMenus
  5. 連休で暇になりトライ

まとめ

これまでも公開していないですがいくつか Sketch Plugin は書いていましたがいずれも CocoaScript で書いたものばかりでしたが、今回はじめて Objective-C で書きました。zeplin の emoji-autocomplete-sketch-plugin のおかげで割とすんなり作れました。今はまだソースコードは公開していませんが時間が取れたら公開したいと思います。

このプラグインを使うことで Symbol の命名規則のブレをある程度抑えることができると思いますので使ってみてください。

ErgoDash を作ってみた

結構前に作ってたんだけどキーキャップの到着が遅かったりしてエントリ書けてなかったので書いてみます。といっても Twitter 貼り付けるだけになってます。

組み立ての感想

今回はこれまでで1番苦戦しました。

  • ファームウェアを焼こうと思ったらそもそもリセットスイッチが反応しない
    • ジャンパのところでショートさせてた
  • 表面実装のLED取り付けに失敗しいくつか点灯しない
    • ハンダ不良なのか熱しすぎて壊したのか不明
    • あやしそうな箇所を余分に買ってたLEDにひとつ付け替えるだけで解消
  • キースイッチが1個だけ反応しない

これらのミスは今回4個目のキーボード作成だったので中途半端な慣れによるところが大半でした。LEDは難しかったですがフラックスを初導入して塗ってみるとかなり作業が図ったのは収穫でした。

今回はじめてケースのデータを遊舎工房さんに依頼して親指キーが多いケースを切ってもらいました。Illustrator を使うのも初めてでしたが元データから切り貼りするだけだったのでなんとかなりました。今は作者のかたがそのまま遊舎工房さんに投げられるようなデータをシェアしてくれているので簡単にケースはゲットできると思います。