(これまでの記事の目次。同ページのブックマークも参考にどうぞ)
ご無沙汰でした。今回でこのシリーズを最終回にします。ここまでくるともう「はじめて」じゃないし(笑)
コマンドライン・ツールを作ろう
Go 言語の使いどころはいろいろあると思いますが,今回はコマンドライン・ツールを作ります。一般的なコマンドライン・ツールの要件は以下のとおり。
- shell(bash やコマンドプロンプトなど)からの起動を前提とする
- コマンドラインの引数または標準入力からデータや条件を入力する
- 結果を標準出力に出力する
- 結果以外の情報は標準エラー出力に出力する
- shell へ実行結果の状態を返す
「shell へ実行結果の状態を返す」というのは分かりにくいかもしれませんが,これはコマンドライン・ツールが shell に対して返す値で, bash などであれば $?
,コマンドプロンプトであれば ERRORLEVEL
に格納される値です。正常終了する場合は 0 を,異常終了であれば 0 以外をセットするのがお約束になっています。
題材を何にしようか考えていましたが,先日面白い題材を見つけたので,これを利用したいと思います。
Git.io 短縮 URL 取得ツールを作る
「Git.io 短縮 URL を golang コードで取得してみる」で最終的に提示したコードがこれ。
package main
import (
"io/ioutil"
"log"
"net/http"
"net/url"
)
func main() {
resp, err := http.PostForm("http://git.io", url.Values{"url": {"https://github.com/spiegel-im-spiegel"}})
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
log.Println(" Status: ", resp.Header.Get("Status"))
log.Println("Location: ", resp.Header.Get("Location"))
log.Println(" Body: ", string(body))
}
このコードをベースにして,引数で渡された URL を短縮 URL に変換して標準出力に返すツールを作ってみます。
まずは素直に変形。
package main
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
)
func main() {
//arguments
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, os.ErrInvalid)
return
}
urlStr := os.Args[1]
//shortening url
resp, err := http.PostForm("http://git.io", url.Values{"url": {urlStr}})
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
fmt.Fprintln(os.Stderr, resp.Header.Get("Status"))
if string(body) != urlStr {
fmt.Fprintln(os.Stderr, string(body))
}
fmt.Fprint(os.Stdout, resp.Header.Get("Location"))
}
このうち変換処理の部分を spiegel-im-spiegel/gitioapi に外出しします。引数解析の部分も flag パッケージを使った方法に変えてみます(Thanx @mattn)。
package main
import (
"flag"
"fmt"
"os"
"github.com/spiegel-im-spiegel/gitioapi"
)
func main() {
//arguments
flag.Parse()
if flag.NArg() < 1 {
fmt.Fprintln(os.Stderr, os.ErrInvalid)
return
}
urlStr := flag.Arg(0)
//shortening url
shortUrl, err := gitioapi.Encode(&gitioapi.Param{Url: urlStr})
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
fmt.Print(shortUrl)
}
だいぶスッキリしました。これでも一応動作します。
C:>go run gitio5.go https://github.com/spiegel-im-spiegel
http://git.io/vOj52
このコードの問題点
このコードの問題点は大きく2つあります。
-
main
関数に直接ロジックを書いているためテストが出来ない(特に引数の解析部分) -
main
関数のreturn
では shell に値を返せない
このうち2番目は os.Exit()
関数を使うことで解決しますが, os.Exit()
関数は defer
構文と相性が悪いのが難点です。いずれにしろ main
関数にロジックを直書きするというのは実用的なコードではありえない話なので,ここからもう少し変形していきます。
Go のエラーハンドリング
ここで,ちょろんと寄り道。
なのだが,エラーハンドリングについては以下の記事にて全面的に書きなおした。
tcnksm/gcli を使ってコマンドライン・ツール用のコードを自動生成する
話を元に戻しましょう。
コマンドライン・ツールに関しては同様の悩みを持っておられる方が多いらしく,様々な支援パッケージが公開されています。この中で,今回は tcnksm/gcli を紹介します。
tcnksm/gcli は,以前は cli-init という名前で公開されていたもののようです。
tcnksm/gcli では更に機能が強化されていて, codegangsta/cli 以外にも,標準パッケージの flag や mitchellh/cli にも対応しているようです。
tcnksm/gcli のインストール
tcnksm/gcli のインストールは少々コツがいるっぽいです。また UNIX 系の環境を前提としているようで Windows 環境ではインストールが少々面倒くさい感じです。ここでは手順としてコマンドラインの流れを紹介します。
C:>go get -d -v github.com/tcnksm/gcli
C:>cd %GOPATH%\src\github.com\tcnksm\gcli
C:\path\to\gopath\src\github.com\tcnksm\gcli>go get -v golang.org/x/tools/cmd/vet
C:\path\to\gopath\src\github.com\tcnksm\gcli>go get -v github.com/golang/lint/golint
C:\path\to\gopath\src\github.com\tcnksm\gcli>go get -v github.com/jteeuwen/go-bindata/...
C:\path\to\gopath\src\github.com\tcnksm\gcli>go get -v -d -t ./...
C:\path\to\gopath\src\github.com\tcnksm\gcli>git describe --always
3ff629e
C:\path\to\gopath\src\github.com\tcnksm\gcli>pushd skeleton
C:\path\to\gopath\src\github.com\tcnksm\gcli\skeleton>go-bindata -pkg="skeleton" resource/...
C:\path\to\gopath\src\github.com\tcnksm\gcli\skeleton>popd
C:\path\to\gopath\src\github.com\tcnksm\gcli>go install -ldflags "-X main.GitCommit \"3ff629e\""
go get
コマンドの -d
オプションは GOPATH へのダウンロードのみでインストールを行わないようにするためのオプションです。また git describe --always
で取得した commit ID を go install
時にバイナリに埋め込んでいます。
これで gcli
コマンドがインストールされました。最後に動作確認します。
C:>gcli version
[0;0mgcli version v0.2.0 (3ff629e)[0m
[0;31m
Your versin of gcli is out of date! The latest version is 0.2.1.[0m
おおっと,ターミナル前提か。まぁいいや。バージョンが古いと怒られているのは version.go
ファイルの記述が古いままになってるからのようです。これを直せば
C:>gcli version
gcli version v0.2.1 (3ff629e)
となります(エスケープシーケンスは削除しています,見た目がウザいので)。あんまりメンテされてないのかなぁ。
tcnksm/gcli によるコード自動生成
今回は標準の flag パッケージを使った一番簡単なコードを生成してみます。
C:>gcli new -F flag -flag=c:String gitio6
Created gitio6\main.go
Created gitio6\CHANGELOG.md
Created gitio6\version.go
Created gitio6\cli_test.go
Created gitio6\README.md
Created gitio6\cli.go
====> Successfully generated gitio6
ここではオプションとして -c
を追加してみました。生成されたファイルの内訳は以下のとおりです。
-
main.go
:main
関数 -
cli.go
: 処理本体 -
cli_test.go
:cli.go
のテスト -
version.go
: バージョン情報
いやぁ,テストまで自動生成してくれるってありがたいです。ちなみに main.go
は以下のようになっています。
package main
import "os"
func main() {
cli := &CLI{outStream: os.Stdout, errStream: os.Stderr}
os.Exit(cli.Run(os.Args))
}
cli
インスタンスに標準(エラー)出力および引数情報を渡しているのがわかると思います。テストの際はこれらにテスト用の情報をセットしてテストするわけです。これはこれで完成形なので,ほぼ弄る必要はありません(標準入力も対象にするなら手直しする必要あり)。
cli.go
が実際にロジックを書くところです。ここでは cli.go
の最終形のみ示します。
package main
import (
"flag"
"fmt"
"io"
"os"
"github.com/spiegel-im-spiegel/gitioapi"
)
// Exit codes are int values that represent an exit code for a particular error.
const (
ExitCodeOK int = 0
ExitCodeError int = 1 + iota
)
// CLI is the command line object
type CLI struct {
// outStream and errStream are the stdout and stderr
// to write message from the CLI.
outStream, errStream io.Writer
}
// Run invokes the CLI with the given arguments.
func (cli *CLI) Run(args []string) int {
var (
c string
url string
)
// Define option flag parse
flags := flag.NewFlagSet(Name, flag.ContinueOnError)
flags.SetOutput(cli.errStream)
flags.StringVar(&c, "c", "", "'code' parameter.")
flVersion := flags.Bool("version", false, "Print version information and quit.")
// Parse commandline flag
if err := flags.Parse(args[1:]); err != nil {
return ExitCodeError
}
// Show version
if *flVersion {
fmt.Fprintf(cli.errStream, "%s version %s\n", Name, Version)
return ExitCodeOK
}
// Parse argument
switch flags.NArg() {
case 0 :
fmt.Fprintln(cli.errStream, os.ErrInvalid, "No GitHub URL")
return ExitCodeError
case 1 :
url = flags.Arg(0)
default :
fmt.Fprintln(cli.errStream, os.ErrInvalid, flags.Arg(1))
return ExitCodeError
}
// shortening URL
shortUrl, err := gitioapi.Encode(&gitioapi.Param{Url: url, Code: c})
if err != nil {
fmt.Fprintln(cli.errStream, err)
return ExitCodeError
}
fmt.Fprint(cli.outStream, shortUrl)
return ExitCodeOK
}
// Parse argument
以下の行が追記したロジックです。この程度の処理ならほとんど書くことありませんよね(笑)
一応,動作確認。
C:>go build
C:>gitio6.exe https://github.com/spiegel-im-spiegel
http://git.io/vOj52
C:>gitio6.exe -c t https://github.com/technoweenie
http://git.io/t
今回の構成を Domain-Driven 的に考えるのなら, CLI
クラスが Application Service で gitioapi
パッケージが Domain Service に相当するのかな。だいぶ無理矢理ですけど。
自動生成されたコードの著作権
今回使用した tcnksm/gcli を含めいわゆる CASE(Computer Aided Software Engineering)が生成するコードは誰に帰属するのでしょう。
- 著作権審議会第9小委員会(コンピュータ創作物関係)報告書 | 著作権審議会/文化審議会分科会報告 | 著作権データベース | 公益社団法人著作権情報センター CRIC
- コンピュータ自動生成物は著作物ではない - ものがたり
- 自動生成物の著作権について、すでに判決が下された判例が(あれ… - 人力検索はてな
- コンピュータが自動生成した創作物 - Footprints
「自動生成」がどこまでのレベルを指すのかにもよると思いますし,個々の判例では「自動生成されたコードに創作性はない」とするものもあるようですが,一般的に「こう」と言えるものはない感じです。
自動生成ツールを公開する場合は,生成されたコードの扱いについて明記しておくのが(今のところは)安全だと思います。
この記事で紹介したコードは,個人的には「実証コード」レベルの品質だと思っているので, CC0 で公開しています。ただし自動生成されたコードについては,ツールの製作者の方が何らかのライセンスが必要であるとするのであれば CC0 を撤回する場合もあると思います。
これ AI が進歩して完動品のコードを生成するようになったら,どうなるんでしょうねぇ。
最後に
最初に述べたように,「はじめての Go 言語 (on Windows)」シリーズは今回で最終回にします。とはいえ,個人的には Go 言語はかなり気に入ってるので,仕事で使えるレベルにはもう少し勉強していきたいと思います。そのときは別途記事を立てることもあるかと思います。
では,また。