[Swift] Swiftで簡単なCLIを作ってみる (その2)

2019.12.23

はじめに

CX事業本部の中安です。

今回は [Swift] Swiftで簡単なCLIを作ってみる (その1) の続きです。

swiftswiftc コマンドを使って、指定したファイルの絶対パスを出力する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つのプロセスがデータを書き込み、他のプロセスがそのデータを読み取ります。 パイプを通過するデータはバッファリングされます

(リファレンスより)

と書かれているように、ProcessstandardOutputに代入した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の中で他のコマンドを呼び出すことができました。そして、その結果を取得保持することもできました。

次はもう少し今回書いたソースコードを整理できないかを模索してみたいと思います

では