Help us understand the problem. What is going on with this article?

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ファイルを生成します。

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

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をインストールしておきます。

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

Go言語で実装

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

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

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は文字列スライスですので、参照ししなくては、呼び出し元に結果を返すことはできません。

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

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)
            }
        }
    }
}

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

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

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした