tweeeetyのぶろぐ的めも

アウトプットが少なかったダメな自分をアウトプット<br>\(^o^)/

【go】golangのCLIパッケージ(urfave/cli)を使ってみるメモ

はじめに

command line cliなscriptを作りたいとき、
たいていどの言語でも引数を扱うライブラリがありますよね。

golangではとても便利なurfave/cliというパッケージがあります。
(以前は github.com/codegangsta/cli というリポジトリでした)

この使い方を簡単にメモ

アジェンダ

  1. getting start的な
  2. Arguments とか
  3. Flags 使ってみる
  4. Subcommands でいろいろ

その前に

この記事に使用したソースです。
https://github.com/tweeeety/go-command-line-sample/tree/master/src/script

1. getting start的な

install

go getするだけです。
glide使ってればglide installするだけですね。

$ go get github.com/urfave/cli

使ってみる

前提

これ以降の記述は、すべて以下のようなdir構成になっているものとして進めます。
今回の記事ようにgo-command-line-sampleというプロジェクト ディレクトリを作ってます。

# 作成したgo-command-line-sample
$ pwd
パス/go-command-line-sample

$ tree -L 3 ../go-command-line-sample
../go-command-line-sample
└── src
    └── script # この配下にscriptごとにdirを切る
        ├── glide.lock
        ├── glide.yaml
        └── vendor
簡単に試す

まずはprintするだけ、help出すだけ。
公式と同じですね

src/script/cli_10/main.go
package main

import (
  "10t"
  "os"

  "github.com/urfave/cli"
)

func main() {
  app := cli.NewApp()
  app.Name = "boom"
  app.Usage = "make an explosive entrance"

  app.Action = func(c *cli.Context) error {
    fmt.Println("boom! I say!")
    return nil
  }

  app.Run(os.Args)
}
実行
# 叩いてみる
$ go run src/script/cli_10/main.go 
boom! I say!

# helpってみる
$ go run src/script/cli_10/main.go help
NAME:
   boom - make an explosive entrance

USAGE:
   main [global options] command [command options] [arguments...]

VERSION:
   0.0.0

COMMANDS:
     help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help
   --version, -v  print the version
go install

おもむろにgo installしてみます。

# scriptがいる場所まで移動
$ cd src/script/cli_10

# install
$ go instal

# プロジェクト直下にもどる
$ cd パス/go-command-line-sample

# binにcli_10が出来てる
$ tree -L 3
.
├── bin
│   └── cli_10
├── pkg
│   └── darwin_amd64
│       └── script
└── src
    └── script
        ├── cli_10
        ├── glide.lock
        ├── glide.yaml
        └── vendor

# たたいてみる
$ ./bin/cli_10
boom! I say!

ここまで動かすのもめっちゃ簡単ですね!

2. Arguments とか

app := cli.NewApp() した後の基本的な使い方です。

src/script/cli_20/main.go
package main

import (
  "fmt"
  "os"

  "github.com/urfave/cli"
)

func main() {
  app := cli.NewApp()
  app.Name = "cli_20"
  app.Usage = "cli_20 sample"
  app.Version = "1.2.1"

  // before
  app.Before = func(c *cli.Context) error {
    fmt.Println("-- Before --")
    return nil
  }

  // action
  app.Action = func(c *cli.Context) error {
    fmt.Println("-- Action --")

    fmt.Printf("c.NArg()        : %+v\n", c.NArg())
    fmt.Printf("c.Args()        : %+v\n", c.Args())
    fmt.Printf("c.Args().Get(0) : %+v\n", c.Args().Get(0))
    fmt.Printf("c.Args()[0]     : %+v\n", c.Args()[0])
    fmt.Printf("c.FlagNames     : %+v\n", c.FlagNames())

    // Help表示
    //cli.ShowAppHelp(c) 

    // version表示
    cli.ShowVersion(c) 

    return nil
  }

  // after
  app.After = func(c *cli.Context) error {
    fmt.Println("-- After --")
    return nil
  }

  // true: go run app.go helpと打ってもhelpが出なくなる
  app.HideHelp = true

  app.Run(os.Args)
}
実行
# そのまま実行
$ go run src/script/cli_20/main.go hoge fuga piyo
-- Before --
-- Action --
c.NArg()        : 3
c.Args()        : [hoge fuga piyo]
c.Args().Get(0) : hoge
c.Args()[0]     : hoge
c.FlagNames     : []
cli_20 version 1.2.1
-- After --

# help
$ go run main.go help
-- Before --
NAME:
   cli_20 - cli_20 sample

