🥴

【Golang】1日でGoのcobraでサクッとCLIが作れちゃった話

2022/06/27に公開約7,700字2件のコメント

作ったもの

リポジトリ

初めに

作った背景

技育博という学生のIT団体が交流するイベントがありまして、
その前日に「イベント用に何か作ろう!」ということで作り始めたというのが開発の背景といなります。
まさかのイベント前日に「1日ハッカソン」が開催されたというわけです、(笑)

ちなみに自分は今回紹介するGoでCLIを作る訳になったんですけど、この段階でGo言語を使った経験がバリバリにあるわけでもなく、
チュートリアルを少し触った程度であり、直近の自分のGoのリポジトリを見てみたら最後の更新が昨年の10月だったということもあり約8ヶ月ぶりにGoを触るという、、

このことから伝えたいことは、俺すごいだろ!ではなく、思ったより楽に見栄えがいい制作物が作れるよ!であり、LTなど発表する機会がある時に、時間も発表する材料もないという時にオススメじゃん!ということが伝わればいいなと思ってます!

GoでCLIってどんなして作るの??

色々なフレームワークなどがあるみたいですけど、色々と調べた限り下記3つ!

この3つの選択肢の中から今回はcobraを使ってCLIを作成することに決めました!
選んだ理由は2つ

  • 有名どころにも使われているらしく、参考になる記事が比較的多い
  • ボイラーテンプレートが使えて最初に簡単な雛形を作ってくれる!
    • ライセンスの作成などもやってくれる

go install go getなどまだまだ勉強が足りずどちらを使えばいいのかわからないので、状況に合してうまくできなかったらどっちかを試してみるなどやってみてください。参考

やっていく

Goのインストール・環境構築

自分の環境ではGoのインストールは行っていなかったので、VSCodeのRemote Containerを使って、ポチ、ポチ、ポチでGoの環境を作りました!
こんな感じで簡単に立ち上げができる

$ mkdir myapp
$ cd myapp
// go modの作成
$ go mod init <importpath>

cobra・cobra-cliのインストール

cobra-cli is a command line program to generate cobra applications and command files. It will bootstrap your application scaffolding to rapidly develop a Cobra-based application. It is the easiest way to incorporate Cobra into your application.

訳)cobra-cli は cobra アプリケーションとコマンドファイルを生成するための コマンドライン・プログラムです。これは、Cobraベースのアプリケーションを迅速に開発するために、アプリケーションの雛形をブートストラップします。

$ go install github.com/spf13/cobra-cli@latest
$ go get -u github.com/spf13/cobra@latest

cobra-cli init

# コマンドのボイラーテンプレートを作成。
# viper=trueにすれば設定ファイル読み込み機能ありなものになる。
$ cobra init --license MIT --viper=false

こんな感じの雛形が出来上がる

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

コマンド実行

最低限の準備ができたのでまずは動くか試してみる。うまく動けばデフォルトのディスクリプションが表示される。

$ 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.

サブコマンドの追加

// cobra-cli add <サブコマンド名>
$ cobra-cli add version
├── cmd
│   └── root.go
│   └── version.go
cmd/version.go

package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)

var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "Print the version number of geekhaku-cli",
	Long: "All software has versions. This is geekhaku-cli's",
	Run: func(cmd *cobra.Command, args []string) {
    // ここに処理を書いていく
		fmt.Println("version 0.9 -- HEAD")
	},
}

func init() {
	rootCmd.AddCommand(versionCmd)
}

実行

$ go run main.go version
version 0.9 -- HEAD

アスキーアートを表示してみる

今回は可愛い猫のアスキーアートを表示するコマンドを作成します。

アスキーアート

ルートディレクトにaaディレクトリを作って.txtファイルに表示したいアスキーアートを貼り付けます。

アスキーアートについては、「アスキーアート ジェネレーター」「アスキーアート コピー」などで検索してみると色々と出てくるのでお好きなもので試してみてください。

