【Shellscript】AtCoder用g++コマンドを自作した話

世間は大寒波みたいですね。僕と言えば期末試験期間真っ最中にも関わらずお酒を飲みながらこのブログを書いてるわけです。おかしいですね。

今回はAtCoder用のg++コマンド、ag++を作成したので簡単に紹介します。

まず、なぜこのコマンドが必要になったかというのは、ABC288のC問題においてUnion-Findを知り、さらにこのデータ構造がC++のSTLで用意されてないためACLの導入を考えたことにあります。ACLとは、AtCoder Libraryの略でAtCoderにおけるコンテストで用いることができるライブラリです。ACLには様々なデータ構造、アルゴリズムが用意されており、なんとその中にUnion-Findもあるのです!!!

これは利用しない手はないと思ったのですが、ACLを用いるにはコンパイル時にACLのGithubからダウンロードしたac-libraryフォルダへとインクルードパスを通さなければなりません。コンパイルをするたびにインクルードパスを指定するのは面倒だったので、簡単にシェルスクリプトを作成しようと思い立ったわけです。ついでに、AtCoderで行われるコンパイルと同様のコンパイルをするように実装しています。

以下がag++のコードです。

#!/bin/bash

ACL_PATH="../ac-library" # ACLへのパス

# 使い方
function Usage() {
	cat 1>&2 <<EOF
Usage: ag++ [OPTIONS] <filename>

Options:
	-h	Display Usage(this message)
	-t	Compile in test mode
EOF
}

# オプション解析
while getopts ':th' OPT
do
	case $OPT in
		h)	# -hオプションは使い方紹介
			Usage
			exit 1 ;;
		t)	# -tオプションはテストモードとしてコンパイルする
			flag='test' ;;
		?)	# 不明なオプションについてエラー処理
			echo -e "ag++: \x1b[31merror:\x1b[0m illegal option. try -h to show usage."
			exit 1 ;;
	esac
done

shift $((OPTIND - 1)) # オプション解析後、切り詰める
arg=$@ # オプションを除いたコマンドライン引数

if [ $# -eq 0 ]; then # コマンドライン引数がない場合は-hの使用を促す
	echo -e "ag++: \x1b[31merror:\x1b[0m no input files. try -h to show usage."
	exit 1
fi
if [ -z ${flag} ]; then # -tオプションが指定されていない場合、本番用でコンパイル
	DEFINE="-DONLINE_JUDGE"
	echo -e "ag++: \x1b[36mCompile in submit mode\x1b[0m"
else # -tオプションが指定されている場合、テスト用でコンパイル
	echo -e "ag++: \x1b[36mCompile in test mode\x1b[0m"
fi
g++ $arg -std=gnu++17 $DEFINE -Wall -Wextra -O2 -I $ACL_PATH

ACL_PATH変数に自身の環境におけるACLディレクトリを指定してください。このプログラムを~/binとかに配置して、パスを通してあげればコマンドとして使えるはずです。なお、実行環境はWSL2+GCC 9.4.0です。

何もオプションが指定されなかった場合、AtCoderのテストサーバで行われるものと同様のコンパイルをする仕様になっています。これはAtCoderのルールを参考にしています。案外知られていないですが、AtCoderのサーバ上ではONLINE_JUDGEというマクロ定義がされた上で実行されます。このマクロの使用例については後述します。

getoptsを用いてオプション解析をしたのち、

shift $((OPTIND - 1)) # オプション解析後、切り詰める

によってオプション部分を捨てています。[getopts] シェルスクリプトのオプションを処理するなどが参考になると思います。

34行目の例外処理はg++に任せても良いのですが、ag++-hオプション使用を促す為にあえて例外処理を行っています。

実際の使用例を以下に示します。以下のPC_A.cppというファイルをag++を用いてコンパイルしようと思います。これは、AtCoder Practice Contest A-Welcome to AtCoderという問題に対するC++回答プログラムです。

#include <iostream>
#include <string>
using namespace std;

int main()
{
	int a, b, c;
	string s;
	cin >> a >> b >> c >> s;
#ifndef ONLINE_JUDGE
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
	cout << "s = " << s << endl;
#endif
	cout << a + b + c << " " << s << endl;

	return 0;
}
$ ag++ -h
Usage: ag++ [OPTIONS] <filename>

Options:
        -h      Display Usage(this message)
        -t      Compile in test mode
$ ag++ PC_A.cpp
ag++: Compile in submit mode
$ ./a.out
1
2 3
test
6 test
$ ag++ -t PC_A.cpp
ag++: Compile in test mode
$ ./a.out
1
2 3
test
a = 1
b = 2
c = 3
s = test
6 test
$

先に少しだけ書いたように、AtCoderではONLINE_JUDGEというマクロ定義がされた状態で実行されます。つまり、ONLINE_JUDGEが定義されていれば本番用に、そうでなければテスト(デバッグ)用に実行するようにコンパイルできるわけです。

実際、このコードはAtCoder上ではACします。

プリプロセッサ処理(#if, #ifdef, #ifndef)等によるデバッグはよくやる(と思う)ので、知っておいて損はないと思います。

元々の目的であるACLを簡単に使うという目的も達成されました。以下のようなC++ソースコードに対しても、ag++ APCL_A.cpp とするだけでACLをインクルードした上でコンパイルすることができます。ターミナルはAPCL A – Disjoint Set Unionのサンプルを通してみた例です。

#include <iostream>
#include <atcoder/all>

using namespace std;
using namespace atcoder;

int main()
{
	int N, Q;
	cin >> N >> Q;
	vector<int> t(Q), u(Q), v(Q);
	dsu tree(N);

	for (int i = 0; i < Q; i++) cin >> t[i] >> u[i] >> v[i];

	for (int i = 0; i < Q; i++){
		if (t[i]) cout << tree.same(u[i], v[i]) << endl;
		else tree.merge(u[i], v[i]);
	}

	return 0;
}

$ ag++ ALPC_A.cpp
ag++: Compile in submit mode
$ ./a.out
4 7
1 0 1
0 0 1
0 2 3
1 0 1
1 1 2
0 0 2
1 1 3
0
1
0
1
$

長々となりましたが、AtCoder用g++コマンドを自作した話でした。これでレッドコーダー入り間違いなしです。