読者です 読者をやめる 読者になる 読者になる

golangでコマンドラインツールを作る #4 ファイルをパースする

f:id:masato47744:20150816150451p:plain

その3の続き

project.pbxprojのファイル形式

さて、どうやってパースしようかと考えて中身を見てみると key = valueの形でなんか何かの形式っぽいんだけど、 拡張子が変なやつだからXcode特有かと思ったらそうじゃなかった。 how to parse project.pbxprojとかググってたら出てきた。

Xcode のファイルツリーを名前順に並べたいという話 (フェンリル | デベロッパーズブログ)

この中で以下のように書いてあった。

このファイルは古い plist 形式になっていて、読み書きが大変やりにくいわけです。

そこでさらに調べ見ると、plutil というコマンドがある事を知りました。このコマンドを使用すると古い plist を新しい XML 形式の plist に変換できます。これで project.pbxproj の読み書きが可能になります。

見た目は特殊っぽいけど、plist形式の一種だったんだ。それならMacにそういうコマンドがあっても不思議ではない。 で、plutil*1コマンドを見てみると、どうやらjsonにもformat変換ができるぽいぞ。

 -convert fmt
rewrite property list files in format
fmt is one of: xml1 binary1 json

よし、ということで、plutilコマンドでjsonに変換してそれをgoで扱えばいいかなという方針にする。 pluitilのコマンドはこんな感じ。

$ plutil -convert json -o tmp.json -r project.pbxproj

これで、tmp.jsonに人間でも読めるようにインデントされたjsonが出力される。

外部コマンドを実行する

Golangで外部コマンド実行するにはどうすればいいか調べる。ちゃんと用意されてるね。

exec パッケージ - golang.jp

あとは、コマンドを実行してみる。

   json := "tmp.json"
    cmd := exec.Command("plutil", "-convert", "json", "-o", json, "-r", c.Args()[0])
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.Run()

go installして実行すると、tmp.jsonできてた

