pythonとgolangでcliツールを作ってみる

この記事はデジタルキューブグループ エンジニアチームアドベントカレンダー2024 12月10日の記事です。

https://qiita.com/advent-calendar/2024/digitalcube-heptagon

# 取り組みの経緯

今後プラットフォームエンジニアリングといった動きを強めたいと思った場合に、社内向けツールなどを作る機会も増えるかもしれない

https://www.gartner.co.jp/ja/articles/what-is-platform-engineering

普段システムの開発や保守を行わない人たちに対してはwebアプリとして提供したり、また、インフラチームなどに対してはcliから使えるものの方が都合のよい場合もある

普段はpythonでのバックエンド開発が多いため、pythonでのcliツール作成を試してみる
また、linux環境などでも使いやすいバイナリとして生成できるgolangでのcliツール作成も試す

goを触るのが初めてなので、cliツールのサンプル作成を通してgoも完全に理解する

# 自分のバックグラウンド

開発では以下の言語に触れてきた経験が多い
- node(typescript)
- python

なので、自分の頭の整理がてら、この辺の言語で使われてる仕組みと比較しながら理解していくメモ書きがあると思う

# 環境

- python 3.12.2
- golang 1.23.4



# python版

clickというライブラリを使うと手軽でいいっぽい

https://click.palletsprojects.com/en/stable/

以下の感じで普通にインストールすれば使える

```bash
$ pip install click
```

clickが無難にポピュラーそうなので使ってみたが、標準ライブラリの `argparse` を使ったりするケースもありそうだし、他の競合として `fire` 、 `typer` なんてのもあるっぽい

https://google.github.io/python-fire/guide/

https://github.com/fastapi/typer

githubのスター数見るとfireが結構人気っぽいのでfireもそのうち試してみる

フォルダ構成は以下

```
.
├── cli_python.py
└── clipython
```

`clipython` は `cli_python.py` へのシンボリックリンク

本来、実行コマンドは以下となっている

```bash:いくつかの例
$ python cli_python.py --help
$ python cli_python.py hello --help
$ python cli_python.py goodbye -n Sugo
```

シンボリックリンクを設置することで以下のように実行できるようにしている

```bash:いくつかの例
$ ./clipython --help
$ ./clipython hello --help
$ ./clipython goodbye -n Sugo
```

サブコマンドとしてhelloとgoodbyeが実行できるようになっている

```python:cli_python.py
#!/usr/bin/env python

import click


@click.group()
def cli():
    pass


@cli.command(help="こんにちはを誰かに言う")
@click.option("-n", "--name", type=str, help="Specify your name")
def hello(name):
    click.echo(f"Hello {name}")


@cli.command(help="さよならを誰かに言う")
@click.option("-n", "--name", type=str, help="Specify the name of the other party")
def goodbye(name):
    click.echo(f"Goodby {name}")


def main():
    cli()


if __name__ == "__main__":
    main()

```

以下は実際にコマンドを実行した例

```bash
$ ./clipython --help
Usage: clipython [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  goodbye  さよならを誰かに言う
  hello    こんにちはを誰かに言う


$ ./clipython hello --help
Usage: clipython hello [OPTIONS]

  こんにちはを誰かに言う

Options:
  -n, --name TEXT  Specify your name
  --help           Show this message and exit.


$ ./clipython goodbye -n Sugo
Goodbye Sugo
```


# golang版

cobraというcliツール作成のためのフレームワーク的なのを使うのがいいっぽい
また、cobra-cliをインストールして使えるようにし、それでinitするとベースのものがいい感じに用意できるっぽい

https://github.com/spf13/cobra

とりあえず始めてみる

```bash
$ go mod init cli_go
$ go get -u github.com/spf13/cobra@latest
$ go install github.com/spf13/cobra-cli@latest
$ cobra-cli init --license MIT --viper=false
```

`go mod init` がまだよくわからないし、たぶん命名規則もちょっと守れてなさそうではある
`go get` が `npm i` 相当なものなんだろうということは理解できた
`go install` もあまりわかっていない、ツールのグローバルインストールなイメージ

この段階で以下のような状態になる

```
.
├── main.go
├── cmd/
│   └── root.go
├── go.mod
├── go.sum
└── LICENSE
```

ちょっと雑だけど、 `go.mod` は `package.json` 、 `go.sum` は `package-lock.json` くらいのイメージ

一旦デフォの状態で動くか見てみる

```bash
$ go run main.go
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

```

動くのでサブコマンドを実行できるよう続きを進める


```bash
$ cobra-cli add hello
$ cobra-cli add goodbye
```

この段階で以下のような状態になる

