Go 言語用 CLI プログラミング支援パッケージ
本パッケージ gocli
は Go 言語 で CLI (Command-Line Interface) を構成する際に必要になるであろう細々とした機能をまとめたライブラリである。
ただし,このパッケージをそのまま使うことは想定しておらず(そのまま使ってもいいけど)何らかのアレンジを加えた上で,それぞれの CLI ツール用に組み込むことを念頭に置いている。
このため gocli
では Go コンパイラが提供する標準パッケージ以外の外部パッケージはなるべく使わないようにし,ライセンスも,あらゆる権利を放棄した CC0 を設定している。
なお gocli
パッケージは Go 1.13 以上を要求する。
ご注意を。
標準入出力と終了コード
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
}
参考図書
- プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- 丸善出版 2016-06-20
- 単行本(ソフトカバー)
- 4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN), 9784621300251 (ISBN)
- 評価
著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。