{
  "classes" : {

  },
  "objectVersion" : "46",
  "archiveVersion" : "1",
  "objects" : {
    "53DA2D1B1B7CEAF900A18036" : {
      "buildConfigurationList" : "53DA2D3B1B7CEAFA00A18036",
省略

おっけー、あとはこのjsonファイルをgoでパースするだけだ。

jsonをパースする

golangでは標準のjsonパースできるやつが用意されてるようだ。

json パッケージ - golang.jp

やり方としては、パースするjsonの型が分かってればそれをせっせと定義して、Unmarshalすればいいらしい。 でも、pbxprojをjsonにしたやつよく見ると型がめちゃくちゃだった。

外観はこんな感じでkey名が固定されてそうなんだけど、

{
  "classes" : {

  },
  "objectVersion" : "46",
  "archiveVersion" : "1",
  "objects" : {}
  "rootObject" : "53DA2D141B7CEAF900A18036"
}

objectsの中身がてんでバラバラ。しかもkey名が謎の識別子みたいなやつ。

    "53DA2D271B7CEAF900A18036" : {
      "isa" : "PBXBuildFile",
      "fileRef" : "53DA2D251B7CEAF900A18036"
    },
    "53DA2D351B7CEAFA00A18036" : {
      "isa" : "PBXGroup",
      "name" : "Supporting Files",
      "children" : [
        "53DA2D361B7CEAFA00A18036"
      ],
      "sourceTree" : "<group>"
    },
省略

isaってやつがとれればどれが何を指してるかは分かりそう。これはどうやってパースすればいいんだろうか・・ 型が分からないからそういうやり方でやらないといけないかも。

型が分からない場合のjsonのパース

標準パッケージのやり方はInterface{} ってやつで受けて値の型ごとに処理を分割するってやつらしい。 ライブラリで簡単にできる風な感じな bitly/go-simplejson · GitHub でやってみる。 Goで簡単にJSON形式を扱うパッケージ: go-simplejson - Qiitaを参考にあうる。

パッケージをgetする。

$ go get github.com/bitly/go-simplejson

使い方はこんな感じ。stringを取りたい場合は、こう。

v := js.Get("objectVersion").MustString()
fmt.Println("objectVersion = " + v)

rangeってのでmapを回せる。

cs := js.Get("classes").MustMap()
for k, v := range cs {
    fmt.Println(k + " = ")
    fmt.Println(v)
}

rangeで回したあとの、中でまたmapがあった場合は、キャストがいる。 キャストの仕方は、xxxx.(キャストしたい型)てやるようだ。 map[string]interface{}ってなんじゃらほいって感じだ。

objects := js.Get("objects").MustMap()
for _, m := range objects {
    for k, v := range m.(map[string]interface{}) {
        if k == "isa" {
            fmt.Println(k + " = " + v.(string))
        }
    }
}

project.pbxprojをjsonに変換して、それをパースして、各objectのisaを出力するところまできた。

======== classes =========
======== objectVersion =========
objectVersion = 46
======== archiveVersion =========
archiveVersion = 1
======== rootObject =========
rootObject = 53DA2D141B7CEAF900A18036
======== objects =========
isa = PBXNativeTarget
isa = PBXBuildFile
isa = PBXNativeTarget
isa = PBXProject
isa = PBXGroup
isa = PBXGroup
isa = PBXGroup
isa = PBXBuildFile
isa = PBXResourcesBuildPhase
isa = PBXGroup
isa = XCConfigurationList
isa = PBXBuildFile
isa = PBXFrameworksBuildPhase
isa = PBXFrameworksBuildPhase
isa = PBXFileReference
isa = PBXBuildFile
isa = PBXVariantGroup
isa = PBXResourcesBuildPhase
isa = XCConfigurationList
isa = PBXFileReference
isa = XCBuildConfiguration
isa = PBXFileReference
isa = PBXContainerItemProxy
isa = PBXSourcesBuildPhase
isa = XCBuildConfiguration
isa = PBXGroup
isa = PBXFileReference
isa = PBXFileReference
isa = XCBuildConfiguration
isa = PBXBuildFile
isa = PBXSourcesBuildPhase
isa = PBXFileReference
isa = XCBuildConfiguration
isa = PBXGroup
isa = PBXTargetDependency
isa = PBXVariantGroup
isa = PBXFileReference
isa = PBXFileReference
isa = PBXBuildFile
isa = PBXFileReference
isa = XCConfigurationList
isa = XCBuildConfiguration
isa = PBXFileReference
isa = XCBuildConfiguration

あとは、これらのisaをうまい感じにグルーピングしていい感じに表現してあげればよさそう。

ここまでの感想

  • OSコマンドの実行方法と、jsonのパースの仕方がなんとなく分かった。
  • あと、変数名のつけかたの流儀がなかなかつかめない。なんか長い変数名にするのはgoっぽくないってのは聞いたことあるんだけど、 どのくらい短くすればいいのかが感覚としてつかめてない。
  • golang開発してていいなって思ったのは、使ってない変数警告を出すってのはいいかも。
  • atomの予測変換がかなり優秀。IDEかと思っちゃう。
  • interfaceってのがよく出てくるけどよく分かってない。
  • .(string)でキャストできるぽいけどこれ間違っててもランタイムエラーにならないのか?
  • assertionって言葉がよく出てくるけどこれはgolangにそういう仕組みがあるのか?Optional binding的な
  • 使ってもらうときの依存関係はどこに書いておくんだろう?cocoapodsのPodfile的なのいらない?
  • 小文字でprivate functionを表現するということを理解した。
  • log.fatalとpanicの使いどころの違いと、io、ioutilの違いがよく分かってない。

その5

参考URL

*1:よく調べてみると同じか分からないけどperlスクリプトのやつもあるっぽい。 plutil.pl for Windows/Linux