🐈

競プロライブラリのスニペット化作業の自動化

2021/07/10に公開

はじめに

  • 筆者は競技プログラミングで使用頻度の高い処理を関数やクラスとしてライブラリ化しています。
  • そのライブラリをコンテスト中に素早く確実に使用するためにスニペット(snippet)化しました。
  • ライブラリからsnippet化への作業をライブラリ更新の度に変更を手動で反映させるのは面倒かつ不確実なのでその作業を自動化するシェルスクリプトを作成しました。
  • 本記事ではsnippet化に伴い行った作業やスクリプト、感じたことをまとめておきます。

依存関係

  • vim
  • coc.nvim(https://github.com/neoclide/coc.nvim)
  • coc-snippets(https://github.com/neoclide/coc-snippets)
    coc.nvimプラグインからcoc-snippetsというsnippet用の拡張であるcoc-snippetsをインストールしています。
  • coc.nvim自体のvimへの導入については割愛します。以下coc.nvimが導入された環境を前提として話を進めます。

coc-snippetsの導入

vimのコマンドで以下のようにしてcoc-snippetsをinstallします。

:CocInstall coc-snippets

次にcoc.nvimプラグインの設定ファイルでsnippetが定義されたファイルが置かれたパスを指定します。

:CocConfig

coc-settings.jsonを開いて以下の内容を追記します。
~/path/to/your/snippetsの部分は環境に応じて書き換えてください。

{
    "snippets.ultisnips.directories":["UltiSnips","~/path/to/your/snippets"]
}

デフォルトの"UltiSnips"に加えて新たに自分がライブラリから生成するsnippet定義ファイルが存在するパスを定義しています。
ここで複数のパスを定義しているのは競プロライブラリの更新に伴い更新するsnippetの内容を書き換えるため、競プロライブラリ以外のsnippetを退避させるためです。
自動生成ではなくsnippetを自分で定義する際は:CocCommand snippets.editSnippetsを実行するとファイルタイプに応じてpython.snippetsのようなファイルが開かれるのでそのファイルを編集すれば良いです。

ライブラリからsnippet生成

coc-snippetが要求するsnippetのフォーマット

coc-snippetが要求するsnippetのフォーマットを満たした例を示します。
詳細なオプションを知りたい場合は
https://github.com/SirVer/ultisnips/blob/master/doc/UltiSnips.txt
を参照してください。

snippet is_even ["judge n is even"]
def is_even(n:int) -> bool:
    if n % 2 == 0:
        return True
    else:
        return False
endsnippet

1行目のsnippetの右のis_evenの部分がtrigger_wordでこの文字列を入力して
以下endsnippetまでの行を呼び出せるようになります。

したがってこのフォーマットから自分のライブラリの中身を
snippet~の行とendsnippetの行で挟めば良いです。

snippet生成

ディレクトリ構成

以下のようなディレクトリ構成とします。
この状態で後述するlib2snips.shを実行するとpython.snippetsファイルが生成されます。

.
├── lib2snips.sh
├── mylib
│   ├── is_even.py
│   └── is_odd.py
└── tests
    ├── test_is_even.py
    └── test_is_odd.py

ライブラリからsnippet定義ファイルの生成するシェルスクリプト

lib2snips.shの内容を示します。

snippet_file_to_update=python.snippets
modules_dir='./mylib'
if [ -e $snippet_file_to_update ]; then
    rm $snippet_file_to_update
fi
touch $snippet_file_to_update
for pyfile in `ls ${modules_dir}/*.py`;do
    filename_noext=`basename $pyfile .py`
    echo $filename_noext
    echo snippet ${filename_noext} [${filename_noext}] >> $snippet_file_to_update
    cat $pyfile >> $snippet_file_to_update
    echo endsnippet >> $snippet_file_to_update
done

指定したパスからpythonファイルを列挙して先程述べたsnippetのフォーマットをpython.snippetsファイルが生成されます。trigger_wordは元のpythonファイルの名前を利用しています。(is_even.pyであればis_even)

注意点としてはこのスクリプトの実行の度に生成されるファイルはライブラリの内容をもとに毎回ファイルを0から作り直す点です。もしもライブラリを編集したい場合は生成後のファイルに直接編集を行うべきではなく生成元であるライブラリ自体に編集を行ってスクリプトを実行するべきです。

先述したようにcoc-settings.jsonでこのsnippet定義ファイルpython.snippetsが存在するパスを追加すればこのsnippetが呼び出せるようになると思います。snippetが読み込まれているかは以下のコマンドで確認できます。

:CocCommand workspace.showOutput snippets

ファイルのsnippet化に関してmain()関数の削除

  • 上のスクリプトではファイル全体をsnippetにしています。そのため使い方を示すようなmain関数などがファイルに含まれているのが提出ファイルには余分な場合があります。
  • その対処法としてmain関数の代わりにdocstringとしてコメントに用例を示す方法があると思います。以下はdocstringを加えた素数判定のコードです。
  • 余談ですがdocstringをもとにテストを行うdoctestを使用するとライブラリ改修(0-index,1-indexedの変更など)に伴って生じうる用例の乖離などが防げるメリットがあると思います。
def is_prime(n):
    """
    素数判定 O(√N)
    >>> is_prime(1)
    False
    >>> is_prime(2)
    True
    >>> is_prime(3)
    True
    """
    i = 2
    if n <= 1:
        return False
    while i ** 2 <= n:
        if n % i == 0:
            return False
        else:
            i += 1
    return True

参考

Discussion