rajyanのメモ帳

競技プログラミング、CTFなどに関することを適当にまとめます

Git hooksで競プロ用スニペットの自動生成

注意

自分のやったことの理解とメモのために書いています。
この記事によって何か不利益が生じても責任は取れません。

やったこと

Git hooksのpre-commitと適当なシェルスクリプトを書いて、競技プログラミング用のライブラリを作成・編集してコミットする際に、スニペットを自動生成するようにしました。
Visual Studio用のスニペットですが、テンプレートを変えれば何にでも使えると思います。

目的

競技プログラミング用のライブラリをすぐに呼び出せるように スニペットを書いているが、ライブラリ編集したのにスニペットの編集を忘れることがあるし、そもそも内容はほぼ同じでわざわざコピペするのも面倒なので自動化したい。

求める仕様

  1. ライブラリはlibrary/library下、スニペットはlibrary/snippet下(ディレクトリの命名……)
  2. スニペットのtitle, shortcut, code の部分があれば十分
  3. titleはファイル名、shortcutはmy+ファイル名小文字にしたい
  4. ライブラリ側には、#includeやusing lint = long long などの記述があるが、スニペット側では実装部分だけで良い
  5. ただし、競技プログラミング用のテンプレートデバッグ用のテンプレートは#includeとかも含めてスニペットにしたい
  6. cppファイルの作成、編集したときにスニペットを自動生成して、削除(リネーム)した時には自動で消して欲しい
  7. 変更を常時監視しなくても良いけど何かしらのタイミングで反映したい

内容

スニペット自動生成のシェルスクリプト

# autosnippet.sh

#! /bin/bash
template="snippet/auto_template.snippet"

for cppfile in "$@"
do
        if echo "$cppfile" | grep -q template; then
                content=$(sed  -e '/[^\r\n]/,$!d' -e 's/[\&/]/\\&/g' -e 's/$/\\n/' $cppfile | tr -d '\n')
        else
                content=$(sed -e '/^#include/d' -e '/^using/d' -e '/^constexpr/d' -e '/[^\r\n]/,$!d' \
                              -e 's/[\&/]/\\&/g' -e 's/$/\\n/' $cppfile | tr -d '\n')
        fi

        filename=${cppfile%.cpp}
        filename=${filename##*/}

        sed -e "s/:Name:/$filename/" -e "s/:Shortcut:/my${filename,,}/" \
            -e "s/:Content:/$content/" $template > "snippet/$filename.snippet"
done

コードの簡単な説明

  1. cppファイル名が引数として渡されることを想定 for cppfile in "$@"で全部に対してやる
  2. #! /bin/bash シェバン
  3. templateはスニペットのもととなるファイル
  4. 仕様5のため、ファイル名に"template"が入っているかで分岐
  5. '/^#include/d'とかで特定の文字列が最初に来る行を削除、'/[^\r\n]/,$!d'で初めて改行以外の文字が来る行まで削除
  6. -e 's/[\&/]/\\&/g' -e 's/$/\\n/' $cppfile | tr -d '\n' sedが読んでしまう文字をエスケープ(\/&改行)
  7. ${cppfile%.cpp}, ${filename##*/}パラメータ展開の後方最短マッチと前方最長マッチでファイル名だけにする
  8. sed -e "s/:Name:/$filename/" -e "s/:Shortcut:/my${filename,,}/" \ -e "s/:Content:/$content/" $template > "snippet/$filename.snippet"テンプレートの各文字列を置換、snippet/ファイル名.snippetに出力

という感じです。シェルスクリプト初めて書いたのですが、sedが読んでしまう文字をエスケープしたりするところが非常に面倒でした。もっと簡単に書けそうなら教えてください(そもそも他のスクリプト言語で書いた方が楽なのか?)。とりあえずこれでcppファイルを渡したら自動でスニペットを作ってくれる部分はできました。

あとは、仕様7をどうすれば良いか考えたのですが、Git hooksのpre-commitってやつを使いました。Git hookはGit の操作に合わせて特定のスクリプトを実行してくれる仕組みなんですが、その中でpre-commitはコミットの直前に実行され、終了ステータスが0以外ならコミットされないといったものです。.git/hooks下にpre-commitといファイルを作って実行権限を与えれば良い感じです。

こんなシェルスクリプトを書きました。

# pre-commit

#! /bin/bash
git diff --cached --name-only --diff-filter=d | grep '\.cpp$'| xargs ./autosnippet.sh
res=$?

git diff --cached --name-only --diff-filter=D | grep '\.cpp$' | sed -e 's/library/snippet/g' -e 's/cpp$/snippet/g' | xargs rm -f

exit $res || $?

git diff --cached --name-onlyで変更されたファイル名を取り出して、末尾が.cppのファイルをgrepして先ほどのコマンドに渡します。 --diff-filter=dで削除したファイル以外の変更が見れます。

その後、--diff-filter=Dで、削除したファイルだけ取り出して、スニペットファイルを取り除いています。終了ステータスは渡しているもののどっちも0しか返らない気がしています(わからん)。

まとめ

ライブラリの変更に合わせてスニペットファイルが自動生成されるようになって快適になりました(うれしい)。 シェルスクリプトまだまだよくわかってないので、他にいい感じのやり方があったら教えてください。

次はverify自動化のためにverification-helper導入したいなあ(まずはライブラリをもっと充実させましょう……)。