Golangの標準パッケージで簡単なCLIツールを作ってみる

## やること

Golangの標準パッケージであるflagを使って簡単なコマンドラインツールを作ってみます。 

## flagパッケージ

flagはオプションの解析やサブコマンド、ヘルプメッセージなど
コマンドラインツールの処理を簡単に実装できるような機能が提供されています。  

まずは基本的な使い方から

```go
package main

import (
  "fmt"
  "flag"
)

func main() {
  // 左から順にオプション名、デフォルトの値、helpテキストが引数に入る
  s := flag.String("s", "Hello, world!", "String help message")
  flag.Parse()
  fmt.Println(*s)
}
```

実行結果

```bash
$ go run example.go
Hello, world!

# -sオプションを付与して実行してみる
$ go run example.go -s foobar
foobar

# -hでヘルプメッセージが表示される
$ go run example.go -h
Usage of example:
  -s string
    	String help message (default "Hello, world!")
exit status 2
```

#### 型へのマッピング

flagパケージにはコマンドラインから入力された値をGoの型へマッピングする機能があります。

```go
func main() {
  strCmd := flag.String("s", "Hello, world!", "String help message")
  intCmd := flag.Int("i", 1, "Int help message")
  boolCmd := flag.Bool("b", false, "Bool help message")

  flag.Parse()

  fmt.Println("str: ", *s)
  fmt.Println("int: ", *i)
  fmt.Println("bool: ", *b)
}
```

実行結果

```bash
$ go run example.go
str: Hello, world!
int: 1
bool: false

# それぞれのオプションを付与して実行する
$ go run example.go -s foobar -i 26 -b
str: foobar
int: 26
bool: true

# -iはint型を受け取るので整数以外を渡すとエラーになります
$ go run example.go -i foo
invalid value "foo" for flag -i: strconv.ParseInt: parsing "foo": invalid syntax
Usage of example:
  -b	Bool help message
  -i int
    	Int help message (default 1)
  -s string
    	String help message (default "Hello, world!")
exit status 2
```

#### ヘルプメッセージのカスタマイズ

ヘルプメッセージをより細かく書くこともできます。  

```go
import (
  "fmt"
  "os"
  "flag"
)

func main() {
  flag.Usage = func() {
    usageTxt := `Usage example [option]
An example of customizing usage output

    -s, --s  STRING argument, default: String help message
    -i, --i  INTEGER argument, default: Int help message
    -b, --b  BOOLEAN argument, default: Bool help message`
    
    fmt.Fprintf(os.Stderr, "%s\n", usageTxt)
  }
  // 以下略
}
```

実行結果  

```bash
$ go run example.go -h
Usage example [option]
An example of customizing usage output

    -s, --s  STRING argument, default: String help message
    -i, --i  INTEGER argument, default: Int help message
    -b, --b  BOOLEAN argument, default: Bool help message
exit status 2
```

#### サブコマンドを作成する

`git clone`のようなサブコマンドを簡単に作れる機能も提供されています。  

```go
// 最初の引数にサブコマンドの名前を、2番目にエラーハンドリングを指定する
subCmd := flag.NewFlagSet("subcmd", flag.ExitOnError)

// エラーハンドラの種類
// flag.ContinueOnError => エラーが発生しても処理を続ける
// flag.ExitOnError     => エラーが発生したらステータスコード2として終了する
// flag.PanicOnError    => ランタイムパニックを発生させる
```

## なんか作ってみる

上記の機能を利用して作成したものが以下になります。 
機能的には10進数を2進数か16進数へ変換するだけです。

```go
package main

import (
  "fmt"
  "flag"
  "os"
  "strconv"
)

func flagUsage() {
  usageText := `example is an example cli tool.

Usage:
example command [arguments]
The commands are:
convhex    convert Number to Hex
convbinary convert Number to binary
Use "Example [command] --help" for more infomation about a command`

  fmt.Fprintf(os.Stderr, "%s\n\n", usageText)
}

func main() {
  flag.Usage = flagUsage
  convHexCmd := flag.NewFlagSet("convhex", flag.ExitOnError)
  convBinaryCmd := flag.NewFlagSet("convbinary", flag.ExitOnError)

  if len(os.Args) == 1 {
    flag.Usage()
    return
  }

  switch os.Args[1] {
  case "convhex":
      i := convHexCmd.Int64("i", 0, "Convert number to hex")
      convHexCmd.Parse(os.Args[2:])
      fmt.Println(strconv.FormatInt(*i, 16))
    case "convbinary":
      i := convBinaryCmd.Uint64("i", 0, "convert number to binary")
      convBinaryCmd.Parse(os.Args[2:])
      fmt.Println(strconv.FormatUint(*i, 2))
    default:
      flag.Usage()
  }
}
```

実行結果

```bash
$ go run example.go convhex -i 11111
2b67
$ go run example.go convbinary -i 151
10010111

$ go run example.go --help
example is an example cli tool.

Usage:
example command [arguments]
The commands are:
convhex    convert Number to Hex
convbinary convert Number to binary
Use "Example [command] --help" for more infomation about a command
```