DailyHack

文系出身で Software engineer として渋谷で働いています。

Goでシェルを実行するCLIToolを書く

goで外部コマンド(デフォルトのコマンドやshellスクリプト)を実行するCLIツールを作ったのでその触りをまとめます。
内容は、goでCLIツール書く時の実装方法について。

やったこと

  1. shellスクリプトを外部コマンドとして叩く
  2. goの os/exec パッケージを利用してコマンドをひたすら記述していく

参考 Golangで外部コマンドを実行する方法まとめ

ディレクトリ構成

-- 
 |- app.go
 |- test.sh

shellスクリプトを外部コマンドとして叩く

使ったシェルスクリプトはとりあえず Hello を表示するだけ

#!bin/shell
echo "Hello"

goのスクリプトこち

package main

import (
    "fmt"
    "os"
    "os/exec"
)

func main() {
    // カレントディレクトリを移動
    os.Chdir("./cmds")
    // コマンドを実行 .(ピリオド)は使えずにちゃんと sh コマンドを使う
    out, err := exec.Command("sh", "./test.sh").Output()
    if err != nil {
        fmt.Println(err.Error())
        os.Exit(1)
    }
    fmt.Println(string(out))
}

実行結果

$ go run app.go
Hello

できた。

パッケージを利用してコマンドをひたすら記述していく

使用パッケージ: os/exec

外部コマンド実行に際してとく使うインターフェイス

  1. exec.Command(cmd, args).Run() 結果を出力せずに実行。戻り値はerror
  2. exec.Command(cmd, args).Output() 実行結果で何かしら出力される場合はこちら。戻り値はoutput結果([]byte型)とerror
package main

import (
  "fmt"
  "os.exec"
)

func main(){
out, err := exec.Command("ls", "-la").Output()
  if err != nil {
    fmt.Println(err.Error())
    os.Exit(1)
  }
}

引数が複数ある時

git add XXXX 等など…

この場合、exec.Command(cmd, args) のargs に git 以下を入れるんですが、argsはslice型で取らないければなりません。 そこで git を例に取るならば add XXXX がargsの中身なり、これはargsは配列でする必要がある。

import (
  // 追加
  "strings"
)

func main (){
  // スペースごとに区切って配列化
  args := Strings.Filelds("add XXXX") // => ["add", "XXXX"]
  out, err := exec.Command("git", args...).Output() 
  if err != nil {
    fmt.Println(err.Error())
    os.Exit(1)
  }
  fmt.Pringln(string(out))

}

exec.Command は第一引数にコマンドを取り、それ以外は第2引数に入れなければならない。

goで実際にコマンドをつらつら書きながら進める場合、上記実装のようにひたすらコマンドの実行と成功の可否(outputとエラー内容)を愚直に確認しながら書いていきます。
もし処理が止まったら必ずexitしましょう。

goからshを呼ぶのはただたんにshellを書いているだけ。むしろそちらよりもgoにコマンドを実行してもらう方法の方がgoでCLIツールを書くには向いているような気がする。

【fish】CLI上で利用する変数を保持する

fishでのお話。

ターミナル上で特定の文字列を変数にセットして使う方法

$ set x (ls) | echo $x
# 当該ディレクトリ上で ls した内容を出力する

set x (何か出力をともなうコマンド) とすると () 内で実行されたコマンドの出力結果を x に格納する。

セットした変数をコマンドとして実行する

sample:

$ set x "pwd"; and echo $x
pwd # 文字列をそのまま出力するだけでは文字列化された `pwd` コマンドは実行されない
$ set x "pwd"; and eval $x
~/PROJECT_ROOT # 出力された

eval コマンドを実行すると文字列化されたコマンドを入れた変数をコマンドとして実行する。

fishでのevalの設定方法

fish を使っていて、 eval の設定方法がわからなかったので調べました。

参考
eval command in config.fish

# evalの設定方法
# bashrc での rbenv の設定
# eval "$(rbenv init -)" と同様のことを書きたい時
eval (rbenv init - | source)

# その他の書き方
# eval () ← zshやbashで言うところの $ を付けない
# source () 直に指定

などなど

rubyでオブジェクトの持つメソッドを探索する

ユースケース

オブジェクトの中に意図するプロパティを取り出せるのかを確かめる方法に、オブジェクトが持つメソッドを調査するという方法があります。
rubyではjsonオブジェクトのプロパティもメソッドとして取り出せるので、プロパティ not found エラーが発生してプログラムの処理が中断してしまわないか予め調べておくことができたら便利です。

