ハイパーマッスルエンジニア

Vim、ShellScriptについてよく書く

tmuxでiTermのimgcatを使う

iTermの拡張コマンドであるimgcatはtmux上で実行すると挙動がおかしくなる。 これはどうやらtmuxではpane分割があるため、widthやheightがうまく計算できないので描画がおかしくなってしまうらしい。

https://github.com/Rasukarusan/blog-assets/blob/master/tmux-imgcat/demo1.gif?raw=true
tmuxでimgcatを実行したときの挙動。一瞬だけ表示されて消える。

ただ、どうやらimgcatの実行後、何らかの方法で処理をブロッキングすることで描画がちゃんとされることがわかった。

https://github.com/Rasukarusan/blog-assets/blob/master/tmux-imgcat/demo2.gif?raw=true
imgcat実行後、処理をブロッキングした場合のimgcatの挙動

画像はTerminalに残らず、catではなくlessっぽい挙動になってしまうが、十分使える感じはある。

方法

例えばreadコマンドを使って処理をブロッキングすることができる。

imgcat_for_tmux() {
  imgcat "$1"
  # ENTERで画像表示を終了できる。
  read && clear && exit
}
alias imgcat='imgcat_for_tmux'

もしくはsleepコマンドで処理をブロッキングし、trapコマンドでCtrl-cをフックしてもいい。

imgcat_for_tmux() {
  trap 'clear && exit' SIGINT
  imgcat "$1"
  sleep 100
}

ただいちいちCtrl-cするのも面倒だし、trap仕掛けるのもアレなのでreadが一番シンプルだと思う。

引数やカーソル位置等を諸々考慮した版を一応載せておく。

