Go言語でiOS開発ツールを作成する：Localizable.stringsファイルからenumを生成する

# はじめに
　`Go言語でiOS開発ツールを作成する` というモチベーションは、まずGo言語が自分にとってとても興味深い言語であるいうところから始まっています。もともとLinux上でC言語を使って組込製品などを作ってきた人間にとっては、Go言語はすごく分かりやすくて便利な言語だという感触がありました。しかも高速（らしい）です。
　実は以前にSwiftでMacで動くiOS開発ツールは作った経験があるのですが、今回Go言語でチャレンジした感想は、Swiftに比べてもGo言語はとても優れているということです。（特にMacOSやLinuxで動くツールを作成する場合）

# 目標

```:Localizable.strings

"clode_window" = "閉じる";
"delete something" = "削除する";
"cancel" = "キャンセル";
"OK" = "OK";
"_Only_this_" = "Only this";
```

例えば上記のような `Localizable.strings` から下記のようなswiftファイルを生成します。

```swift:LocalizableStrings.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を使ってコマンドラインの環境も構築しておきます。

```bash:terminal
$ brew install go
$ go version
```

GO言語は、`GOPATH` で指定されたパスに、ライブラリーや実行ファイルをダウンロードします。
go version 1.8以降はしてされない場合は、ホームディレクトリーの `go` ディレクトリーが使われます。

```:.bashrc
$ export GOPATH=$HOME"/go"
$ export PATH="$GOPATH/bin:$PATH"
```

VSCodeでGoのデバッグをするのにdelveをインストールしておきます。

```bash:terminal
$ go get -u github.com/derekparker/delve/cmd/dlv
```

# Go言語で実装

## コマンドライン引数を取得する

まずコマンドライン引数を取得する必要があります。

```golang:go
flag.Parse()
topDir := flag.Arg(0)
enumName := flag.Arg(1)
```

flag.Parse()で、コマンドライン引数を拾うことができます。Swiftで同じ機能を書くより簡単で、C言語より分かりやすいです。
`fmt.Printf`や、`fmt.Sprintf`もC言語に慣れていれば、容易に理解できます。

## ディレクトリーを再帰的にスキャンする

ディレクトリーを再帰的にスキャンするコードは以下のように書けます。
この関数は、ディレクトリーを再帰的にスキャンしながら、
１）関係ないディレクトリーはスキップし、
２）analyzerという関数コールバックを実行していきます。
３）そしてtextsというスライスの参照に対して結果を書き込んでいきます。

textsは文字列スライスですので、参照ししなくては、呼び出し元に結果を返すことはできません。


```golang:go
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` であれば解析を行います。
ここでは、
１）スペースを取り除き
２）`//` 以降はコメントなので削除
３）`=` が存在し、最後が `;` であれば先頭の `"`で囲まれた領域を文字列スライスに格納します。

```golang:go
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)
			}
		}
	}
}
```

## キャメルケースの文字列を生成する

空白は全て `_` に変換しておきます。その上で下記の関数に通すと、スネークケースをキャメルケースに変換することができます。（説明いらないですね・・・）

```golang:go

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文をあげておきます。

```golang:go
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にて継続して開発中です。

https://github.com/BlueEventHorizon/EnumGenerator