この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
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)
こんな感じです。
もうしばらく続きます。ではー