_imgcat_for_tmux() {
    # @See: https://qastack.jp/unix/88296/get-vertical-cursor-position
    get_cursor_position() {
        old_settings=$(stty -g) || exit
        stty -icanon -echo min 0 time 3 || exit
        printf '\033[6n'
        pos=$(dd count=1 2> /dev/null)
        pos=${pos%R*}
        pos=${pos##*\[}
        x=${pos##*;} y=${pos%%;*}
        stty "$old_settings"
    }
    command imgcat "$1"
    [ $? -ne 0 ] && return
    [ ! "$TMUX" ] && return
    get_cursor_position
    # 2行分画像が残ってしまうためtputで再描画判定させて消す
    read && tput cup `expr $y - 2` 0
}

色々問題はある

Terminalのサイズが小さすぎると画像が表示されないとか、画面の下の方で実行すると描画がおかしくなったりと、色々問題はあるけど全く使えないよりはいいかなっていうレベル。
とりあえずtmuxでもimgcat使えるじゃん!!って嬉しかったのでこの世に残しておく。

zshで関数内で実行したコマンドを履歴に残す

通常、コマンドを実行したら履歴に残り、Ctrl-p/n上/下の矢印キーで実行したコマンドを遡ることができる。再度同じコマンドを実行するときはとても便利。
この履歴に手動で追加するにはどうするか。

結論から言うと

print -s 履歴に残したいコマンド

でいける。

例えばecho hogeというコマンドを履歴に残したいなら下記のようにする。

$ print -s echo hoge

Ctrl-pをするとecho hogeが出てくるはず

# historyを確認してみる
$ history | tail -n 1
 2494  echo hoge

どうしてこれが必要なのか

fzfやpecoでコマンドをまとめているときに必要になるんですよね。
dockerやgitのコマンドをfzfで選択して実行する関数を、.zshrc等に書いている人は結構多いと思います。
それ自体はめちゃくちゃ便利で、長すぎるコマンドとかオプションを覚えずにすむので重宝するのですが、 その関数で実行したものを再度実行しようとしたときにもう一度選び直さないといけないというのが面倒でした。

例えば自分はyarnのコマンドを下記のようにまとめていて、

_fzf_yarn() {
    local gitRoot=$(git rev-parse --show-cdup)
    local packageJson=$(find ${gitRoot}. -maxdepth 2  -name 'package.json')
    [ -z "$packageJson" ] && return
    local action=$(cat ${packageJson} | jq -r '.scripts | keys | .[]' \
        | fzf --preview "cat ${packageJson} | jq -r '.scripts[\"{}\"]'" --preview-window=up:1)
    [ -z "$action" ] && return
    yarn $action
}
alias yy='_fzf_yarn'

yyを実行するだけで、package.jsonに書かれたコマンドを選択して実行できる形にしています。

f:id:rasukarusan:20200728235605p:plain:w500
package.jsonを探索して中のコマンドを引っ張ってくる`yy`コマンド
ただ、この状態で「あっさっきのコマンドもう一回実行したい」となった場合にCtrl-pを押すとyyしか出てきません。
さっき実行したyarn startをもう一回実行したいだけなのに、もう一度yyと打ち、starぐらいまで入力し選択して実行、というステップを取らなければなりません。(yarn startと打つのはもっとダルいので却下)

こんなときに便利なのがprint -sという話。先程の関数の最後に追加するだけでOK。

_fzf_yarn() {
    local gitRoot=$(git rev-parse --show-cdup)
    local packageJson=$(find ${gitRoot}. -maxdepth 2  -name 'package.json')
    [ -z "$packageJson" ] && return
    local action=$(cat ${packageJson} | jq -r '.scripts | keys | .[]' \
        | fzf --preview "cat ${packageJson} | jq -r '.scripts[\"{}\"]'" --preview-window=up:1)
    [ -z "$action" ] && return
    yarn $action
+    print -s "yarn $action"
}
alias yy='_fzf_yarn'

historyに実行したyarnコマンドが残るようになりますやったね。

※ちなみにhistoryファイルに強引に書き込むという方法もあるっちゃあるが、人によってはタイムスタンプを記録するようにしていたりしてフォーマットが違うので汎用性が低くなってしまう。

終わり

仕事中煮詰まったときにコマンドのHelpを見て楽しんでいるのだが,そこで偶然見つけてとても嬉しかったので記事にした。
最初はhistoryコマンドにaddオプションみたいなものがあると思ってずっと探していたが見つからず、できないのかなと諦めていたが、まさかprintにこんなオプションあるなんて思わないよね。

fzfは非表示にした列でフィルタリングすることはできない

--with-nthで特定の列だけを表示した上でフィルタリングがしたかったが、どうもできないらしい。

github.com

上記のIssueによると、できないというよりはそれを実装してしまうと混乱を招きそうだから実装しない、ということらしい。

何がしたかったのか

fzfの絞り込み結果は日本語で表示し、フィルタリングは英語で行う、ということをしたかった。

例えばこんなファイルがあるとする。

Ken.tsv

神奈川    Kanagawa
東京  Tokyo
横浜  Yokohama

このファイルをfzfに食わせると下記のようになる。

f:id:rasukarusan:20200726232403p:plain

上記のように列が2つだけならこのままフィルタリングしていっても特に問題はない。
ただ列が多くなってきたときに、特定の列だけを表示した上でフィルタイングしたいときがある。
今回の場合だと、表示は神奈川県などの日本語だけにして、フィルタリングはkanagawaなどローマ字でやりたいっていうケースですね。

f:id:rasukarusan:20200726232437p:plain
イメージこんな感じ

そしてこれはできないよっていうのが冒頭のIssueになります。以下ポエム。

--nth,--with-nthについて

--nth,--with-nthは列(field)を扱うオプション。 ざっくり違いをまとめると以下。

  • --nthフィルタリングを適用させたい列を指定することができる
  • --with-nth表示したい列を指定することができる

ちなみにnthというのは4th5thの数字の部分をNとした形のこと。いわゆる序数ってやつですね。

nth系の挙動を下記のようなファイルで試してみる。

Ken.tsv

神奈川(kanagawaken)   Kanagawa
東京(tokyoto) Tokyo
横浜(yokohamashi) Yokohama

--nth=1(1列目でフィルタリング)

まずは--nthの挙動から確かめてみる。

cat Ken.tsv | fzf --nth=1

f:id:rasukarusan:20200726232504p:plain
1列目でフィルタリングされる

1列目の(kanagawaken)でフィルタリングされている。

--nth=2(2列目でフィルタリング)

では--nth=2にした場合どうなるか。

cat Ken.tsv | fzf --nth=2

f:id:rasukarusan:20200726232526p:plain
2列目でフィルタリングされる

2列目のKanagawaでフィルタリングされている。

このように--nthはフィルタリングを適用させる列を指定することができる。

--with-nth=1(1列目だけ表示)

cat Ken.tsv | fzf --with-nth=1

f:id:rasukarusan:20200726232543p:plain
1列目だけ表示される

1列目だけ表示されている。もちろん--with-nth=2とすると2列目だけ表示される。

--with-nth=N..(N列目以降表示)

2..のように..を使用することでN列目以降を表現できる。

Ken.tsv

神奈川    Kanagawa    1234km
東京  Tokyo   5678km
横浜  Yokohama    10km
cat Ken.tsv | fzf --with-nth=2..

f:id:rasukarusan:20200726232624p:plain
2列目以降が表示される

--nth,--with-nthを組み合わせれば非表示の列でフィルタリングできるんじゃない!?

下記のように--nth--with-nthを組み合わせればいけると思った。

# フィルタリング対象を2列目、1列目だけを表示する
cat Ken.tsv | fzf --nth=2 --with-nth=1

と思ったけどできなかった。これについては下記。

github.com

終わり

fzfを使い始めてからずっと疑問だったことが解消されたし、--nthについてもちゃんと理解することができたので結果オーライ。
どうしてもやりたかったらForkして修正したものを使えばいいという選択肢があるだけで神。
ああfzf楽しい。

TerminalからfzfでBluetooth機器を選択して接続できると幸せになれる

f:id:rasukarusan:20200610212400p:plain:w500
TerminalからBluetoothの接続をする

MacでBluetoothのデバイスを接続するとき、ステータスバーからデバイスを選択しているやつおる?
エンジニアならTeminalからBluetoothデバイスを選択して接続しないといけない

AirPodsの接続をTerminalからやりたい

AirPodsは最後に接続された機器のみ自動で接続してくれる。なのでiPhoneで接続→接続解除→Macで接続、とするとMacでは接続ボタンを再度押さないといけない。

f:id:rasukarusan:20200610184825p:plain
ステータスバーから一々接続ボタンを押す必要がある

結構この作業をやることが多いので、Terminalから接続のON/OFFをできるようにする。

AppleScript in ShellScriptで実現

ShellScript内にAppleScriptを埋め込む形にして、fzfでデバイスを選択できようにした。

https://user-images.githubusercontent.com/17779386/84265271-8e8af100-ab5d-11ea-972b-0653090f6852.gif
fzfでデバイスを選択して接続。previewにはなんとなくデバイスの情報を出している。

github.com

ちゃちゃっと試したい人は以下

$ brew tap Rasukarusan/tap
$ brew install Rasukarusan/tap/fzf-bluetooth-connect
$ bluetooth-fzf

AppleScriptでframework読み込めるの知らんかった

実はAppleScript内ではuse framework "フレームワーク名"とすることでCocoaの機能が使える。
今回はBluetooth関連をいじりたかったのでuse framework "IOBluetooth"を使っている。

pairedDevices() {
osascript << EOF
    use framework "IOBluetooth"
    use scripting additions
    ...
EOF

SwiftやObjective-Cとは若干使い方が異なるが、ほとんど直感的にいける。
例えばBluetoothのペアリング済みのデバイスを取得したいときは下記のような感じ。
(IOBluetoothのpairedDevices()を使用)

pairedDevices() {
osascript << EOF
    use framework "IOBluetooth"
    use scripting additions
    current application's IOBluetoothDevice's pairedDevices() as list
EOF
}

# 実行すると下記のような出力が得られる
# «class ocid» id «data optr0000000070F2E86AB97F0000», «class ocid» id «data optr000000002006E96AB97F0000»

application's IOBluetoothDevice's pairedDevices()のように〇〇'sで繋いでいくだけでOK。

Bluetoothの接続情報はコマンドでも取得可能

今回はペアリング済みのデバイスを取得する際にAppleScriptを使っているが、下記コマンドでも取得できる。

$ system_profiler SPBluetoothDataType -json

出力結果

{
  "SPBluetoothDataType" : [
    {
      "apple_bluetooth_version" : "7.0.5f6",
      "device_title" : [
        {
          "tnk’s AirPods Pro" : {
            "device_addr" : "B4-40-A4-B2-79-EE",
            "device_classOfDevice" : "0x04 0x06 0x240418",
            "device_core_spec" : "5.0",
            "device_fw_version" : "2<200b>D<200b>15",
            "device_isconfigured" : "attrib_Yes",
            "device_isconnected" : "attrib_No",
            "device_ispaired" : "attrib_Yes",
            "device_majorClassOfDevice_string" : "Audio",
            "device_manufacturer" : "Apple (0x9, 0x7D31)",
            "device_minorClassOfDevice_string" : "Headphones",
            "device_productID" : "0x200E",
            "device_services" : "AAP Server, Audio Sink, AVRCP Controller, Handsfree, AVRCP Target",
            "device_supportsEDR" : "attrib_Yes",
            "device_supportsESCO" : "attrib_Yes",
            "device_supportsSSP" : "attrib_Yes",
            "device_vendorID" : "0x004C"
        },
        {
          "QY8" : {
            "device_addr" : "1C-52-16-06-A0-F1",
            "device_classOfDevice" : "0x04 0x01 0x240404",
            "device_core_spec" : "4.0",
            "device_isconfigured" : "attrib_Yes",
            "device_isconnected" : "attrib_No",
            "device_ispaired" : "attrib_Yes",
            "device_majorClassOfDevice_string" : "Audio",
            "device_manufacturer" : "Cambridge Silicon Radio (0x6, 0x21C8)",
            "device_minorClassOfDevice_string" : "Headset",
            "device_services" : "Headset, Hands-Free unit, CSR GAIA邃「",
            "device_supportsEDR" : "attrib_Yes",
            "device_supportsESCO" : "attrib_Yes",
            "device_supportsSSP" : "attrib_Yes"
          }
        },
....

jsonで吐き出すことができるので、あとはjqでイージーですね。

system_profilerで取得できる情報はSPBluetoothDataTypeの他にもあり、system_profiler -listDataTypesで一覧が出る。

終わり

AppleScript内でuse framework "xxx"ができるならもはや何でもありで、GUIのアプリも作れてしまう。
とはいえGUIアプリ作るなら素直にSwiftとかで書いたほうが絶対楽だと思うので、AppleScriptでやる意味はないと思う。いちいちAppleScriptの記法に変換するのはだるいしXcodeの補完も使えないしね。
Shellの可能性は広がったので知れてよかった。

Terminalの現在行をエディタで編集して実行する

長いワンライナーを打っていると編集が面倒くさい

ワンライナーじゃなくてもいいのだが、Terminalでコマンドを打っているときに、修正するときのカーソル移動が結構面倒くさい。コマンド履歴をさかのぼり、真ん中の方のコマンドだけ編集したいときなど地獄の作業だ。この世で最も無駄な時間だと言ってもいい。

https://github.com/Rasukarusan/blog-assets/blob/master/fc-command-ex/hell.gif?raw=true
ちまちまと修正する地獄の作業

Vimに入ってしまえば慣れたキーバインド、テキストオブジェクトやプラグインを駆使して爆速で編集できるのに...

Terminalの現在行をVimで編集できればいい

そう、つまり現在行をそのままVimに持っていき、編集し終わったら内容がTerminalに打たれた状態であればいい。 出来上がったのがこれ。

https://github.com/Rasukarusan/blog-assets/blob/master/fc-command-ex/demo.gif?raw=true
Vimで爆速で編集して戻る

キーバインドCtrl-wにzleのウィジェットを登録して呼び出せるようにしている。

zle(Zsh Line Editor)を使って実現できる

下記を.zshrcに書けば終了。Ctrl-wを押せば現在行をVimで編集できる。Vimを終了すれば編集した内容がTerminalに打たれている。

edit_current_line() {
    local tmpfile=$(mktemp)
    echo "$BUFFER" > $tmpfile
    vim $tmpfile -c "normal $" -c "set filetype=zsh"
    BUFFER="$(cat $tmpfile)"
    CURSOR=${#BUFFER}
    rm $tmpfile
    zle reset-prompt
}
zle -N edit_current_line
bindkey '^w' edit_current_line

やっていることは簡単

  1. 一時ファイル作る
  2. 現在行($BUFFER)を一時ファイルに書き込んでVimで開く
  3. 現在行を一時ファイルの内容で書き換える

これだけ。
zleについては昨日の記事でも触れたので詳しくはそちら。

Vimで一時ファイルを開く際に

vim $tmpfile -c "normal $" -c "set filetype=zsh"

としているのは行末に移動するためと、VimのSnippetを効かせるためにFiletypeをzshに設定している。

fcコマンドでいいんじゃないの?

似たような挙動をするものでfcコマンドというものがある。
これは直前に実行したコマンドをVimで開いて編集することができるもの。保存して終了すれば編集した内容が即実行される。

https://github.com/Rasukarusan/blog-assets/blob/master/fc-command-ex/fc.gif?raw=true
fcコマンドの挙動。直前のコマンドのみ編集可能

直前のコマンドを編集したい場合だったらfcコマンドでいい。ただ直前のコマンドを編集したいケースはあまりなく、よくあるのはコマンド履歴から遡ってそれを編集といったことが多い。また編集が終わったら即実行されるのもちょっと心臓に悪い。

そういう場合に今回のは使えると思う。

終わり

一時ファイル作るのにmktempを使って、あとで削除(rm)するのはもうちょっとスマートになると思う。たぶん一時ファイル作らずにプロセス置換とかを駆使すればいけるかなあ。
とはいえgit commitとかfcとかと似たような挙動になったのでまあいい。

Terminal上でSnippetを実現する ~ zle(Zsh Line Editor) × fzf ~

f:id:rasukarusan:20200419192018p:plain:w400

ワンライナー打つのしんどい

CSV落として該当の列だけ抜き出すときにcat hoge.csv | awk ...とか、fzfでプレビューして〇〇するみたいなときにcat hoge.txt | fzf --preview...など、 大体打つコマンドって決まってくると思う。

.zshrc.shファイルなどに直接Shellを書いていくときは、エディタのコード補完が効くので特に面倒ではないが、 Terminal上でサクッとワンライナーで実行したいときに、いちいち定型文を書いていくのがとても面倒である。
「Terminalに打っている段階でもSnippetみたいな補完ができたらいいな...」と思って、その方法について考えてみる。

zle(Zsh Line Editor)とfzfの組み合わせで実現できる

こんな感じで打っている途中にfzfで補完を出して、選択したらTerminalに打ち込まれるようになる。

https://github.com/Rasukarusan/blog-assets/blob/master/terminal-snippet/demo.gif?raw=true
Snippetっぽい動きを実現

zleってなんなの?

下記のサイトが死ぬほど参考になるのでぜひ見てほしい。

ようはzshに搭載されている機能で、Terminal上の文字列やカーソル位置をShellScript上で操作できる。
Terminal上の文字列やカーソルはtputコマンドを駆使すればやれんこともないが、キーバインドに設定したりコマンド実行後に〇〇するなど、何かの動作をhookしたいときにzleを使うのが便利なので覚えておいて損なし。

もっと細かく知りたい人はman zshzleで見れる。Web版もある。

Snippetっぽいものを実現する

.zshrcに下記を記載して、source ~/.zshrcすれば終了。Terminal上でCtrl-oを押せばSnippetが出てくる。

# 自作ウィジェット
show_snippets() {
    local snippets=$(cat ~/zsh_snippet | fzf | cut -d':' -f2-)
    LBUFFER="${LBUFFER}${snippets}"
    zle reset-prompt
}
# 自作ウィジェットを登録
zle -N show_snippets
# 自作ウィジェットを`Ctrl-o`で呼び出す
bindkey '^o' show_snippets

Snippetファイルとして~/zsh_snippetを作成しておく。自分の場合はこんな感じ。

awk:'{print $1}'
fzf:--prompt ""
fzf:--preview
fzf:--preview-window=down:40%
printf:printf "\e[33m${1}\e[m\n"

このファイルをcat ~/zsh_snippet | fzf | cut -d':' -f2-で読み込んでるっていう感じ。
jsonファイルにしてjqコマンド使ってfzfのpreviewにコマンドの説明を出す、みたいにちょっとリッチにしてもいいと思う。
まああくまで補助的なものだしParser的な問題になってくるのでそんなにこだわらなくてもいいんじゃないかな。シンプルイズベスト。

ちょびっと解説

肝は$LBUFFERっていう変数。これはzshで最初から用意されているもので、Left BUFFERの略。
何が入っているかというと、現在のカーソル位置より左にある文字列が入っている。

f:id:rasukarusan:20200419190245p:plain
この状態だったらLBUFFER="cat ~/zsh_snippet"となる

なのでLBUFFER="${LBUFFER}${snippets}"の部分で
「今まで打っていたコマンド」+「fzfで選択した文字列」
を繋げてるって感じ。
他にも色々変数は用意されているので詳しくはman zshzle

あとキーバインド枯渇問題があるが、bindkey '^x^o' show_snippetsのようにすればCtrl-xoと複数の文字で登録できるので自分なりのprefixを設定すれば良さそう。
bindkeyで今設定されているものが見れるので自作のウィジェットを登録する前に要確認。

終わり

  1. 自作関数つくる
  2. ウィジェットとして登録する
  3. キーバインドに登録する

っていうめちゃくちゃ簡単な3ステップなのでスーパーベリーイージーマーケット。
当初はHyperのプラグインとして作るしかないかなあと思っていたがzleとfzfだけで十分機能を満たせた。
VimのSnippetっぽく$1に設定したところにジャンプするとか、直前の単語であらかじめsnippetを絞るとかもっとリッチにはできそうだがとりあえずこの辺に留める。

whichコマンドでaliasではなくPATHを表示する(where, whence, where, type, commandどれ使えばいいのか決める)

f:id:rasukarusan:20200217024852p:plain:w300

コマンドにaliasを貼っていると、whichコマンドでPATHを知りたいのにaliasが表示されてしまう。

$ which grep
grep: aliased to grep --color=auto

PATHを知りたいときは-pをつけるとPATHが表示できる。(※-pはzshのみ有効)

$ which -p grep
/usr/local/opt/grep/libexec/gnubin/grep

ちなみに-aをつけるとaliasとPATHが両方表示される。(複数存在する場合、全部表示される)

$ which -a grep
grep: aliased to grep --color=auto
/usr/local/opt/grep/libexec/gnubin/grep
/usr/bin/grep

コマンドのPATHを知るいくつかの方法

PATHを知ることができるのはwhichコマンドの他にもいくつかある。

  • where(zshのみ)
$ where grep
grep: aliased to grep --color=auto
/usr/local/opt/grep/libexec/gnubin/grep
/usr/bin/grep
  • whereis
$ whereis grep
/usr/bin/grep
  • type
$ type -a grep
grep is an alias for grep --color=auto
grep is /usr/local/opt/grep/libexec/gnubin/grep
grep is /usr/bin/grep
  • command
$ command -vp grep
/usr/bin/grep
  • whence(zshのみ)
$ whence -a grep
grep --color=auto
/usr/local/opt/grep/libexec/gnubin/grep
/usr/bin/grep

で、結局どれを使えばいいの?

terminalでちょろっと打つぐらいなら自分が覚えてるやつを打てばいいと思う。

だが、ShellScriptにして配布等するとき、つまり汎用性を高くしたいならちょっと考えたほうがいい。
ShellScript内でよく使われる記法としては、下記のようにコマンドがインストールされているかを判定するものが挙げられる。

# lsが使用可能か判定
if which ls >/dev/null 2>&1; then
    echo 'Found!'
else
    echo 'Not Found!'
fi

このような使い方をする場合、とりえあずwhichwhencewhereは避けたほうがいい。
whichはギリセーフ感はあるが、-pがzshにしか無いのでできれば避けたい。また、ビルトインコマンドではなく外部コマンドなのでちょっと遅い。判定のために外部プロセスを使うのはちょっとリッチすぎるかなっていう感じ。
whencewhereはzshでしか動かないので却下。

となるとwhereistypecommandになるが、whereisも脱落してもらう。
なぜならwhereisはGNU系ではコマンドだけではなく、 バイナリ, ソース, マニュアルページのファイルを探すためにも使用されるコマンドのため、思考停止でshellを読むことが難しくなる(まあパッと見たらわかると思うけど、読み手の労力は極力減らすべき)。

さて、残るはtypecommandだけだが、個人的にcommandはあんまり使わない。 commandはパスを知りたい場合-vpをつける必要があり、仮につけなかった場合にエラーとなってしまう。(正確には「引数で指定したコマンドを実行する」という挙動になってしまう)

# -vpをつける必要がある
$ command -vp grep
/usr/bin/grep

# エラーになる(シンプルにgrepが実行されてしまう)
$ command grep
Usage: grep [OPTION]... PATTERNS [FILE]...
Try 'grep --help' for more information.

whereisと同じでcommandはパスを知る以外にも用途が存在するため、あまり使いたくない。
(が、確かcommandはPOSIX互換性があるとかあった気がするし、typeと違って終了ステータスが明確になっているので、全然悪い選択肢ではなく、むしろ良い。)

以上から、汎用性も高く、書きやすく、読み手にも何をしたいのか一撃でわかるという理由からtypeを使用するべき。もしくはcommand -vp

つまりコマンドの存在を調べるshellはこれ

コマンドの存在を何回もチェックするような場合、下のような関数を作ることが多い。

# jsっぽい関数名でhas()とかでもいいかもね
exists() {
    type "$1" >/dev/null 2>&1
}

# 使用するとき
if exists ls; then
    echo 'Found!'
else
    echo 'Not Found!'
fi

# もしくは一行で
exists ls && echo 'Found!' || echo 'Not Found!'

終わり

意外に奥深いよね。 (あとhashコマンドもあった。まあいいか。)

builtin-commandsのmanの見方

f:id:rasukarusan:20200128175901p:plain:w400

man readとかするとbuiltin commandsのmanが出てきてしまって、read自体の説明にたどり着けない。

f:id:rasukarusan:20200128180049p:plain
我々が見たいのはこのmanではない

これはmanがないのではなく、ちゃんと別のところに書いてあるのでそれを覗きに行く。

bashの場合

使っているShellがBashならば下記でいける。

$ man bash

いつもどおりmanが開くので、そこでreadを検索すると見つかる。 ただ普通に検索していてはめちゃくちゃ面倒くさいので、下記のようにするとすぐ飛べる。

$ man bash | less -p "^       read "

だがしかしこのコマンドを毎回打つのも非常に面倒くさいので、関数にしちゃったほうが圧倒的楽。

# builtin-commandsのmanを参照
manbash() {
    man bash | less -p "^       $1 "
}

https://github.com/Rasukarusan/blog-assets/blob/master/builtin-commands/demo.gif?raw=true
manが見放題

zshの場合

Zshの場合は下記

$ man zshbuiltins

先ほどと同様にless -pを関数にしちゃうと良い。

# builtin-commandsのmanを参照
manzsh() {
    man zshbuiltins | less -p "^       $1 "
}

終わり

BashとZshでそれぞれmanbashmanzsh関数を作るのは嫌だったら、$SHELLでログインシェルを判別してzshbashかを分けてもいいと思う。

参考

bash - Where to view man pages for builtin commands? - Stack Overflow

sourceコマンドは複数のスクリプトを読み込めない

f:id:rasukarusan:20200128173009p:plain:w500

結論

先に結論だけ言っておくとsource file1 file2はできない。file1, file2をsourceしたいなら下記のようにする。

find ~/local_scripts |while read script; do
    source $script
done

# もしくはfor文でもいい
for script $(find ~/local_scripts); do
    source $script
done

いずれにせよsourceコマンドは1つずつ実行する必要がある。
sourceコマンドの第二引数以降はスクリプトの引数($1$2)としてみなされてしまうからだ。
sourceコマンドのmanにもそう書いてある。というかsourceはただのコマンド実行だからそりゃそうだよねっていう。

source filename [arguments]

ここからポエム

.zshrcには載せたくないローカル用のスクリプトがある。
そんな時は.zshrc.localのようなスクリプトを用意して、そこにローカル用の関数を書いていくってこと結構あると思う。

.zshrc.local

#!/usr/bin/env bash

echo_local() {
    echo 'This is local'
}

上記のような.zshrc.localを用意したら.zshrcの末尾にsource ~/.zshrc.localと書けば、echo_local()がコマンドとして使えるようになる。

$ echo_local
This is local

が、関数が多くなるにつれてファイル分割したくなってくる。そんなときに罠にかかった話。

sourceコマンドで読み込めばいいじゃんと思うじゃん

例えば下記のように関数をファイル分割した場合

ディレクトリ構造

~/local_scripts
    |-- echo_local.sh
    `-- echo_local2.sh

echo_local.sh

#!/usr/bin/env bash

echo_local() {
    echo 'This is local'
}

echo_local2.sh

#!/usr/bin/env bash

echo_local2() {
    echo 'This is local 2'
}

.zshrcに下記を記載すればいけると思った。

source ~/local_scripts/*

が、これはできない。展開するとsource ~/echo_local.sh ~/echo_local2.shとなるからだ。

終わり

あとは最初の結論にも書いたとおり。
読み込みたいファイルが引数として解釈されてしまうから、1つずつsourceしましょうねって話。

業務で使うツール(iTerm2,SequelPro,Chrome)をShellScriptでハイパーテクニックする

f:id:rasukarusan:20191214234427p:plain:w500
業務で使うツール(iTerm2,SequelPro,Chrome)を
ShellScriptでハイパーテクニックする

はじめに

この記事は今年イチ!お勧めしたいテクニック by ゆめみ feat.やめ太郎 Advent Calendar 2019の20日目の記事です。
今年は「お勧めテクニック」ということで、業務効率化ッ!!を盾に業務時間の30%はShellScript遊びに当てている私にピッタリな企画ですね、ありがとうございます。

今回は業務でよく使うツールを、ShellScriptでハイパーテクニックする方法をいくつかご紹介。
今回紹介するコードは全部Githubにあげているので実際に試したい人はどうぞ。

ハイパーテクニックする対象

  • GoogleChrome
  • iTerm2
  • SequelPro

GoogleChrome

ブラウザに移動せずTerminalで自在にタブ移動する

Web開発をしているとTerminalとブラウザを行ったり来たりすることが多い。
一々ブラウザに移動してからタブ選択、なんてことはせずにTerminalで全部やっちゃいましょう。

https://rasukarusan.github.io/blog-assets/advent-calender-2019-yumemi/chrome_tab.gif
fzfでchromeのTabを選択する

コード▼

#!/usr/bin/env bash

function getTabs() {
osascript << EOF
    set _output to ""
    tell application "Google Chrome"
        set _window_index to 1
        repeat with w in windows
            set _tab_index to 1
            repeat with t in tabs of w
                set _title to get title of t
                set _url to get URL of t
                set _output to (_output & _window_index & "\t" & _tab_index & "\t" & _url & "\t" & _title & "\n")
                set _tab_index to _tab_index + 1
            end repeat
            set _window_index to _window_index + 1
            if _window_index > count windows then exit repeat
        end repeat
    end tell
    return _output
EOF
}

function setActiveTab() {
local _window_index=$1
local _tab_index=$2
osascript -- - "$_window_index" "$_tab_index" << EOF
on run argv
    set _window_index to item 1 of argv
    set _tab_index to item 2 of argv
    tell application "Google Chrome"
        activate
        set index of window (_window_index as number) to (_window_index as number)
        set active tab index of window (_window_index as number) to (_tab_index as number)
    end tell
end run
EOF
}

function main() {
    local selected
    IFS=$'\t' read -r -a selected < <(
        getTabs | sed '/^$/d' | fzf --delimiter $'\t' --with-nth 4 --preview 'echo {3}' --preview-window down:1 "$@"
    )
    [ ${#selected[@]} -lt 2 ] && return 130

    setActiveTab ${selected[0]} ${selected[1]}
}

main "$@"

てっとり早く試したいという方はbrewからインストールできます。

$ brew tap Rasukarusan/tap
$ brew install Rasukarusan/tap/fzf-chrome-active-tab

# 実行
$ chrome-tab-activate

ここでお勧めしたいのは、AppleScriptはヒアドキュメントでShellScript内に埋め込むことができることです。ヒアドキュメントでShellScriptに埋め込むことによって、

  • 成果物がShellファイル1つになる
  • Git管理しやすい(AppleScriptの.scpt拡張子だとバイナリになってしまう)
  • ヒアドキュメント内でShell変数($1など)が使える

といった恩恵を受けれます。

特に「ヒアドキュメント内でShell変数($1など)が使える」はAppleScriptと非常に相性が良いです。今回は「引数も渡せるよ」ということを示すために下記のようにしていますが、

local _window_index=$1
osascript -- - "$_window_index" << EOF
on run argv
    set _window_index to item 1 of argv
    set index of window _window_index
end run
EOF

ヒアドキュメント内で直接変数を埋め込むことで、ShellScriptからAppleScriptへの引数の受け渡しが不要になります。

local _window_index=$1
osascript << EOF
    set index of window ${_window_index}
EOF

on run argvitem 1 of argvなど一々書かずに済むのでとてもシンプルにできますね。^ ^

Chromeの履歴を一覧表示して開く

これは「今日何やってたっけな」とか「さっき開いてたページ何だっけ」となった場合に便利です。これはfzfインストールした人ならまずやりたいことの1つだと思うので、結構やってる人いそうですね。

https://rasukarusan.github.io/blog-assets/advent-calender-2019-yumemi/chrome_history.gif
fzfで日付を選択し履歴を表示しつつ、プレビュー内でページ概要を表示

コード▼

#!/usr/bin/env bash

#
# Chromeの履歴をfzfで選択して開く
# sh chrome_history.sh [PATTERN]
#

function export_chrome_history() {
    # Chromeを開いているとdbがロック状態で参照できないので、コピーしたものを参照する
    cp ~/Library/Application\ Support/Google/Chrome/Default/History ~/
    local SQL="
    SELECT
        url,
        title,
        DATETIME(last_visit_time / 1000000 + (strftime('%s', '1601-01-01') ), 'unixepoch', '+9 hours') AS date
    FROM
        urls
    GROUP BY
        title
    ORDER BY
        date DESC
    LIMIT
        10000 ;
    "
    sqlite3 ~/History -cmd '.mode tabs' "$SQL"
}

function show_chrome_history() {
    local filter=${1:-""}
    local chrome_history=$(export_chrome_history)
    local selected=$(
        echo "\texport\n$chrome_history" \
        | grep -P "(\texport|$filter)" \
        | awk '!title[$2]++' \
        | fzf --delimiter $'\t' --with-nth 2,3 --preview 'w3m -dump {1}'\
        | tr -d '\n'
    )
    [ -z "$selected" ] && return
    # 'export'を選択した場合、ページは開かず端末に全てを出力する
    if [ "$(/bin/echo -n "$selected" | tr -d ' ')" = 'export' ]; then 
        echo "$chrome_history" | awk -F '\t' '{print $3"\t"$2}'
        return
    fi
    open $(echo $selected | awk '{print $1}')
}

function show_by_date() {
    local chrome_history=$(export_chrome_history)
    # 表示したい日付を選択する
    local select_date=$(
        echo "$chrome_history" \
        | awk -F '\t' '{print $3}' \
        | awk -F ' ' '{print $1}' \
        | grep -P '^[0-9]{4}-.*' \
        | sort -ur \
        | xargs -I {} gdate '+%Y-%m-%d (%a)' -d {} \
        | fzf
    )
    [ -z "$select_date" ] && return
    show_chrome_history $select_date
}

function main() {
    if [ "$1" = '-d' ]; then 
        show_by_date
    else
        show_chrome_history $1
    fi
}

main $1

Chromeは~/Library/Application\ Support/Google/Chrome/Default/に履歴やらブックマークやらの情報が格納されているので色々いじり甲斐がありますよね。
今回はpreview内でw3mを実行しページの概要を表示していますが、リクエストを投げる分少々重くなるのでpreviewにはURLを表示する形でもグッド。

iTerm2

画面分割、新タブ作成、ブロードキャスト入力もコマンドで実行する

iTerm2にはCmd+tで新規タブを開いたりCmd+dで縦分割したりと、便利なキーショートカットがたくさんあります。普段使いではキーで実行すればいいのですが、一連の作業を自動化する際はコマンドで実行出来たほうが何かと便利です。
下記は「新タブ作成 → ssh実行 → 画面分割 → ブロードキャスト入力ON」の一連の作業を自動で行っています。

https://rasukarusan.github.io/blog-assets/advent-calender-2019-yumemi/iterm_multi_ssh.gif
新タブ作成→ ssh →画面分割 →ブロードキャスト入力の一連の作業を自動化
tmuxのほうがスマートにできるって言う人はちょっと黙っていましょうね

コード▼

#!/usr/bin/env bash

function create_new_tab() {
osascript << EOF
tell application "iTerm"
  tell current window
      create tab with default profile
  end tell
end tell
EOF
}

function send_command() {
osascript -- - "$1" << EOF
on run argv
set _command to item 1 of argv
tell application "iTerm"
  tell current window
      tell current session
          write text _command
      end tell
  end tell
end tell
end run
EOF
}

function split_vertically() {
osascript << EOF
tell application "iTerm"
  tell current window
      tell current session
          select (split vertically with default profile)
      end tell
  end tell
end tell
EOF
}

function split_horizontally() {
osascript << EOF
tell application "iTerm"
  tell current window
      tell current session
          select (split horizontally with default profile)
      end tell
  end tell
end tell
EOF
}

function broadcast_input() {
osascript << EOF
tell application "iTerm"
  activate
  tell application "System Events"
      tell application process "iTerm2"
          tell menu "Shell" of menu bar item "Shell" of menu bar 1
              tell menu "Broadcast Input" of menu item "Broadcast Input"
                  click menu item "Broadcast Input to All Panes in Current Tab"
              end tell
          end tell
      end tell
  end tell
end tell
EOF
}

function select_session_by_id() {
osascript -- - "$1" << EOF
on run argv
set _session_id to item 1 of argv
tell application "iTerm"
  tell session id _session_id of current tab of current window
      select
  end tell
end tell
end run
EOF
}

function get_current_session_id() {
osascript << EOF
tell application "iTerm"
  tell current session of current tab of current window
      id
  end tell
end tell
EOF
}

function get_current_columns() {
osascript << EOF
tell application "iTerm"
  tell current session of current tab of current window
      columns
  end tell
end tell
EOF
}

function multi_ssh_split() {
    create_new_tab
    local max_panel_count=3
    local width=$(get_current_columns)
    local min_width=$(expr ${width} / ${max_panel_count})

    local target_servers=($@)
    local servers_count=$#
    local row=$(expr $servers_count / $max_panel_count)
    local session_ids=(`get_current_session_id`)

    [ `expr $servers_count % $max_panel_count` -eq 0 ] && row=$(expr $row - 1)

    # split horizontally and store a session id
    while [ $row -gt 0 ];do
        split_horizontally
        row=$(expr $row - 1)
        session_ids=(${session_ids[@]} `get_current_session_id`)
    done

    # select first pane
    select_session_by_id ${session_ids[$row]}
    send_command "printf '\e]1337;SetBadgeFormat=%s\a' $(/bin/echo -n ${target_servers[0]} | base64)"
    send_command "ssh ${target_servers[0]}"
    [ $servers_count -eq 1 ] && return

    # -1 is that because ssh first-server via first pane
    local vertical_cell_count=$(expr ${servers_count} - 1)

    # split vertically and ssh
    for i in `seq $vertical_cell_count`; do
        if [ `get_current_columns` -lt $min_width ];then
            row=$(expr $row + 1)
            select_session_by_id ${session_ids[$row]}
        else
            split_vertically
        fi
        send_command "printf '\e]1337;SetBadgeFormat=%s\a' $(/bin/echo -n ${target_servers[$i]} | base64)"
        send_command "ssh ${target_servers[$i]}"
    done
    broadcast_input
}

main() {
    local target_servers=$(cat ~/.ssh/config | grep 'Host ' | sed 's/Host //g' | fzf)
    [ -z "$target_servers" ] && return 130
    multi_ssh_split ${target_servers[@]}
}
main >/dev/null

ほとんど全てのキーショートカットはAppleScriptで代用できます。
例えば「新タブ作成→画面分割」をやりたかったら以下のように小さい関数にまとめておくと再利用&コマンド実行可能なのでお勧めです。

#!/usr/bin/env bash

function create_new_tab() {
osascript << EOF
tell application "iTerm"
  tell current window
      create tab with default profile
  end tell
end tell
EOF
}

function split_vertically() {
osascript << EOF
tell application "iTerm"
  tell current window
      tell current session
          select (split vertically with default profile)
      end tell
  end tell
end tell
EOF
}

# 新タブ作成して画面分割
create_new_tab
split_vertically

※ AppleScriptでkeystroke "t" using {command down}のようにして新タブ作成のCmd+tを実現してもいいですが、人によってはキーマップを変更していることもあるので、素直にiTermに命令していくほうが汎用性は高いです。

このような関数を.zshrc等にまとめておくと色々な場面で使えて便利です。 または~/iterm.shかなんかに関数をまとめておいて、.zshrc等にsource ~/iterm.shと記述しておくとまとまりがあっていいですね ^ ^

気軽に背景色を変える

背景色を変えたいときってありますよね。隣の人の目をチカチカさせたいときなどにも使えます。

https://rasukarusan.github.io/blog-assets/advent-calender-2019-yumemi/iterm_background_color.gif
隣の人の目をチカチカさせるテクニック

コード▼

#!/usr/bin/env bash

function get_current_background_color() {
osascript << EOF
tell application "iTerm"
    activate
    tell current session of current window
        return background color
    end tell
end tell
EOF
}

function change_background_color() {
local r=$(echo "scale=2; ($1/255) * 65535" | bc)
local g=$(echo "scale=2; ($2/255) * 65535" | bc)
local b=$(echo "scale=2; ($3/255) * 65535" | bc)
osascript -- - $r $g $b << EOF
on run argv
    tell application "iTerm"
        activate
        tell current session of current window
            set red to (item 1 of argv)
            set green to (item 2 of argv)
            set blue to (item 3 of argv)
            set background color to {red, green, blue, 1}
        end tell
    end tell
end run
EOF
}

# my colors
function print_colors_rgb() {
    local colors=(
        # "color_name R G B"
        "Black     0    0   0",
        "Red     201   27   0",
        "Green     0  194   0",
        "Yellow  199  196   0",
        "Blue      2   37 199",
        "Magenda 201   48 199",
        "White     0  197 199",
    )
    echo ${colors[@]} | tr ',' '\n' | sed 's/^ //g'
}

function main() {
    local current_color
    IFS=',' read -a current_color < <(get_current_background_color)
    local current_color_rgb=(
        $(echo "scale=2;(${current_color[0]}/65535)*255" | bc)
        $(echo "scale=2;(${current_color[1]}/65535)*255" | bc)
        $(echo "scale=2;(${current_color[2]}/65535)*255" | bc)
    )

    local color
    read -r -a color < <(
        print_colors_rgb | fzf --delimiter=" " --with-nth 1 --bind "ctrl-p:execute-silent($0 {2} {3} {4})"
    )

    if [ -z "$color" ];then
        $0 ${current_color_rgb[@]}
    else
        $0 ${color[@]:1:3}
    fi
}

if [ $# -eq 0 ];then
    main
else
    change_background_color $@
fi

ポイントはfzfの--bind機能でENTERを押さなくてもCtrl-pで色を変えられるようにするところです。一々選択していたら相手に休憩時間を与えてしまいます。
これ実際に会社でやったら隣の先輩が「うぐっ!ぐ、ぐぁあああコポォ!」とリアルに反応してくれたので作った甲斐がありました。ごめんなさいね ^ ^

透明度も自在に変更する

色を変えるなら当然透明度も変えたくなりますね。

https://rasukarusan.github.io/blog-assets/advent-calender-2019-yumemi/iterm_transparency.gif
Ctrl-pで透明度をプレビューしながら設定する

コード▼

#!/usr/bin/env bash

function change_transparency() {
osascript -- - $1 << EOF
on run argv
    tell application "iTerm"
        activate
        tell current session of current window
            set transparency to (item 1 of argv as number)
        end tell
    end tell
end run
EOF
}

function main() {
    local transparency=$(seq -f "%.1f" 0.0 0.1 1 \
        | fzf --header 'transparency' --bind "ctrl-p:execute-silent($0 {})")
    [ -z "$transparency" ] && return
    change_transparency $transparency
}

if [ $# -eq 0 ];then
    main
else
    change_transparency $@
fi

先程の色変えでも実装していましたが、fzfの--bindで下記のように自分自身($0)を呼び出すことで若干テクニカルさを醸し出しています。

local transparency=$(seq -f "%.1f" 0.0 0.1 1 \
    | fzf --header 'transparency' --bind "ctrl-p:execute-silent($0 {})")
...略

if [ $# -eq 0 ];then # スクリプト実行時はこっちに入る
    main
else # --bind "ctrl-p:execute-silent($0 {})"で実行するときはこっち
    change_transparency $@
fi

つまりfzfに入らず外からも実行可能にしてるよってことですね。

$ ./iterm_transparency.sh # fzfで選択しながら実行
$ ./iterm_transparency.sh 0.5 # 透明度を直接指定して実行

バッジを設定する

iTerm2のバッジ機能、皆さんご存知でしょうか。
設定 > Profiles からも設定できますが、コマンドでも設定可能です。

https://rasukarusan.github.io/blog-assets/advent-calender-2019-yumemi/iterm_badge.gif
コマンドでバッジを設定する

function _set_badge() {
    printf "\e]1337;SetBadgeFormat=%s\a" $(/bin/echo -n "$1" | base64)
}
alias ba='_set_badge'

sshするときにホスト名を表示とかに便利です。 ※ tmuxを起動していると表示されないので注意。

SequelPro

「お気に入り」から複数選択して自動で接続する

Sequel ProはUIもイケてて非常に便利ですが、接続するときに「お気に入り」から選択するのが億劫なんですよね。
フィルタリングしながら選択したいときや一度に複数接続したいときに便利です。

https://rasukarusan.github.io/blog-assets/advent-calender-2019-yumemi/sequelpro_auto_select.gif
fzfで「お気に入り」を表示&選択して自動で接続する

コード▼

#!/usr/bin/env bash

#
# SequelProで指定した接続を開く
# 引数にはSequelProの「お気に入り」の行番号を示すインデックスが入る
#
function connect() {
osascript -- - "$@" << EOF
on run argv
tell application "Sequel Pro"
    activate
    delay 0.5
    tell application "System Events"
        tell process "Sequel Pro"
            set frontmost to true
            delay 0.5
            repeat with i from 1 to (count argv)
                keystroke "t" using {command down}
                tell window "Sequel Pro"
                    delay 0.5
                    tell outline 1 of scroll area 1 of splitter group 1 of group 2
                        # because row1 is "QUICK CONNECT" and row2 is "FAVORITES", the top of favorites is row3.
                        set _row_index to (item i of argv as number) + 2
                        select row _row_index
                    end tell
                    tell scroll area 2 of splitter group 1 of group 2
                        click button 2
                    end tell
                end tell
            end repeat
        end tell
    end tell
end tell
end run
EOF
}

function main() {
    local favorites=$(plutil -convert json ~/Library/Application\ Support/Sequel\ Pro/Data/Favorites.plist -o - \
        | jq -r '."Favorites Root".Children[].name')
    local targets=($(echo "${favorites}" | fzf))
    local rows=()
    for target in ${targets[@]}; do
        echo $target
        local row=$(echo "${favorites}" | grep -n ${target} | cut -d ':' -f 1)
        rows=(${rows[@]} $row)
    done
    [ ${#rows[@]} -eq 0 ] && return 130
    connect ${rows[@]} >/dev/null
}

main

ここでイチオシしたいのは「お気に入り」一覧を取得する箇所のplutilコマンドです。Macには標準でインストールされています。

local favorites=$(plutil -convert json ~/Library/Application\ Support/Sequel\ Pro/Data/Favorites.plist -o - \
    | jq -r '."Favorites Root".Children[].name')

Macではよく見るplistファイルですが、plutilコマンドでjson化することが出来ます。 また、-oの引数に通常は出力ファイル名を指定しますが-を指定することで標準出力しています。

# ファイルに保存せずstdoutする
$ plutil -convert json ~/Library/Application\ Support/Sequel\ Pro/Data/Favorites.plist -o - | jq
{
  "Favorites Root": {
    "IsExpanded": true,
    "Name": "お気に入り",
    "Children": [
      {
        "sslCACertFileLocation": "",
        "host": "127.0.0.1",
        "database": "docker",
        ...

あとはjqでいかようにもできるのでべりぃイージーですね。

ワンタイムパスがあるDBでも自動入力で接続する

DB情報を「お気に入り」に固定化できない場合に便利です。DBパスなどがワンタイムパスになっており、コマンド等で取得するときなどにテクニカルできます。
引数に入力項目を渡せば自動で入力してくれます。

https://rasukarusan.github.io/blog-assets/advent-calender-2019-yumemi/sequelpro_auto_input.gif
接続情報を自動入力

コード▼

#!/usr/bin/env bash

function connect() {
local name=$1
local host=$2
local username=$3
local password=$4
local database=$5
local port=$6

osascript << EOF
tell application "Sequel Pro"
    activate
    delay 0.5
    tell application "System Events"
        tell process "Sequel Pro"

            # 新規タブ作成
            set frontmost to true
            keystroke "t" using {command down}

            tell window "Sequel Pro"
                # 「クイック接続」をクリック
                tell outline 1 of scroll area 1 of splitter group 1 of group 2
                    select row 1
                end tell

                # DB情報を入力
                tell tab group 1 of scroll area 2 of splitter group 1 of group 2
                    # 名前
                    set focused of text field 5 to true
                    keystroke "${name}"
                    key code 100

                    # MySQLホスト
                    delay 0.3
                    set focused of text field 6 to true
                    keystroke "${host}"
                    key code 100

                    # ユーザ名
                    delay 0.3
                    set focused of text field 4 to true
                    keystroke "${username}"
                    key code 100

                    # パスワード
                    delay 0.3
                    set focused of text field 3 to true
                    keystroke "${password}"
                    key code 100

                    # データベース
                    delay 0.3
                    set focused of text field 2 to true
                    keystroke "${database}"
                    key code 100

                    # ポート
                    delay 0.3
                    set focused of text field 1 to true
                    keystroke "${port}"
                    key code 100
                end tell

                # 接続
                tell scroll area 2 of splitter group 1 of group 2
                     click button 2
                end tell
            end tell
        end tell
    end tell
end tell
EOF
}

function main() {
    # DB情報はコマンドで取得して渡す想定
    local name='docker1'
    local host='127.0.0.1'
    local username='docker'
    local password='docker'
    local database='testdb'
    local port='13306'
    connect $name $host $username $password $database $port >/dev/null
}
main

set focused of text field 番号 to trueで各テキストフィールドにフォーカスを当てて入力していますが、このテキストフィールドは上から1,2,3...のように連番になっていないので注意です。各テキストフィールドの割当は以下のようになっています。

# 標準タブの場合
text field 1 to true --ポート
text field 2 to true --データベース
text field 3 to true --パスワード
text field 4 to true --ユーザ名
text field 5 to true --名前
text field 6 to true --ホスト

# SSHタブの場合
text field 1 to true --名前
text field 2 to true --SSHポート
text field 3 to true --SSHパスワード
text field 4 to true --SSHユーザ
text field 5 to true --SSHホスト
text field 6 to true --ポート
text field 7 to true --データベース
text field 8 to true --パスワード
text field 9 to true --ユーザ名
text field 10 to true --MySQLホスト

[Tips] Sequel Proでは~/.ssh/configの設定が使える

これはテクニックとはちょっと違うのですが、Sequel Proは~/.ssh/configに設定した内容を使えます。 例えば下記のように~/.ssh/configに設定があるとすると、

Host raspi
    HostName 192.168.100.137
    User pi
    Port 28987
    IdentityFile ~/.ssh/id_rsa
    ServerAliveInterval 20

Sequel Proではraspiと記述するだけで接続できます。

f:id:rasukarusan:20191214162551p:plain
~/.ssh/configの設定が適用される

おまけ

たまにデスクトップのアイコン全部消したくなるときありますよね。 表示・非表示はコマンドで実行できるのでGIF動画撮るときなどに便利です。

https://rasukarusan.github.io/blog-assets/advent-calender-2019-yumemi/toggle_desktop_icon.gif
デスクトップアイコンの表示・非表示を切り替える
※ 非表示になっているだけでファイルは消えないので安心していきましょう

# デスクトップ上アイコンの表示/非表示を切り替える
function toggle_desktop_icon_display() {
    local isDisplay=$(defaults read com.apple.finder CreateDesktop)
    if [ $isDisplay -eq 1 ]; then
        defaults write com.apple.finder CreateDesktop -boolean false && killall Finder
    else
        defaults write com.apple.finder CreateDesktop -boolean true && killall Finder
    fi
}
alias dt='toggle_desktop_icon_display'

終わり

業務効率化と称した自作スクリプト作りめちゃくちゃ楽しいですよね ^ ^
今回紹介したものがピンポイントで使えなくても、「もしかしてこういう使い方もできるんじゃない?」とか「別のアプリでこの考え方適用させたらもっと面白いんじゃない?」とか皆さんの中で色々アイデアが広がるようなものだったらこれ幸い。
何かに特化したテクニックとかも勿論好きだが、自分の中でもっともっと広げられるようなテクニックだとアドレナリンが止まらなくなる。エンジニアの醍醐味ですよね。
今年のアドベントカレンダーもとっても楽しいテーマで感謝です!

SSHのデフォルトログインユーザーをrootにする

f:id:rasukarusan:20191111212442p:plain シンプルに下記の1行を.bashrcに追加するだけで終わる。

sudo su -

zshの人は.zshrcなど適宜読み替えて追加するべし。
.XXXshrc系のファイルはログイン時に自動で読み込まれるので、自動でsudo su -を実行してくれるって感じ。

何がしたかったのか

kali-linuxをvagrantで立ちあげるとき、毎回sudo su -するのが面倒だった。
kaliで使うコマンドはsudo権限持ってるやつじゃないと実行できないのが多いため、ほぼ確実にrootで実行することになる。

勿論今回の.bashrcに記述する方法じゃなくて、/etc配下の設定ファイルをいじったり、[Vagrantfileの中にデフォルトユーザーをrootにするような方法もあるが、面倒くさい。

終わり

ログアウトするときにexitを2回打つのが面倒。何かいい方法ないかな。

【備忘録】Homebrewで自作ツールを配布する

f:id:rasukarusan:20191103211239p:plain

Homebrewでの配布は簡単だが毎回忘れる

Goでバイナリ作ったり、ShellScript書いて配布したいなと思ったときにHomebrew使うことが割とある。
が、その都度毎回調べてやり方を思い出しながらするのが億劫だったので、一回まとめてみる。

ざっくり手順

  1. Homebrew配布用のリポジトリを作る
  2. tagをpushしてReleaseノートを作る
  3. Formulaファイルを作ってpush

1. Homebrew配布用のリポジトリを作る

自作ツールのインストールファイルを置くためのリポジトリを作る。このリポジトリにバイナリとかの成果物を置くわけではない。置くのはインストールファイルである.rbのみ。このリポジトリはbrew tap XXXXX/YYYYするときに指定するやつ。

1つ注意したいのは、リポジトリを作る際プレフィックス名はhomebrew-にしておく。これは守らないといけない。今回はhomebrew-tapというリポジトリ名にした。

$ mkdir homebrew-tap
# インストールファイルを格納するFormulaディレクトリを作っておく
$ mkdir homebrew-tap/Formula

# ディレクトリ構造。READMEは別になくていい。
~/Desktop/homebrew-tap  master ✔
$ tree
.
|-- Formula
`-- README.md

Githubで見るとこんな感じ。Formula内のファイルは後で作成するから今は気にしない。

GitHub - Rasukarusan/homebrew-tap

2. tagをpushしてReleaseノートを作る

配布したいツールのリポジトリでtagをpushする。

$ git tag 0.0.1
$ git push origin 0.0.1

今回は例としてこのリポジトリで行った。

~/homebrew-gitblamer  master ✔
$ git tag 0.0.1

~/homebrew-gitblamer  master ✔
$ git push origin 0.0.1
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/Rasukarusan/homebrew-gitblamer.git
 * [new tag]         0.0.1 -> 0.0.1

Releaseタブをクリックして0.0.1が登録されていればOK。

f:id:rasukarusan:20191103203444p:plain:w500
Releaseタブで確認

f:id:rasukarusan:20191103203519p:plain:w500
0.0.1のtagが登録されていればOK

tagをpushできたのでReleaseノートを作る。Homebrewでインストールする先のURLを作る作業。
先程tagを確認したページ内のDraft new releaseボタンをクリックするとReleaseノート編集画面にうつる。

f:id:rasukarusan:20191103203609p:plain:w500
Releaseノートを作る

上記画像のように必要項目を埋め、publish releaseボタンをクリック。
下記画像のような感じになればOK。

f:id:rasukarusan:20191103203636p:plain:w500
Releaseノート作成完了

3. Formulaファイルを作ってpush

後はインストールファイル(Formulaファイル)を作ったら終了。

Formulaファイルを作成するコマンド

brew create https://github.com/Rasukarusan/homebrew-gitblamer/releases/download/0.01/gitblamer

ここで指定するURLは先程作ったReleaseノートのバイナリやShellScriptなどの実行ファイル本体のURL。

f:id:rasukarusan:20191103204134p:plain:w500
実行ファイル本体のURLをコピー

コマンドを実行すると下記のようなファイルがvimで開かれる。

# Documentation: https://docs.brew.sh/Formula-Cookbook
#                https://rubydoc.brew.sh/Formula
# PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST!
class Gitblamer < Formula
  desc "指定したユーザーの編集したファイルを全て出力するシェルスクリプト。"
  homepage ""
  url "https://github.com/Rasukarusan/gitblamer/releases/download/0.01/gitblamer"
  sha256 "8627dfae4335cbcfc1de18df4e08cc4a571c27d1c9065119b7ffc9139041c536"

  # depends_on "cmake" => :build

  def install
    # ENV.deparallelize  # if your formula fails when building in parallel
    # Remove unrecognized options if warned by configure
    system "./configure", "--disable-debug",
                          "--disable-dependency-tracking",
                          "--disable-silent-rules",
                          "--prefix=#{prefix}"
    # system "cmake", ".", *std_cmake_args
  end

  test do
    # `test do` will create, run in and delete a temporary directory.
    #
    # This test will fail and we won't accept that! For Homebrew/homebrew-core
    # this will need to be a test that verifies the functionality of the
    # software. Run the test with `brew test gitblamer`. Options passed
    # to `brew install` such as `--HEAD` also need to be provided to `brew test`.
    #
    # The installed folder is not in the path, so use the entire path to any
    # executables being tested: `system "#{bin}/program", "do", "something"`.
    system "false"
  end
end

リポジトリに説明文を書いていれば勝手に挿入される。巷でよく書かれているsha256も自動で作成される。 自動で作成されなかったら下記コマンドでsha256を作成して挿入する。別に空っぽでも大丈夫だった気がする。

$ openssl dgst -sha256 gitblamer

Formulaファイルを編集する。いらない箇所は削除し、今回の場合は実行ファイルをインストールしたいのでbin.installを記載する。諸々削除して最終的なものは以下。

class Gitblamer < Formula
  desc "指定したユーザーの編集したファイルを全て出力するシェルスクリプト。"
  homepage ""
  url "https://github.com/Rasukarusan/gitblamer/releases/download/0.01/gitblamer"
  sha256 "8627dfae4335cbcfc1de18df4e08cc4a571c27d1c9065119b7ffc9139041c536"

  def install
    bin.install "gitblamer"
  end
end

このファイルを手順1で作ったhomebrew-tapリポジトリにのFormulaディレクトリ内に入れる。ファイル自体を移動させてもいいし、コピーして新たにファイルを作っても良い。

最終的にはこんなディレクトリ構造になっているはず。

~/Desktop/homebrew-tap  master ✗
$ tree
.
|-- Formula
|   `-- gitblamer.rb
`-- README.md

あとはpushして終了。

~/Desktop/homebrew-tap  master ✔
$ git push origin master

インストールできるか試してみる

$ brew tap Rasukarusan/tap
$ brew install Rasukarusan/tap/gitblamer

インストールしたコマンドが使えるかも確認する

$ gitblamer
/Users/rasukaru/Desktop/homebrew-tap/Formula/fzf-crhome-active-tab.rb
/Users/rasukaru/Desktop/homebrew-tap/Formula/gitblamer.rb

Total 2 file found!

OK!!

死ぬほど参考にしたサイト

特にこの下のサイト、homebrew-XXXのリポジトリを1つにまとめることができるって初めて知ったので、ハイパーリスペクトを贈りたい。ありがとうございます。

今回使用したGithubリポジトリ

自分が配布したいツール github.com

配布するために必要なリポジトリ(今後配布したいツールが現れたらここに.rbファイルを追加していく) github.com

終わり

文章で書くと長いが、めちゃくちゃ簡単に自作ツールが配布できるのでHomebrewは偉大。 たぶんここまでの道のりを楽にするツールとか探せばいっぱいあると思うけど、1回は手動でやったほうが身に染みるよね。

fzfでChromeのtab移動をする

f:id:rasukarusan:20191103190334p:plain

VimでWeb開発してるとブラウザとTerminalを行ったり来たりすることが多い。
ブラウザのタブ切り替えをブラウザでやるのではなく、Terminalでできればノンストレスな開発ができるのではないか。そうだろう。

fzfで実現した

github.com

追記:fzf本家の方、Junegunnさんからプルリクを頂き、よりパワフルでスマートなものになった。ほんと感謝、I love fzf。

Demo.gif
fzfでchromeのTabを選択する

こんな感じでfzfで今現在開いているChromeのTab一覧を出力して、選択したらアクティブにするスクリプト。

インストール

$ brew tap Rasukarusan/tap
$ brew install Rasukarusan/tap/fzf-chrome-active-tab

使い方

$ chrome-tab-activate

ソース

シンプルにAppleScriptとShellScriptの夢のコラボレーション。
成果物がShellScript1つだけってのはミニマムで良いよね。

#!/bin/sh

function getTabs() {
osascript << EOF
    set _output to ""
    tell application "Google Chrome"
        set _window_index to 1
        repeat with w in windows
            set _tab_index to 1
            repeat with t in tabs of w
                set _title to get title of t
                set _url to get URL of t
                set _output to (_output & _window_index & "\t" & _tab_index & "\t" & _title & "\t" & _url & "\n")
                set _tab_index to _tab_index + 1
            end repeat
            set _window_index to _window_index + 1
            if _window_index > count windows then exit repeat
        end repeat
    end tell
    return _output
EOF
}

function setActiveTab() {
local _window_index=$1
local _tab_index=$2
osascript -- - "$_window_index" "$_tab_index" << EOF
on run argv
    set _window_index to item 1 of argv
    set _tab_index to item 2 of argv
    tell application "Google Chrome"
        activate
        set index of window (_window_index as number) to (_window_index as number)
        set active tab index of window (_window_index as number) to (_tab_index as number)
    end tell
end run
EOF
}

function main() {
    local tabs=$(getTabs)
    local title=$(echo "${tabs}" | awk -F '\t' '{print $3}' | fzf)
    local window_index tab_index
    read window_index tab_index <<< "$(echo "${tabs}" | grep -F "${title}" | head -n 1 | awk -F '\t' '{print $1, $2}')"
    setActiveTab $window_index $tab_index
}

main

学び

今回は結構色々学びがあった。

  • ヒアドキュメントでAppleScript書けばShellScript内に埋め込める
osascript << EOF
    tell application "Google Chrome"
    ...
EOF

みたいな形でShellScript内にAppleScriptを埋め込めるのはいい。今までAppleScriptは別のシェルファイルとして作ってたから、 1つのファイルにまとまるのは非常に良い。

  • readコマンドの変数のぶち込み方
read window_index tab_index <<< "$(echo "${tabs}" | grep -F "${title}" | head -n 1 | awk -F '\t' '{print $1, $2}')"

これも「ああなるほどなぁ...」と思うreadの使い方だった。fzfのexamplesに載っていた。
今まで複数の変数を定義する場合、

local window_index=$(echo "${tabs}" | awk -F '\t' '{print $1}')
local tab_index=$(echo "${tabs}" | awk -F '\t' '{print $2}')

こんな感じで1個ずつ定義していたが、何回も同じものをechoしていて冗長だなと感じていた。
read a1 a2 <<< awk '{print $1, $2}'ならphpのlist()みたいな形で一気に複数の変数を定義できる。これは賢い。

  • AppleScriptへのコマンドによる引数の渡し方

ヒアドキュメントでAppleScript書く場合、引数はどうやって渡すのかと悩んだが、下記でいけるらしい。

osascript -- - "$_window_index" "$_tab_index" << EOF
on run argv
    set _window_index to item 1 of argv
    set _tab_index to item 2 of argv
    ...
EOF

終わり

そもそもChromeのTabをfzfで選択したい、ってのは1年ぐらい前に思いついたが今まで手を出していなかった。
当時はAppleScriptを使うっていう脳もなかったし何から調べたら良いのかわからなかったけど、そう思うと以前よりやれること増えた。

アウトプットを気軽にするために作ったコマンド

f:id:rasukarusan:20191001002258p:plain:w400

記事書くときはVimで大体書いてからはてぶで整形するというのを繰り返しているが、最初の「Vimで記事を書き始める」という障壁をできるだけ低くし、なんとなくモチベーションが続きそうなやり方をコマンドで実践してる。

どんな感じ

こんな感じ。

https://rasukarusan.github.io/blog-assets/shell-article-command/demo.gif
コマンドを入り口に新規記事を書き始める

下書き記事をfzfで表示して、000000.mdを選択すると新規記事を作成する。
書き終わったら「完」と入力して「投稿済みディレクトリ」に移動する。

https://rasukarusan.github.io/blog-assets/shell-article-command/demo.gif
「完」ものは投稿済みディレクトリに移動

何が良いのか

  • ネタを思いついたら速攻書ける
  • 後から見返すことが出来るのでアウトプットが埋もれることを防げる
  • Terminalから動かなくて良い(これ大事)

別に利点らしい利点は無く、結局自分が書きやすいスタイルなら何でも良いよね。
自分の場合は「Terminalから動かないこと」が一番重要だったのでコマンドスタイルになった。

コマンド

特別なことは特にしていない。.zshrcに関数書いてalias登録するだけ。この気軽さもいい。

ちょっとお気に入りな点を言うなら000000.mdを選択肢に表示して、選択すると新規記事が作成されるところ。ファイル名を00000にしてるから必ず一番上に表示することができて気が滅入る前に記事を新規作成できる。

alias art='_writeArticle'
alias mpa='_movePostedArticles'

# 記事メモコマンド
function _writeArticle() {
    # 下書き記事の保存場所
    local ARTICLE_DIR=/Users/`whoami`/Desktop/ru-she-1nian-mu/articles
    local article=$(ls ${ARTICLE_DIR}/*.md | xargs basename | fzf)

    # 何も選択しなかった場合は終了
    if [ -z "$article" ]; then
        return 0
    fi

    if [ "$article" = "00000000.md" ]; then
        echo "タイトルを入力してくだい"
        read title
        today=$(date '+%Y_%m_%d_')
        vim ${ARTICLE_DIR}/${today}${title}.md
    else
        vim ${ARTICLE_DIR}/${article}
    fi
}
# 投稿した記事を別ディレクトリに移動
function _movePostedArticles() {
    # 投稿完了を意味する目印
    local POSTED_MARK='完'
    # 下書き記事の保存場所
    local ARTICLE_DIR=/Users/`whoami`/Desktop/ru-she-1nian-mu/articles

    # 投稿が完了した記事を保存するディレクトリ
    local POSTED_DIR=$ARTICLE_DIR/posted

    for file in `ls $ARTICLE_DIR`; do
        tail -n 1 ${ARTICLE_DIR}/${file} | grep $POSTED_MARK > /dev/null
        # 投稿が完了したファイルを別ディレクトリに移す
        if [ $? -eq 0 ]; then 
            mv ${ARTICLE_DIR}/${file} $POSTED_DIR/
            printf "\e[33m${file} is moved!\e[m\n"
        fi
    done
}

終わり

みんな記事書くときどういうスタイルでやってるのか気になるな。
アプリを使うやつとかは正直興味わかないけど、それこそコマンドでやってる人いたらその方法はとっても気になる。

oh-my-zshからの脱却

f:id:rasukarusan:20190717201836p:plain

今更マサラタウンだけどoh-my-zshから脱却した。
全然遅さとか感じていなかったが、Profileしてみるとoh-my-zsh関連がビビるほどの遅延を発生させており、腹がたったからやった。

oh-my-zshを脱却して困ったこと

oh-my-zshを入れていると、色々と勝手に設定ファイルを読み込んでくれる。
なのでoh-my-zshを脱却すると、自分の場合下記のことができなくなった。

  • Tab関連
    • あいまい検索(大文字小文字無視)
    • 補完候補に色
  • alias
  • ヒストリーの重複削除
  • テーマの読み込み
    • lsの色
    • タブの色
    • Gitの表示(クリーンな状態なら✔を、差分があったら✗を表示するなど)

人によっては.zshrcにsetopt XXXXみたいな形で個別に設定している項目もあるので、人それぞれできなくなることは違う。

上記のことはoh-my-zshが良きに計らってくれていたので、個別に設定する必要がある。今回はそのために設定したことを書いた。

脱却して良かったこと

  • tmuxで色がつくようになった
  • 起動が速くなった

起動速くなったのが一番かな。短縮したのは0.5秒ぐらいだけど、0.5秒って体感的にこんなに違うんだと思ったよ。
起動だけじゃなくて基本的な動作(lsの表示速度など)も速くなってだいぶノンストレスになった。

oh-my-zsh脱却時の差分

  • 脱却のための削除
- export ZSH=/Users/$(whoami)/.oh-my-zsh
- ZSH_THEME="avit"
- plugins=(git)
- source $ZSH/oh-my-zsh.sh
  • Tab, aliasなど設定関連を追加
# テーマ読み込み
source ~/dotfiles/zsh-my-theme.sh
# Tabで選択できるように
zstyle ':completion:*:default' menu select=2
# 補完で大文字にもマッチ
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'
# ファイル補完候補に色を付ける
zstyle ':completion:*' list-colors ${(s.:.)LS_COLORS}
setopt auto_param_slash       # ディレクトリ名の補完で末尾の / を自動的に付加し、次の補完に備える
setopt mark_dirs              # ファイル名の展開でディレクトリにマッチした場合 末尾に / を付加
setopt auto_menu              # 補完キー連打で順に補完候補を自動で補完
setopt interactive_comments   # コマンドラインでも # 以降をコメントと見なす
setopt magic_equal_subst      # コマンドラインの引数で --prefix=/usr などの = 以降でも補完できる
setopt complete_in_word       # 語の途中でもカーソル位置で補完
setopt print_eight_bit        # 日本語ファイル名等8ビットを通す
setopt extended_history       # record timestamp of command in HISTFILE
setopt hist_expire_dups_first # delete duplicates first when HISTFILE size exceeds HISTSIZE
setopt share_history          # 他のターミナルとヒストリーを共有
setopt histignorealldups      # ヒストリーに重複を表示しない
setopt hist_save_no_dups      # 重複するコマンドが保存されるとき、古い方を削除する。
setopt extended_history       # $HISTFILEに時間も記録
setopt print_eight_bit        # 日本語ファイル名を表示可能にする
setopt hist_ignore_all_dups   # 同じコマンドをヒストリに残さない
setopt auto_cd                # ディレクトリ名だけでcdする
setopt no_beep                # ビープ音を消す
# コマンドを途中まで入力後、historyから絞り込み
autoload -Uz history-search-end
zle -N history-beginning-search-backward-end history-search-end
zle -N history-beginning-search-forward-end history-search-end
bindkey "^P" history-beginning-search-backward-end
bindkey "^N" history-beginning-search-forward-end

alias l='ls -ltrG'
alias la='ls -laG'
alias ll='ls -lG'
alias ls='ls -G'
alias grep='grep --color=auto'
alias ...='cd ../../'
alias his='history -E -i 1 | fzf'

テーマは別ファイルにして読み込んでる(上記のsource ~/dotfiles/zsh-my-theme.sh)

  • テーマファイル zsh-my-theme.sh
export LSCOLORS="dxfxcxdxbxegedabagacad"
export LS_COLORS='di=33;:ln=35;40:so=32;40:pi=33;40:ex=31;40:bd=34;46:cd=34;43:su=0;41:sg=0;46:tw=0;42:ow=0;43:'
export GREP_COLOR='1;33'

# PROMPTテーマ
setopt prompt_subst #プロンプト表示する度に変数を展開
local BLACK=$'%{\e[30m%}'
local RED=$'%{\e[31m%}'
local GREEN=$'%{\e[32m%}'
local YELLOW=$'%{\e[33m%}'
local BLUE=$'%{\e[34m%}'
local PURPLE=$'%{\e[35m%}'
local CYAN=$'%{\e[36m%}'
local GRAY=$'%{\e[37m%}'
local WHITE=$'%{\e[1;37m%}'
local DEFAULT=$'%{\e[1;m%}'
local RAINBOW=$'%{\e[$[color=$[31+$RANDOM%6]]m%}'
PROMPT="
${CYAN}%~${reset_color}
$ "

autoload -Uz vcs_info
zstyle ':vcs_info:git:*' check-for-changes true      # formats 設定項目で %c,%u が使用可
zstyle ':vcs_info:git:*' stagedstr "%F{red}"         # commit されていないファイルがある
zstyle ':vcs_info:git:*' unstagedstr "%F{red}"       # add されていないファイルがある
zstyle ':vcs_info:*' formats "%F{green}%b %c%u%m %f" # 通常
zstyle ':vcs_info:*' actionformats '[%b|%a]'         # rebase 途中,merge コンフリクト等 formats 外の表示
precmd () { vcs_info }
PROMPT="
${CYAN}%~${reset_color}"
PROMPT=$PROMPT'  ${vcs_info_msg_0_}
${GRAY}$ ${reset_color}'

zstyle ':vcs_info:git+set-message:*' hooks git-is_clean git-untracked
# 状態がクリーンか判定
function +vi-git-is_clean(){
    if [ -z "$(git status --short 2>/dev/null)" ];then
        hook_com[misc]+="✔"
    fi
}
# unstaged, untrackedの検知
function +vi-git-untracked() {
    if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
        hook_com[unstaged]+='%F{red}✗%f'
    fi
}