【Golang】urfave/cliを使って計算機アプリをcliにする
Urfave
CLIを実装する上で面倒な
- サブコマンド
- リッチなヘルプインターフェース
等の作成をサポートしてくれます。
今回の記事では基本的なメソッドや構造体を説明した後、演習として実際に計算機をcli化させる過程を紹介します。
なお、演習部分は以下の動画の翻訳兼まとめです。是非元動画もご参照ください。
ep008 Using 'urfave-cli/v2' Package
インストール
go get -u github.com/urfave/cli/v2
使い方
func main() { // 構造体作成 app := cli.NewApp() app.Name = "mycalc" app.Usage = "The best calculator" app.Description = "this is calculator<ffff>" // 実行したい関数 app.Action = mainAction // フラグ --引数 値 app.Flags = []cli.Flag{ &cli.StringFlag{Destination: &oper, Name: "oper", Value: "add", Usage: "add, sub, mul or div operations on two int"}, &cli.IntFlag{Destination: &opt1, Name: "opt1", Value: 0, Usage: "first int"}, &cli.IntFlag{Destination: &opt2, Name: "opt2", Value: 0, Usage: "second int"}, } // command app.Commands = []*cli.Command{ *cli.Command { return &cli.Command{ Name: "add", Aliases: []string{"a"}, Action: func(ctx *cli.Context) error { res := opt1 + opt2 fmt.Printf("%v + %v = %v", opt1, opt2, res) return nil }, } } app.Run(os.Args) }
実行結果
// app.Name - app.Usage NAME: mycalc - The bes実行t calculator USAGE: main [global options] command [command options] [arguments...] // app.Description DESCRIPTION: this is calculator<ffff> // app.Command COMMANDS: add, a help, h Shows a list of commands or help for one command // app.Flags GLOBAL OPTIONS: --oper value add, sub, mul or div operations on two int (default: "add") --opt1 value first int (default: 0) --opt2 value second int (default: 0) --help, -h show help (default: false)
各構造体やフィールドの役割は以下です
cli.App
- 元になる構造体。このメンバ変数に様々な値を代入します。
Name
,Usage
,Description
- ヘルプに表示したい情報を付与
Action
- 実行したい関数を定義
Flags
- Actionで実行する際、使いたいflag引数を定義。helpで参照可能
Commands
cli.Command
ポインタ型のスライスを渡す事でサブコマンドを実装cli.Command
構造体にもcli.App
と同様にdescriptionやActionを定義していきます。小さいcli.App
を沢山作っていく感覚です。
Run()
- 実行メソッド
アプリ解説
これらを踏まえて簡単な計算機アプリを段階的にCLI化させていきます。
今回使う計算機アプリです
var ( oper string op1 int op2 int ) func main() { flag.StringVar(&oper, "oper", "add", "add, sub, mul or div operations on two int") flag.IntVar(&opt1, "opt1", 0, "first int") flag.IntVar(&opt2, "opt2", 0, "second int") flag.Parse() switch oper { case "add": res := opt1 + opt2 fmt.Printf("%v + %v = %v", opt1, opt2, res) case "mul": res := opt1 * opt2 fmt.Printf("%v * %v = %v", opt1, opt2, res) } }
$ go run main.go --opt1 1 --opt2 2 --oper mul 1 * 2 = 2 $ go run main.go --opt1 1 --opt2 2 --oper add 1 + 2 = 3
現状
- 二つの数字とオペレーションを渡す
- スイッチ文で処理を変える
これだけ。しかしcliというからには
- -hオプションで分かりやすいヘルプを表示する
- オペレーションはオプションではなくサブコマンドで行う
- 引数は2つだけではなく複数可能
位の機能は欲しい!
1. ヘルプを整える
まずアプリケーションの情報を付け加えます
var ( oper string op1 int op2 int ) func main() { app := cli.NewApp() app.Name = "mycalc" app.Usage = "The best calculator" app.Description = "this is calculator<ffff>" app.Action = mainAction app.Run(os.Args) } func mainAction(ctx * cli.Context) error { flag.StringVar(&oper, "oper", "add", "add, sub, mul or div operations on two int") flag.IntVar(&opt1, "opt1", 0, "first int") flag.IntVar(&opt2, "opt2", 0, "second int") flag.Parse() switch oper { case "add": res := opt1 + opt2 fmt.Printf("%v + %v = %v", opt1, opt2, res) case "mul": res := opt1 * opt2 fmt.Printf("%v * %v = %v", opt1, opt2, res) } return nil }
変更点
- main関数でcli.app構造体を作成。各種情報を渡す
- 既存のアプリケーションを
app.Action
の引数として渡せる様引数と返り値を調整
結果
$ go run main.go -h NAME: mycalc - The best calculator USAGE: main [global options] command [command options] [arguments...] DESCRIPTION: this is calculator COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false)
そこそこリッチなヘルプが表示される様になりました。
2. オプションを表示
ヘルプにフラグ引数の案内は欲しい所ですよね(GLOBAL OPTIONS以下
)
そこでmainAction内のフラグ引数をappオブジェクトに移動します
func main() { // 追加 // destinationに変数のポインタ。それ以外はフラグと同じ app.Flags = []cli.Flag{ &cli.StringFlag{Destination: &oper, Name: "oper", Value: "add", Usage: "add, sub, mul or div operations on two int"}, &cli.IntFlag{Destination: &opt1, Name: "opt1", Value: 0, Usage: "first int"}, &cli.IntFlag{Destination: &opt2, Name: "opt2", Value: 0, Usage: "second int"}, } } func mainAction(ctx * cli.Context) error { // フラグを消す switch oper { case "add": res := opt1 + opt2 fmt.Printf("%v + %v = %v", opt1, opt2, res) case "mul": res := opt1 * opt2 fmt.Printf("%v * %v = %v", opt1, opt2, res) } return nil }
これでGLOBAL OPTIONSに案内が表示される様になりました
$ go run main.go -h NAME: mycalc - The best calculator ... GLOBAL OPTIONS: --oper value add, sub, mul or div operations on two int (default: "add") --opt1 value first int (default: 0) --opt2 value second int (default: 0) --help, -h show help (default: false)
一度挙動も確認してみましょう
$ go run --op1 1 --op2 2 --oper add 1 + 2 = 3
問題なく動くはずです。
しかし--oper
が引数になってる点は違和感です。oper
はアプリケーションの動作自体を変えている為引数ではなく
go run add --op1 1 --op2 2
の様にコマンドにした方が分かり安いcliインターフェースであると言えそうです
3. operをサブコマンドに
func main() { ... app.Commands = []*cli.Command{ addCommand(), mulCommand(), } app.Run(os.Args) } func addCommand() *cli.Command { return &cli.Command{ Name: "add", Aliases: []string{"a"}, Action: func(ctx *cli.Context) error { res := opt1 + opt2 fmt.Printf("%v + %v = %v", opt1, opt2, res) return nil }, } } func mulCommand() *cli.Command { return &cli.Command { Name: "mul", Aliases: []string{"m"}, Action: func(ctx *cli.Context) error { res := opt1 * opt2 fmt.Printf("%v * %v = %v", opt1, opt2, res) return nil }, } } func mainAction(ctx * cli.Context) error { // スイッチ文消す return nil }
app.Commands
に*cli.Command
のスライスを渡す事でサブコマンドが実装出来ます。
cli.Command
構造体はcli.App
構造体と同じ様に個別のコマンドやヘルプ、オプションを持つ事が出来るので、コマンドを複製するイメージで使う事ができます。
又、動画ではネストが深くならない様にaddCommand()
関数の返り値を渡しています。
$ go run main.go -h USAGE: main [global options] command [command options] [arguments...] COMMANDS: add, a mul, m help, h Shows a list of commands or help for one command
これでサブコマンドが実装出来ました。
Usageに従っていくつか実行して見ましょう
$ go run main.go --opt1 6 add 6 + 0 = 6% $ go run main.go --opt1 3 --opt2 4 a 3 + 4 = 7% $ go run main.go --opt1 6 --opt2 1 mul 6 * 1 = 6%
ちゃんとエイリアスも機能しています。
が、引数の順番がおかしいです。引数より先にサブコマンドを指定する方が自然ですよね。
$ go run main.go mul --opt1 6 --opt2 1
Urfave
ではUsageの通りglobal options
command
の順で実行しなければならない為command
の後に引数を実行したい場合はglobal options
の引数をcli.Command
以下に移動させる必要があります。
4. optionsの移動
func main() { app := cli.NewApp() app.Name = "mycalc" app.Usage = "The best calculator" app.Description = "this is calculator<ffff>" app.Action = mainAction //app.Flags = []cli.Flag{ // &cli.StringFlag{Destination: &oper, Name: "oper", Value: "add", Usage: "add, sub, mul or div operations on two int"}, // &cli.IntFlag{Destination: &opt1, Name: "opt1", Value: 0, Usage: "first int"}, // &cli.IntFlag{Destination: &opt2, Name: "opt2", Value: 0, Usage: "second int"}, //} app.Commands = []*cli.Command{ addCommand(), mulCommand(), } app.Run(os.Args) } func addCommand() *cli.Command { return &cli.Command{ Name: "add", Aliases: []string{"a"}, Action: func(ctx *cli.Context) error { res := opt1 + opt2 fmt.Printf("%v + %v = %v", opt1, opt2, res) return nil }, Flags: []cli.Flag{ &cli.StringFlag{Destination: &oper, Name: "oper", Value: "add", Usage: "add, sub, mul or div operations on two int"}, &cli.IntFlag{Destination: &opt1, Name: "opt1", Value: 0, Usage: "first int"}, &cli.IntFlag{Destination: &opt2, Name: "opt2", Value: 0, Usage: "second int"}, }, } } func mulCommand() *cli.Command { return &cli.Command { Name: "mul", Aliases: []string{"m"}, Action: func(ctx *cli.Context) error { res := opt1 * opt2 fmt.Printf("%v * %v = %v", opt1, opt2, res) return nil }, Flags: []cli.Flag{ &cli.StringFlag{Destination: &oper, Name: "oper", Value: "add", Usage: "add, sub, mul or div operations on two int"}, &cli.IntFlag{Destination: &opt1, Name: "opt1", Value: 0, Usage: "first int"}, &cli.IntFlag{Destination: &opt2, Name: "opt2", Value: 0, Usage: "second int"}, }, } }
$ go run main.go add --opt1 1 --opt2 2 1 + 2 = 3
引数の順番が自然になりました。
又、ヘルプを見るとGlobal Optionsからフラグ引数の案内が消え、サブコマンドのヘルプに表示されるようになっています。
$ go run main.go add -h NAME: main add - USAGE: main add [command options] [arguments...] OPTIONS: --oper value add, sub, mul or div operations on two int (default: "add") --opt1 value first int (default: 0) --opt2 value second int (default: 0) --help, -h show help (default: false)
5. フラグ引数を省く
これでフラグ引数をサブコマンドの後に実行できる様になりました。
が、果たして計算機アプリにフラグ引数は必要なのでしょうか?態々opt1 opt2
の様に必要な引数を説明せずともuserは分かってくれるはずです。寧ろ--optn
と打ち込む手間の方が煩わしいと思います。
又、計算機アプリなのに与えられる引数の個数が限定されているのも宜しくありません。
go run main.go add 1 2 3 1 + 2 + 3 = 6
この様に実行できるのが理想ではないでしょうか
そこで引数の受け取り方を変えてみます。
func addCommand() *cli.Command { return &cli.Command{ Name: "add", Aliases: []string{"a"}, Action: func(ctx *cli.Context) error { // 引数の数 n := ctx.NArg() if n == 0 { return fmt.Errorf("no argument provided for add operation") } a := ctx.Args().Get(0) res, _ := strconv.Atoi(a) // 一つ目の値をprint fmt.Print(res) // 二つ以上引数があった場合、順繰りにprint for i := 1; i < n; i++ { a := ctx.Args().Get(i) op, _ := strconv.Atoi(a) res += op fmt.Printf(" + %v", op) } // 結果 fmt.Printf(" = %v", res) return nil }, //Flags: []cli.Flag{ // &cli.StringFlag{Destination: &oper, Name: "oper", Value: "add", Usage: "add, sub, mul or div operations on two int"}, // &cli.IntFlag{Destination: &opt1, Name: "opt1", Value: 0, Usage: "first int"}, // &cli.IntFlag{Destination: &opt2, Name: "opt2", Value: 0, Usage: "second int"}, //}, } }
cli.Flagを全て消し、代わりにctx.Args()メソッドで引数を一つづつ取り出す方式に変更しました。
実行してみます
$ go run main.go add 1 2 1 + 2 = 3 $ go run main.go mul 1 2 3 -1 1 * 2 * 3 * -1 = -6 $ go run main.go a 1 4 10 -1 1 + 4 + 10 + -1 = 14
alias含め複数の引数を渡しても正常に動作している事が分かります。
最後に
引数が与えられなかった場合もヘルプが表示されるようにしましょう
package main import ( "fmt" "github.com/urfave/cli/v2" "os" "strconv" ) func main() { app := cli.NewApp() app.Name = "mycalc" app.Usage = "The best calculator" app.Description = "this is calculator<ffff>" app.Action = mainAction app.Commands = []*cli.Command{ addCommand(), mulCommand(), } app.Run(os.Args) } func addCommand() *cli.Command { return &cli.Command{ Name: "add", Aliases: []string{"a"}, Action: func(ctx *cli.Context) error { // 引数の数 n := ctx.NArg() if n == 0 { return fmt.Errorf("no argument provided for add operation") } a := ctx.Args().Get(0) res, _ := strconv.Atoi(a) fmt.Print(res) for i := 1; i < n; i++ { a := ctx.Args().Get(i) op, _ := strconv.Atoi(a) res += op fmt.Printf(" + %v", op) } fmt.Printf(" = %v", res) return nil }, } } func mulCommand() *cli.Command { return &cli.Command { Name: "mul", Aliases: []string{"m"}, Action: func(ctx *cli.Context) error { n := ctx.NArg() if n == 0 { return fmt.Errorf("no argument provided for add operation") } a := ctx.Args().Get(0) res, _ := strconv.Atoi(a) fmt.Print(res) for i := 1; i < n; i++ { a := ctx.Args().Get(i) op, _ := strconv.Atoi(a) res = res * op fmt.Printf(" + %v", op) } fmt.Printf(" = %v", res) return nil } } func mainAction(ctx * cli.Context) error { ctx.App.Command("help").Run(ctx) return nil }
まとめ
urfave
を使えば既存アプリを簡単にcli化する事ができます。
同系統のライブラリとしてcobra
も人気ですが、個人的にはurfave
のインターフェースの方がシンプルで好きです。