はじめに
Go言語でiOS開発ツールを作成する
というモチベーションは、まずGo言語が自分にとってとても興味深い言語であるいうところから始まっています。もともとLinux上でC言語を使って組込製品などを作ってきた人間にとっては、Go言語はすごく分かりやすくて便利な言語だという感触がありました。しかも高速(らしい)です。
実は以前にSwiftでMacで動くiOS開発ツールは作った経験があるのですが、今回Go言語でチャレンジした感想は、Swiftに比べてもGo言語はとても優れているということです。(特にMacOSやLinuxで動くツールを作成する場合)
目標
"clode_window" = "閉じる";
"delete something" = "削除する";
"cancel" = "キャンセル";
"OK" = "OK";
"_Only_this_" = "Only this";
例えば上記のような Localizable.strings
から下記のようなswiftファイルを生成します。
import Foundation
enum LocalizableStrings: String {
case clodeWindow = "clode_window",
case deleteSomething = "delete something",
case cancel = "cancel",
case OK = "OK",
case OnlyThis = "_Only_this_",
}
環境構築
go言語の開発環境の構築に関してはいろいろなところで語られているので、詳細は述べません。
ただ、vscodeを使った開発環境はとても良いです。基本的な機能ではありますが、break pointが張れたりするのが嬉しいです。
macの場合は、HomeBrewを使ってコマンドラインの環境も構築しておきます。
$ brew install go
$ go version
GO言語は、GOPATH
で指定されたパスに、ライブラリーや実行ファイルをダウンロードします。
go version 1.8以降はしてされない場合は、ホームディレクトリーの go
ディレクトリーが使われます。
$ export GOPATH=$HOME"/go"
$ export PATH="$GOPATH/bin:$PATH"
VSCodeでGoのデバッグをするのにdelveをインストールしておきます。
$ go get -u github.com/derekparker/delve/cmd/dlv
Go言語で実装
コマンドライン引数を取得する
まずコマンドライン引数を取得する必要があります。
flag.Parse()
topDir := flag.Arg(0)
enumName := flag.Arg(1)
flag.Parse()で、コマンドライン引数を拾うことができます。Swiftで同じ機能を書くより簡単で、C言語より分かりやすいです。
fmt.Printf
や、fmt.Sprintf
もC言語に慣れていれば、容易に理解できます。
ディレクトリーを再帰的にスキャンする
ディレクトリーを再帰的にスキャンするコードは以下のように書けます。
この関数は、ディレクトリーを再帰的にスキャンしながら、
1)関係ないディレクトリーはスキップし、
2)analyzerという関数コールバックを実行していきます。
3)そしてtextsというスライスの参照に対して結果を書き込んでいきます。
textsは文字列スライスですので、参照ししなくては、呼び出し元に結果を返すことはできません。
package main
import (
"io/ioutil"
"path/filepath"
"strings"
)
func Scandir(dir string, analyzer func(string, *[]string), texts *[]string) {
files, err := ioutil.ReadDir(dir)
if err != nil {
panic(err)
}
for _, file := range files {
name := file.Name()
path := filepath.Join(dir, name)
if file.IsDir() {
// . xcodeprojディレクトリはスキャンしない
if strings.HasSuffix(name, ".xcodeproj") {
continue
}
// .xcworkspaceディレクトリはスキャンしない
if strings.HasSuffix(name, ".xcworkspace") {
continue
}
// buildディレクトリはスキャンしない
if name == "build" {
continue
}
// Carthageディレクトリはスキャンしない
if name == "Carthage" {
continue
}
// Podsディレクトリはスキャンしない
if name == "Pods" {
continue
}
Scandir(filepath.Join(path), analyzer, texts)
continue
}
analyzer(path, texts)
}
}
Localizable.strings
を解析する
Localizable.strings
だけではなく、ファイル拡張子が .strings
であれば解析を行います。
ここでは、
1)スペースを取り除き
2)//
以降はコメントなので削除
3)=
が存在し、最後が ;
であれば先頭の "
で囲まれた領域を文字列スライスに格納します。
package analyzer
import (
"bufio"
"fmt"
"os"
"strings"
)
func AssetAnalyzer(path string, texts *[]string) {
// xxx.strings のみを解析
if !strings.HasSuffix(path, ".strings") {
return
}
// ファイルをOpenする
file, err := os.Open(path)
// 読み取り時の例外処理
if err != nil {
fmt.Println("error")
}
// 関数が終了した際に確実に閉じるようにする
defer file.Close()
sc := bufio.NewScanner(file)
for i := 1; sc.Scan(); i++ {
if err := sc.Err(); err != nil {
// エラー処理
break
}
text := sc.Text()
text = strings.TrimSpace(text)
index := strings.Index(text, "//")
if index > 0 {
// コメントを削除する
text = text[:index]
text = strings.TrimSpace(text)
}
// 構文を検査し、 `=` から後ろ、 `"` 、空白を削除
if strings.HasSuffix(text, ";") {
index := strings.Index(text, "=")
if index > 0 {
text = text[:index]
text = strings.TrimSpace(text)
text = strings.Trim(text, "\"")
}
// 既に同じキーがスライスに存在すれば、result = true
var result bool = false
for _, element := range *texts {
if element == text {
result = true
break
}
}
if result == false {
// 初めてのキーなので、スライスに格納
*texts = append(*texts, text)
}
}
}
}
キャメルケースの文字列を生成する
空白は全て _
に変換しておきます。その上で下記の関数に通すと、スネークケースをキャメルケースに変換することができます。(説明いらないですね・・・)
func convertToCamelCase(text string) string {
var keyword string
var foundUnderScore = false
for i := 0; i < len(text); i++ {
letter := text[i : i+1]
if letter == "_" {
foundUnderScore = true
continue
}
if foundUnderScore {
foundUnderScore = false
keyword = keyword + strings.ToUpper(letter)
} else {
keyword = keyword + letter
}
}
return keyword
}
main文を書く
最後にmain文をあげておきます。
package main
import (
"flag"
"fmt"
"strings"
"./analyzer"
)
func main() {
flag.Parse()
topDir := flag.Arg(0)
enumName := flag.Arg(1)
if topDir == "" {
topDir = "./"
}
if enumName == "" {
enumName = "LocalizableStrings"
}
output(fmt.Sprintf("import Foundation\n\n"))
output(fmt.Sprintf("enum %s: String {\n", enumName))
texts := make([]string, 100, 500)
Scandir(topDir, analyzer.LocalisableStringsAnalyzer, &texts)
for _, text := range texts {
if text == "" {
continue
}
// 空白はアンダースコアに置換
keyword := strings.Replace(text, " ", "_", -1)
keyword = convertToCamelCase(keyword)
output(fmt.Sprintf(" case %s = \"%s\",\n", keyword, text))
}
output(fmt.Sprintf("}\n"))
}
func output(text string) {
fmt.Print(text)
}
GitHub
GitHubにて継続して開発中です。