DRYな備忘録

Don't Repeat Yourself.

Revelのコマンドを読む[第1回:new]

revelとは

読む場所

% cd $GOPATH/src/github.com/revel/cmd

revel new hoge/fuga

revel/new.go

var cmdNew = &Command{
    UsageLine: "new [path] [skeleton]",
    Short:     "create a skeleton Revel application",
    Long:     `略`,
}
// Command structって何だ?

revel/rev.go

// Cribbed from the genius organization of the "go" command.
type Command struct {
    Run                    func(args []string)
    UsageLine, Short, Long string
}
func (cmd *Command) Name() string
// 各サブコマンドの定義と説明文と、あと名前へのgetterだけっぽい

revel/new.go

func init() {
    cmdNew.Run = newApp
}
// 関数リテラルで宣言したフィールドに関数オブジェクトそのまま詰めれるのか

revel/new.go

func newApp(args []string) {
    // check for proper args by count
    if len(args) == 0 {
        errorf("No import path given.\nRun 'revel help new' for usage.\n")
    }
    if len(args) > 2 {
        errorf("Too many arguments provided.\nRun 'revel help new' for usage.\n")
    }
    // バリデーションしてる

    // checking and setting go paths
    initGoPaths()
    // ん?なんだこれ
    // 中断して見に行く

同ファイル

// lookup and set Go related variables
// var gopath, srcRoot, gocmd string
// の決定
func initGoPaths() {
    // lookup go path
    // $GOPATHを取得し、無かったらエラー
    gopath = build.Default.GOPATH
    if gopath == "" {
        errorf("Abort: GOPATH environment variable is not set. " +
            "Please refer to http://golang.org/doc/code.html to configure your Go environment.")
    }
    // set go src path
    // filepath.SplitList
    //     取得した$GOPATHがたとえば:つなぎ複数だった場合に、その最初を使う
    // そこに"src"をつなげたものを、Goのsrcパスとして決定する
    srcRoot = filepath.Join(filepath.SplitList(gopath)[0], "src")
    // check for go executable
    // 上記は$GOPATH/srcの決定。
    // 下記はwhich goの決定
    var err error
    gocmd, err = exec.LookPath("go")
    if err != nil {
        errorf("Go executable not found in PATH.")
    }
}

ひきつづき、同ファイルnewAppの続き

    // checking and setting application
    setApplicationPath(args)
// importPath, appPath, appName, basePath string と
// revelPkg *build.Packageの決定
func setApplicationPath(args []string) {
    var err error
    importPath = args[0]
    // revel new hoge/fuga で渡すhoge/fugaは
    // 絶対パスではいけない件
    if filepath.IsAbs(importPath) {
        errorf("Abort: '%s' looks like a directory.  Please provide a Go import path instead.",
            importPath)
    }
    // $GOPATH/src/hoge/fugaが既に存在してたらエラー
    _, err = build.Import(importPath, "", build.FindOnly)
    if err == nil {
        errorf("Abort: Import path %s already exists.\n", importPath)
    }
    // そもそもrevelソースが無い?
    revelPkg, err = build.Import(revel.REVEL_IMPORT_PATH, "", build.FindOnly)
    if err != nil {
        errorf("Abort: Could not find Revel source code: %s\n", err)
    }
    // $GOPATH/src/hoge/fugaを決定
    appPath = filepath.Join(srcRoot, filepath.FromSlash(importPath))
    // アプリケーション名をfugaに決定
    appName = filepath.Base(appPath)
    // 一個上のpath持っとく?なぜ?
    basePath = filepath.ToSlash(filepath.Dir(importPath))
    if basePath == "." {
        // we need to remove the a single '.' when
        // the app is in the $GOROOT/src directory
        basePath = ""
    } else {
        // we need to append a '/' when the app is
        // is a subdirectory such as $GOROOT/src/path/to/revelapp
        basePath += "/"
    }
}

つぎ、

    // checking and setting skeleton
    setSkeletonPath(args)
// skeltonPathを決定する
func setSkeletonPath(args []string) {
    var err error
    if len(args) == 2 { // user specified
        // 利用するスケルトンをユーザが指定できる?
        // 中略
    } else {
        // use the revel default
        // $GOPTH/src/github.com/revel/revel/skeltonになるハズ
        skeletonPath = filepath.Join(revelPkg.Dir, "skeleton")
    }
}

さいご

    // copy files to new app directory
    copyNewAppFiles()
func copyNewAppFiles() {
    var err error
    // appPathでmkdir -pする
    err = os.MkdirAll(appPath, 0777)
    panicOnError(err, "Failed to create directory "+appPath)
    // mustCopyDir? 何ソレ?
    mustCopyDir(appPath, skeletonPath, map[string]interface{}{
        // app.conf
        "AppName":  appName,
        "BasePath": basePath,
        "Secret":   generateSecret(),
    })

    // Dotfiles are skipped by mustCopyDir, so we have to explicitly copy the .gitignore.
    gitignore := ".gitignore"
    mustCopyFile(filepath.Join(appPath, gitignore), filepath.Join(skeletonPath, gitignore))

}

revel/util.go

// copyDir copies a directory tree over to a new directory.  Any files ending in
// ".template" are treated as a Go template and rendered using the given data.
// Additionally, the trailing ".template" is stripped from the file name.
// Also, dot files and dot directories are skipped.
func mustCopyDir(destDir, srcDir string, data map[string]interface{}) error {
    var fullSrcDir string
    // Handle symlinked directories.
    f, err := os.Lstat(srcDir)
    // このf.Mode()&os.ModeSymlinkっていう表記なんだ?
    // symlinkの扱い
    if err == nil && f.Mode()&os.ModeSymlink == os.ModeSymlink {
        fullSrcDir, err = os.Readlink(srcDir)
        if err != nil {
            panic(err)
        }
    } else {
        fullSrcDir = srcDir
    }
    // すべてのディレクトリ・ファイルについてCopyしてる
    return filepath.Walk(fullSrcDir, func(srcPath string, info os.FileInfo, err error) error {
        // Get the relative path from the source base, and the corresponding path in
        // the dest directory.
        relSrcPath := strings.TrimLeft(srcPath[len(fullSrcDir):], string(os.PathSeparator))
        destPath := path.Join(destDir, relSrcPath)

        // Skip dot files and dot directories.
        if strings.HasPrefix(relSrcPath, ".") {
            if info.IsDir() {
                return filepath.SkipDir
            }
            return nil
        }
        // Create a subdirectory if necessary.
        if info.IsDir() {
            err := os.MkdirAll(path.Join(destDir, relSrcPath), 0777)
            if !os.IsExist(err) {
                panicOnError(err, "Failed to create directory")
            }
            return nil
        }
        // If this file ends in ".template", render it as a template.
        if strings.HasSuffix(relSrcPath, ".template") {
            mustRenderTemplate(destPath[:len(destPath)-len(".template")], srcPath, data)
            return nil
        }

        // Else, just copy it over.
        mustCopyFile(destPath, srcPath)
        return nil
    })
}

結局

revelパスにあるsekltonを、指定されたプロジェクトパスにcp -rしてるだけのような気がする。foo/barで指定したときのアプリケーションってbar的な文字列ほとんど含んでないっけ?

調査

% revel new hoge/piyopiyo
~
~ revel! http://revel.github.io
~
Your application is ready:
   /Users/otiai10/proj/go/src/hoge/piyopiyo

You can run it with:
   revel run hoge/piyopiyo
%
% cd $GOPATH/src/hoge/piyopiyo
% grep piyopiyo **/*.*
conf/app.conf:app.name=piyopiyo
%

なるほど、conf/app.conf以外にpiyopiyoという名前は登場しない。ファイルパスだけがhoge/piyopiyoという名前にひもづいてる状態。

mustCopyDirにアプリ名などを与え、mustCopyDir内で.templateを使ってapp.confをアプリ名仕様にしてる。

$GOPATH/src/github.com/revel/revel/skeleton/conf/app.conf.template

app.name={{ .AppName }}
app.secret={{ .Secret }}
http.addr=
http.port=9000
http.ssl=false
http.sslcert=
# 以下略

結論

  • 基本的には$GOPATH/src/github.com/revel/revelというパスを決定し、そこのskeltonディレクトリの内容を、revel new hogeで指定された$GOPATH/src/hogeディレクトリにコピーしてるだけ(あとapp.confつくってるくらい)

っぽい

雑感

  • 次はrunコマンド読むよ