[Swift] Swiftで簡単なCLIを作ってみる (その2)
はじめに
CX事業本部の中安です。
今回は [Swift] Swiftで簡単なCLIを作ってみる (その1) の続きです。
swift
と swiftc
コマンドを使って、指定したファイルの絶対パスを出力するCLIツールを作りました。
今回は、もう少し発展型なものを作っていきます。
他のコマンドを使う
今回は
- JSONを返すWebAPIを
curl
コマンドを使って叩いて、取得したデータを人の目に見やすい形に出力する
そんなツールを作ってみようかなと思います。
「そんなのjq
使いなよ」と言いたくなるところですが、作ってみることが大事です。(たぶん)
ちなみに今回は試しなので、ダミーなJSONを返してくれるサービスを使います
http://dummy.restapiexample.com
http://dummy.restapiexample.com/api/v1/employees
をキックすると架空の従業員リストが返ってきます。
下準備
だいたい前回と同じなので簡単に。今回はcustomjq
という名前にしました
$ mkdir customjq $ cd customjq $ touch main.swift
curl
コマンドを使うためにcurl
の場所を先に確認しておきます。
(もし、なかった場合のインストール方法は割愛します)
$ which curl /usr/bin/curl
Process
今回は Process
というクラスが登場します。
Process
は「現在のプロセスのサブプロセスを表すオブジェクト」と説明されていて、旧来のNSTaskがリネームされたクラスです
詳しい説明よりソースコードを見たほうが早いかなと思います。
import Foundation let process = Process() process.launchPath = "/usr/bin/curl" process.arguments = ["http://dummy.restapiexample.com/api/v1/employees", "-s"] process.launch() process.waitUntilExit()
curl
のパスを指定して、arguments
(引数)に API の URL を渡し、launch()
を呼び出してコマンドを起動しています。
そして、その処理が終わるまで待つために waitUntilExit()
を呼び出しています。
ちなみに launch()
してからでないと waitUntilExit()
はしてはいけない等、呼び出しの順番も重要そうです。
これで
$ swift main.swift
で実行してみると、JSON文字列が出力されると思います。
[{"id":"1","employee_name":"salam","employee_salary":"30000","employee_age":"26","profile_image":""},{"id":"89459","employee_name":"Jon1111","employee_salary":"13999","employee_age":"20","profile_image":""},{"id":"91224", ...(省略)...
この時点でできたような感じはしますが「取得したデータを人の目に見やすい形に出力する」というものはまだ実現できていません
Pipe
Processを使って curl
で得られた文字列データは、そのままであると得られたままが出力されます。
これは先程も書いたとおり期待の動作と違います。
そこで、コマンドパイプラインを抽象クラス化したPipe
クラスを使用します。
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()
先程のソースコードにハイライトした部分を追加しました。
で、再度
$ swift main.swift
を実行してみると、今度は何も出力されなくなると思います。
これは
A pipe is a one-way communications channel between related processes; one process writes data, while the other process reads that data. The data that passes through the pipe is buffered
パイプは、関連するプロセス間の一方向の通信チャネルです。 1つのプロセスがデータを書き込み、他のプロセスがそのデータを読み取ります。 パイプを通過するデータはバッファリングされます
(リファレンスより)
と書かれているように、Process
のstandardOutput
に代入したPipe
にデータをバッファリングしているためです。
なので、そのデータを文字列化してやれば、そのプロセスの結果を文字列として受け取れます。
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() let str = String(data: data, encoding: .utf8) ?? "" print(str)
これは先程のパイプ不使用時と同じ結果が出力されますが、意味合いが違います。
curl
を実行した結果を変数にまずは保持しておくことができるからです。
print
をしなければ、curl
は実行されるものの、出力されることはありません。
JSONを整形する
今回のサンプルAPIは、[[String:String]]
型なJSONの戻り値なので、
JSONのデコーダとエンコーダを利用して、すごく単純な方法でJSONを整形してみます。
(実際にはもう少し工夫したやり方で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が読みやすい状況で出力されたではないでしょうか。
[ { "id" : "1", "profile_image" : "", "employee_name" : "salam", "employee_salary" : "30000", "employee_age" : "26" }, { "employee_age" : "20", "profile_image" : "", "employee_name" : "Jon1111", "employee_salary" : "13999", "id" : "89459" }, : : (省略) : : ]
最後に
Swiftの中で他のコマンドを呼び出すことができました。そして、その結果を取得保持することもできました。
次はもう少し今回書いたソースコードを整理できないかを模索してみたいと思います
では