cat.txt

          ;' ':;,,     ,;'':;,
         ;'   ':;,.,.,.,.,.,,,;'  ';,   っ
        ,:'           : :、
       ,:'ノ       \   ::::::::',   っ
       :'  ●     ●      :::::i.
       i ///(_人_) ////*  :::::i   
       ,i.     し'         :::::i     
      (⌒)        (⌒) ::::::::: /        
       ;, :'       ; : : ::::::::`:、
        ; ,:'        ';,. : : ::::::`:、


サブコマンドの作成

$ cobra-cli add cat
├── cmd
│   └── root.go
│   └── version.go
│   └── cat.go

ファイルの読み込み出力の処理を追加

cat.go
package cmd

import (
  "fmt"
  "os"

	"github.com/spf13/cobra"
)

var catCmd = &cobra.Command{
	Use:   "cat",
	Short: "Print the ascii art of cat",
	Run: func(cmd *cobra.Command, args []string) {
    b, err := os.ReadFile(fp)
    if err != nil {
      fmt.Println(err)
    }
    fmt.Print(string(b))
	},
}

func init() {
  rootCmd.AddCommand(catCmd)
}

実行

$ go run main.go cat

          ;' ':;,,     ,;'':;,
         ;'   ':;,.,.,.,.,.,,,;'  ';,   っ
        ,:'           : :、
       ,:'ノ       \   ::::::::',   っ
       :'  ●     ●      :::::i.
       i ///(_人_) ////*  :::::i   
       ,i.     し'         :::::i     
      (⌒)        (⌒) ::::::::: /        
       ;, :'       ; : : ::::::::`:、
        ; ,:'        ';,. : : ::::::`:、

🎉🎉事表示できました!!

リリースしてみる

静的ファイルがバイナリに含まれるように変更

Go は通常、ビルド時にソースファイル以外の静的ファイルをバイナリに含めることができないので(今回で言うと.txtファイル) statikというパッケージを使って、 ビルド時に一緒にバイナリに含めていきます!

こちらの記事で紹介されている通りにやっていけば問題ないと思います!

今回はstatikを使った方法を紹介していますが、
下記のコメントにあるようにGo1.16からオフィシャルとしてサポートされた、go:embedを使った方が良さそうです!

go:embedを使用した方法

ファイルを読み込む設定をする

embedでは、埋め込むファイルはembedを記述するファイルが有る場所からの相対パスになります。例えば、 libs/hoge.go の中でembedする場合、 libs/ 以下のファイルのみ埋め込むことができます。
今回はaa以下のファイルを読み取りたいので、embed.goファイルをaaディレクトリ下に作成し、独自の独自のパッケージとしてプログラムの他の部分でも使用できるようにします。

.
├── aa
│   └── cat.txt
│   └── embed.go
embed.go
package aa

import "embed"

//go:embed *.txt
var Aa embed.FS

生成されたファイルを読み込む

cat.go
package cmd

import (
  "fmt"
  "os"

 	 "<mod initの時に宣言したimportpath>/aa"
	"github.com/spf13/cobra"
)

var catCmd = &cobra.Command{
	Use:   "cat",
	Short: "Print the ascii art of cat",
	Run: func(cmd *cobra.Command, args []string) {
    file, err := aa.Open("cat.txt") // 相対パスなので / を取り除いてファイル名を指定
    if err != nil {
      fmt.Println(err)
    }
    defer file.Close()

    // ファイルを読み込んで出力
    buf := new(bytes.Buffer)
    buf.ReadFrom(file)

    fmt.Print(buf.String())
	},
}

func init() {
  rootCmd.AddCommand(catCmd)
}
statikを使用した方法

ファイルを生成する

 $ statik -src=./aa

statik/statik.go というファイルが生成されます。

.
├── aa
│   └── cat.txt
└── statik
    └── statik.go

生成されたファイルを読み込む

cat.go
package cmd

import (
  "fmt"
  "os"

 	_ "<mod initの時に宣言したimportpath>/statik"
	"github.com/rakyll/statik/fs"
	"github.com/spf13/cobra"
)

var catCmd = &cobra.Command{
	Use:   "cat",
	Short: "Print the ascii art of cat",
	Run: func(cmd *cobra.Command, args []string) {
    statikFS, err := fs.New()
    if err != nil {
      fmt.Println(err)
    }

    file, err := statikFS.Open("/cat.txt") // 絶対パスで / が先頭に付く
    if err != nil {
      fmt.Println(err)
    }
    defer file.Close()

    // ファイルを読み込んで出力
    buf := new(bytes.Buffer)
    buf.ReadFrom(file)

    fmt.Print(buf.String())
	},
}

func init() {
  rootCmd.AddCommand(catCmd)
}

GitHub Actions と GoReleaser を使って brew コマンドでインストールできるようにする

Go で書いた CLI ツールを GitHub Actions と GoReleaser を使って brew コマンドでインストールできるようにした

この記事を参考にリリースまでを行うことができたので貼っておきます!

.github/workflows/release.ymlのGITHUB_TOKENについては今回はリポジトリがorganizationの配下だったため、こちらの記事の手順でtokenを設定することで無事にリリースまでできました。

最後に

今回は簡単にアスキーアートを表示するだけのCLIでしたが、Goがわからないなりにリリースまでできたので、見栄えがいい制作物をサクッと作れたり、Goの勉強がてらに色々といじってみたりしたら勉強にもなるし楽しいだろうなと思いました!

また、今回は紹介しなかった CLIでの選択式の実装はpromptuiで楽に実装できるので、もしよければそちらも試してみてください!
最後に今回の記事で動かない場所やうまくいかなかった場所などがあれば、コメント等で指摘いただけるとありがたいです!

GitHubで編集を提案

Discussion

Go は通常、ビルド時にソースファイル以外の静的ファイルをバイナリに含めることができないので(今回で言うと```.txt``ファイル)statikというパッケージを使って、ビルド時に一緒にバイナリに含めていきます!

為念、go 1.16からは標準機能のgo:embedを使う方がよいかと思います。statikライブラリは2020年で更新が止まっているようなので。

もうすでに標準に組み込まれていたんですね!

有益な情報ありがとうございます!🙏

ログインするとコメントできます