Go 言語用 CLI プログラミング支援パッケージ

no extension

本パッケージ gocliGo 言語 で CLI (Command-Line Interface) を構成する際に必要になるであろう細々とした機能をまとめたライブラリである。 ただし,このパッケージをそのまま使うことは想定しておらず(そのまま使ってもいいけど)何らかのアレンジを加えた上で,それぞれの CLI ツール用に組み込むことを念頭に置いている。

このため gocli では Go コンパイラが提供する標準パッケージ以外の外部パッケージはなるべく使わないようにし,ライセンスも,あらゆる権利を放棄した CC0 を設定している。

なお gocli パッケージは Go 1.13 以上を要求する。 ご注意を。

check vulns lint status GitHub license GitHub release

標準入出力と終了コード

gocli/rwi パッケージは標準入出力をコンテキスト情報として格納する構造体を提供する。 また gocli/exitcode パッケージは CLI 終了時の終了コードを定義する。

両者は以下のように使う。

package main

import (
    "os"

    "github.com/goark/gocli/exitcode"
    "github.com/goark/gocli/rwi"
)

func run(ui *rwi.RWI) exitcode.ExitCode {
    ui.Outputln("Hello world")
    return exitcode.Normal
}

func main() {
    run(rwi.New(
        rwi.WithReader(os.Stdin),
        rwi.WithWriter(os.Stdout),
        rwi.WithErrorWriter(os.Stderr),
    )).Exit()
}

gocli/rwi パッケージを使うメリットはテストで発揮される。 たとえば上述の run() 関数をテストするのであれば

outBuf := new(bytes.Buffer)
outErrBuf := new(bytes.Buffer)
code := run(rwi.New(
    rwi.WithWriter(outBuf),
    rwi.WithErrorWriter(outErrBuf),
))

として実行結果を code, outBuf および outErrBuf から取り出し評価することができる。

SIGNAL をハンドリングする

【2021-09-19 追記】

Go 1.16 から signal.NotifyContext() が導入され context パッケージと連携できるようになった。 本節の機能は既に deprecated であり,利用はおすすめしない。

gocli/signal パッケージは標準の context パッケージと組み合わせて SIGNAL のハンドリングを行う。 たとえば,こんな感じ

package main

import (
    "context"
    "fmt"
    "os"
    "time"

    "github.com/goark/gocli/signal"
)

func ticker(ctx context.Context) error {
    t := time.NewTicker(1 * time.Second) // 1 second cycle
    defer t.Stop()

    for {
        select {
        case now := <-t.C: // ticker event
            fmt.Println(now.Format(time.RFC3339))
        case <-ctx.Done(): // cancel event from context
            fmt.Println("Stop ticker")
            return ctx.Err()
        }
    }
}

func Run() error {
    errCh := make(chan error, 1)
    defer close(errCh)

    go func() {
        child, cancelChild := context.WithTimeout(
            signal.Context(context.Background(), os.Interrupt), // cancel event by SIGNAL
            10*time.Second, // timeout after 10 seconds
        )
        defer cancelChild()
        errCh <- ticker(child)
    }()

    err := <-errCh
    fmt.Println("Done")
    return err
}

func main() {
    if err := Run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
}

このコードでは signal.Context() 関数で指定した SIGNAL 用の context.Context インスタンスを生成している。 SIGNAL または親 context.Context インスタンスによるキャンセルイベントを受信した場合は,子 context.Context インスタンスにキャンセルが伝搬する。

context パッケージを使ったキャンセルの伝搬については以下を参照のこと。

ワイルドカードを含むファイルの検索

gocli/file パッケージを使ったファイル検索は標準の filepath.Glob() 関数を拡張する形で実装している。 こんな感じに使える。

package main

import (
    "fmt"

    "github.com/goark/gocli/file"
)

func main() {
    result := file.Glob("**/*.[ch]", nil)
    fmt.Println(result)
    // Output:
    // [testdata/include/source.h testdata/source.c]
}

file.Glob() 関数の第2引数には検索時の条件を設定できる。 こんな感じ。

package main

import (
    "fmt"

    "github.com/goark/gocli/file"
)

func main() {
    result := file.Glob(
        "**/*.[ch]",
        file.NewGlobOption(file.WithFlags(file.GlobStdFlags|file.GlobAbsolutePath)))
    fmt.Println(result)
    // Output:
    // [/home/username/work/gocli/file/testdata/include/source.h /home/username/work/gocli/file/testdata/source.c]
}

指定できるフラグは以下の通り。

//Operation flag in Glob() function.
const (
    GlobContainsFile GlobFlag = 1 << iota
    GlobContainsDir
    GlobSeparatorSlash
    GlobAbsolutePath
    GlobStdFlags = GlobContainsFile | GlobContainsDir
)

