はじめに
最近、学生プログラマや現役プログラマの間で競技プログラミングが流行ってますね(体感)。
私はコンテストに参加はしませんが、新しい言語を覚えるときの試し打ちとして競プロの問題を解くことが多いです。なかなか面白いです。
問題はたいてい、入力例が提示されていますが、それを標準入力するのって面倒ではありませんか?
できれば自動化したいです。自動化すればトライアンドエラーの効率が上がって得点アップが期待できますね!
自動化する方法はいろいろありますが、この記事ではmakeコマンドを用いるものを紹介します。
MakefileはサンプルとしてC++,Java,Python用のものを載せます。
コンパイル言語、Java系言語、スクリプト言語のサンプルを提示すれば、後は使用者が言語に合わせて書き換えてくれるだろう
という魂胆があります。
本記事のMakefileやディレクトリ構造をgithubに上げました。よかったら使ってください。
https://github.com/aoi-stoic/AtcorderTemplate
想定読者
- makeコマンドを使ったことがある方
- Linux/Macユーザ
- 入力例ごとにいちいち標準入力している方→「自動でできたら楽だなあ」と考えている方
想定していない読者
- Windowsユーザ(私がwindowsを使わないため)
- Makefileを自分で書ける方
- すでに別の方法で"標準入力"を自動化できている方
検証環境
OS
$ cat /etc/os-release | head -n 2
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"
ubuntu使ってます。他OS、ディストリビューションについては割愛するので適宜読み替えてください。
C++
gcc version 9.3.0 (Ubuntu 9.3.0-10ubuntu2)
Java
$ echo `javac -version` `java -version`
javac 1.8.0_265
openjdk version "1.8.0_265"
OpenJDK Runtime Environment (build 1.8.0_265-8u265-b01-0ubuntu2~20.04-b01)
OpenJDK 64-Bit Server VM (build 25.265-b01, mixed mode)
Javaはほとんど書いたことがないので適当です。Javaでコンテストに挑戦する人は、コンテストに合わせたバージョンを選んでください。
Python3
$ python3 --version
Python 3.8.2
仮想環境等使っていないもの
使い方
疑似コンテストABCXXXのA問題「4つの標準入力を受け取ってその和を標準出力せよ」を題材として、作業ディレクトリの構築やmakeコマンド実行例を提示します。
C++で解きます。
A問題の入力例と出力例は次の通り
入力例1
1 1
1 1
出力例1
4
入力例2
1 2
3 4
出力例2
10
入力例3
1000 100
10 1
出力例3
1111
0. 前準備
makeコマンドインストール
sudo apt isntall make
作業ディレクトリの構築
# 作業ディレクトリの作成&移動
mkdir ABCXXX && cd ABCXXX
# テンプレートディレクトリのクローン(--depth=1で最小限の履歴だけクローン)
git clone --depth=1 https://github.com/aoi-stoic/AtcorderTemplate.git
# A問題用にAtcorderTemplate/TemplateCppをコピー&移動
cp AtcorderTemplate/TemplateCpp/ problemA -r && cd problemA
この時点で、problemAディレクトリの中身はこうなってます
この構成を再現するのであればgitからcloneせずに手作業で構築し、Makefileだけこの記事からコピペしてもよいです。
$ tree
.
├── main.cpp # 解答用ソースファイル
├── Makefile # C++用Makefile
└── Testfiles # テストファイルディレクトリ
├── test1.test # 入力例1のテストファイル
├── test2.test
├── test3.test
└── test4.test
1. 入力例のコピペ
入力例をそれぞれのテストファイルにコピペします。
結果として次のようになります。
ABCXXXのA問題の入力例は1~3しかないので、test4.testは空ファイルになっています。
$ cat Testfiles/test1.test
1 1
1 1
$ cat Testfiles/test2.test
1 2
3 4
$ cat Testfiles/test3.test
1000 100
10 1
$ cat Testfiles/test4.test
2. コーディング
いつもどおり、main.cppに解答コードを書きました。
// main.cpp
#include <iostream>
int main(){
int a, b, c, d;
std::cin >> a >> b >> c >> d;
std::cout << a + b + c + d << std::endl;
}
3. makeコマンドの実行
problemAディレクトリでmakeコマンドを実行すると、
コンパイル後、入力例ごとに実行を自動で行ってくれます。
test4.testは空ですから、入力例4は実行されません。
$ make
g++ -std=c++11 -Wall main.cpp -o main.out
----------------------
exec Testfiles/test1.test
input:
1 1
1 1
output:
4
----------------------
exec Testfiles/test2.test
input:
1 2
3 4
output:
10
----------------------
exec Testfiles/test3.test
input:
1000 100
10 1
output:
1111
4. 提出 or 修正
入力例と出力例の対応が正しいことを確認し、コードが正しいことに確信が持てたら提出しましょう。
次のB問題へ進む場合は、同様にTemplateCppディレクトリをコピーしてproblemBを作成し、そのディレクトリ内で作業しましょう。
makeターゲット
今回掲載したMakefileには以下のターゲットを用意しています
pythonはコンパイル作業がないため、buildやcleanといったターゲットを設けていません。
ターゲット | コマンド | 概要 |
---|---|---|
(なし) | make | make testを実行 |
build | make build | コンパイルのみ実行 |
run | make run | 必要に応じコンパイルし、実行する。 標準入力は手作業で行う。 |
test | make test | 必要に応じコンパイルし、実行する。 テストファイルの中身を標準入力する。 |
clean | make clean | コンパイル後の生成物を削除する。 |
clean_test | make clean_test | テストファイルの中身を空にする。 A問題のディレクトリをコピーしてB問題の解答に再利用するときに使ったりする。 |
Makefile
C++用Makefile
COMP = g++ -std=c++11 -Wall
SRC = main.cpp
BIN = main.out
TESTDIR = Testfiles
# 暗黙ルールを無効にする, ディレクトリ移動時の出力を無効にする
MAKEFLAGS :=+ --no-builtin-rules --no-print-directory
# 引数(ターゲット)なし実行でtestをターゲットにする
all: test
$(BIN): $(SRC)
$(COMP) $(SRC) -o $(BIN)
# make run は手作業で標準入力を行う実行
.phony:run
run: $(BIN)
./$(BIN)
# make test
# TESTDIR内のtestファイルの内容を標準入力する実行(空のファイルは無視する)
.phony:test
test: $(BIN)
@for testfile in $(wildcard $(TESTDIR)/*.test) ; \
do \
if [ -s $$testfile ]; then \
echo ---------------------- ;\
echo "exec $$testfile " ;\
echo "input: "; \
cat $$testfile ; \
echo "" ;\
echo "output: " ;\
cat $$testfile | ./$(BIN) ;\
fi \
done
.phony:clean
clean:
-rm $(BIN)
# testファイルをすべて空にする
.phony:clean_test
clean_test:
@for testfile in $(wildcard $(TESTDIR)/*.test) ; \
do \
: > $$testfile ;\
done
Java用Makefile
Javaはほとんど書いたことがないです。
COMP = javac
EXEC = java
SRC = Main.java
CLASS = Main.class
# Main.class -> Main
APP = $(basename $(CLASS) .class)
TESTDIR = Testfiles
# 暗黙ルールを無効にする, ディレクトリ移動時の出力を無効にする
MAKEFLAGS :=+ --no-builtin-rules --no-print-directory
#
# 引数(ターゲット)なし実行でtestをターゲットにする
all: test
$(CLASS): $(SRC)
$(COMP) $(SRC)
# make run は手作業で標準入力を行う実行
run: $(CLASS)
$(EXEC) $(APP)
# make test
# TESTDIR内のtestファイルの内容を標準入力する実行(空のファイルは無視する)
test: $(CLASS)
@for testfile in $(wildcard $(TESTDIR)/*.test) ; \
do \
if [ -s $$testfile ]; \
then \
echo ---------------------- ;\
echo "exec $$testfile" ;\
echo "input: " ; \
cat $$testfile ; \
echo "" ;\
echo "output: " ;\
cat $$testfile | $(EXEC) $(APP) ;\
fi \
done \
.phony:clean
clean:
-rm $(CLASS)
# testファイルをすべて空にする
.phony:clean_test
clean_test:
@for testfile in $(wildcard $(TESTDIR)/*.test) ; \
do \
: > $$testfile ;\
done
Python用Makefile
Makefileのあり方的に、スクリプト言語のMakefileというのは違和感がありますが。
SRC = main.py
EXEC = python3
TESTDIR = Testfiles
# 暗黙ルールを無効にする, ディレクトリ移動時の出力を無効にする
MAKEFLAGS :=+ --no-builtin-rules --no-print-directory
# 引数(ターゲット)なし実行でtestをターゲットにする
all: test
# make run は手作業で標準入力を行う実行
.phony:run
run:
$(EXEC) $(SRC)
# make test
# TESTDIR内のtestファイルの内容を標準入力する実行(空のファイルは無視する)
.phony:test
test:
@for testfile in $(wildcard $(TESTDIR)/*.test) ; \
do \
if [ -s $$testfile ]; then \
echo ---------------------- ;\
echo "exec $$testfile " ;\
echo "input: "; \
cat $$testfile ; \
echo "" ;\
echo "output: " ;\
cat $$testfile | $(EXEC) $(SRC) ;\
fi \
done
# testファイルをすべて空にする
.phony:clean_test
clean_test:
@for testfile in $(wildcard $(TESTDIR)/*.test) ; \
do \
: > $$testfile ;\
done
余談
パイプライン
パイプライン(|)を使ってコマンドの出力を次のコマンドの入力に橋渡しすることができます。
$ cat Testfiles/test1.test
1 1
1 1
$ cat Testfiles/test1.test | ./main.out # catコマンドの出力を./main.out入力にしている
4
これを使えば、実行ファイルの標準入力をスクリプト化することができます。
make test
はこの機能を活用しているわけです。
既存ファイルの中身を空にする方法
新規に空ファイルを作る場合は
touch empty_file
で十分なわけですが、この方法は既存ファイルには使えませんね。
下のように"削除してtouch"という手段は私の趣味じゃないです
rm empty_file & touch empty_file # これは趣向に合わない
私がよく使うのは以下の2通りです。
# 何もしないコマンド(:) の結果をリダイレクトする
: > empty_file
# /dev/null をcpする
cp /dev/null empty_file
他にもいろいろ方法がありそうです。
この規模ならシェルスクリプトで十分じゃない?
そのとおりです。
しかしエディタに実行コマンドのショートカットを設定することを考えると、シェルスクリプトの実行よりも汎用性の高いmakeコマンドに対してショートカット設定するほうがベターだと考えています。
echoの出力に色をつけたら見やすいんじゃない?
そのとおりです。環境に合わせて色つけしてください。
ubuntu系ディストリビューションではシェルスクリプトの実行にbashではなくdashを用いているものがあります。
bashとdashでは一部コマンドの動作に違いがあります。
例えば、bashではechoの出力に色をつけるにはエスケープコードを用いて echo -e "\033[31m GREEN \033[m"
としますが、dashでは-eのオプションが不要でオプションの解析が行われません。
今回は公開するにあたり、この違いが問題になることが予想されたので今回はecho出力に色をつけませんでした。