xonsh
XonshDay 22

スニペット管理でワンライナーから脱却

はじめに

もう全然間に合ってないんですが、個人的に時間もないので最近作って微妙に使っている機能を紹介します。

ワンライナーやスニペットのCUI上での管理

多分これとかhttp://oscardelben.com/sheet/ にあたる機能ですね。Xonshというかptk使えば一瞬で作れるので、作りました。
あるものを使えと言われそうですが、あえて自分で作ったモチベーションを述べておきます。
ワンライナーだけならhistoryにpecoするだけで割と大丈夫なんですが、マルチラインが快適に編集できるXonshでワンライナーにこだわる必要って正直無いと思ってるんでスニペットという形で管理しつつ、peco的なやつで横断的に検索できる機能があったらいいなーと思ったのです。
アホな言語とその上で行われる無茶なワンライナーからの脱却という点が、Shellという観点で見たときのXonshの隠れた大きな進歩である気が個人的にしてきています。シェル芸とか難しいことを考えず、pythonで簡潔に書いた上でそれをちゃんと管理すべきじゃないのかと考えたわけです。

実装から先に

実装は大したことがないので、先に見せてしまいます。
ちゃんとVISUALかEDITORをお好きな感じに設定して使って下さいね!

import os
import sys
import tempfile
from functools import partial
from prompt_toolkit import prompt
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.keys import Keys
from prompt_toolkit.filters import EmacsInsertMode
from xonsh.tools import unthreadable, uncapturable

# クリップボード周りはOSによるので、分岐します
if sys.platform == "linux" or sys.platform == "linux2":
    aliases['pbcopy'] = ['xsel', '--clipboard', '--input']
elif sys.platform == "darwin":
    pass
else:
    print('Go to Hell!!')


# XONSH_DATA_DIRには履歴とか内部的なデータが入ってるので、その配下でスニペットも管理します
memo_dir = partial(os.path.join, __xonsh_env__['XONSH_DATA_DIR'], 'xonsh_memo')
snippet_memo_dir = partial(memo_dir, 'snippets')
oneliner_memo_path = memo_dir('oneliners.list')


def make_if_not_exists(path):
    if not os.path.exists(path):
        os.makedirs(path)


make_if_not_exists(memo_dir())
make_if_not_exists(snippet_memo_dir())


# ワンライナーをpecoで検索して、今のバッファに挿入します
def _search_and_insert_oneliner(event):
    line = $(cat @(oneliner_memo_path) | peco)
    line = line[line.find(':')+1:].strip()
    event.current_buffer.insert_text(line)


# ワンライナーをpecoで検索して、クリップボードにコピーします
def _search_and_copy_oneliner(event):
    line = $(cat @(oneliner_memo_path) | peco)
    line = line[line.find(':')+1:].strip()
    echo -n @(line) | pbcopy

# スニペット(マルチライン)をpecoで検索して、今のバッファに挿入します
def _search_and_insert_snippet(event):
    line = $(rg "" @(snippet_memo_dir()) | peco)
    res = open(line[:line.find(':')]).read()
    event.current_buffer.insert_text(res)

# スニペット(マルチライン)をpecoで検索して、クリップボードにコピーします
def _search_and_copy_snippet(event):
    line = $(rg "" @(snippet_memo_dir()) | peco)
    res = open(line[:line.find(':')]).read()
    echo -n @(res) | pbcopy


@unthreadable
@uncapturable
def _register_oneliner():
    # エディタで開くための一時ファイルを作ります。インストラクションも追加しときます
    descriptor, filename = tempfile.mkstemp()
    os.write(descriptor, b"# Input your oneliner")
    os.close(descriptor)

    # エディタで開きます
    Buffer()._open_file_in_editor(filename)

    line = open(filename).read().strip()

    # 新しいワンライナーを管理ファイルに追記します
    with open(oneliner_memo_path, 'a') as out:
        print(line, file=out)


@unthreadable
@uncapturable
def _register_snippet():
    # スニペットのファイル名を聞いておきます。シンタックスハイライトが欲しければ拡張子を付けてください
    print()
    name = prompt('Input snippet file name: ')
    snippet_path = snippet_memo_dir(name)

    # なければ作ります。既にあればそのまま編集できます
    if not os.path.exists(snippet_path):
        with open(snippet_path, 'w') as out:
            print("# Input your snippet", end='', file=out)

    Buffer()._open_file_in_editor(snippet_path)


@events.on_ptk_create
def custom_keybindings(bindings, **kw):
    handler = bindings.registry.add_binding
    insert_mode = EmacsInsertMode()

    @handler(Keys.ControlX, Keys.ControlO, filter=insert_mode)
    def search_oneliner(event):
        _search_and_insert_oneliner(event)

    @handler(Keys.ControlX, Keys.ControlP, Keys.ControlO, filter=insert_mode)
    def register_oneliner(event):
        _register_oneliner()

    @handler(Keys.ControlX, Keys.ControlX, Keys.ControlO, filter=insert_mode)
    def search_oneliner_cb(event):
        _search_and_copy_oneliner(event)

    @handler(Keys.ControlX, Keys.ControlS, filter=insert_mode)
    def search_snippet(event):
        _search_and_insert_snippet(event)

    @handler(Keys.ControlX, Keys.ControlX, Keys.ControlS, filter=insert_mode)
    def search_snippet_cb(event):
        _search_and_copy_snippet(event)

    @handler(Keys.ControlX, Keys.ControlP, Keys.ControlS, filter=insert_mode)
    def register_snippet(event):
        _register_snippet()

探してみます

実際に動くところを見せます。stumpwmとemacsを使って構築された超かっこいいデスクトップを見て下さい。
シナリオは以下です。
- distributed tensorflowでワーカのデバイスどうするんだっけ?C-x, C-s!
- ユークリッド埋め込みしたい!triplet lossってどこにあったっけ?C-x, C-s!
- あ、半正定値対称行列が欲しい!どこだ!C-x, C-s!

caps.gif

登録してみます

  • エディタでコードを書いています。あとで使うだろうなーというところを見つけました
  • 該当部分をクリップボードに入れます
  • XonshでC-x, C-p, C-sを押してスニペット登録を呼び出します
  • スニペット名を入力します
  • エディタが起動するので、スニペットを貼り付けてsave, closeします
  • 検索してみます

caps2.gif

色々な使い方

自分はスニペットだけと言わず参考になったURLとかにも名前をつけて呼び出せるようにしたりしてます。
bibtexとかもできるかもなーと思ってますが、自分は書く度にずっと秘伝のタレの同じファイルにぶち込んでいくスタイルなので関係ありませんでした。

おわり

zsh, fishスクリプトが苦手だった自分からすると、Xonshだと前だったらシェル上でやるのは気が引けたちょっと複雑な作業を比較的気軽にこなせるようになりました。
そんな中でやっぱりなんかよく使ってるなーというスニペットが出てきて、それ を別のアプリを使ってまで管理するのは面倒だし、どうしようかなーって思った時にpythonが使えるなら自分で余裕で作れるわと思って作りました。

以上です。遅れてすんませんでした。