USAGE:
   main [global options] command [command options] [arguments...]

VERSION:
   1.2.1

COMMANDS:
     help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help
   --version, -v  print the version
-- After --


# `app.HideHelp = true` なのでhelpは引数として見なされる
$ go run src/script/cli_20/main.go help
-- Before --
-- Action --
c.NArg()        : 1
c.Args()        : [help]
c.Args().Get(0) : help
c.Args()[0]     : help
c.String()      : 
cli_20 version 1.2.1
-- After --
context

c.XXXXで取れるcontextについてはこの辺を見るとかなりたくさんありますね。
https://godoc.org/github.com/urfave/cli#Context

3. Flags 使ってみる

flagsは-lang english-l english的なオプションを受け取るためのものです。
ショートオプションや省略時のdefault optionも指定できます。

一瞬argumentsと混同しましたが、

使ってみる

src/script/cli_30/main.go
package main

import (
  "fmt"
  "os"

  "github.com/urfave/cli"
)

func main() {
  app := cli.NewApp()
  app.Name = "cli_30"
  app.Usage = "cli_30 sample"
  app.Version = "1.2.1"

  os.Setenv("SAMPLE_ENV", "sample env dayo")

  // flags
  app.Flags = []cli.Flag{
    cli.StringFlag{
      Name:  "lang, l",
      Value: "english",
      Usage: "language for the greeting",
    },
    cli.StringFlag{
      Name:  "meridian, m",
      Value: "AM",
      Usage: "meridian for the greeting",
    },
    cli.StringFlag{
      Name:  "time, t",
      Value: "07:00",
      // ``で囲むとhelp時のPlaceholderとしても使える
      // https://github.com/urfave/cli#placeholder-values
      Usage: "`your time` for the greeting",
    },
    cli.StringFlag{
      Name:  "aaa, a",
      Value: "sample",
      // default値をValueからではなくEnvから取る
      EnvVar: "SAMPLE_ENV",
    },
  }

  // action
  app.Action = func(c *cli.Context) error {
    fmt.Println("-- Action --")

    fmt.Printf("c.GlobalFlagNames() : %+v\n", c.GlobalFlagNames())
    fmt.Printf("c.String(\"lang\")    : %+v\n", c.String("lang"))
    fmt.Printf("c.String(\"m\")       : %+v\n", c.String("m"))
    fmt.Printf("c.String(\"time\")    : %+v\n", c.String("time"))
    fmt.Printf("c.String(\"a\")       : %+v\n", c.String("a"))

    return nil
  }

  app.Run(os.Args)
}
実行
# 実行
# 引数のkey:valueはスペースでもイコールでもいける
$ go run src/script/cli_30/main.go -l hoge -m=PM
-- Action --
c.GlobalFlagNames() : [aaa lang meridian time]
c.String("lang")    : hoge
c.String("m")       : PM
c.String("time")    : 07:00
c.String("a")       : sample env dayo

# help
$ go run src/script/cli_30/main.go -h
NAME:
   cli_30 - cli_30 sample

USAGE:
   main [global options] command [command options] [arguments...]

VERSION:
   1.2.1

COMMANDS:
     help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --aaa value, -a value           (default: "sample") [$SAMPLE_ENV]
   --lang value, -l value          language for the greeting (default: "english")
   --meridian value, -m value      meridian for the greeting (default: "AM")
   --time your time, -t your time  your time for the greeting (default: "07:00")
   --help, -h                      show help
   --version, -v                   print the version

Flagの種類

Flagとして設定できる種類がいくつかあるのでその例です。
StringFlagBoolFlagが指定できます。

src/script/cli_31/main.go
package main

import (
  "fmt"
  "os"

  "github.com/urfave/cli"
)

func main() {
  app := cli.NewApp()
  app.Name = "cli_31"
  app.Usage = "cli_31 sample"
  app.Version = "1.2.1"

  // flags
  app.Flags = []cli.Flag{
    // StringFlag
    cli.StringFlag{
      Name:  "name, n",
      Value: "tarou",
      Usage: "your name",
    },
    // BoolFlag
    cli.BoolFlag{
      Name:  "gay, g",
      Usage: "are you gay boy?",
    },
  }

  // action
  app.Action = func(c *cli.Context) error {
    fmt.Println("-- Action --")
    fmt.Printf("c.GlobalFlagNames() : %+v\n", c.GlobalFlagNames())
    fmt.Printf("c.String(\"name\")    : %+v\n", c.String("name"))
    fmt.Printf("c.String(\"g\")       : %+v\n", c.Bool("g"))

    return nil
  }

  app.Run(os.Args)
}

