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

Vim、ShellScriptについてよく書く

Finderで開いているディレクトリにコマンドで移動する(open . ではない)

f:id:rasukarusan:20190508225753p:plain

ShellScriptでドヤりたいGWアドベントカレンダー6日目いくで。

よくある「open .」でFinderで開く方法ではなく、逆にFinderで開いているディレクトリをTerminalで開く。 何を言っているかよくわからないのでとりあえず動作デモ。

https://rasukarusan.github.io/blog-assets/shell-finder/demo1.gif
Finderで開いているディレクトリに移動

伝わるか・・・?Finderで今開いているディレクトリにTerminalで移動している。

何がしたいのか

僕はよくGithubでソースみたいときに、とりあえずZipでダウンロードしてChromeで、Finderのプレビューで少し眺めてから
詳しく見たいやつをVimで開くっていう流れが多い。

https://rasukarusan.github.io/blog-assets/shell-finder/demo2gif
Finderで開いているディレクトリに移動したいケース

しかしFinderからTerminalに移動して、cd XXXX/YYYYとする際、今DLしたものの名前を覚えておく必要があり、そこを楽にしたかった。
(最初からgit cloneで持ってこいよという話はナシにしましょうね。)

Finderで今開いているパスを知りたい

大体なんとなくダウンロードしたディレクトリ名は覚えているから特に困りはしないのだが、
そもそも覚えていたくない。のでTerminalから取得できるようにする。
AppleScriptを使えば一発だ。

finder.sh

#!/usr/bin/osascript

## 現在Finderで開いているディレクトリのPATHを取得
tell application "Finder"
    if exists Finder window 1 then
        set currentDir to target of Finder window 1 as Unicode text
        set posixPath to get POSIX path of currentDir
    end if
end tell

実行

$ osascript finder.sh
/Users/rasukaru/Downloads/shell-clipboard-magager-master/

ネックなのがif exists Finder window 1 thenとしているので、Finderを複数開いている場合1番目に開いたFinderのディレクトリしか取得できない。

