[Swift] Swiftで簡単なCLIを作ってみる (その3)
はじめに
CX事業本部の中安です。
今回は [Swift] Swiftで簡単なCLIを作ってみる (その2) の続きです。
前回は、Process
とPipe
を使って、他のコマンドを実行してみるというところまでを書きました。
ソースコードがベタ書きだったので、今回はそれを整理して、複数のコマンドを繋げられるところまでやってみたいと思います。
前回まで
おさらいですが、Process
とPipe
というクラスを使用して、
ダミーなWebAPIからJSONを取得して、人の目に優しい形で出力するところまでを行いました
ソースコードはこんな感じです。
import Foundation let pipe = Pipe() let process = Process() process.launchPath = "/usr/bin/curl" process.arguments = ["http://dummy.restapiexample.com/api/v1/employees", "-s"] process.standardOutput = pipe process.launch() process.waitUntilExit() let data = pipe.fileHandleForReading.readDataToEndOfFile() do { let decoder = JSONDecoder() let decodedData = try decoder.decode([[String:String]].self, from: data) let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let encodedData = try encoder.encode(decodedData) let encodedString = String(data: encodedData, encoding: .utf8) ?? "error." print(encodedString) }
今回は、JSONエンコーダ/デコーダを使ってJSON文字列を整形している部分を jq
コマンドを使って整形をしてみたいと思います。
jq
コマンドが使えない場合は事前にインストールしておいてください。
https://stedolan.github.io/jq/
Processを繋げられるようにする
Process
は Pipe
を使って処理を繋げることができます。
例えば普通にコマンドを打つとすれば下記のようなイメージです
$ curl "http://dummy.restapiexample.com/api/v1/employees -s | jq`
Pipe
はここでいうところの |
にあたります。
ただ、これをベタに書いていくと冗長なソースコードになってしまいます。
そこで、extension
を使って、Process
同士をPipe
でつなぐのを楽にしてみたいと思います。
Processのイニシャライザを追加する
let process = Process() process.launchPath = "/usr/bin/curl" process.arguments = ["http://dummy.restapiexample.com/api/v1/employees", "-s"]
Process
を作成するとき、上のようなlaunchPath
とarguments
への代入はどうせ行う処理です。
コンビニエンスイニシャライザを使って、初期化時に与えられるようにしておきます。
extension Process { convenience init(_ launchPath: String, _ arguments: [String]? = []) { self.init() self.launchPath = launchPath self.arguments = arguments } }
これにより、Process
は以下のように生成できます。
Process("/usr/bin/curl", ["http://dummy.restapiexample.com/api/v1/employees", "-s"])
この例ではメソッドのラベルを取ってしまいましたが、もちろん付けてもいいと思います。
出力データを取得する
process.standardOutput = pipe process.launch() process.waitUntilExit() let data = pipe.fileHandleForReading.readDataToEndOfFile()
上のようなPipe
を用いてバッファを取り出す機能もProcess
に加えておきます。
extension Process { func output() -> Data { let pipe = Pipe() standardOutput = pipe launch() waitUntilExit() return pipe.fileHandleForReading.readDataToEndOfFile() } }
これに加えて、出力を文字列化する処理も足しておきます。 (出力が文字列じゃないときや、データだけ欲しいときのことも考慮してメソッドは分けておきます)
extension Process { func outputString() -> String { let data = output() return String(data: data, encoding: .utf8) ?? "" } }
こままでで、今までの処理は下記のように2行に収まるようになりました
let str = Process("/usr/bin/curl", ["http://dummy.restapiexample.com/api/v1/employees", "-s"]).outputString() print(str)
パイプで繋げられるようにする
冒頭でも書いたようにPipe
はコマンドでいうところの |
にあたります。
なので、プログラムも|
でProcess
同士を繋げられれば直感的かなと考えました。
ということで、|
を演算子として定義することにします
extension Process { static func | (lhs: Process, rhs: Process) -> Process { let pipe = Pipe() lhs.standardOutput = pipe rhs.standardInput = pipe lhs.launch() return rhs } }
ここで行っているのは、左辺のProcess
の出力を右辺のProcess
の入力に繋いでることです。
Process
はlaunch
しないと次のProcess
に渡せないとのことなので、
Pipe
に繋いだ時点で左辺のProcess
はlaunch
することにしました。
さて、jq
コマンドをソースコード上に展開するには、curl
同様にパスを取得しておかないといけません。
$ which jq /usr/local/bin/jq
で、先程のソースコードに追記するとこのようになります。
let str = ( Process("/usr/bin/curl", ["http://dummy.restapiexample.com/api/v1/employees", "-s"]) | Process("/usr/local/bin/jq", [""]) ).outputString() print(str)
ターミナルjq
コマンドは引数がなくても動作するのですが、実は引数として空文字を与えないといけないっぽいです。
ということで、これを実行すると
[ { "id": "1", "employee_name": "H-001", "employee_salary": "10000", "employee_age": "100", "profile_image": "" }, { "id": "94054", "employee_name": "ww", "employee_salary": "222", "employee_age": "1987", "profile_image": "" }, : : (略)
最後に
Process
をextension
して、前回までの実装をシンプルにしました。
後々でまた使えそうです。
ここまでのソースコードをまとめると
import Foundation extension Process { convenience init(_ launchPath: String, _ arguments: [String]? = []) { self.init() self.launchPath = launchPath self.arguments = arguments } static func | (lhs: Process, rhs: Process) -> Process { let pipe = Pipe() lhs.standardOutput = pipe rhs.standardInput = pipe lhs.launch() return rhs } func output() -> Data { let pipe = Pipe() standardOutput = pipe launch() waitUntilExit() return pipe.fileHandleForReading.readDataToEndOfFile() } func outputString() -> String { let data = output() return String(data: data, encoding: .utf8) ?? "" } } let str = ( Process("/usr/bin/curl", ["http://dummy.restapiexample.com/api/v1/employees", "-s"]) | Process("/usr/local/bin/jq", [""]) ).outputString() print(str)
こんな感じです。
もうしばらく続きます。ではー