実行
# 指定無しで叩いてみる
$ go run src/script/cli_31/main.go 
-- Action --
c.GlobalFlagNames() : [name gay]
c.String("name")    : tarou
c.String("g")       : false

# 指定有りで叩いてみる
# BoolFlagは、オプションを指定することでtrueとなるFlag
$ go run src/script/cli_31/main.go --name hoge -g
-- Action --
c.GlobalFlagNames() : [name gay]
c.String("name")    : hoge
c.String("g")       : true

4. Subcommands でいろいろ

公式の通りですが、
git-like のようなコマンドが設定できます。

普通に使ってみる

まずは公式のお試し

src/script/cli_40/main.go
package main

import (
  "fmt"
  "os"

  "github.com/urfave/cli"
)

func main() {
  app := cli.NewApp()
  app.Name = "cli_40"
  app.Usage = "cli_40 sample"

  // command action
  // これまでのActionとは違い、flagsごとの挙動(Action)が設定できる
  app.Commands = []cli.Command{
    // `go run cli_40/main.go add パラメータ`でactionするコマンド
    {
      Name:    "add",
      Aliases: []string{"a"},
      Usage:   "add a task to the list",
      Action: func(c *cli.Context) error {
        fmt.Println("added task: ", c.Args().First())
        return nil
      },
    },
    // `go run cli_40/main.go complete パラメータ`でactionするコマンド
    {
      Name:    "complete",
      Aliases: []string{"c"},
      Usage:   "complete a task on the list",
      Action: func(c *cli.Context) error {
        fmt.Println("completed task: ", c.Args().First())
        return nil
      },
    },
    {
      Name:    "template",
      Aliases: []string{"t"},
      Usage:   "options for task templates",
      Subcommands: []cli.Command{
        // `go run cli_40/main.go template add パラメータ`でactionするコマンド
        {
          Name:  "add",
          Usage: "add a new template",
          Action: func(c *cli.Context) error {
            fmt.Println("new task template: ", c.Args().First())
            return nil
          },
        },
        // `go run cli_40/main.go template remove パラメータ`でactionするコマンド
        {
          Name:  "remove",
          Usage: "remove an existing template",
          Action: func(c *cli.Context) error {
            fmt.Println("removed task template: ", c.Args().First())
            return nil
          },
        },
      },
    },
  }

  app.Run(os.Args)
}

実行

実行してみるとこんな感じ

# addコマンドとパラメータで実行
$ go run src/script/cli_40/main.go t add hoge
added task:  hoge

# t(template)コマンドとaddサブコマンドで実行
$ go run src/script/cli_40/main.go t t add hoge
new task template:  hoge

$ go run src/script/cli_40/main.go -h
NAME:
   cli_04 - cli_04 sample

USAGE:
   main [global options] command [command options] [arguments...]

VERSION:
   0.0.0

COMMANDS:
     add, a       add a task to the list
     complete, c  complete a task on the list
     template, t  options for task templates
     help, h      Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help
   --version, -v  print the version

COMMANDStemplateのSubcommandsが表示されないじゃないかーと思いますが
templateまで打って-hすればちゃんと出ます。

かしこい!

$ go run cli_40/main.go t -h
NAME:
   cli_04 template - options for task templates

USAGE:
   cli_04 template command [command options] [arguments...]

COMMANDS:
     add     add a new template
     remove  remove an existing template

OPTIONS:
   --help, -h  show help

before/action/afterとの関係を見る

COMMANDS(のAction)とbefore/action/after らへんはどういう関係で動くか。

src/script/cli_41/main.go
import (
  "fmt"
  "os"

  "github.com/urfave/cli"
)

func main() {
  app := cli.NewApp()
  app.Name = "cli_41"
  app.Usage = "cli_41 sample"

  // before
  app.Before = func(c *cli.Context) error {
    fmt.Println("-- Before --")
    return nil
  }

  // command action
  app.Commands = []cli.Command{
    // `go run cli_04/main.go sample パラメータ`でactionするコマンド
    {
      Name:    "sample",
      Aliases: []string{"s"},
      Usage:   "sample task",
      Action: func(c *cli.Context) error {
        fmt.Println("-- Sample Action --")
        fmt.Println("sample task: ", c.Args().First())
        return nil
      },
    },
  }

  // action
  app.Action = func(c *cli.Context) error {
    fmt.Println("-- Nomal Action --")
    return nil
  }

  // after
  app.After = func(c *cli.Context) error {
    fmt.Println("-- After --")
    return nil
  }
  app.Run(os.Args)
}
実行