tell application "Finder"
    set total_num_finder to (count of (every window where visible is true)
end tell

で今開いているFinderの数は取得できるので、うまいことやれば全部のディレクトリ取得できて、fzfに食わせればいい感じになるかもわからん。

Terminalから移動する

もうあとはaliasやら関数に登録して終了だ。あっけない。

.zshrc

alias cdf='cd $(osascript ~/finder.sh)'

Finderのツールバーにスクリプトを配置してもうちょっと楽(?)にする

ぶっちゃけあんまり使えないかもだが一応紹介しておく。
Commandを押しながらファイルをドラッグアンドドロップでFinderのツールバーに持っていくと、ツールバーに登録できる。ツールバーにあるものはクリックして実行が可能。

https://rasukarusan.github.io/blog-assets/shell-finder/demo3.gif
Finderのツールバーにスクリプトを登録

これを利用してTerminalに戻らずFinderから直接Terminalの画面に移ることができる。

ツールバー用のfinder.scptを作成する

ツールバー登録用としてfinder.scptを作成する。
finder.scpt

## 現在Finderで開いているディレクトリのPATHを取得
tell application "Finder"
    if exists Finder window 1 then
        set currentDir to target of Finder window 1 as Unicode text
        set posixPath to get POSIX path of currentDir
    end if
end tell

## Terminal上でPATHへ移動する
tell application "iTerm"
    activate
    set _current_session to current session of current window
    tell _current_session
        write text "cd " & posixPath
    end tell
end tell

これをツールバーに登録して実行した挙動が以下。

https://rasukarusan.github.io/blog-assets/shell-finder/demo4.gif
Finderのツールバーからスクリプトを実行

まあ、、、見事Terminalにいちいち戻らず移動できたわけだが問題点がいくつかある。

  1. finder.scptをクリックしたらいちいち「Run」を押さないと行けない
  2. Terminalに直接cd XXXの命令を書き込んでいるので、VimなどをTerminalで開いていた場合そこにcdd XXXと書き込まれてしまう

・・・ゴミだな

終わり

まあなんとも言えない感じのものが出来上がってしまったが、作ったからにはアウトプットしておこうと思った。
ただ、Finderのツールバー登録は初めて知ったしもしかしたらアイデア次第で何か使えるかもしれん。
こんなふうに使っているよという人いたらぜひとも教えて欲しい。

ShellScriptでクリップボードの履歴管理する(ついでに水平連結もする)

ShellScriptでドヤりたいGWアドベントカレンダー5日目いくで。

クリップボード管理アプリをShellScriptで作った。
そもそもはスプレッドシートから列をコピーしてA列とB列を入れ替えて貼り付けたいと思ったのが発端で、
どちらかというと副産物的な感じでクリップボード管理ができた感じ。

github.com

デモ

こんな感じのやつね。

https://rasukarusan.github.io/blog-assets/shell_clipboard/demo1.gif

クリップボード履歴を表示して、fzfで選択した順に水平に連結してコピーする。

クリップボードの監視

とりあえず既存のクリップボード管理アプリはどうやっているのか調べてみた。

「Clipy」というアプリはtimerを使用しているみたい。

github.com

// MARK: - Clips
func startMonitoring() {
    disposeBag = DisposeBag()
    // Pasteboard observe timer
    Observable<Int>.interval(0.75, scheduler: scheduler)
        .map { _ in NSPasteboard.general.changeCount }
        .withLatestFrom(cachedChangeCount.asObservable()) { ($0, $1) }
        .filter { $0 != $1 }
        .subscribe(onNext: { [weak self] changeCount, _ in
            self?.cachedChangeCount.accept(changeCount)
            self?.create()
        })
        .disposed(by: disposeBag)

他のアプリも見てみる。「Maccy」というアプリ。これもtimerを利用している。

github.com

func startListening() {
Timer.scheduledTimer(timeInterval: timerInterval,
                     target: self,
                     selector: #selector(checkForChangesInPasteboard),
                     userInfo: nil,
                     repeats: true)
}

ということで割とタイマーでクリップボードを監視するのが一般的っぽい。 コピーの動作をhookしてやるのかなと思っていたのでちょっと意外だった。hookできないのかもしれん。まあとりあえず今は置いておこう。

データの保存方法

クリップボードの内容を保存する方法は自由。
一応、「Clipy」ではRealmでDB保存、別のアプリではUserDefaultsを使って.plistファイルに保存している。

「Clipy」の場合

fileprivate func save(with data: CPYClipData) {
    let realm = try! Realm()
    // Copy already copied history
    let isCopySameHistory = AppEnvironment.current.defaults.bool(forKey: Constants.UserDefaults.copySameHistory)
    if realm.object(ofType: CPYClip.self, forPrimaryKey: "\(data.hash)") != nil, !isCopySameHistory { return }
    // Don't save invalidated clip
    if let clip = realm.object(ofType: CPYClip.self, forPrimaryKey: "\(data.hash)"), clip.isInvalidated { return }
...

「Maccy」の場合

func all() -> [String] {
guard var savedHistory = UserDefaults.standard.array(forKey: storageKey) as? [String] else {
  return []
}

let maxSize = UserDefaults.standard.integer(forKey: sizeKey)
while savedHistory.count > maxSize {
  savedHistory.remove(at: maxSize - 1)
}

return savedHistory
}

とりあえずシンプルにファイル保存でいいかなと思う。

実装の流れ

ということでShellScriptでやるとしたらおそらくこの流れ。

  1. クリップボードの中身をファイルに書き込むShellScriptを作成(clilpObserver.sh)
  2. 1.で作ったスクリプトをcronで回してクリップボード監視
  3. ファイルを読み込んで履歴として表示する(clipboard.sh)

1. クリップボードの中身をファイルに書き込むShellScriptを作成

求めているのは以下。

  • クリップボードの中身が空だったら何もしない
  • 前の内容と同じだったら何もしない
  • クリップボードの内容をファイルに書き込む
  • 最大保存件数を超えていたら古いものから削除

clipObserver.sh

#!/bin/sh
#
# 現在のクリップボードの内容をログファイルとして保存する
# crontabにセットしてクリップボード監視を行うためのスクリプト
# ログ保存場所は~/.cliplog/連番のファイル
#

DIR_CLIP_LOG=~/.cliplog
MAX_CLIP_CNT=100

clip_log_cnt=$(ls -1 $DIR_CLIP_LOG | wc -l | tr -d " ")
latest_clip_log=$(ls -t $DIR_CLIP_LOG | head -n 1)
oldest_clip_log=$(ls -t $DIR_CLIP_LOG | tail -n 1)

# クリップボードのログ置き場が存在しなければ作成する
if [ ! -e $DIR_CLIP_LOG ]; then
    mkdir $DIR_CLIP_LOG
fi

# クリップボードが空の場合は何もしない
if [ "$(pbpaste)" = "" ]; then
    exit
fi

# 一番最初はファイルが存在しないのでログファイル作成して終了
if [ -z $latest_clip_log ]; then
    pbpaste 2>/dev/null | /usr/local/bin/nkf -w > $DIR_CLIP_LOG/1
    exit
fi

# 最大履歴保存数を超えていたら一番古いファイルを削除する
if [ $clip_log_cnt -gt $MAX_CLIP_CNT ]; then
    rm $DIR_CLIP_LOG/$oldest_clip_log 2>/dev/null
fi

new_clip_log=$(expr $latest_clip_log + 1)
# クリップボードログを作成
pbpaste 2>/dev/null | /usr/local/bin/nkf -w > $DIR_CLIP_LOG/$new_clip_log

# 最新のクリップボードのログが現在の内容と同じだったら今作ったログを削除する
# ファイルを一旦作ってdiffを取るほうが動作が安定する
is_different=$(diff -EbB $DIR_CLIP_LOG/$latest_clip_log $DIR_CLIP_LOG/$new_clip_log)
if [ -z "$is_different" ]; then
    rm $DIR_CLIP_LOG/$new_clip_log 2>/dev/null
fi

クリップボードの履歴表示をするときにsortで楽に並び替えするためにログファイル名を連番にしてる。idのような扱い。

$ sh ~/clipObserver.sh

を実行するとクリップボードの中身がファイルに出力される。

~/.cliplog
$ ls
1

https://rasukarusan.github.io/blog-assets/shell_clipboard/demo2.gif

2. スクリプトをcronで回してクリップボード監視

cronに登録するだけ。秒数は適当でいいと思うが、とりあえず1秒で設定してる。
別に5秒とかでも十分。そんなに急がなくてもいいじゃない。

$ crontab -l
* * * * * for i in `seq 0 1 59`;do (sleep ${i}; sh ~/clipObserver.sh) & done;

https://rasukarusan.github.io/blog-assets/shell_clipboard/demo3.gif

コピーするたびにログファイルが追加されていってますね。

3. ファイルを読み込んで履歴として表示する

1,2のスクリプトによって作成されたクリップボードのログファイルを閲覧・選択してコピーする、フロントエンド側のスクリプトを作る。
選んだものを水平連結したかったので、pasteコマンドに渡せるようfzfで選択したものはログ1 ログ2のような形で変数に格納し、

$ paste ログ1 ログ2

と出来るようにした。

clipboard.sh

#!/bin/sh
#
# クリップボードの履歴をfzfを表示し、選択したものを水平連結する
# clipObserver.shで作成されたクリップボードログファイルを参照するスクリプト
#
DIR_CLIP_LOG=~/.cliplog

clip_logs=$(ls -t $DIR_CLIP_LOG | fzf --height 100% --prompt "CHOOSE" --preview "cat $DIR_CLIP_LOG/{}")
select_files=''
for clip_log in $clip_logs; do
    select_files="$select_files ${DIR_CLIP_LOG}/${clip_log}"
done

if [ -n "$select_files" ]; then
    # 水平連結したものをクリップボードにコピーする
    paste $select_files | pbcopy
    pbpaste
fi

あとは呼び出しやすいようにaliasに設定して終わり。

.zshrc

alias cl='sh ~/clipboard.sh'

終わり

pbpasteを使っているのでMac限定となっているが、xclipとかに変えれば他のOSでも動くかな。たぶん。
今回のは完全にオナニースクリプトだ。

更新日を見ているので連番で振り直せないし、sqliteで管理したほうが楽だったんじゃないかとか思うけど、まあいいでしょう。 クリップボードの中身でfzfの絞り込みを使いたかったが、現状特に不便はない。大体直前のものが欲しいだけだから。

ただやっぱりcronで回すと読み書きが頻繁に起こることが嫌でもわかってしまうので、それだけは心苦しい。なんとかしたい。

Shellで定義済みの関数を流し読みしながら表示する

ShellScriptでドヤりたいGWアドベントカレンダー4日目いくで。

皆さん.zshrcや.bashrcは充実していますか?
Vimの世界では「.vimrcの量=vim力」と言われているらしいですが、shellも似たようなものでしょうか。

それはさておきshellには様々な関数がありますよね。zshやbashで標準で組み込まれている関数やzshrcなどに書き込んだ自作の関数。

「あの関数どうやって書いてたかなあ」とか「あれってどうやって実装されてるんだ?」といったときに便利なやつを作ってみた。

流し読みしながら気になる関数の中身を出力する

https://rasukarusan.github.io/blog-assets/shell_show_func/demo.gif
関数の中身をチラ見しながら選べる

みんな大好きfzfを利用しています。
左側に定義済みの関数名、右側にその中身をプレビューで出力しています。
ENTER押したらその内容を出力するものです。 ちなみにこれ地味に苦労した。。。どこに詰まったかは後述する。

とりあえず最終形だけ載せておきますね。
.zshrcに記載してください。bashの人はsource .zshrcのところを.bashrcに書き換えてね

function func() {
    local func=$(
       typeset -f \
       | grep ".*() {$" \
       | grep "^[a-z_]" \
       | tr -d "() {"   \
       | fzf --height 100% --preview "source ~/.zshrc; typeset -f  {}"
   )
    if [ -z "$func" ]; then
        return
    fi
    typeset -f $func
}

定義済み関数を全て出力する

これはtypeset -fで一発ですね。

$ typeset -f 

_SUSEconfig () {
    # undefined
    builtin autoload -XUz
}
__phpbrew_load_user_config () {
    if [[ -f $PHPBREW_HOME/init ]]
    then
        . $PHPBREW_HOME/init
        __phpbrew_set_path
    fi
}
...

zshだと同様の出力をするfunctionsという関数があります。

$ functions

_SUSEconfig () {
    # undefined
    builtin autoload -XUz
}
__phpbrew_load_user_config () {
    if [[ -f $PHPBREW_HOME/init ]]
    then
        . $PHPBREW_HOME/init
        __phpbrew_set_path
    fi
}
....

ただfunctions関数はman zshbuiltinsで見てみると、

functions [ {+|-}UkmtTuWz ] [ -x num ] [ name ... ]
functions -M [-s] mathfn [ min [ max [ shellfn ] ] ]
functions -M [ -m pattern ... ]
functions +M [ -m ] mathfn ...
      Equivalent to typeset -f, with the exception of the -x, -M  and  -W
      options.   For  functions  -u and functions -U, see autoload, which
      provides additional options.
      ....

Equivalent to typeset -fとなっているのでtypeset -fと同じであることがわかります。

関数名だけ出力する

シンプルにgrepの正規表現で抜き取ります。

$ typeset -f \
| grep ".*() {$" \
| grep "^[a-z_]" \
| tr -d "() {"   \

_SUSEconfig
__phpbrew_load_user_config
__phpbrew_reinit
__phpbrew_remove_purge
__phpbrew_set_lookup_prefix
__phpbrew_set_path
...

たぶんもっと良い正規表現があると思いますが、とりあえずこれで十分でしょう。

fzfで関数の中身をpreviewする

ここがめちゃくちゃ詰まったんですよねえ・・・ 関数の中身を出力するには

$ typeset -f 関数名

で出力できるので、普通にfzf --preview 'typeset -f {}'でイケるやろ!と思ってました。下のような感じで。

typeset -f \
| grep ".*() {$" \
| grep "^[a-z_]" \
| tr -d "() {"   \
| fzf --preview 'typeset -f {}'

するとね、何も表示されないんですよ。。。
なんでや!?と思いxargsで渡す方法にしてみた。

fzf --preview 'echo {} | xargs typeset -f'

すると以下のエラー文がプレビューに表示された。

xargs: typeset : No such file or directory

なんですかこれ・・・

これ調べてみると割と既知のエラーらしく、どうやらxargsにshellの関数を渡すときは一工夫しないといけないらしい。 詳しくは以下のサイトが参考になる。

stackoverflow.com unix.stackexchange.com

ということでどうしようかと色々調べていると、fzfのissuesに同じ質問があった。

github.com


Bash functions are not visible to child processes if you don't explicitly export them.

どうやら子プロセスではshell関数って表示されないみたい。定義されていないとみなされるのかな?

ということでissuesにも載っているfzfのpreview内でsource ~/.zshrcすることで落ち着いた。

$ typeset -f \
| grep ".*() {$" \
| grep "^[a-z_]" \
| tr -d "() {"   \
| fzf --height 100% --preview "source ~/.zshrc; typeset -f {}"

f:id:rasukarusan:20190426232719p:plain
preview画面に関数の中身を表示

やったぜ。(ただ毎回sourceするからちょっと遅い)

終わり

最初思いついたときはすぐ出来るだろと思っていたが、思わぬところで詰まった。
ただこういうところからコマンドの知識が増えていくのがShellScriptの醍醐味ではある。

コマンドで文字列画像を生成する

ShellScriptでドヤりたいGWアドベントカレンダー3日目いくで。

適当な画像が必要なときって結構あると思う。
そんな時一々Googleで画像検索するのが億劫になったのでコマンド一発で作れるようにした。

デモ

こんな感じで好きな文字列の画像を生成する。

https://rasukarusan.github.io/blog-assets/shell_convert_image/demo2.gif

方法

function create_bg_img() {
    local size=1280x760
    local backgroundColor="#000000"
    local fillColor="#ff8ad8" # 文字色
    # フォントによっては日本語対応しておらず「?」になってしまうので注意
    local fontPath=/System/Library/Fonts/ヒラギノ明朝\ ProN.ttc 
    local default_caption='(・∀・)'
    local caption=${1:-$default_caption}
    local imgPath=~/output.png
    convert \
      -size $size  \
      -background $backgroundColor\
      -fill $fillColor \
      -font $fontPath \
      caption:$caption \
      $imgPath
}

convertコマンドを使用するので、imageMagickが必要。
Macだったらbrew install imagemagickでインストールできるかな。

実行

$ create_bg_img hoge

実行結果 f:id:rasukarusan:20190425215524g:plain

サイズを選択して生成したい

「1024px以下の画像のみ対応」とかあるので、ある程度サイズ指定して生成したい。
fzfでサイズを選択して生成の流れが一番楽。

function create_bg_img() {
    local sizes=(100x100 320x240 360x480 500x500 640x480 720x480 1024x768 1280x960)
    local size=$(echo ${sizes} | tr ' ' '\n' | fzf)
    local backgroundColor="#000000"
    local fillColor="#ff8ad8" # 文字色
    # フォントによっては日本語対応しておらず「?」になってしまうので注意
    local fontPath=/System/Library/Fonts/ヒラギノ明朝\ ProN.ttc 
    local default_caption='(・∀・)'
    local caption=${1:-$default_caption}
    local imgPath=~/output.png
    convert \
      -size $size  \
      -background $backgroundColor\
      -fill $fillColor \
      -font $fontPath \
      caption:$caption \
      $imgPath
}

サイズ一覧はWikipediaから適当に取ってきた。

画面解像度 - Wikipedia

実行結果

https://rasukarusan.github.io/blog-assets/shell_convert_image/demo2.gif
サイズを指定して生成

iTermの背景画像を差し替える

AppleScriptと組み合わせればこんなこともできる。

https://rasukarusan.github.io/blog-assets/shell_convert_image/demo2.gif
SSHした時にiTerm上にホスト名を表示する

SSHしたときにホスト名の文字列画像を生成して、AppleScriptでiTermの背景画像に設定しています。

iTermの背景画像を設定するAppleScript
iterm_change_bg_img.sh

#!/usr/bin/osascript
on run argv
    tell application "iTerm"
        activate
        set _current_session to current session of current window
        tell _current_session
            set image_path to (item 1 of argv)
            set background image to image_path
        end tell
    end tell
end run

sshする時にitermの画像を設定する
.zshrc

function create_bg_img() {
    local size=1280x960
    local backgroundColor="#000000"
    local fillColor="#ff8ad8" # 文字色
    local fontPath=/System/Library/Fonts/ヒラギノ明朝\ ProN.ttc 
    local default_caption='(・∀・)'
    local caption=${1:-$default_caption}
    local imgPath=~/output.png
    convert \
      -size $size  \
      -background $backgroundColor\
      -fill $fillColor \
      -font $fontPath \
      caption:$caption \
      $imgPath
}

function ssh_local() {
    local ssh_config=~/.ssh/config
    local server=$(cat $ssh_config | grep "Host " | sed "s/Host //g" | fzf)
    if [ -z "$server" ]; then
        return
    fi
    # 画像生成
    create_bg_img $server
    # itermに画像を設定
    osascript ~/iterm_change_bg_img.sh ~/output.png
    ssh $server
}

終わり

shellの良いところはすぐ試せるし、いろんなものと絡められるところ。
あと依存は多少あるが、注意すれば割とどこでも動くところがスキ。

SSHしたときに画像を差し替えるってのは実際は使ってない。iTermのバッジでホスト名を表示するほうが美しかったので普段はそっちを使っている。

Chromeの履歴をShellScriptで弄り倒す

ShellScriptでドヤりたいGWアドベントカレンダー1日目いくで。

ブラウザの履歴ってその人がその日何をやっていたのか示すものとして結構な情報詰まってると思うのよね。
「俺今日何やってたっけ・・・」っていう時の思い出し作業や、「さっき見てたページもう一回開きたい」ときとか ターミナルからさくっと開けたら便利じゃねってことで作った。

github.com

https://user-images.githubusercontent.com/17779386/56350061-dc7bc180-6204-11e9-84cc-11f0cf919426.gif

Requirement

  • Mac
  • fzf

Chrome履歴のDBの場所

Chromeに関するファイルは~/Library/Application\ Support/Google/Chrome/にある。
履歴に関するものはSQLiteのDBで保存されている。

~/Library/Application\ Support/Google/Chrome/Default/History 

ChromeHistoryにアクセスする

SQLiteなのでsqlite3コマンドで扱える。

$ sqlite3 ~/Library/Application\ Support/Google/Chrome/Default/History 

でテーブル等を見ることができるが、Chromeを開いているときはChromeがLockしているので

Error: database is locked

とアクセスが弾かれてしまう。 Chromeを一旦閉じて接続するか、一旦DBのコピーをしてそれに接続するといった形を取る。

# Lockされているのでコピーを取る
$ cp ~/Library/Application\ Support/Google/Chrome/Default/History ~/

# コピーしたDBに接続する
$ sqlite3 ~/History

こうすればChromeを開いたままChromeHistoryにアクセスできる。

ChromeHistoryからアクセス履歴を取得する

Historyにはいくつかテーブルがあるが、アクセス履歴を保存しているのはurlsテーブル。
sqlite3のコマンドは他で調べたらいっぱい出てくるので適当にググってね。

$ sqlite3 ~/Library/Application\ Support/Google/Chrome/Default/History 

SQLite version 3.19.3 2017-06-27 16:48:08
Enter ".help" for usage hints.
sqlite> .headers on
sqlite> select * from urls;

id|url|title|visit_count|typed_count|last_visit_time|hidden
57647|https://www.google.co.jp/save?authuser=0|コレクション|2|0|13200489910808958|0
57648|https://number333.org/|iPhone・Mac・ガジェットブログ "monograph(モノグラフ)"|1|0|13200489927232416|0
57649|https://qiita.com/luccafort/items/e36faff2d7e4320f2bf3|tigがLibrary not loadedとエラーになってしまう現象の対応 - Qiita|1|0|13200491048935113|0
57650|https://orebibou.com/2019/01/macos%E3%81%A7%E3%80%8Cdyld-library-not-loaded-usr-local-opt-readline-lib-libreadline-7-dylib%E3%80%8D%E3%81%AA%E3%82%8B%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%8C%E5%87%BA%E3%82%8B%E3%82%88%E3%81%86/|MacOSで「dyld: Library not loaded: /usr/local/opt/readline/lib/libreadline.7.dylib」なるエラーが出るようになる | 俺的備忘録 〜なんかいろいろ〜|1|0|13200491115865059|0

アクセス時刻を表すlast_visit_timeカラムの値が通常のunixtimeとは異なるから注意。
どうやら基本時刻が1601-01-01 00:00:00に設定されているらしいので、変換が必要になる。

時刻の変換、ソート等をしたら最終的には下のようなクエリになった。

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
;

実行結果

url|title|date
https://orebibou.com/2019/01/macos%E3%81%A7%E3%80%8Cdyld-library-not-loaded-usr-local-opt-readline-lib-libreadline-7-dylib%E3%80%8D%E3%81%AA%E3%82%8B%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%8C%E5%87%BA%E3%82%8B%E3%82%88%E3%81%86/|MacOSで「dyld: Library not loaded: /usr/local/opt/readline/lib/libreadline.7.dylib」なるエラーが出るようになる | 俺的備忘録 〜なんかいろいろ〜|2019-04-23 20:05:15
https://www.google.co.jp/search?q=dyld:%20Library%20not%20loaded:%20/usr/local/opt/readline/lib/libreadline.7.dylib|dyld: Library not loaded: /usr/local/opt/readline/lib/libreadline.7.dylib - Google 検索|2019-04-23 20:05:12
https://qiita.com/luccafort/items/e36faff2d7e4320f2bf3|tigがLibrary not loadedとエラーになってしまう現象の対応 - Qiita|2019-04-23 20:04:08
https://qiita.com/nwtgck/items/f5427c0d0f7827658bd5|Macでawkを実行するとエラー: "dyld: Library not loaded: /usr/local/opt/readline/lib/libreadline.7.dylib" となる問題の解決法 - Qiita|2019-04-23 20:02:08

あとはこれをShellで包んであげればOKだ。

ShellScriptでイジイジする

基本的な流れは

  1. ChromeHistoryをコピーする
  2. コピーしたChromeHistoryにexpectでクエリ投げる
  3. クエリの結果をawkで整形してfzfに食わせる

でfin。イージーですね。

chromeHistory.sh

# ChromeのDBの内容をcsvで一時保存するパス
PATH_CHROME_HISTORY=/Users/`whoami`/chrome_history.csv

function export_chrome_history() {
    local USER=`whoami`
    # 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
    ;
    "
    # そのままSQLを流すとエラーが出るので改行を消す
    local SQL=$(echo "${SQL}" | tr '\n' ' ')

    # コマンドで参照できるようにDBの内容をcsvに書き出す
    expect -c "
        spawn sqlite3 /Users/$USER/History
        expect \">\"
        send \".mode csv\r\"
        expect \">\"
        send \".output $PATH_CHROME_HISTORY\r\"
        expect \">\"
        send \"$SQL\r\"
        expect \">\"
    " >/dev/null
}

function show_chrome_history() {
    local filter=${1:-""}
    local chrome_history=$(cat $PATH_CHROME_HISTORY | tr -d '"')

    # 見栄えを良くするためURLは表示しない
    local select_history=$(
        echo "$chrome_history" \
        | grep "$filter" \
        | awk -F ',' '!a[$2]++' \
        | awk -F ',' '{print $3"\t"$2}' \
        | tr -d "\r" \
        | fzf \
        | tr -d "\n"
    )

    # URL取得処理
    if [ -n "$select_history" ]; then
        # URLを取得する際タイトルでgrepするため
        local title=`echo "$select_history" | awk -F '\t' '{print $1}'`
        local url=`echo "$chrome_history" | grep "$title"  | head -n 1 |awk -F ',' '{print $1}'`
        open $url
    fi
}

export_chrome_history
show_chrome_history $1

実行

$ sh chromehistory.sh

https://rasukarusan.github.io/blog-assets/gif/chromehistory/demo1.gif

sh chromehistory.sh PATTERNでgrepできるようにしている。予めある程度絞り込んだ結果を出したいときやURLで絞り込みたい時に使う。
fzfの選択画面では見栄えのため、URLを表示させていないから用意した。ぶっちゃけ使った試しはない。

日付ごとに出す

直近のアクセスしたページを出力するのならば上記で事足りるが、「昨日みたあのページなんだっけ・・・」という時にfzfの画面で一々下に行くのはなんとも愚かだと思うので、日付ごとに出力するようにする。

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

GNU系のdateコマンドで曜日込みの日付を出力して、日付を第一引数に渡してgrepさせているだけですね。
sh chromehistory.sh -dみたいな形で実行したかったので最終的には以下。

chromehistory.sh

PATH_CHROME_HISTORY=/Users/`whoami`/chrome_history.csv

function export_chrome_history() {
    ...
}

function show_chrome_history() {
    ...
}

function show_by_date() {
    ...
}

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

main $1

https://rasukarusan.github.io/blog-assets/gif/chromehistory/demo2.gif

終わり

fzfに食わせるだけでいろんなものがだいぶ便利になるよね。
大体面倒くさい作業あったらどうにかしてfzfに食わせられないか考えちゃうんだけどそれだけで午前の業務が終わることが多々ある。ごめんなさいね。

俺よりiTerm使いこなしてるやつおる?

f:id:rasukarusan:20190413180124p:plain:w400

お前らはまだiTermの本当の力を知らない

純正Terminal、Hyper、Alacritty等を使ってるやつ、今すぐ捨ててiTermに乗り換えたほうが良い。
iTermから乗り換えた人も今一度iTermのことを知ってほしい。きっと知らない機能があるはず。

目次

Shell統合をさっさとしろ

これから色々と紹介していくが、とりあえずShell統合(Shell Integration)は済ましてこい。話はそれからだ。

www.iterm2.com

下記コマンドでインストールは一瞬で終わる。

curl -L https://iterm2.com/misc/install_shell_integration.sh | bash

また、気に入らなかったらインストール後に.zshrcに追記される1行を削除するだけで良いのでお手軽だ。

# 下記を削除またはコメントアウト
test -e "${HOME}/.iterm2_shell_integration.zsh" && source "${HOME}/.iterm2_shell_integration.zsh"

Terminal上にファイルをドラッグアンドドロップしたらSSH先でも送信できるとかあるから、まじで入れたほうが良い。

Copy ModeでTerminal上を自由に動く

まずはこれ。
Terminalに出力された文字をコピーするとき、一々マウスカーソルでピッとコピーしてるやつなんていないと信じたい。
iTerm使ってるなら今すぐCmd+Shift+Cを押せ。Terminal上をVimライクに自由に動き回ることができてコピーが可能だ。

f:id:rasukarusan:20190413165650g:plain
CopyModeでTerminal上を自在に動く

tmuxでも出来るよってやつはとりあえず引っ込んでろ。

Toolbeltで痒い作業は行う

iTermには実は通常の画面の他に、もう一つ作業スペースが存在する。
それが「Toolbelt」。Cmd+Shift+BでToolebeltを表示できる。

f:id:rasukarusan:20190413165741g:plain
Toolbeltでノート、ジョブ、ペースト履歴を参照

Toolbeltでは、

  • ノート
  • ディレクトリ履歴
  • ペースト履歴
  • ジョブ
  • コマンド履歴
  • etc...

等々結構いろんなものを表示できる。詳しくはメニューを見てくれ。

f:id:rasukarusan:20190413165925p:plain:w400
Toolbeltで利用できる作業一覧

ただぶっちゃけ全部コマンド上で実現できることなので、あまり使っていない。
ビジュアル的にかっこいいから表示させている。

バッジ機能を利用してSSH先のホスト名を表示する

バッジって何やねんという方のために。

f:id:rasukarusan:20190413173317g:plain
SSHした時にiTerm上にホスト名を表示する

こんな感じで右上に自分の好きな文字列を表示することができる。iTerm上の設定なので、無論SSHした後でも有効。

バッジの設定方法は、Preferences > Profiles > Generalで以下の用に\(user.任意の名前)を設定する。

f:id:rasukarusan:20190413171939p:plain:w500
バッジの設定

その後.zshrcに以下を記載。

# Shell Integration済みであること
iterm2_print_user_vars() {
  iterm2_set_user_var 任意の名前 $(my_badge)
}
# バッジで表示する文字列を返す関数
function my_badge() {
    if [ "$USER" = "root" ];then 
        echo "rootだよ"
    else 
        echo "rootじゃないよ"
    fi
}

iTermを再起動すればバッジが表示される。
表示させたい文字列をShellスクリプトで設定できるからかなり応用は効くと思う。

が、もう少し気楽にバッジを設定したい。
そんなときは直接Shellで書き換えることも可能。

function badge() {
    printf "\e]1337;SetBadgeFormat=%s\a"\
    $(echo -n "$1" | base64)
}

f:id:rasukarusan:20190413173633g:plain
コマンドでバッジを設定する

SSHする時にバッジでホスト名を表示するのは上記を利用して、下記のような形でスクリプトにしている。

.zshrc

function badge() {
    printf "\e]1337;SetBadgeFormat=%s\a"\
    $(echo -n "$1" | base64)
}

function ssh_local() {
    local ssh_config=~/.ssh/config
    local server=$(cat $ssh_config | grep "Host " | sed "s/Host //g" | fzf)
    if [ -z "$server" ]; then
        return
    fi
    badge $server
    ssh $server
}

トリガーを利用して五感に訴える

トリガーはProfile > Advanceから設定できる。拾いたい文字列を正規表現で設定し、引っかかった場合にしたいActionを設定するという流れ。

f:id:rasukarusan:20190413170454p:plain:w500
トリガーの設定

f:id:rasukarusan:20190413170249g:plain
rootでログインしたときのみ赤くする

これの良いところはiTermで設定をするので、SSH先等で設定せずに済むところ。本番サーバーなどでzshrcの設定を入れられないようなところでも動作するのがGOOD。

あとは諸事情でデバッガが使えず、printfvar_dumpなど出力でしかデバッグできないときちょっと便利かもしれん。if文の中で「通ったよ」みたいなデバッグ文を仕込んで、それが表示されてるか目で確認するような想定ね。

f:id:rasukarusan:20190413170600g:plain
デバッグ文をトリガーに設定して後から確認しやすいように

ただ、Vimなどで開いた時も拾われてしまうので正規表現はきつめに設定すると良い。

f:id:rasukarusan:20190413175411p:plain
Vimで書いた文字列も認識されてしまうので少々使いづらい

また、色を変えるだけではなくコマンドも実行できる。

f:id:rasukarusan:20190413175234g:plain
トリガーで任意の文字列が出現したらコマンド実行

上記はActionに「Run Command」、「Regular Expression」にroot@を設定、Parametersには「(date; echo "rootになりました") >> ~/Desktop/hoge.txt」とセットし、「root@という文字列が出現した場合に設定したコマンドを実行する」という設定をした例。

このように特定の文字列が出現されたらコマンドを実行する、といったことも出来るので応用範囲は広い。広すぎて何に使えるかわからん。

マークを設置して飛びたい場所に飛ぶ

iTermはCmd+Shift+Mでマークを設置できる。マークを設置すると一番左にが表示される。
設置後はCmd+Shift+JまたはCmd+Shift+↑↓で設置箇所に移動することができる。
マークを削除したいときはCmd+Shift+KでOKだ。

f:id:rasukarusan:20190413170904g:plain
マークを設置して一瞬でジャンプする

インストールコマンドなど、長いコマンドを打つ前に設置しておけば、実行が終わった後移動して上から順に処理を追うことができる。

終わり

タイトル煽ってしまっているけど、シンプルにiTermに感動したので共有したかった。
普段自分がなんとなく使っているツールも一度は本気を出して調べると知らない機能が結構あるよね。
iTermはDocumentがシンプルに見えて実はとっても充実しているので、さすがNo.1。

iTermをAppleScriptで操作する

f:id:rasukarusan:20190406224356p:plain:w500

基本的には以下の公式ドキュメントがわかりやすいし参考になる。

www.iterm2.com

ただ実際使う時どうなるの、どうすんの?っていうのを紹介する。

AppleScriptでiTermを操作する基本的な構文

iterm.sh

#!/usr/bin/osascript
tell application "iTerm"
    activate
    set _current_session to current session of current window
    tell _current_session
        # ここに操作したいことを書く
        set columns to 90 # ex.) カラム数を変更
    end tell
end tell

実行は以下

$ osascript iterm.sh

# もしくは実行権限を与えて
$ ./iterm.sh

f:id:rasukarusan:20190406215804g:plain
カラム数を変更

.scptで保存してosascript iterm-test.scptと実行してもいいが、.scptだとgitでpushした時にバイナリ文字列になってしまうので管理がし辛い。
なのでシェバンにosascriptと書いてしまうのが色々とやりやすいと思う。
パスは一応which osascriptで確認するのが吉ね。

透明度を変更

set transparency to 0.8

f:id:rasukarusan:20190406220145g:plain
透明度を変更

0~1を設定できて1に近づくほど透明度が上がる。

画面を分割

# 縦分割
split vertically with default profile

# 横分割
split horizontally with default profile

f:id:rasukarusan:20190406221116g:plain
画面分割

プロファイルが複数あるなら、プロファイル名を指定して分割することも可能。

split vertically with profile "プロファイル名"

# ex.) プロファイル名が"MyTestProfile"の場合
# split vertically with profile "MyTestProfile"

f:id:rasukarusan:20190406220243p:plain

f:id:rasukarusan:20190406220316g:plain
Profileを指定して画面分割

分割先にだけ適用できるから地味に便利。

画面分割後、分割先の画面でコマンド実行

tell application "iTerm"
    activate
    set _current_session to current session of current window

    # 画面を分割
    tell _current_session
        split vertically with default profile
    end tell

    # 分割先画面での実行
    tell last session of current tab of current window
        # 分割先のttyを取得
        set last_session_tty to tty as text
        do shell script "echo " & quoted form of last_session_tty
    end tell

end tell

f:id:rasukarusan:20190406222545g:plain
画面分割後、分割先でコマンド実行

画面分割後、フォーカスを分割後の画面に移したいときはselectを使う

...
tell last session of current tab of current window
    # フォーカスを分割後の画面に移す
    select
    # コマンドを実行する
    write text "echo hoge"
end tell

f:id:rasukarusan:20190406222800g:plain
画面分割後、分割先にフォーカルを移しコマンドを実行

ちなみにtell last session of current tab of current windowではなく

tell first session of current tab of current window
tell second session of current tab of current window
tell third session of current tab of current window
tell ....

のようにfirst, second, ...で指定することもできる。

バックグラウンドの色を変更

set background color to {red, green, blue, alpha}

f:id:rasukarusan:20190406220417g:plain
バックグラウンドの色を変更

各値には0~65536の値を入れる。(256ではないので注意) 例えば青っぽい色に変えたいならこんな感じ。

set background color to {8700, 400, 24200, 1}

適当に青っぽいRGB値に二桁0を足せば似たような色になる(上記は元の値は(87,4,242)

バックグラウンド画像を変更

set background image to "/Users/rasukarusan/output.png"

f:id:rasukarusan:20190406223741g:plain
バックグラウンド画像を変更

注意したいのは画像パスは絶対パスで書くこと。~で書くと別の意味で解釈されてしまうのでうまいこと設定できない。「POSIX Applescript path」とかで調べると変換方法とか出てくるので調べてみて。

元に戻すときは画像パスに空文字を指定してあげる

# 背景画像をもとに戻す
set background image to ""

現在のカラム数やttyを取得する

取得できる値は公式ドキュメントに色々書かれている。 こんな形で取得できるよという形を紹介しておく。

tell application "iTerm"
    activate
    set _current_session to current session of current window
    tell _current_session
        # カラム数を取得
        set current_column to columns
        do shell script "echo " & current_column
    end tell
end tell

f:id:rasukarusan:20190406215456g:plain
カラム数を取得

基本的にはset 変数名 to 取得したいもの(iTermで定められている変数名)で取得できる。 例えばttyを取得したいなら

set current_tty to tty

で取得できる。

終わり

他にも色々あるけどとりあえず使えそうなやつはこんなものだろうか。
特に画面分割やttyを取得できるのは色々と応用できそうなので、またそれも後日まとめる。

AppleScriptは単体ではなく、ShellScriptと組み合わせると幅が広がるので可能性は無限大だと思う。

Shellでランダムな文字列を生成する(文字列/数字文字列/乱数)

f:id:rasukarusan:20190329223857p:plain:w500

ふとした瞬間に「あ、ランダムな文字列生成したい」というときがあると思う。
そんなときのShellコマンドを紹介。

ランダムな文字列にも色々種類がある

今回生成するのは以下。

  • ランダムな文字列生成(例:'ex792Lhk')
  • ランダムな数字文字列生成(例:'189765')
  • 指定範囲内のランダムな整数生成(例:1〜100の数字)

ランダム系によく使われるやつ

  • /dev/random
  • /dev/urandom
  • od

上記は全てlinux系には最初から入っているっぽいので環境気にせず使える(UbuntuとかMacとか)。
randomurandomどっち使えばええんや問題があるらしいが、とりあえずurandomを使ってれば良い。「urandomは乱数種を再利用するから安全性がァー」とかあるらしいが別にセキュリティに利用するわけでもないのでおk。

ランダムな文字列生成(例:'ex792Lhk')

# ランダムな文字列を生成。第一引数に桁数を指定。デフォルトは10。
alias randomStr='_generateRandomString'
function _generateRandomString() {
    local length=${1:-10}
    cat /dev/urandom | base64 | fold -w $length | head -n 1
}

出力

$ randomStr
Ry7pAIf2De

# 20文字の文字列生成
$ randomStr 20
eiQC0aHDg18ffH6x3oUt

ランダムな数字文字列生成(例:'189765')

乱数ではなく数値文字列であることに注意。

# ランダムな数値文字列を生成。第一引数に桁数を指定。デフォルトは4。
alias randomStrNum='_generateRandomNumberStr'
function _generateRandomNumberStr() {
    local length=${1:-4}
    od -vAn -to1 </dev/urandom  | tr -d " " | fold -w $length | head -n 1
}

出力

$ randomStrNum
3311

# 10桁の数字文字列を生成
$ randomStrNum 10
3151543003

指定範囲内のランダムな整数生成(例:100を指定した場合、1〜100の数字)

# 指定範囲内のランダムな整数を生成。第一引数に範囲を指定。デフォルトは100。
alias randomNum='_generateRandomNumber'
function _generateRandomNumber() {
    local range=${1:-100}
    awk 'BEGIN{srand();print int(rand() * '"${range}"')}'
}

出力

$ randomNum
20

# 1~1000の範囲で乱数を生成
$ randomNum 1000
344

解説

cat /dev/urandom | base64 | fold -w $length | head -n 1

cat /dev/urandomと打つと以下のように文字化けしたような出力が得られる

$ cat /dev/urandom
����5>�W�R>�v��n������ڡ2C��9ϔ@{hRiNɂ|�_�7�G�b�u^q��E6�$������/}~����QC�w��{��@̝���H�Y U���$/%2  ���WNp��y�a������}�u��{���@5�ţ��*�g[!��H�U������o:yKw�
...(略)

このままだと読めないのでbase64でデコード。すると下記のように無限に文字列が出力される。Ctrl+Cで終了できる。

4HkkhYJXrKlApvoBlmcw+X8O45Xz9JHo149+vLRYEY3SYATpgzJK/uhPNwfU6YzagjpJS6JpXDhJUzdzCq3BI93AedrZ6iHRRaNnELFA4bsVpM4Aw3WyVa8VxDl4ryzh1ZTF1bKi7Std7s9hqNK6PHCKsWY90Cfzi/TmZ2fw05lgE001t8p9k3W9PX+um7fXowoinq9EVFLgAshKY4bl6slPCmPBcBrq1fa7tNUQfKdKrymlnxgYVSIBQN1vhLw5kDkh0o
...(略)

あとはお好きな方法で文字を切り取るだけ。 今回はfold -wを使って、指定文字数で区切って改行させ、head -n 1で先頭行だけ抽出させている。

od -vAn -to1 </dev/urandom | tr -d " " | fold -w $length | head -n 1

odなんてコマンドこれを調べるまで全く知らなかったが、以下のサイトが詳しい。

x68000.q-e-d.net

標準入力を8進数や16進数にダンプしてくれるらしいので、これを乱数作成に利用する。 また、od -vAn -td4など、10進数で出力してくれる-td4や16進数の-tx4をよく見るが、私は-to18進数1バイトの出力を利用している。 理由としては桁数が綺麗に出力出来る&数字のみになるから。

-td4だとマイナスが表示されてしまうし、-tx4だと文字列と数字が混合してしまう。

$ od -vAn -td4 </dev/urandom
-1821141382      -849610530     -1013568552       245312278
...

$ od -vAn -tx4 </dev/urandom
04177049        f189f437        367061e5        4e9b108e
...

あとはやってみればわかると思うが桁数を綺麗に表示するには3桁区切りの-to1が一番扱いやすい。(-to1,-to2,-to4とか試すと面白い。) tr -d " "で空白を消した時に、思ったとおりの桁数が得られたのが-to1だったってのもある。空白を消さなければ-to4でもなんでもええんちゃうか。

$ od -vAn -to1 </dev/urandom
025 054 220 145 350 345 110 041 174 245 177 253 157 302 157 304
...

awk 'BEGIN{srand();print int(rand() * '"${range}"')}'

これはもうシンプルにawkゲーですわ。便利だね、で終わらせましょう。

終わり

zshだと(bashにもあるのかな?)$RANDOMという環境変数が用意されているが、使い勝手が悪いので却下した。
ちなみにふとした瞬間に「あ、ランダムな文字列生成したい」となったことはまだ一度もない。

【bashrc/zshrc】alias、functionの俺的命名規則3つ

aliasにする?functionのままにする?

f:id:rasukarusan:20190304231921p:plain:w400

.bashrcや.zshrcにaliasやfunction()を記載することってよくあると思う。
またfunctionはそのままfunction名をコマンドとして打てば実行できるし、

  • 簡単なコマンドの羅列はalias
  • 少し複雑な処理が必要なのはfunction

などと決めている人もいると思う。

alias hoge='_function'と書いている人に出会った

下のようにfunctionをaliasに設定している人に出会った。

# カレントディレクトリのファイルをちら見してvimで開く
alias look='_look'
function _look() {
    find_result=`find . -maxdepth 1 -type f`
    target_file=`echo "$find_result" | sed 's/\.\///g' | fzf 'bat --color always {}'` 
    if [ "$target_file" = "" ]; then
        return 0
    fi
    vim $target_file
}

当時は「functionは書いたらそのまま実行できるのになんでいちいちアンダーバーをつけてaliasにしているのだろうか」と疑問に思った。 最近ようやくその理由が自分なりにわかってきたので紹介する。

【理由1】自作関数ということを示すため

functionsというコマンドを打ってもらえばわかるが既に定義済みの関数がたくさんある。
(もしかしたらfunctionsを実行できるのはzshまたはoh-my-zsh入れてる人だけかもしれん)

これらの関数と区別するために、その人にとって目印_だったのかなと推測する。

その他にも

  • 既に定義済みの関数と名前がバッティングしないようにするため
  • privateな意味を込めたいから

など理由があるのかもしれない。

【理由2】関数名はわかりやすく、aliasは短く打ちやすいものにするため

確かに関数は.zshrcなどに定義しただけでコマンドとして使えるが、わかりやすい関数名にするとどうしても長くなる。
上記の_lookなどはまだなんとなくわかるが、例えば「ターミナルからGoogle検索をする」goo関数を作ったとしよう。コマンドは手軽に打てるよう短めな名前がベストだ。

# terminal上からGoogle検索
function goo() {
    # 第一引数がない場合はpbpasteの中身を検索単語とする
    [ -z "$1" ] && searchWord=`pbpaste` || searchWord=$1
    open https://www.google.co.jp/search\?q\=$searchWord
}

関数を作ってしまえばエイリアスに登録せずに実行できるので、gooと打てばGoogleで検索することができる。
ただ、この関数を見ただけで何をしているのか非常に分かりづらい
.zshrcを整理する時に困るし、その都度コメントを書くのも億劫だ。

なので以下のように関数名を変更し、その関数をエイリアスに登録する。

alias goo='_searchByGoogle'
function _searchByGoogle() {
    # 第一引数がない場合はpbpasteの中身を検索単語とする
    [ -z "$1" ] && searchWord=`pbpaste` || searchWord=$1
    open https://www.google.co.jp/search\?q\=$searchWord
}

これでgooと打てば実行できるし関数名を見ただけで何をするのか把握することができる。

【理由3】aliasコマンドで一覧表示するため

【理由1】でも紹介したが、定義済みの関数を表示するfunctionsコマンドというものが存在する。
しかし関数の中身まで出力されるので、関数名の一覧を表示したい時は非常に見づらい。(別にgrepすればいいかもしれんが)

functionsコマンド

$ functions 

_SUSEconfig () {
   # undefined
    builtin autoload -XUz
}
__phpbrew_load_user_config () {
    if [[ -f $PHPBREW_HOME/init ]]
    then
     . $PHPBREW_HOME/init
        __phpbrew_set_path
    fi
}
__phpbrew_reinit () {
    if [[ -z "$1" ]]
    then
        local _PHP_VERSION=""
    else
        local _PHP_VERSION=$1
    fi
    if [[ ! -d "$PHPBREW_HOME" ]]
    then
.....(略)

対照にaliasコマンドは見やすい。.zshrcに記述するaliasじゃない、コマンドとしてaliasを実行するやつね。

aliasコマンド

$ alias | head

-='cd -'
...=../..
....=../../..
.....=../../../..
......=../../../../..
1='cd -'
2='cd -2'
3='cd -3'
4='cd -4'
...
tigd=_gitDiffPreviewCopy
tigg=_gitLogPreviewOpen

aliasコマンドはaliasに登録されている一覧を出力する。 alias hoge='_foohoge'のように自作関数をaliasとして登録しておけば、自分で作った関数をaliasコマンドで把握することができる

少しオマケになるが、以下の流れで自作関数の中身を知ることができる。

# aliasに登録されているものを出力
$ alias

-='cd -'
....=../../..
.....=../../../..
......=../../../../..
goo=_searchByGoogle
...

# 関数の中身を出力
$ functions _searchByGoogle

_searchByGoogle () {
    [ -z "$1" ] && searchWord=`pbpaste`  || searchWord=$1
    open https://www.google.co.jp/search\?q\=$searchWord
}

終わり

たぶん人それぞれaliasにするか関数にするか理由があると思うんだけど、とりあえず今の俺はこんな感じでzshrcにシコシコaliasとfunctionを書いている。
人のzshrcとかあんまり気にならなかったけど、みんなどうやって書いているか興味出てきた。

サーバーのコマンド実行をお願いするときに気をつけること5選

f:id:rasukarusan:20190223163320p:plain 本番サーバーでコマンド実行する際、権限を持っている人にコマンド実行を依頼することってあるじゃん。全台サーバーに実行みたいな。
そんな時に「このコマンドエラー出るよ」と言われないために気をつけるべきことを書いていくぞ。

1. rm, cpには\をつけてaliasを無効化しろ

サーバーによってはrmやcpに-iをつけたものをaliasとして登録していることがある。

alias rm='rm -i'  
alias cp='cp -i'  

-iをつけていると以下のように削除する際、毎回確認を求めてくるようになる。

$ rm -i hoge.txt  
remove hoge.txt?  

例えば「.txtを全て削除したい」といった際、実行者はいちいちyを入力しないと削除できない。

$ rm *.txt  
remove foo.txt? y  
remove fuga.txt? y  
remove hoge.txt?  
...  

なので\をつけてaliasを無効化しよう。

$ \rm *.txt  

rmやcpに限った話ではないのでとりあえず付けておくことが望ましい。

2. sudoが必要なコマンドはsudoをつけろ

「rm -rf」って書いてあるんだから実行者がつけてくれるだろうと思ってはいけない。
もしくは「実行前にrootユーザーになってくれるっしょ」など他人任せにしてはいけない。
実行者としてはコピってそのまま動くのが望ましい。要するに優しさを見せましょうってことや。

# 優しさが足りないコマンド  
$ \rm -rf target_dir  
  
# 優しさにあふれるコマンド  
$ sudo \rm -rf target_dir  

3. cpをする時は権限に気をつけろ

画像の保存先を変更するに、既存のディレクトリを新しい保存先にコピーすることがあるだろう。その際普通にcpでやるなら注意が必要だ。

# public/imagesからupload/imagesにコピー
$ sudo \cp -r /home/www/public/images /home/www/public/upload/images 

この場合upload/imagesの所有者がrootになる

# コピーする前は所有者がapache
$ ls -l /home/www/public/ 
total 4.0K
drwxr-xr-x 2 apache apache 4.0K  223 03:50 images

# コピーした後は所有者がrootに変わる
$ ls -l /home/www/public/upload
total 4.0K
drwxr-xr-x 2 root root 4.0K  223 03:54 images

そうすると画面(ブラウザ)から画像をアップロードできないなど問題が発生する。 これを防ぐためにcpのオプションである-pを付けよう。

# public/imagesからupload/imagesに所有者・権限もそのままでコピー
$ sudo \cp -pr /home/www/public/images /home/www/public/upload/images 

# コピーした後も以前と属性が変わらない
$ ls -l /home/www/public/upload
total 4.0K
drwxr-xr-x 2 apache apache 4.0K  223 03:54 images

ちなみにmvなら所有者も権限も変更されない。

4. 実行場所に依存しないようにしろ

例えば以下のコマンド。 app/shell内のシェルスクリプトに実行権限を与える。

# app/shell配下にはshellスクリプトが置いてあるとする  
$ ls app/shell  
hoge.sh  
  
# 実行権限を付与  
$ ls app/shell/ | xargs chmod +x  

一見成功しそうではあるがこれは失敗する。
例えばこのコマンドを/etc配下で実行した場合、app/shellなんてディレクトリないよと怒られる。コマンドの実行者に「〇〇ディレクトリ配下で実行してください」などとお願いするのはナンセンスだろう。

[root@linux: /etc]$ pwd  
/etc  
# 実行場所によってエラーが出る  
[root@linux: /etc]$ ls app/shell   
ls: app/shell: No such file or directory  

なのでlsで展開させるディレクトリの場所を絶対パスで書くべきである。以下のような形。

$ ls /home/www/app/shell/ | xargs chmod +x  

しかしこれでも不完全だ
なぜならlsで出力されるのはファイル名のみで絶対パス表記じゃないからである。
このコマンドを/etcで実行するとパイプ先のxargs chmodhoge.sh no such fileと怒られてしまう。/etc配下にhoge.shはないからね。

# lsで出力されるのはファイル名のみ  
[root@linux: /etc]$ ls /home/app/shell   
hoge.sh  
[root@linux: /etc]$ ls /home/app/shell | xargs chmod +x  
chmod: hoge.sh: No such file or directory  

解決法としては事前にコマンドでディレクトリを移動してから実行する。

cd /home/www/; ls app/shell/ | xargs chmod +x  

しかし一々移動するのもアレなのでfindで指定してしまおう。

# findの場合、絶対パスで出力してくれる  
$ find /home/www/app/shell -type f -name "*.sh"  
/home/www/app/shell/hoge.sh  
  
# コマンド実行時にどこのディレクトリにいても実行可能  
$ find /home/www/app/shell -name "*.sh" | xargs chmod +x  

5. できるだけ細かくファイルは指定しろ

複数のファイルに変更を加える場合、できるだけ細かくファイルの種類を特定するべきである。
誤って他のファイルを消さないか?など、コマンドの実行者に余計な考慮をさせるわけにはいかない。

例えば以下のようなディレクトリ構造があるとする。

$ tree /home/www/public/images  
.  
|-- user1  
|   `-- hoge.png  
`-- user2  
    |-- foo.png  
    |-- fuga.jpeg  
    `-- log  

「userX内の画像ファイルを消したい」コマンドを実行するとしよう。
この時images/userXには画像しかないと思い込んでいると以下のようなコマンドを実行してしまう。

$ sudo \rm /home/www/public/images/user*/*  

そうするとrm: user2/log: is a directoryとエラーが出てしまう。
コマンドを実行する前にディレクトリ内は確認するとは思うが、複数サーバーが存在する場合見逃してしまう可能性もある
このようなミスをなくすためできるだけコマンドの方で対象のファイルのみを指定するのがいいだろう。

# userX内のpngファイルのみ削除  
$ sudo \rm $(find /home/www/app/images/user*/ -name "*.png")  

また、以下のようにxargsで削除してもよいが、

$ find /home/www/app/images/user*/ -name "*.png" | xargs sudo \rm  

もしファイル名に空白を含むものがあった場合xargs -i sudo \rm {}などとオプションを指定する必要があるので、rm $(find ~~~)のように$()``で「コマンドの実行結果に対してコマンドを実行する」形式のほうがオプションの付け忘れ等気にしなくてすむ。
バッククォートだと実行者がシングルクォートと見間違えたり見づらかったりするので$()をおすすめする

まとめ

普段自分一人で開発している時は気にする必要はないが、コマンドの実行を依頼したりWikiに載せるときは色々気をつけたほうがいいよねって話。 要は優しさですよね。

fzfでAuthorを指定して編集ファイル一覧を出力する

優秀だなと思った人のソースは全部読みたい

自社開発や大規模なプロジェクトで開発しているとたまに「ああなんてわかりやすいコードを書くんや...」と思うことがある。
とりあえずgit blameして誰が書いたのか特定したはいいものの「もっとこの人の書いたコード見たいぞ」となった時、いい感じのコマンドがなかったので作った。

github.com

必要なもの

  • fzf
  • ag

別にagはgrepでもいいけどね。GNU版と分けるのが面倒だったのでagで書いた。

インストール

$ brew tap rasukarusan/gitblamer
$ brew install gitblamer

初めて自作スクリプトをHomebrewでインストールできるようにしてみたが、なかなか楽で良い。

動作

demo.gif

fzfでユーザーを指定するとその人が書いたソースファイル名が表示される。
ファイル名出力後にfzfに食わせて選択したらvimで開くってやってもよかったがやりすぎ感が否めないので出力するまでにした。
-l 追加した行数でXX行以上追加したファイルだけ表示というオプションもつけた。(ってかこれないとファイル出力されすぎ問題)

流れ

  1. ユーザー一覧を取得
  2. ユーザーが編集したファイルを取得

easyですね。

ユーザー一覧を取得

# ユーザー一覧取得   
authors=`git log --no-merges | ag 'Author:' | ag -o '(?<=: ).*(?= \<)' | sort -u`  

これはgitでコマンドあるのかなと思ったが、調べた限りなかった。
なのでagの正規表現でごそっと取得する形に。

git log --no-merges | ag 'Author:'で以下のような形で取得できる。

$ git log --no-merges | ag 'Author:'  
Author: Tanaka Naoto <XXXXXX@YYYYY.co.jp>  
Author: yamada taro <XXXXXX@YYYYY.co.jp>  
...  

後はag -o '(?<=: ).*(?= \<)'の「後読み/先読み」で指定して抜き取った。

$ git log --no-merges | ag 'Author:' | ag -o '(?<=: ).*(?= \<)' | sort -u  
Tanaka Naoto   
yamada taro  
...  

指定したユーザーの編集したファイルを取得

# 指定したユーザーの編集したファイルを取得  
files=`git -C ${gitDir} log --no-merges --numstat --author="${author}" | ag '[0-9]?\t'  | awk -v filter_add_line=${FILTER_ADD_LINE} -v gitTop=${gitTop} '{if($1>filter_add_line)print gitTop"/"$3}' | sort -u`   

git log --no-merges --numstat --author='Author名'で編集したファイル名が取得できる。

1つ目数字が追加行数、2つ目が削除行数を表している。

$ git log --no-merges --numstat --author='Tanaka Naoto'  
  
commit 1debf84c3dccd577a64664b599cb5d9b991c75fc (origin/tanaka_40416, tanaka_deploy_test, tanaka_40416)  
Author: Tanaka Naoto <XXXXXX@YYYYYY.co.jp>  
Date:   Tue May 8 01:08:41 2018 +0900  
  
    passとpasswordで表記揺れしていたのを修正  
  
    refs #tanaka_40416  
  
2   2   README.md  
9   10  web/lib/config/Config.php  
1   1   web/lib/session/clients/RedisClient.php  

見てわかるように[数字][タブ][ファイル名]というフォーマットなので、これをキーにファイル名を抜き出した。ここで「XX行以上追加したファイルだけ」というフィルタリングも行ってる。

ag '[0-9]?\t'  | awk -v filter_add_line=${FILTER_ADD_LINE} '{if($1>filter_add_line)print $3}  

まとめ

別にお披露目するものでもないが、せっかくだからアウトプットしておこうと思うよ。
でも最近shellに飽きてきたので、次はこういうスクリプト系を違う言語で書いてみようかな。

はてなAPIをcurlでサクッと実行する

とりあえずはてなAPIをサクッとshellで実行したい人に向けて。

自分の記事を取得する

curl -u {はてなID}:{APIキー} https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry

はてなAPIはOAuth認証、WSSE認証、Basic認証のいずれかを行う必要があるので、curlで一番楽なBasic認証を使用する。
curlでのBasic認証はcurl -u ユーザーID:ユーザーPASSでいけますね。

はてなID、ブログID、APIキーは「はてぶのダッシュボード > 設定 > 詳細設定 > AtomPub」から取得できる。

f:id:rasukarusan:20181227154724p:plain
ダッシュボード>設定>詳細設定>AtomPub

記事の取得のエンドポイントは/entryなので、 実際に記事を取得するときは以下のような感じ。

curl -u hatena_id:XXXXXXXXX https://blog.hatena.ne.jp/hatena_id/blog_id/atom/entry

これで最新10件の記事を取得できる。
ドキュメントには最新7件と書いてあるが、現在は10件取得できるようだ。

スクリプトにしておくと楽。 hatena.sh

#!/bin/sh

API_KEY='XXXXXXXX'
HATENA_ID='hatena_id'
BLOG_ID='blog_id'

ENDPOINT_ROOT="https://blog.hatena.ne.jp/${HATENA_ID}/${BLOG_ID}/atom"
ENDPOINT_ENTRY='/entry'

# 記事の取得
curl -su ${HATENA_ID}:${API_KEY} ${ENDPOINT_ROOT}${ENDPOINT_ENTRY}

特定の記事を取得する

自分の書いた特定の記事を取得したい場合、/entry/エントリーIDで取得する。
ただ、このエントリーIDの取得が少々面倒くさく、/entryで叩いたときのレスポンスからしか取得できない。
/entryのレスポンス内の <link rel="edit" href="https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry/10257846132688559263"/>10257846132688559263の部分。これがエントリーID。

なのでcurlで叩くとしたらこう。

# 特定の記事を取得
curl -su ${HATENA_ID}:${API_KEY} ${ENDPOINT_ROOT}${ENDPOINT_ENTRY}/10257846132688559263

また、/entryでは最新10件しか取得できないので10件目以降を取得するには/entry?page={ページID}で取得する必要がある。
このページIDも/entryを叩いたときのレスポンスからしか取得できない。

/entryのレスポンス

<link rel="first" href="https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry" />
<link rel="next" href="https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry?page=1536762675" />

ページIDを指定して取得するとしたらこう

# 2ページ目の記事一覧を取得
curl -su ${HATENA_ID}:${API_KEY} ${ENDPOINT_ROOT}${ENDPOINT_ENTRY}?page=1536762675

少しまとめると

  • 特定の記事を取得したい場合、その記事のエントリーIDが必要になる
  • エントリーIDは/entryのレスポンスからしか取得できない
  • 1ページ目以降の記事を取得したい場合、ページIDを指定する必要がある
  • ページIDは/entryのレスポンスからしか取得できない

ちょっと面倒くさく感じるけど/entryのレスポンスに欲しい情報が全部詰まってるから楽っちゃ楽かな。他のAPI叩く必要がない。

全ての記事のエントリーIDを取得する

もうここからは単純にシェル芸になる。
流れとしては

  1. /entryを叩く
  2. 各記事のエントリーIDを抽出
  3. 2ページ目もあればページIDを抽出
  4. /entry?page=ページIDを叩く
  5. 2~4を繰り返す
#!/bin/sh 

# ローカルにアカウント情報をまとめたファイルを置いているのでそこから取得
# 別に直接書いても問題ない
API_KEY=`cat ~/account.json | jq -r .hatena.api_key`
HATENA_ID=`cat ~/account.json | jq -r .hatena.user_id`
BLOG_ID=`cat ~/account.json | jq -r .hatena.blog_id`

ENDPOINT_ROOT="https://blog.hatena.ne.jp/${HATENA_ID}/${BLOG_ID}/atom"
ENDPOINT_ENTRY='/entry'

# 全記事のエンドポイントを取得する
function getEntryId() {
    page=`curl -su ${HATENA_ID}:${API_KEY} ${ENDPOINT_ROOT}${ENDPOINT_ENTRY}?page=$1`

    # 各記事のエンドポイントを取得
    # 極稀にgrepでBinary file (standard input) matchesと表示され処理が止まるのを防ぐため-aをつける
    echo "$page" \
    | grep -a 'link rel="edit"' \
    | grep -oP 'href=".*"' \
    | sed 's/href="//g' \
    | tr -d '"'

    # 次のページがある場合、再帰して各記事のエンドポイントを出力する
    next=`echo "$page" | grep 'link rel="next"'`
    if [ $? -eq 0 ] ; then
        pageId=`echo "$next" | grep -oP "page=[0-9]*" | tr -d "page="`
        getEntryId $pageId
    fi
}

結果

$ sh hatena.sh
https://blog.hatena.ne.jp/hatena_id/blog_id/atom/entry/9801XXXXX55524080
https://blog.hatena.ne.jp/hatena_id/blog_id/atom/entry/9801XXXXX39215842
https://blog.hatena.ne.jp/hatena_id/blog_id/atom/entry/9801XXXXX39148152
https://blog.hatena.ne.jp/hatena_id/blog_id/atom/entry/1025XXXXX32700158645
https://blog.hatena.ne.jp/hatena_id/blog_id/atom/entry/1025XXXXX32690969669
https://blog.hatena.ne.jp/hatena_id/blog_id/atom/entry/1025XXXXX32688559263
...(省略)

簡単ですね。grepのところはもう少しスマートになりそうだけどとりあえずこれで良しとしよう。

エントリーIDというか各記事へのエンドポイントを取得ですね。もしエントリーIDだけ欲しかったらここから更にgrep -oPで抽出すればいいんじゃないかな。

画像も記事内容も全てローカルにバックアップする

もはや気軽でもなんでもないが、そもそも私がしたかったのは全記事のバックアップ
シェルを叩いたら一撃で全ての記事の内容、画像をローカルにダウンロードしてくる、というのを実現したかった。
ということでここからはもう完全にオナニーだがせっかく作ったのでお披露目しておこう。

github.com

本スクリプトを実行すると、日付・記事タイトル・記事で使用されている画像が出力される。

f:id:rasukarusan:20190203002203g:plain
動作画面

hatena.sh

#!/bin/sh

API_KEY=`cat ~/account.json | jq -r .hatena.api_key`
HATENA_ID=`cat ~/account.json | jq -r .hatena.user_id`
BLOG_ID=`cat ~/account.json | jq -r .hatena.blog_id`

ENDPOINT_ROOT="https://blog.hatena.ne.jp/${HATENA_ID}/${BLOG_ID}/atom"
ENDPOINT_ENTRY='/entry'

# エンコードされた特殊文字(&,",',<,>)をデコードする
function decodeSpecialChars() {
     sed 's/&amp;/&/g'  \
   | sed 's/&quot;/"/g' \
   | sed "s/&#39;/'/g"  \
   | sed 's/&lt;/</g'   \
   | sed 's/&gt;/</g'
}

# 全記事のエンドポイントを取得する
function getEntryId() {
    page=`curl -su ${HATENA_ID}:${API_KEY} ${ENDPOINT_ROOT}${ENDPOINT_ENTRY}?page=$1`

    # entry_idの取得
    # たまにBinary file (standard input) matchesと表示され処理が止まるのを防ぐため-aをつける
    echo "$page" \
    | grep -a 'link rel="edit"' \
    | grep -oP 'href=".*"' \
    | sed 's/href="//g' \
    | tr -d '"'

    # 次のページがある場合、再帰してエントリーIDを出力する
    next=`echo "$page" | grep 'link rel="next"'`
    if [ $? -eq 0 ] ; then
        pageId=`echo "$next" | grep -oP "page=[0-9]*" | tr -d "page="`
        getEntryId $pageId
    fi
}

# contentタグの中身だけ取得。特殊文字はデコードして出力
# 第一引数に対象の記事のエンドポイントをとる
function getContent() {
    # contentタグの始めと終わりの行番号を取得するためのラベル
    START_CONTENT_LABEL='<content type="text\/x-markdown">'
    END_CONTENT_LABEL='<\/content>'

    endPoint=$1
    article=`curl -su ${HATENA_ID}:${API_KEY} ${endPoint}`

    # コンテンツ内容を投稿日時毎のディレクトリに保存し、ファイル名をタイトルにするため
    postDate=`echo "$article" | grep 'link rel="alternate"' | grep -oP "[0-9]{4}/[0-9]{2}/[0-9]{2}"`
    title=`echo "$article" | grep -oP "(?<=\<title\>).*(?=\<\/title\>)"`

    # 画像ファイルを取得するため
    blogUrl=`echo "$article" | grep 'link rel="alternate"' | grep -oP '(?<=href=").*(?=")'`
    blog=`curl -s ${blogUrl}`
    imgUrls=`echo "$blog" | grep -oP '<img src.*itemprop="image"' | grep -oP '(?<=src=").*(?=" alt)'`

    printf "\e[92m\e[1m$postDate\e[m\n"
    echo $title

    # 記事毎に内容と画像を保存したいので、投稿日時ごとのディレクトリを作成
    mkdir -p $postDate

    # 画像をダウンロードし、投稿日時ディレクトリに保存
    for imgUrl in `echo "$imgUrls"`; do
        imgName=`echo $imgUrl | grep -oP "[0-9]{12}.*"`
        echo "$imgName"
        echo "${postDate}/${imgName} $imgUrl"
        wget -q -O ${postDate}/${imgName} $imgUrl
    done

    # contentタグの中身のみ取得したいため、始めと終わりの行番号を取得
    contentLineNo=`
    echo "$article" \
    | grep -nE "(${START_CONTENT_LABEL}|${END_CONTENT_LABEL})" \
    | sed 's/:.*//g'
    `

    # 記事の内容を投稿日時ディレクトリにmd形式で保存
    start=`echo "$contentLineNo" | head -n 1`
    end=`echo "$contentLineNo" | tail -n 1`
    content=`echo "$article" | awk "NR==${start},NR==${end}"`
    echo "$content" | decodeSpecialChars \
    | sed "s/$START_CONTENT_LABEL//g" \
    | sed "s/$END_CONTENT_LABEL//g" \
    > $postDate/$title.md
}

# 全ての記事の内容を取得する
function main() {
    for i in `getEntryId`
    do
        getContent $i
    done
}

main

このシェルを実行すると記事投稿日毎にディレクトリが作成され、記事ファイルとその記事で使用されている画像ファイルが作成される。 私はMarkdownで記事を書いているのでmd形式の記事ファイル。はてぶに上げた形式のままになるよう整形しているので仮に記事が消えてもそのままこのmdファイルをアップロードすれば復元できる。

# シェル叩いた後
$ ls
2018      2019      hatena.sh

# ディレクトリ構成
$ tree 2019
.
\`-- 01
    |-- 07
    |   \`-- 【Swift4】UIImageでURLで画像を指定する.md
    |-- 26
    |   \`-- apacheのDOCUMENT_ROOTを知る方法.md
    \`-- 27
        |-- 20190126015937.png
        |-- 20190126020540.png
        \`-- laravel+apacheでTesting 123...と出てしまう問題の解決法.md

記事もそのまま

$ cat  01/27/laravel+apacheでTesting\ 123...と出てしまう問題の解決法.md

[f:id:rasukarusan:20190126020540p:plain]

サーバーにlaravelで作ったアプリを設置するときに若干詰まった。

結局シンプルな変更漏れっていうオチなんですけどね。

## 解決1

DOCUMENT_ROOTを設定するときにhttpd.confの設定で変更漏れがあった。
\```zsh
DocumentRoot /var/www/html/laravel-app/public
ServerName example.com

#<Directory "/var/www/html"<
<Directory "/var/www/html/laravel-app/public"< # ←こっちも変更する
...
\```
...(省略)

まとめ

APIを試す時は大体shellのcurlでサクッとやってしまうのだが他の人はどうなんだろうか。
今回はてぶAPIを試す際色んな記事を見たがrubyやpythonでやってる人が多かった。

私みたいに「APIを試す時はcurlで!」という方の助けになればと思い書いてみたが、最後は気軽じゃないスクリプト載っけてしまった。絶対他の人読めない気がする。(人の書いたShellって読みづらいよね)
まあでもやりたいことは出来たので満足。

Webエンジニアの作業効率を1.3倍ぐらいにするワンライナー

この記事は今すぐalias登録すべきワンライナー by ゆめみ① Advent Calendar 2018の25日目の記事です。

ワンライナーでサクッと作業完了できる人かっこいいですよね。

"今すぐ登録すべきalias"ということで普段業務中にでごりごりと使っているワンライナーをご紹介。

とりあえず自分がWebエンジニアなのでこのタイトルにしたけど、特にWeb系エンジニアに限った話ではない。(ただMac限定が多々あるけど許して)

ローカルサーバー立ち上げ

さくっとビルドインサーバー立ち上げたい時ありますよね。

alias phpS='php -S localhost:9000'

Googleの検索

ソース読んでて知らない気になる関数出てきた時、コピペしていちいちブラウザから検索みたいなことはしてませんよね。(本当はググらなくてもわかるような男になりたい)

alias goo='open https://www.google.co.jp/search\?q\=`pbpaste`'

f:id:rasukarusan:20181224232148g:plain
gooコマンド

コピペしたものじゃなくてブラウザに打ち込むように検索したいという方はこちら。

alias goo='open https://www.google.co.jp/search\?q\=`read s;echo $s`'

f:id:rasukarusan:20181222211644g:plain

ワンライナーじゃないのでごめんなさいですが、普段はこっちの関数にした版を使ってます。

alias goo='searchByGoogle'
function searchByGoogle() {
    # 第一引数がない場合はpbpasteの中身を検索単語とする
    [ -z "$1" ] && searchWord=`pbpaste` || searchWord=$1
    open https://www.google.co.jp/search\?q\=$searchWord
}

jsを実行

手軽にjsを実行したいとき

alias js='osascript -l JavaScript'

# js test.js

一番直近に変更があったファイルをlessする

これは結構気に入ってる。
カレントディレクトリ内の最新のファイルをlessでちら見できる。本番サーバーに入って調査する時重宝マン。
今日のアクセスログ見るときとかに便利。

alias late='less $(echo `ls -t | head -n 1`)'

f:id:rasukarusan:20181222201703g:plain
lateコマンド

lessをなくして単純にファイル名だけ取得したいエイリアスも登録してる。

alias fin='echo `ls -t | head -n 1`'

f:id:rasukarusan:20181224232257g:plain

modifiedのファイルを全てVimのタブで開く

vimのタブ機能便利ですよね。 編集してたファイルをとりあえず一気に開きたい時に便利です。「s」は複数形のsです。

alias vims='vim -p `git diff --name-only`'

f:id:rasukarusan:20181222201849g:plain
vimsコマンド

ブラウザからコピーした時など、プレーンテキストに戻したい時に使用

GmailやYahooでコピペしてそのまま貼り付けるとリッチテキストになるのでそれを回避。

alias pcopy='pbpaste | pbcopy'

空行を削除

こんぐらい覚えておけよって感じですが打つのもしんどいので登録してる。

alias demp='sed "/^$/d"'

さくっとwifiをOFF/ONする

なんか通信の調子が悪いなって時、まず真っ先にwifiを切りますよね。ええ。

alias wifiConnect='networksetup -setairportpower en0 off && networksetup -setairportpower en0 on'

Git系

git系はたぶん皆さん色々設定してますよね。よく使うやつだけ載せておきます。

# 空白等の差分は無視して表示
alias gd='git diff -b'

# 現在のブランチをoriginにpushする
alias po='git push origin $(git branch | grep "*" | sed -e "s/^\*\s*//g")'

# 現在のブランチをpullする
alias gpl='git pull --rebase origin $(git branch | grep "*" | sed -e "s/^\*\s*//g")'

# git checkout branchをfzfで選択
alias co='git checkout $(git branch -a | tr -d " " |fzf --height 100% --prompt "CHECKOUT BRANCH>" --preview "git log --color=always {}" | head -n 1 | sed -e "s/^\*\s*//g" | perl -pe "s/remotes\/origin\///g")'

# 全てのファイルをgit checkout
alias gca='git checkout $(git status -s | grep -v \? |  grep -E "^.M" | perl -pe "s/(?<=^.{0}).{3}//g")'

特にcoはもう手放せないかなあ。git checkout ブランチ名をfzfでサクッと実行するやつなんだけど、普通にgit checkout XXXと打つのが考えられないくらいには楽。

f:id:rasukarusan:20181224233611g:plain
coコマンド

終わり

改めてzshrcを見直してみるとaliasに設定してあるのはあんまりなくて、大体関数にしちゃっていた。

ワンライナーじゃなくて「お前らが考えるハイパー便利なshellスクリプト教えろ」とかもあったら参加したい。
BitbucketとかRedmineなど実際の業務だと連携するツールがあると思うので、他の人がやっている楽ちん連携コマンドとか見てみたい。

いやあでもやっぱり他の人が設定してるalias見るの楽しい。来年もぜひこのアドベントカレンダーやってほしい。

fzfで末尾に?がついてしまう現象

以下のような現象。

f:id:rasukarusan:20180825134545p:plain

多段scpを試みたときに出会った現象。ProxyCommandによる多段scpができなかったので、しょうがなく愚直に2回scpをしようとした。 expectで踏み台サーバーsshして、ls。lsの結果をfzfで絞り込んでscpする以下のようなスクリプトを組んだ。

serverFiles=`expect -c "
    spawn ssh -t serverA \"ssh $targetServer\"
    expect \"$USER\"
    send \"ls -1\n\"
    expect \"]\"
    expect \"$USER\"
    send \"exit\n\"
    "`
# 先頭行から6行目まではexpectの実行コマンドなので削除し、lsの結果だけを表示する
targetFile=`echo "${serverFiles}"| sed -e '1,6d' | grep -v $USER | fzf`
# 対象サーバーからserverAにコピー
ssh serverA "scp -r $USER@$targetServer:$targetFile $LOCAL_PATH"
# ローカルにコピー
scp -r serverA:$targetFile $LOCAL_PATH
# serverAにコピーしたファイルを削除
ssh serverA "rm -r $targetFile"

実行結果

▶sh scpServer.sh
: No such file...

scpで「そんなファイルないよ」と怒られる。

末尾の「?」は改行コードのCRだった

結論から言うと

targetFile=`echo "${serverFiles}"| sed -e '1,6d' | grep -v $USER | fzf`

targetFile=`echo "${serverFiles}"| tr -d '\r' | sed -e '1,6d' | grep -v $USER | fzf`

のようにtr -d '\r'をすればうまくいった。 どうやらlsした結果にCRが紛れ込んでいたようだった。

CRにたどり着くまでの道のり

推測1:fzfで絞り込んだ結果の$targetFileが空になっている?

targetFile=`echo "${serverFiles}"| sed -e '1,6d' | grep -v $USER | fzf`
echo $targetFile

# 実行結果
~
▶sh scpServer.sh
hoge.txt

変数名にはちゃんと入っているようだ。

推測2:「?」を消してみる

tr -d "?"で削除を試みたが結果は変わらず。。。

推測3:文字コードがおかしい?

どうやら目には見えないが内部的になにか変化しているのだろうと推測し、まずは文字コードからあたってみることにした。 nkf --guessをしてみる。

targetFile=`echo "${serverFiles}"| sed -e '1,6d' | grep -v $USER | fzf`
echo  $targetFile | nkf --guess

# 実行結果
▶sh scpServer.sh
ASCII (CRLF)

ほうほうなるほど。試しに普通にechoで文字列をawk --guessしたときどうなるか見てみる

echo "hoge" | nkf --guess
ASCII (LF)

ここで理解した。CRが紛れ込んでいたんだなと。 ecpect -c内の\nがsendでやると\rに変換されるのかなあ。 なぜCRが紛れ込むのかはまだ調査中だが、とりあえず一件落着だった。

fzfで捗る自作コマンド一覧(zsh)

みんな大好きfzf

fzfは結果をインタラクティブに絞り込むだけのコマンドだが、組み合わせ次第でかなり使えるコマンドだ。日本ではpecoの方が有名だが海外ではfzfの方が人気らしい。

github.com

筆者が思うpecoと比較したときのfzfのメリットを述べる。

  • 画面クリアがなく目に優しい
  • fuzzy search(曖昧検索)が可能
  • 絞り込んだものをパイプつなぎでコマンドを適用でき、結果をpreviewとして画面に表示できる
  • カスタマイズが豊富

特にfuzzy searchとpreviewできる点が素晴らしい。またfzfの絞り込み結果の画面サイズを指定できたりとカスタマイズ性が高いのもGOOD。

以下はgit logの結果をfzfで絞り込み、previewにgit diffを表示、ENTERで詳細をless表示させているもの。(ちなみに別で全く同じ動作をするtiggコマンドというものがあるらしい)

f:id:rasukarusan:20180812205449g:plain

私のfzfのデフォルトの設定は以下。複数選択と色合いを変化、あとデフォルトだと絞り込み画面が下からでるのでreverseで直下に出している。

# fzfの設定
export FZF_DEFAULT_OPTS='--color=fg+:11 --height 70% --reverse --select-1 --exit-0 --multi'

実際に業務でも使っているコマンド一覧をつらつらと紹介していく。

爆速でディレクトリの移動をするfzf-cdr

これがない時のTerminalの移動はもう考えられない。cdrで過去に行ったことのあるディレクトリを表示し、fzfで絞り込んでディレクトリに移動する。 ちなみに私は「change directory direct」という意味で「cdd」というaliasをつけている。

f:id:rasukarusan:20180812211238g:plain

# fzf-cdr 
alias cdd='fzf-cdr'
function fzf-cdr() {
    target_dir=`cdr -l | sed 's/^[^ ][^ ]*  *//' | fzf`
    target_dir=`echo ${target_dir/\~/$HOME}`
    if [ -n "$target_dir" ]; then
        cd $target_dir
    fi
}

cdrの設定は以下のようにしている。ココらへんちょっと適当かもしれない(いつ書いたか忘れてた)。

# cdrの設定
autoload -Uz is-at-least
if is-at-least 4.3.11
then
  autoload -Uz chpwd_recent_dirs cdr add-zsh-hook
  add-zsh-hook chpwd chpwd_recent_dirs
  zstyle ':chpwd:*'      recent-dirs-max 500
  zstyle ':chpwd:*'      recent-dirs-default yes
  zstyle ':completion:*' recent-dirs-insert both
fi

ctrl+zでストレスなくバックグラウンドに入れる&出す

vimでファイルを開いていて、ふとterminalに戻って操作をしたくなる時があります。そんなときは大体ctrl+zで一旦バックに入れてfgで戻ってくる、という動作をすると思います。 ただ戻るのに一々fgを打つのも面倒くさいのでctrl+zで行ったり来たりするように設定しています。

f:id:rasukarusan:20180812211833g:plain

# fgを使わずctrl+zで行ったり来たりする
fancy-ctrl-z () {
  if [[ $#BUFFER -eq 0 ]]; then
    BUFFER="fg"
    zle accept-line
  else
    zle push-input
    zle clear-screen
  fi
}
zle -N fancy-ctrl-z
bindkey '^Z' fancy-ctrl-z

ただ、これだと直前のバックグランドに入れたものとしか行き来できないので、2個前のプロセスに戻りたい時などはfzfで選択しています。

# fgをfzfで
alias fgg='_fgg'
function _fgg() {
    wc=$(jobs | wc -l | tr -d ' ')
    if [ $wc -ne 0 ]; then
        job=$(jobs | awk -F "suspended" "{print $1 $2}"|sed -e "s/\-//g" -e "s/\+//g" -e "s/\[//g" -e "s/\]//g" | grep -v pwd | fzf | awk "{print $1}")
        wc_grep=$(echo $job | grep -v grep | grep 'suspended')
        if [ "$wc_grep" != "" ]; then
            fg %$job
        fi
    fi
}

agでヒットした行をvimで開く

これは最近vimプラグインで完結するようになったから使わなくなってしまったが、一時期はNOTE:とかFIXME:を探して修正しに行くのに結構使ってた。 f:id:rasukarusan:20180812213417g:plain

# agの結果をfzfで絞り込み選択するとvimで開く
alias agg="_agAndVim"
function _agAndVim() {
    if [ -z "$1" ]; then
        echo 'Usage: agg PATTERN'
        return 0
    fi
    result=`ag $1 | fzf`
    line=`echo "$result" | awk -F ':' '{print $2}'`
    file=`echo "$result" | awk -F ':' '{print $1}'`
    if [ -n "$file" ]; then
        vim $file +$line
    fi
}

各ブランチのgit logを見ながらcheckoutする

これももはや定番かもしれない、gitの操作をfzfに食わせるものだ。 gitの操作は手打ちでやると面倒くさいものも多いので、絞り込みで曖昧に覚えているものを打ち込んで選択するのがすごい楽。

f:id:rasukarusan:20180812213947g:plain

# git checkout branchをfzfで選択
alias co='git checkout $(git branch -a | tr -d " " |fzf --height 100% --prompt "CHECKOUT BRANCH>" --preview "git log --color=always {}" | head -n 1 | sed -e "s/^\*\s*//g" | perl -pe "s/remotes\/origin\///g")'

ちなみに「co」は「CheckOut」の略だ。 git checkoutの他にも

  • git add -pをfzfでdiffを見ながら選択
  • git checkout fileをfzfで選択
  • git resetをfzfでdiffを見ながら選択

といった操作をすべてfzfでやっている。

終わり

fzfのREADMEにあるExample集にもかなりの数があるので参考にしたい。 fzf最高。