mooakiのブログ

貪欲に出来ることはまだあるかい?乱択に出来ることはまだあるかい?

「HACK TO THE FUTURE 2022 予選」の一括テストスクリプトをAWKで書いた話。

「HACK TO THE FUTURE 2022 予選」とは

 フューチャー(株)が主催したいわゆるマラソン形式のプログラミングコンテストです。コンテストページはこちら

一括テストスクリプトとは

 マラソンコンテストで自分のプログラムの性能をより正確に評価するには、ローカル環境で多くのテストケースを与えた出力を見る必要があります。手作業ではとてもやってられないので「機械的な作業は機械にやらせる」の精神で一括でテストするスクリプトを書いてしまおう、という話です(環境はWSLのubuntuです)。


AWKとは

 AWKオーク)は、プログラミング言語の一つ。 テキストファイル、特に空白類(スペースの他、タブなど)やカンマなどで区切られたデータファイルの処理を念頭に置いた仕様となっているが、一般的なプログラミングに用いることも可能である。UNIX上で開発された。

Wikipediaより)

 つい「えーだぶりゅけー」って読んじゃうんですけど「オーク」が正しいようです。

 オークと呼ぼう!えーだぶりゅけー!

 なんで今回数ある言語の中からAWKを選んだかというと、外部コマンドの出力を取り込むのが簡単なような気がしたからです(自分の書ける言語の中では)。


書いてみる

 まずAWKスクリプトを書くときのひな型です。

#!/bin/bash
awk 'BEGIN {
}

 この"{"と"}"の間に処理を書いていきます(本来はBEGINアクションといって前処理を書く部分です)。

 今回は公式テスターと一緒にinフォルダーに0000.txtというような形式でテストケースが与えられますので、まずinフォルダーにあるファイルのファイル名を取得し表示してみます。

#!/bin/bash
awk 'BEGIN {
    while(("ls in" | getline) > 0) {
        print  $1
    }
}

まず

"ls in" | getline

の部分で外部コマンド(ls in)を実行した結果を1行づつ読み込みます。"while( ~ >0)"とすることで入力のある間、その後に書かれた処理を繰り返しています。入力はデフォルトでは空白文字で分割され先頭から$1,$2,...という変数に格納されます(分割まえの行全体は$0)。 なので

print $1

で1個目の項目を表示できます。

これを使って今回実行したいコマンドにしてみます。 今回のテスターは

cargo run --release --bin tester cmd < in.txt > out.txt

という形式で実行します。cmdが自分で作成したプログラム、in.txt、out.txtは入力ファイルと出力ファイルです。 このcmdを変数cmdで、in.txtとout.txtをそれぞれinフォルダとoutフォルダのファイル$1になるよう置き換えます。

#!/bin/bash
awk -v "cmd=$1" 'BEGIN {
    while(("ls in" | getline) > 0) {
        print("cargo run --release --bin tester ./" cmd " < in/" $1 " > out/" $1 " 2>> score.txt")
    }
}

 2行目、「awk」のあとの「-v "cmd=$1"」でコマンド実行時のオプションとして与えられた実行ファイル名を変数cmdに代入しています。 最後の「2>> score.txt」ですが、今回のテスターはスコアを標準エラーに出力するので、score.txtというファイルにリダイレクトで追加書き込みすることにしました。

 4行目、AWKでは文字列や変数をスペースで並べて書けば文字列として連結してくれます。

 実行してみると、

 まだprint文で表示しているだけですが、うまくいっているようです(入力ファイルは10個にしました)。これを変数sに代入してsystem関数で実行します。

#!/bin/bash
awk -v "cmd=$1" 'BEGIN {
    while(("ls in" | getline) > 0) {
        s = "cargo run --release --bin tester ./" cmd " < in/" $1 " > out/" $1 " 2>> score.txt"
        print s
        system(s)
    }
    close("ls in")
    total = 0
    while(("grep Score score.txt" | getline) > 0) {
        print $3
        total = total + $3
    }
    print total
}

8行目は外部コマンドを実行した結果を受け取るために開いたパイプをクローズしています。 その後、score.txtからgrepコマンドで"Score"の含まれた行を抽出し、その3項目目($3)を変数totalに加算していきます。文字列から数値への変換はAWKがいい感じにやってくれます。 最後にprint文でスコアの合計値を出力します。 実行してみると一見うまくいくようですが、2回目以降は残っているscore.txtファイルにさらに追加してしまうので、合計値が大きくなってしまいます。最初にrmコマンドでscore.txtファイルを消去するようにします。

#!/bin/bash
awk -v "cmd=$1" 'BEGIN {
    system("rm -f score.txt")
    while(("ls in" | getline) > 0) {
        s = "cargo run --release --bin tester ./" cmd " < in/" $1 " > out/" $1 " 2>> score.txt"
        print s
        system(s)
    }
    close("ls in")
    total = 0
    while(("grep Score score.txt" | getline) > 0) {
        print $3
        total = total + $3
    }
    print total
}

これで正しいスコアの合計値が出力されます。このままでもテストは出来るのですが、うっかり実行するプログラムの指定を忘れたときのためにusageを出力して終了してみます。

#!/bin/bash
awk -v "cmd=$1" 'BEGIN {
    if(cmd=="") {
        print "usage: mytest cmd"
        exit
    }
    system("rm -f score.txt")
    while(("ls in" | getline) > 0) {
        s = "cargo run --release --bin tester ./" cmd " < in/" $1 " > out/" $1 " 2>> score.txt"
        print s
        system(s)
    }
    close("ls in")
    total = 0
    while(("grep Score score.txt" | getline) > 0) {
        print $3
        total = total + $3
    }
    print total
}

 これでうっかりさんも安心ですね。

 コンテスト中はこんな感じでテストを回していたんですが、各テストケースのスコアが実行後にまとめて出力されるので「seed=5のスコアは?」なんてときに不便です。各テスト実行後にスコアを逐次出力するならこんな感じでしょうか。

#!/bin/bash
awk -v "cmd=$1" 'BEGIN {
    if(cmd=="") {
        print "usage: mytest cmd"
        exit
    }
    system("rm -f score.txt")
    total = 0
    while(("ls in" | getline) > 0) {
        s = "cargo run --release --bin tester ./" cmd " < in/" $1 " > out/" $1 " 2>> score.txt"
        print s
        system(s)
        if(("grep Score score.txt" | getline) > 0) {
            print $3
            total = total + $3
        }
        close("grep Score score.txt")
        system("rm score.txt")
    }
    close("ls in")
    print total
}

テスト実行ごとにscore.txtからスコアを読み取って削除しています。

 この方が便利だったな。

実際に使うには(追記)

 上記の内容をファイルに書き込み(例えばmytest)、

chmod a+x mytest

などとして実行可能にする必要があります。詳しくはこちら

最後に

 自分の乏しい知識で「動きゃあいいんだよ」と書いたものなので、もっと上手い書き方や、もっとお手軽な言語があるかもしれません。あったら教えてください。説明にも正確じゃない所や間違っている点もあるかもしれません。ご容赦ください(教えてくださいとは言わない)。 HTTF2022予選自体は、ビジュアライザに力が入っていて素晴らしかったし、seed=0のビジュアライザをシェア出来るのも、公平さなどには検討の余地があるかもしれませんが盛り上がって楽しかったです。準備された皆さん、本当にありがとうございました。

 解法記事書くほどの面白考察もできなかったので、こんな話題でお茶を濁してみました。いかがでしたか?