ShellScriptでドヤりたいGWアドベントカレンダー5日目いくで。
クリップボード管理アプリをShellScriptで作った。
そもそもはスプレッドシートから列をコピーしてA列とB列を入れ替えて貼り付けたいと思ったのが発端で、
どちらかというと副産物的な感じでクリップボード管理ができた感じ。
デモ
こんな感じのやつね。
クリップボード履歴を表示して、fzfで選択した順に水平に連結してコピーする。
クリップボードの監視
とりあえず既存のクリップボード管理アプリはどうやっているのか調べてみた。
「Clipy」というアプリはtimerを使用しているみたい。
// 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を利用している。
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でやるとしたらおそらくこの流れ。
- クリップボードの中身をファイルに書き込むShellScriptを作成(clilpObserver.sh)
- 1.で作ったスクリプトをcronで回してクリップボード監視
- ファイルを読み込んで履歴として表示する(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
2. スクリプトをcronで回してクリップボード監視
cronに登録するだけ。秒数は適当でいいと思うが、とりあえず1秒で設定してる。
別に5秒とかでも十分。そんなに急がなくてもいいじゃない。
$ crontab -l * * * * * for i in `seq 0 1 59`;do (sleep ${i}; sh ~/clipObserver.sh) & done;
コピーするたびにログファイルが追加されていってますね。
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で回すと読み書きが頻繁に起こることが嫌でもわかってしまうので、それだけは心苦しい。なんとかしたい。