GoでCLIツールを作ってinstallする

GoでCLIを自作してinstallするまでの一連の流れを紹介します。

ツールを作る

例えば、次のようなechoコマンドと同じような動作をするmyechoを作成します。

package main

import (
    "flag"
    "fmt"
)

func main() {
    flag.Parse()
    args := flag.Args()
    cmd := ""
    for _, arg := range args {
        cmd += arg + " "
    }
    fmt.Println(cmd)
}

この時点では次のように実して動作を確認できます。

$ go run *go hoge fuga
hoge fuga

インストールする

modulesを設定しておきます。
例えばmyechoというツール名にしたい場合は、次のようにmodulesを設定します。
ここで設定した名前がツールを使うときに呼び出すコマンドになります。
(modulesを設定していない場合、main.goが置かれているディレクトリ名がツール名になるようです)

$ go mod init myecho

次のコマンドでビルド&インストールします。
GOPATHを設定している場合は$GOPATH/bin、設定していない場合は$HOME/go/binがインストール先になります。

$ go install

実行する

以下のように実行できれば成功です。

$ myecho hoge fuga
hoge fuga

次のようにエラーが出る場合はPATHが通っていない可能性があるので、PATHを通す必要があります。

bash: myecho: command not found

$HOME/go/binにPATHを通す場合は、~/.bash_profilePATH=$PATH:$HOME/go/binのように記載することでPATHを通します。
記入後、次のようにコマンドを実行して設定を有効にします。

$ source ~/.bash_profile

mapの値を更新したい場合

Goでmapの値を更新したい場合

次のようにmapへ直接更新することはできない

type Sample struct {
    state bool
}

var samples = map[string]Sample{
    "hoge": {true},
    "fuga": {false},
}

fmt.Println(samples["hoge"].state) // getはできる
samples["hoge"].state = false      // setはできない

https://play.golang.org/p/8ZobtXRtw86

次のように更新した値でmapの内容を書き換える必要がある。

type Sample struct {
    state bool
}

var samples = map[string]Sample{
    "hoge": {true},
    "fuga": {false},
}

s := samples["hoge"]
s.state = false
samples["hoge"] = s   // 更新したもので置き換える

fmt.Println(samples)

https://play.golang.org/p/UdrVclkYJf9

コマンドラインにオプションを付ける

自作のコマンドラインツールで、オプションon/offで動作を変えたい。
flag packageを使う。
次のように-optを付けるだけで動作を変えたい場合はboolを使うことになる。

mytool -opt
func main() {
    opt := flag.Bool("opt", false, "オプションの説明")  // デフォルト値=falseに設定する例
    flag.Parse()
    if opt {
        // opt設定時の処理
    } else {
        // opt未設定時の処理
    }
}

コマンドラインでディレクトリを受け取る

自作のコマンドラインツールでディレクトリを受け取って、 絶対パス相対パスどちらも同じように扱いたい。
そんな場合はfilepath.Absを使って絶対パスに置き換えてしまうのが良さそう。

filepath.Abs絶対パスを渡しても問題なく動作する。

path1, _ := filepath.Abs("./sample")
fmt.Println(apath)

path2, _ := filepath.Abs("/usr/local/bin")
fmt.Println(upath)

なので、こんな感じで使うことになる。

func main() {
    dir := flag.String("d", "", "ディレクトリ")
    flag.Parse()
    path := filepath.Abs(dir)
    fmt.Println(path)
}

DockerHubで自分が付けたスターのリストを表示する

タイトル通りの内容です。
自分がスターを付けたコンテナの表示方法がわからなかったので探しました。

まずタイトルバー部分の自分のアカウントをクリックします。
表示されたページのStarredがスターを付けたリストです。

f:id:bluemon0919:20210530113738p:plain
スターリスト

ginでHTTP RequestのPathParameterを受け取る

Path Parameterの後ろに階層がある場合

次のようなAPIを定義する

GET
/user/:name/profile

GET
/uesr/:name/history

これをginで実装する方法は公式ページにも紹介されている。

r.GET("/user/:name/*action", func(c *gin.Context) {
    name := c.Param("name")
    action := c.Param("action")
    message := name + " is " + action
    c.String(http.StatusOK, message)
})

もちろん単純に1つずつ実装しても動作する。

r.GET("/user/:name/profile", func(c *gin.Context) {
    name := c.Param("name")
    message := name + " profile"
    c.String(http.StatusOK, message)
})
r.GET("/user/:name/history", func(c *gin.Context) {
    name := c.Param("name")
    message := name + " history"
    c.String(http.StatusOK, message)
})

間に1つ以上の階層が挟まる場合

上で紹介したAPI:nameactionの間にgroupという階層を挟んだ例。

GET
/user/:name/group/profile

GET
/user/:name/group/history

これも同じ内容で実装できる。もちろん単純に1つずつ実装しても動作する。

r.GET("/user/:name/group/*action", func(c *gin.Context) {
    name := c.Param("name")
    action := c.Param("action")
    message := name + " is " + action
    c.String(http.StatusOK, message)
})

または、次のコードでも動作はできる。
ただし、この場合はaction = /group/profileのようにパスが渡される。

r.GET("/user/:name/*action", func(c *gin.Context) {
    name := c.Param("name")
    action := c.Param("action")
    message := name + " is " + action
    c.String(http.StatusOK, message)
})

HTTP Requestのパラメータ

HTTP Requestのパラメータは以下の3種類がある。

  • パスパラメータ
  • クエリパラメータ
  • リクエストボディパラメータ

パスパラメータ(Path Parameter)

単一のパラメータ

基本的な書き方は次の通り

設計書

/item/{pathparameter}
/item/:pathparameter

呼び出し方法

http://example.com/item/1

複数のパラメータ

複数のパスパラメータを含むこともできる

設計書

/item/{group}/user/{level}
/item/:group/user/:level

呼び出し方法

http://example.com/item/1/user/10

Go Ginでの実装方法

r := gin.Default()
r.GET("/item/:pathparameter", func(c *gin.Context) {
    key := c.Param("pathparameter")
    fmt.Println(key)
})

クエリパラメータ(Query Parameter)

単一のパラメータ

設計書

/item{?queryparameter}

呼び出し方法

http://example.com/item?queryparameter=1

複数のパラメータ

設計書

/item{?group, level}

呼び出し方法

http://example.com/item?group=1&level=10

Go Ginでの実装方法

r := gin.Default()
r.GET("/item", func(c *gin.Context) {
    key := c.Query("queryparameter")
    fmt.Println(key)
})

リクエストボディパラメータ

設計書

ParametersRequestに書かれているもの。
json形式で表現されることが多い。

呼び出し方法

bodyに設定する必要がある。
ここでは curlでの呼び出しを例として挙げる。

curl -X POST -H "Content-Type: application/json" -d '{"Name":"bluemon", "Age":"35"}' 'http://example.com/item'

Go Ginでの実装方法

r := gin.Default()
r.POST("/item", func(c *gin.Context) {
    buf := make([]byte, 1024)
    n, _ := c.Request.Body.Read(buf)
    fmt.Println(string(buf[0:n]))
})