```
.
├── main.go
├── cmd/
│   ├── root.go
│   ├── hello.go
│   └── goodbye.go
├── go.mod
├── go.sum
└── LICENSE
```

各サブコマンドを編集していく

```go:hello.go
package cmd

import (
	"fmt"
	"github.com/spf13/cobra"
)

var helloCmd = &cobra.Command{
	Use:   "hello",
	Short: "こんにちはを誰かに言う",
	Long: `こんにちはを誰かに言う
ことを長く説明する箇所`,
	Run: func(cmd *cobra.Command, args []string) {
		flag, _ := cmd.Flags().GetString("name")
		greet := fmt.Sprintf("Hello %s", flag)
		fmt.Println(greet)
	},
}

func init() {
	rootCmd.AddCommand(helloCmd)
	helloCmd.Flags().StringP("name", "n", "ANONYMOUS", "Specify your name")
}

```

```go:goodbye.go
package cmd

import (
	"fmt"
	"github.com/spf13/cobra"
)

var goodbyeCmd = &cobra.Command{
	Use:   "goodbye",
	Short: "さよならを誰かに言う",
	Long: `さよならを誰かに言う
ことを長く説明する箇所`,
	Run: func(cmd *cobra.Command, args []string) {
		flag, _ := cmd.Flags().GetString("name")
		greet := fmt.Sprintf("Goodbye %s", flag)
		fmt.Println(greet)
	},
}

func init() {
	rootCmd.AddCommand(goodbyeCmd)
	goodbyeCmd.Flags().StringP("name", "n", "ANONYMOUS", "Specify the name of the other party")
}
```

実行結果が以下

```bash
$ go run main.go hello
Hello ANONYMOUS

$ go run main.go goodbye
Goodbye ANONYMOUS

$ go run main.go hello --name Sugo
Hello Sugo

$ go run main.go hello -n Sugo
Hello Sugo

$ go run main.go goodbye --name Sugo
Goodbye Sugo

$ go run main.go goodbye -n Sugo
Goodbye Sugo
```

コマンドの説明もついでなのでちょっと書き換えてみる

```diff_go:root.go
package cmd

import (
	"os"

	"github.com/spf13/cobra"
)



var rootCmd = &cobra.Command{
	Use:   "cli_go",
- 	Short: "A brief description of your application",
- 	Long: `A longer description that spans multiple lines and likely contains
- examples and usage of using your application. For example:
- 
- Cobra is a CLI library for Go that empowers applications.
- This application is a tool to generate the needed files
- to quickly create a Cobra application.`,
+ 	Short: "コマンド全体の短い説明文",
+ 	Long: `コマンド全体の長い説明文`,
}

func Execute() {
	err := rootCmd.Execute()
	if err != nil {
		os.Exit(1)
	}
}

func init() {
	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
```


ビルドしてみる

```bash
$ go build
```

実行してみる

```bash
$ ./cli_go
コマンド全体の長い説明文

Usage:
  cli_go [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  goodbye     さよならを誰かに言う
  hello       こんにちはを誰かに言う
  help        Help about any command

Flags:
  -h, --help     help for cli_go
  -t, --toggle   Help message for toggle

Use "cli_go [command] --help" for more information about a command.

$ ./cli_go hello
Hello ANONYMOUS

$ ./cli_go hello -n Sugo
Hello Sugo

$ ./cli_go goodbye
Goodbye ANONYMOUS

$ ./cli_go goodbye --name Sugo
Goodbye Sugo
```

# まとめ

完全に理解した

# 感想

click（python）で作成した方はpythonの実行環境が必要なため、アプリケーションサーバーなどにsshで入って運用/保守などで使うのであれば作るのも手軽でよさそう
pythonの実行環境によってはうまく動作しない可能性もあると思うので、各自に配布してローカルから使ってもらうなどといった場合には向かなさそう

逆にgolang版はバイナリを配るだけでよく、プラットフォーム的な制約は少なさそう
~~ただ、（まだ入門段階だからかもしれないが）golangの開発環境の整備や開発がなかなか面倒には感じた~~
↑
最初はこう思ったけど、ちょっと書いたり調べたりしてるとgolangの書き心地は割といいかもしれない、開発環境整備ももう少し理解したら案外すんなりかも



# 補足

これが地味に助かった

https://tree.nathanfriend.com/

# 参考

https://zenn.dev/skport/articles/ef3b645510a6a5

https://zenn.dev/tama8021/articles/22_0627_go_cobra_cli

https://zenn.dev/optimisuke/articles/105feac3f8e726830f8c

https://www.tohoho-web.com/ex/golang.html

https://qiita.com/nirasan/items/cc2ab5bc2889401fe596

https://zenn.dev/tetoteto/articles/927a8c562f53e2