file.Glob() 関数の第2引数に nil をセットするか file.NewGlobOption() を引数なしで呼び出した場合は file.GlobStdFlags のみがセットされる。

gocli/file パッケージはファイル操作の練習用に作ったもので,それなりには使えるとは思うが,正直に言って素朴すぎて効率はよくない。 実際に使うにはもう少しアレンジが必要になるだろう。

設定ファイルのパスを取得する

Go 1.13 から os.UserConfigDir() 関数が追加されたので,これを使って設定ファイルのパスを取得するパッケージ gocli/config を作ってみた。 こんな感じで使う。

package main

import (
    "fmt"

    "github.com/goark/gocli/config"
)

func main() {
    path := config.Path("app", "config.json")
    fmt.Println(path)
    // Output:
    // /home/username/.config/app/config.json
}

アプリケーション名を指定して設定用ディレクトリのパスを取得する config.Dir(appName string) 関数も用意した。

os.UserConfigDir() 関数で取得したパスにアプリケーション名と設定ファイル名をくっ付けただけの簡単なお仕事である。 Go 1.13 では os.UserConfigDir() 関数は以下のように記述されている。

// UserConfigDir returns the default root directory to use for user-specific
// configuration data. Users should create their own application-specific
// subdirectory within this one and use that.
//
// On Unix systems, it returns $XDG_CONFIG_HOME as specified by
// https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html if
// non-empty, else $HOME/.config.
// On Darwin, it returns $HOME/Library/Application Support.
// On Windows, it returns %AppData%.
// On Plan 9, it returns $home/lib.
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error.
func UserConfigDir() (string, error) {
    var dir string

    switch runtime.GOOS {
    case "windows":
        dir = Getenv("AppData")
        if dir == "" {
            return "", errors.New("%AppData% is not defined")
        }

    case "darwin":
        dir = Getenv("HOME")
        if dir == "" {
            return "", errors.New("$HOME is not defined")
        }
        dir += "/Library/Application Support"

    case "plan9":
        dir = Getenv("home")
        if dir == "" {
            return "", errors.New("$home is not defined")
        }
        dir += "/lib"

    default: // Unix
        dir = Getenv("XDG_CONFIG_HOME")
        if dir == "" {
            dir = Getenv("HOME")
            if dir == "" {
                return "", errors.New("neither $XDG_CONFIG_HOME nor $HOME are defined")
            }
            dir += "/.config"
        }
    }

    return dir, nil
}

キャッシュ用のディレクトリ・ファイルのパスを取得する

os.UserCacheDir() 関数を使ってキャッシュ用のディレクトリ・ファイルのパスを取得するパッケージ gocli/cache を作ってみた。 こんな感じで使う。

package main

import (
    "fmt"

    "github.com/goark/gocli/cache"
)

func main() {
    path := cache.Path("app", "access.log")
    fmt.Println(path)
    // Output:
    // /home/username/.cache/app/access.log
}

os.UserCacheDir() 関数で取得したパスにアプリケーション名とファイル名をくっ付けただけの簡単なお仕事である。 Go 1.13 では os.UserCacheDir() 関数は以下のように記述されている。

// UserCacheDir returns the default root directory to use for user-specific
// cached data. Users should create their own application-specific subdirectory
// within this one and use that.
//
// On Unix systems, it returns $XDG_CACHE_HOME as specified by
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
// non-empty, else $HOME/.cache.
// On Darwin, it returns $HOME/Library/Caches.
// On Windows, it returns %LocalAppData%.
// On Plan 9, it returns $home/lib/cache.
//
// If the location cannot be determined (for example, $HOME is not defined),
// then it will return an error.
func UserCacheDir() (string, error) {
    var dir string

    switch runtime.GOOS {
    case "windows":
        dir = Getenv("LocalAppData")
        if dir == "" {
            return "", errors.New("%LocalAppData% is not defined")
        }

    case "darwin", "ios":
        dir = Getenv("HOME")
        if dir == "" {
            return "", errors.New("$HOME is not defined")
        }
        dir += "/Library/Caches"

    case "plan9":
        dir = Getenv("home")
        if dir == "" {
            return "", errors.New("$home is not defined")
        }
        dir += "/lib/cache"

    default: // Unix
        dir = Getenv("XDG_CACHE_HOME")
        if dir == "" {
            dir = Getenv("HOME")
            if dir == "" {
                return "", errors.New("neither $XDG_CACHE_HOME nor $HOME are defined")
            }
            dir += "/.cache"
        }
    }

    return dir, nil
}

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-07-13)
Kindle版
B099928SJD (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)