Obj.methods.grep(regexp) を使用する。

例えばある特定のAPIを叩いて返ってきたレスポンスオブジェウトのプロパティを調べるという動作を想定します。

response.methods.grep(/任意のメソッド名 or プロパティ名/)

grepメソッドを使って正規表現で調べればいとするメソッドを調査できます。
methods で全てのメソッドを取り出さずとも、特定のメソッド(プロパティ)を取り出すことが可能。

※ 上記サンプルでは前方後方一致を使っています。

これは rails console でも使えるので、api叩いて、ちゃんと想定するレスポンスの型になっているかを事前に調査できるので便利だと思う。

githubのユーザーネームを変更した話

ユーザーネームを変更しようと思った背景

もともとのgithubのユーザーネームは ememhr っていうのを使っていたのですが、これ、前職で会社用のgithub.comを作るために、もともと作っていた個人用のgithubとは別にとったもので、インターネット周りでは全てのユーザーネームを emahiro で統一しているので、どうしても座りが悪いと感じていました。

もともと ememhr で作成したアカウントはそこまで運用していくつもりはなかったのですが、気づくと会社用で作ったアカウントがメインのアカウントとして稼働してしまっていて、もともと個人でもっていたアカウントは使わなくなってしまってました。
githubのアカウントが2つあるのも変な話なので、古いアカウントを削除して、空いたユーザーネームに変更しました。

変更に際して考えられた懸念点

  1. 今まで ememhr で作っていたリポジトリへのアクセス
  2. ローカル環境での古いユーザー名で作ったリポジトリ
  3. Organizationのアカウントのアクセスについて
  4. githubユーザーのリンクを表示しているサイトのリンクの変更

の4つが懸念点として考えられました。

参考

Changing your GitHub username
What happens when I change my username?

githubの公式のヘルプページを参照

変更方法

  1. Settingへ移動
    f:id:ema_hiro:20170518013042p:plain
  2. Account ▷ Change username を選択
    f:id:ema_hiro:20170518013100p:plain
  3. 警告を食らう
    f:id:ema_hiro:20170518013134p:plain

  4. 古いプロフィールページにはリダイレクトしないよ

  5. 古いユーザー名で作成したPage(github page)にはリダイレクトしないよ
  6. リポジトリは新しいユーザー名でリダイレクトするよ ← すご!!
  7. ちょっと時間かかるよ

ってことが書いてある。

なので問題なくユーザー名を変更します。

この時点で懸念点だった1はクリア。
変更後もOrganizationのアカウントには問題なくアクセスできました。 また、旧ユーザー名でリンクを貼っていたサイトもwantedlyとlinkedinとqiitaくらいだったので、この時点ではそこまで問題にならず。

んで最後のローカルのリポジトリについてですが、個人的に開発したものについてはほとんど更新する必要もなかったので、ほっといても良かったのですが、とりあえず地道に1つずつ新しいユーザー名でのURLに変更

# まず globalのユーザーネームを変更する
$ git config --global user.name 'emahiro'

# 変更したいリポジトリで
$ git remote -v  
origin https://github.com/ememehr/(repository_name).git (fetch)
origin https://github.com/ememehr/(repository_name).git (push)

# 新しいユーザーネームの形に変更
$ git remote set-url origin git@github.com:emahiro/(repository_name).git 

これでOK。

※1 もともとhttpで設定していたのに、ssh経由に変えた理由は ※2 リポジトリを一つ一つremote先を変更するのはめんどくさいですが、このためにリポジトリを断捨離しておいて良かったです。たまに、githubにあげておいてもしょうがないようなリポジトリは掃除しておくのもいいかもしれません。

  1. sshの方が容量が大きくてもpushできる
  2. httpでアクセスした時にusernameとpasswordを聞かれたが新しいのに変更してもうまく行かなかったので、ssh形式に変更することで対応。

これでかねてより取りたかったusernameでgithub生活を送れるようになりました。

【go】構造体にメソッドを追加する

goはオブジェクト指向言語とは違って、書いていてC言語を書いている印象に近く、OOPで言うところのクラスが構造体で、クラスにメソッドを定義することは、構造体にメソッドを定義することになる。

しかし、クラスでカプセル化したりするわけではないので、構造体にメソッドを定義する、その定義のしかたがちょっと理解しづらかったのでそのメモ。

理解しづらかったポイント

goを使って簡易的なHTTPサーバーを立てたときに、http.Handle にServeHTTPのメソッドを持つ構造体を作るって箇所で詰まって色々調べた備忘録。

