【Golang】urfave/cliを使って計算機アプリをcliにする

Urfave

golangcli作成ライブラリです。

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のインターフェースの方がシンプルで好きです。