Goでコマンドラインツールを試す
goでコマンドラインツールを作ってみたメモです。
世の中にgo製のコマンドラインツールがたくさんありますが、どうやって作るのんだろうと調べてみたメモです。
goでは各プラットフォーム用に実行可能なバイナリを生成出来るので、
コマンドとして機能するプログラムを書いて、各プラットフォーム毎にバイナリを生成すればよさそうです。
flagパッケージ
コマンドライン引数を自分で処理するのは大変ですが、標準のflagパッケージを使うと簡単に扱えるようです。
(それでも機能が薄いようで、コマンドライン引数を扱うライブラリが色々あるようです)
http://golang.jp/pkg/flag
flagパッケージでは
command -list
のようなオプションを取る形式と、
command -file abc.txt
のようなオプションとオプション引数を取る形式に対応してます。
下記のようにコマンドオプションを、booleanの変数で受ける場合、
$ command -list
BoolVar()で変数にバインド出来ます。
第2引数にオプション名、第3引数でデフォルト値、第4引数でコメントを指定します。
var showList bool flag.BoolVar(&showList, "list", false, "show list.")
最後にflag.Parse()を呼ぶとフラグが解析されます。
flag.Parse()
下記のようにオプションを文字列や数値で受けたい場合は、
$ command -f abc.txt $ command -s 100
StringVar(), IntVar() を使用します。
これで、文字列や数値の変数にバインド出来ます。
var fileName string flag.StringVar(&fileName, "f", "dummy.txt", "file name.") var maxSize int flag.IntVar(&maxSize, "s", 0, "max size.")
lsコマンドのように、ls -ltrのように複数のオプションをまとめて指定する方法を調べてみたんですが、
flagパッケージでは出来ないようでした。
(どうすれば出来るのか調べてみたけどよく分からん)
myls
lsコマンドを模したmylsコマンドを作ってみようと思います。
オプションは全部対応するのは出来ないので、下記3つのオプションだけ実装してみようと思います。
- -l 詳細表示
- -a 隠しファイル表示
- -v バージョン表示
Windowsだとファイルのユーザやグループが取れないので、MacとLinuxだけ対象にしようと思います。
ソースは下記の通り。
(golangに慣れてきたらリファクタリングして書き直したい)
main.go
package main import ( "flag" "fmt" "log" "os" "os/user" "strings" "syscall" ) // -l format long // -a show all // -v version // -h help const ( version = "1.0" codeOk = 0 codeError = 1 ) func main() { os.Exit(executeCommand()) } func executeCommand() int { // parse args option var showVersion bool flag.BoolVar(&showVersion, "v", false, "show version.") var showDetail bool flag.BoolVar(&showDetail, "l", false, "show list with more detail.") var showAll bool flag.BoolVar(&showAll, "a", false, "show all files.") flag.Parse() if showVersion { fmt.Println("version: " + version) return codeOk } // dir name dirName := flag.Arg(0) if dirName == "" { dirName = "." } f, err := os.Open(dirName) if err != nil { log.Fatal("Cannot open dir. Please input correct dir name:") } defer f.Close() fis, err := f.Readdir(0) if err != nil { log.Fatal("Cannot open dir. Please input correct dir name:") } if showDetail { longFormat(fis, showAll) return codeOk } ls(fis, showAll) return codeOk } func ls(fis []os.FileInfo, showAll bool) { for _, fi := range fis { filename := fi.Name() if !showAll && strings.HasPrefix(filename, ".") { // ignore dot files continue } fmt.Print(filename + " ") } fmt.Println("") } func longFormat(fis []os.FileInfo, showAll bool) { var stringFileInfos []*stringFileInfo for _, fi := range fis { info := stringFileInfo{} filename := fi.Name() if !showAll && strings.HasPrefix(filename, ".") { // ignore dot files continue } // file name info.name = filename // permission info.permission = fmt.Sprintf("%v", fi.Mode()) // file size info.size = fmt.Sprintf("%v", fi.Size()) // user name var s syscall.Stat_t syscall.Stat(fi.Name(), &s) u, _ := user.LookupId(fmt.Sprintf("%v", s.Uid)) info.user = u.Username // group name g, _ := user.LookupGroupId(fmt.Sprintf("%v", s.Gid)) info.group = g.Name // mod time modTime := fi.ModTime() info.modDateTime = modTime.Format("1 2 15:04") stringFileInfos = append(stringFileInfos, &info) } fillSpaces(stringFileInfos) } // fill spaces for format func fillSpaces(infos []*stringFileInfo) { var maxLengthName int var maxLengthPerm int var maxLengthSize int var maxLengthUser int var maxLengthGroup int var maxLengthMonth int var maxLengthDay int var maxLengthTime int for _, info := range infos { if maxLengthName < len(info.name) { maxLengthName = len(info.name) } if maxLengthPerm < len(info.permission) { maxLengthPerm = len(info.permission) } if maxLengthSize < len(info.size) { maxLengthSize = len(info.size) } if maxLengthUser < len(info.user) { maxLengthUser = len(info.user) } if maxLengthGroup < len(info.group) { maxLengthGroup = len(info.group) } // split "M D hh:mi" dateTime := strings.Split(info.modDateTime, " ") if maxLengthMonth < len(dateTime[0]) { maxLengthMonth = len(dateTime[0]) } if maxLengthDay < len(dateTime[1]) { maxLengthDay = len(dateTime[1]) } if maxLengthTime < len(dateTime[2]) { maxLengthTime = len(dateTime[2]) } } var spaceSize int for _, info := range infos { // name spaceSize = maxLengthName - len(info.name) info.name += strings.Repeat(" ", spaceSize) // size spaceSize = maxLengthSize - len(info.size) info.size += strings.Repeat(" ", spaceSize) // user name spaceSize = maxLengthUser - len(info.user) info.user += strings.Repeat(" ", spaceSize) // group name spaceSize = maxLengthGroup - len(info.group) info.group += strings.Repeat(" ", spaceSize) // mod date time (month + day + time) dateTime := strings.Split(info.modDateTime, " ") spaceSize = maxLengthMonth - len(dateTime[0]) // month info.modDateTime = dateTime[0] + strings.Repeat(" ", spaceSize+1) spaceSize = maxLengthDay - len(dateTime[1]) // day info.modDateTime += dateTime[1] + strings.Repeat(" ", spaceSize+1) spaceSize = maxLengthTime - len(dateTime[2]) // time info.modDateTime += dateTime[2] + strings.Repeat(" ", spaceSize) } } type stringFileInfo struct { name string permission string size string user string group string modDateTime string }
go build
Mac用バイナリ
go buildで各プラットフォーム用のバイナリを生成します。
Mac用のバイナリを生成してみます。
どのプラットフォーム用にビルドするかは環境変数のGOARCHとGOOSで指定します。
Macの場合、GOOSでdarwinを指定し、GOARCHでamd64のCPUを指定します。
環境変数確認
$ go env GOARCH="amd64" GOOS="darwin"
-oオプションで出力するファイル名を指定します。
$ go build -o myls main.go
または、下記の様に一度に指定することが可能。
$ GOOS=darwin GOARCH=amd64 go build -o myls main.go
バイナリが生成されました。
$ ll -rwxr-xr-x 1 purple staff 2.0M 6 13 20:49 myls
どこでも実行できるようにzshrcでPATHに通しておきます。
$ vi .zshrc export PATH=$PATH:path_to_binary/myls $ source .zshrc
確認
普通のlsコマンドの結果とほぼ同じになることを確認してみます。
lsの結果を確認。
$ ls -la total 24 drwxr-xr-x 8 purple staff 272 6 14 21:47 . drwxr-xr-x 17 purple staff 578 7 9 17:29 .. -rw-r--r-- 1 purple staff 52 6 14 21:46 .vimrc -rw-r--r-- 1 purple staff 97 5 1 08:03 HelloWorld.java -rw-r--r-- 1 purple admin 19 5 21 08:03 abc.txt drwxr-xr-x 2 purple staff 68 12 23 2017 dir1 drwxr-xr-x 2 purple staff 68 11 11 2017 dir2 -rwxrwxrwx 1 purple staff 0 6 11 08:03 launcher
mylsをオプションなしで実行。
$ myls abc.txt dir1 dir2 HelloWorld.java launcher
-aオプション
-aで隠しファイル表示。
$ myls -a
.vimrc abc.txt dir1 dir2 HelloWorld.java launcher
-lオプション
-lで詳細表示。
$ myls -l -rw-r--r-- purple admin 19 5 21 08:03 abc.txt drwxr-xr-x purple staff 68 12 23 08:03 dir1 drwxr-xr-x purple staff 68 11 11 08:03 dir2 -rw-r--r-- purple staff 97 5 1 08:03 HelloWorld.java -rwxrwxrwx purple staff 0 6 11 08:03 launcher
複合オプション(-l, -a)
$ myls -l -a -rw-r--r-- purple staff 52 6 14 21:46 .vimrc -rw-r--r-- purple admin 19 5 21 08:03 abc.txt drwxr-xr-x purple staff 68 12 23 08:03 dir1 drwxr-xr-x purple staff 68 11 11 08:03 dir2 -rw-r--r-- purple staff 97 5 1 08:03 HelloWorld.java -rwxrwxrwx purple staff 0 6 11 08:03 launcher
-vオプション
-vでコマンドのバージョンを表示。
$ myls -v
version: 1.0
-hオプション
-hでヘルプを表示。
$ myls -h Usage of myls: -a show all files. -l show list with more detail. -v show version.
不明なオプション
不明なオプションが指定された場合は、ヘルプと同じ表示がされます。
$ myls -d flag provided but not defined: -d Usage of myls: -a show all files. -l show list with more detail. -v show version.