COMMANDSが渡されたときはapp.Actionは実行されず、
COMMANDSが何も渡されないとapp.Actionが実行されます。

$ go run src/script/cli_41/main.go sample hoge
-- Before --
-- Sample Action --
sample task:  hoge
-- After --

$ go run src/script/cli_41/main.go
-- Before --
-- Nomal Action --
-- After --

c.Commandを見てみる

COMMANDS に指定したAction: func内で使えるc.Commandを見てみます。
こんなのが使えるよー程度ですね。

src/script/cli_42/main.go
package main

import (
  "fmt"
  "os"

  "github.com/urfave/cli"
)

func main() {
  app := cli.NewApp()
  app.Name = "cli_42"
  app.Usage = "cli_42 sample"

  // command action
  app.Commands = []cli.Command{
    {
      Name:    "sample",
      Aliases: []string{"s"},
      Usage:   "sample task",
      Action: func(c *cli.Context) error {
        fmt.Println("-- Sample Action --")

        fmt.Printf("c.Command.FullName()        : %+v\n", c.Command.FullName())
        fmt.Printf("c.Command.HasName(\"sample\") : %+v\n", c.Command.HasName("sample"))
        fmt.Printf("c.Command.Names()           : %+v\n", c.Command.Names())
        fmt.Printf("c.Command.VisibleFlags()    : %+v\n", c.Command.VisibleFlags())

        return nil
      },
    },
  }

  app.Run(os.Args)
}
実行
$ go run src/script/cli_42/main.go sample hoge
-- Sample Action --
c.Command.FullName()        : sample
c.Command.HasName("sample") : true
c.Command.Names()           : [sample s]
c.Command.VisibleFlags()    : [--help, -h show help]

Subcommands x Flags

次はSubcommands x Flagsの例です。
Flags単独の例は前述してますが、各Subcommandsごとにも設定できます。

src/script/cli_43/main.go
import (
  "fmt"
  "os"

  "github.com/urfave/cli"
)

func main() {
  app := cli.NewApp()
  app.Name = "cli_43"
  app.Usage = "cli_43 sample"

  // command action
  app.Commands = []cli.Command{
    // addコマンドに対してFlagsを設定
    {
      Name:    "add",
      Aliases: []string{"a"},
      Usage:   "add a task to the list",
      Action: func(c *cli.Context) error {
        fmt.Println("added task: ", c.Args().First())
        fmt.Println("json      : ", c.String("j"))
        fmt.Println("exec      : ", c.String("e"))
        return nil
      },
      Flags: []cli.Flag{
        cli.StringFlag{
          Name:  "json, j",
          Value: "add.json",
        },
        cli.BoolFlag{
          Name: "exec, e",
        },
      },
    },
    // completeコマンドに対してFlagsを設定
    {
      Name:    "complete",
      Aliases: []string{"c"},
      Usage:   "complete a task on the list",
      Action: func(c *cli.Context) error {
        fmt.Println("completed task: ", c.Args().First())
        fmt.Println("csv           : ", c.String("c"))
        fmt.Println("exec          : ", c.String("e"))
        return nil
      },
      Flags: []cli.Flag{
        cli.StringFlag{
          Name:  "csv, c",
          Value: "add.json",
        },
        cli.BoolFlag{
          Name: "exec, e",
        },
      },
    },
  }
  app.Run(os.Args)
}
実行
# addコマンドを実行。addコマンドに指定したFlagsを指定
$ go run src/script/cli_43/main.go add -j test.json -e
added task:  
json      :  test.json
exec      :  true

# addコマンドを実行。completeコマンドに指定したFlagsを指定
# オプションが違うよ、という事でhelpが出る
$ go run src/script/cli_43/main.go add -c test.csv -e
Incorrect Usage: flag provided but not defined: -c

NAME:
   main add - add a task to the list

USAGE:
   main add [command options] [arguments...]

OPTIONS:
   --json value, -j value  (default: "add.json")
   --exec, -e              

# completeコマンドを実行。addコマンドに指定したFlagsを指定
$ go run src/script/cli_43/main.go complete -c test.csv
completed task:  
csv           :  test.csv
exec          :  false

おわり

urfave/cli いろいろてんこもりで便利ですね。
ここまでがっつり機能いらないよって場合は、標準のflag パッケージの方を使ってサクっと作るのも良さそうです
\(^o^)/