package main

import (
    "fmt"
    "net/http"
)

// AppHandler アプリ
type AppHandler struct {
    appName string
}

func (h *AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Println(h.appName)
}

func main() {
    http.Handle("/", &AppHandler{appName: "myApp"})
    if err := http.ListenAndServe(":8080", nil); err != nil {
        fmt.Println("エラー発生")
    }

    fmt.Println("サーバースタート")
}

こういうコードを想定。特に最初???だったのはここ

func (h *AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Println(h.appName)
}

これは

type AppHandler struct {
  appName string
}

の構造体に ServeHTTP メソッドを定義している。

https://golang.org/pkg/net/http/#Handler を確認すると、

type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
}

Handlerはinterface型なので、定義先はどんな型でもいいのだけれど、ServeHTTPというメソッドを持っていなくてはならない。
つまりAppHandlerの構造体であってもServeHTTPを定義することができる

func (構造体) (登録するfunc) {}

という形で定義が可能。

これでAppHandlerの構造体はServeHTTPというメソッドを持つことができ、http.Handleに登録して、指定したPortでHTTPリクエストを受け付けることができるようになる。

構造体以外にメソッドが定義できる。

HTTPサーバーを立てる際に以下のような定義の仕方もある。

func main() {
  http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        res, err := w.Write([]byte(`Test`))
        if err != nil {
            log.Println("Error")
        }
        log.Printf("%d", res)
  }))
}

HTTPサーバーを立てる上で行っている動作は直観的でわかりやすいのであるが、HanderFuncメソッドは一体どこからきていた何なのかがわからず。これもドキュメントを確認すると下記のようになっている。

type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)

HandlerFuncfunc(ResponseWriter, *Request) の関数型として定義されていて、HanderFunc 型に ServeHTTP が定義されている。これは構造体に ServeHTTP を定義していたのと似ていると思う。特定のエイリアスを関数型として定義して、その関数型に関数を持たせるという個人的には摩訶不思議な言語仕様のなせる技なんだなーと。

goは言語仕様がシンプルで柔軟というはこういうところから来ているような気がしました。

fishに乗り換えた話

zshの管理がめんどくさくなってきたのでもう少しイケてるshell環境をつくれないものかと調べてたら、fish なるものがあるらしく、使い勝手がよさげなので、期間限定で乗り換えてたときの備忘録

パッケージ管理

  1. oh-my-fish
  2. fisherman

1と2もどちらもパッケージ管理ツールに変わりはないので、どちらでも良さげ。
軽く調べたらfishermanの方がイケてるらしい。
最初に1を入れてその後2を入れたものの、 agnoster というテーマを使いたかったのに fisherman では表示がおかしかったので、oh-my-fish を使用。

shellの設定

fishでの標準的なシェルの設定は、 ~/.config/fish/config.fish を使用する。
しかし、oh_my_fish を使うと ~/.config/fish/conf.d/omf.fish になる。
ただし、ここに書きすぎると後々やっぱり fisherman 使いたいなーとか思った時に足かせになるので、自前で ~/.config/fish/config.fish を作って、そちらにちまちま設定を書いていった方が無難(のような気がする)

簡単に設定

pecoとghqを入れておく。

$ brew install peco
$ brew insatll ghq

冗長にbrewで入れようと思ったけど、oh-my-fishを入れると自動的にpecoもghqも入るっぽい。

というわけでoh-my-fishを入れる。

$ curl -L https://get.oh-my.fish | fish

これで omf コマンドが使えるようになる。
fishではoh-my-fishにしろ、fishermanにしろ、パッケージ管理ツールを使ってテーマ等々shellのカスタマイズを行うツールをインストールする。

例えば、agnoster というテーマを入れようと思った場合、

$ omf install agnoster

と入力すると、テーマがインストールされて自動的に反映される。インストールしているテーマは ~/.config/omf/theme のファイルに記録されている。 設定ファイル(config.fish)上で set theme (theme名) と書いておくことで使いたいテーマを決める。※ 1つしか入っていないと自動的に決まるみたい。

※ 注 : agnosterというテーマはPowerlineというフォントを指定しないと正確にテーマが反映されないので注意

とりあえず今回はここまで。
fishはパッケージ管理の容量で簡単にshellがカスタマイズできるので使い勝手が良さげ。専用のコマンドがあるのもありがたい。
設定ファイルはもう少し使い込んで、もろもろ設定してからまたエントリーにする予定。

広告を非表示にする