AtCoderの提出自動化をする（Python）

# はじめに

MacBookを買いました。Air 2020 i7 16GBRAM 512GBSSDのモデルです。
これまで使っていためちゃくちゃ重い(物理)Windowsの2倍は余裕で超えるベンチマークが出るのでその性能ももちろん嬉しかったのですが、Macの一番惹かれていたのは画面内外のデザインとフォントレンダリングです。UIも綺麗で大満足です。せっかくなので環境構築ついでに最近ハマっているAtCoderの提出自動化をすることにしました。提出言語はPythonですが他の言語でも同様です。**20/5/1追記：[AtCoderの仕様変更について](#AtCoderの仕様変更について)**

#準備
## 必要なもの
まだ入っていない場合は以下を事前に入れておきます。ちなみにエディタはコンテスト用にVSCodeと普段のエディタ用にAtomとで兼用しているので、以下適宜読み替えてください。
### Hyper
MacのターミナルはWindowsに比べてだいぶマシですが、それでも多少のダサさがあります。
いろいろ探したところ、[Hyper](https://hyper.is/)というターミナルがとても綺麗です。サードパーティーでポケモンの着せ替えテーマとかもあるらしくて可愛いです。以下、`$`記号が頭につく行はターミナルでそれに続くコマンド（`$ ​`は含めない）を入力しReturn(Enter)で実行することを意味します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599184/ef78844a-5f17-c9b7-9770-ea9495295aa3.png)
コンテスト中はVSCodeのターミナルから作業をすることにしました。

### Homebrew
有名なパッケージ管理システムです。[公式ページ](https://brew.sh/index_ja)に貼ってあるコマンドをコピーしてターミナルに貼り付けて実行します。途中でRETURNキーを押さないといけないところがあります。

```terminal
==> The Xcode Command Line Tools will be installed.
Press RETURN to continue or any other key to abort
```
その直後パスワードの入力を求められます。ターミナル何もわからないぼくはここで詰まりました。パスワードを入力しているのに何も表示されないと思ったら、セキュリティーのためにhiddenされているだけでした... Macのパスワードを入力して進みます。

```terminal
==> /usr/bin/sudo /bin/...
  Password:
```
```terminal
 ==> Installation successful!
```

### Python
[Python公式](https://www.python.org/)からインストーラをダウンロードしてセットアップします。またはbrewを使います。

```terminal
$ brew install python3
```

### Node.js
こちらも[Node.js公式](https://nodejs.org/ja/)からインストーラをダウンロードします。インストーラ使ったほうが早いです。

### ツール
他に必要なツールを入れていきます。テストにはonline-judge-toolsを使います。提出やディレクトリ作成、テストケースの取得にはatcoder-cliをメインに使います。seleniumはジャッジ実行のために必要です。

```terminal
$ pip3 install online-judge-tools
$ oj --version    # check

$ npm install -g atcoder-cli
$ acc -v          # check
$ acc check-oj    # check

$ pip3 install selenium
```

いらないかもしれませんが、ディレクトリ構造を把握するためにツリー表示を使いたいのでインストールします。

```terminal
$ brew install tree
```

VSCodeの`code`コマンドを使いたいので、バージョンチェック（`-v`）してインストールされていなければインストールします。VSCodeのコマンドパレットを`⌘`+`Shift`+`P`で開きshellなどと打ち込み「Shell Command: Install 'code' command in PATH command」を実行します。

```terminal
$ code -v   # check
```


## 設定
まず、Finderで開いているディレクトリにターミナルに移動したくて不便だったので[こちらの記事](https://www.rasukarusan.com/entry/2019/05/02/000000)を参考（丸パクリ）にスクリプトを書きました。テキストエディタなどでホームディレクトリ（`/Users/_username_`＝`~`）直下に言語設定をShellScript／拡張子を.shにして以下の内容を保存します。

```shell:Finderset.sh

#!/usr/bin/osascript

tell application "Finder"
    if exists Finder window 1 then
        set currentDir to target of Finder window 1 as Unicode text
        set posixPath to get POSIX path of currentDir
    end if
end tell
```

次に、ホームディレクトリ直下にエイリアスなどの設定を管理する`.zshrc`を置きます。

```bash
$ cd ~
$ touch .zshrc
$ code .zshrc
```

以下は追記エイリアスの一例です。`cdf`は前述のFinderで開いているディレクトリにターミナルに移動するコマンドのショートカット、`atc`はディレクトリを置く場所への移動ですが、それ以外については下で書きます。（`cd`はターミナルのカレントディレクトリ移動コマンド）個人的にディレクトリはGitHubのローカルリポジトリを置いている場所に作ることにしました。

```shell:.zshrc
# vscode
alias vs='code -r main.py'

# cd
alias cdf='cd $(osascript ~/xxx.sh)'  # さっき書いたファイルの名前
alias atc='cd /Users/xxx...'  # ディレクトリを作りたい場所
alias a='cd ../a ; vs'
alias b='cd ../b ; vs'
alias c='cd ../c ; vs'
alias d='cd ../d ; vs'
alias d='cd ../e ; vs'
alias d='cd ../f ; vs'

# test
alias test='oj t -c "python3 main.py" -d ./tests/'
alias py='python3 main.py'

# start
sta () {
  atc
  acc new $1
  code -r $1
  source cd $1/a
  vs
}

# submit
sd () {
  problem=`pwd | xargs basename`
  contest=`echo $(basename $(pwd | xargs dirname)) | tr '[:upper:]' '[:lower:]'`
  echo "${contest:0:3}$problem" | acc s
  source cd ../`echo $(printf "\x$(printf "%x" $(($(printf "%d" \'$problem)+1)))")`
  vs
}
alias sf='if test ; then sd ; fi'
```

<img width="1104" alt="スクリーンショット 2020-04-28 19.03.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599184/61d5aa80-ab61-dd8c-ca15-c86188a3a503.png">

VSCodeでターミナルを快適に使うためにショートカットを設定しました。`⌘`+`Shift`+`P`でコマンドパレットを出しKeyboardなどと打ち込み「基本設定：キーボードショートカットを開く(JSON)／Preferences: Open Keyboard Shortcuts file (JSON)」を選択します。`keybinding.json`に二つ付け加えます。`Ctrl`+`l`は未使用みたいなのでこれに割り当ててみました。こうすれば`Ctrl`+`l`だけでターミナルとエディタの移動を行えます。

```json:keybinding.json
[
    { "key": "Ctrl+l", 
    "command": "workbench.action.terminal.focus" },
    { "key": "Ctrl+l", 
    "command": "workbench.action.focusActiveEditorGroup", "when":"terminalFocus" },
]
```

次に、AtCoderへのログインをします。

```terminal
$ acc login
$ oj login https://atcoder.jp/
```

コンテストの全ての問題について同時にディレクトリが作られるように設定します。

```terminal
$ acc config default-task-choice all
```
コンフィグディレクトリの場所を取得して移動します。`cd`に`acc`の結果を渡すためには` ' `シングルクォートではなく`` ` `` バッククォートを使います。フォルダを作成し、その中にテンプレート `main.py`とテンプレートの設定ファイル`template.json`を作成します。

```terminal
$ cd `acc config-dir`
$ mkdir py
$ cd py
$ touch main.py template.json
```
`template.json`を開き書き込みます。これでディレクトリ作成と同時に`main.py`を開き提出時も`main.py`を対象とすることができます。

```terminal
$ open -a Atom template.json
```
```json:template.json
{
  "task":{
    "program": ["main.py"],
    "submit": "main.py"
  }
}
```
`main.py`を開き書き込みます。以下のように一行目には`Shebung`を書きます（必須）。テンプレートなので二行目以降にはよく使うコードを書いておいても良いかもしれません。

```terminal
$ open -a Atom main.py
```
```python:main.py
#!/usr/bin/env python3
```
テンプレートの設定をpyにします。

```terminal
$ acc config default-template py
$ acc templates   # check
search template directories in /Users/../Library/Preferences/atcoder-cli-nodejs
[NAME]  SUBMIT-PROGRAM
[py]    main.py
```

# 実際に使ってみる
## ディレクトリ作成
`atc`でディレクトリを作る場所に移動して、`acc new [ContestName]`で新しくディレクトリを生成します。具体的には、AtCoderのURLの該当部分の名前になります。以下のような`tree`が確認できたら成功です。

```terminal
$ atc
$ acc new ABC164    # name of contest 
$ tree
.
├── a
│   ├── main.py
│   └── tests
│       ├── sample-1.in
│       ├── sample-1.out
...
│       └── sample-3.out
├── b
│   ├── main.py
│   └── tests
│       ├── sample-1.in
│       ├── sample-1.out
│       ├── sample-2.in
│       └── sample-2.out
├── c
│   ├── main.py
│   └── tests
│       ├── sample-1.in
...
│       └── sample-3.out
├── contest.acc.json
├── d
│   ├── main.py
│   └── tests
│       ├── sample-1.in
│       ├── sample-1.out
│       ├── sample-2.in
│       ├── sample-2.out
...

12 directories, 43 files
```
先ほどの`.zshrc`には以下のように書いて自動でA問題のフォルダにカレントディレクトリを移しています。これでA問題提出時にディレクトリを変える必要はないです。（同じターミナルで一貫してディレクトリ作成〜提出までの作業をする場合）

```shell:.zshrc(only_one_part)
sta () {
  atc
  acc new $1
  code -r $1
  source cd $1/a
  vs
}
```

## テスト
実際にA問題を開いて解いてみます。`sta`を実行したらそのまま開いた`a/main.py`にプログラムを書きます。そのまま先ほど設定したショートカットでターミナルに移り、問題のディレクトリ(`a`)にいることを確認して`oj t -c "python3 main.py" -d ./tests/`を実行します。長ったらしいので先ほどの`.zshrc`のエイリアスでは`test`に割り当てています。ちゃんと`AC`と判定してくれました。

```terminal
$ pwd     # check current directory (not necessary)
/Users/.../abc164/a

$ test
[*] 3 cases found
time: illegal option -- f
usage: time [-lp] command.
[!] GNU time is not available: time

[*] sample-1
[x] time: 0.022793 sec
[+] AC

...

[x] slowest: 0.022793 sec  (for sample-1)
[+] test success: 3 cases
```

<img width="1136" alt="スクリーンショット 2020-04-29 11.18.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599184/781bd043-ae1a-4719-ec90-cde545da73e0.png">

競プロしかやってないとわかんなくなりそうですが、本来`input()`が実行されるとターミナルの標準入力が入力待機状態にになります。なので、サンプルケース以外の任意のケースで実行したい場合は普通に`python3 main.py`とやって一行ずつ標準入力を打ち込めば良いです。（`open(0)`とかはどうなんだろう...）

## 提出
該当ディレクトリにさえいれば`acc s`だけで可能です。ただ、確認コードを入力するように言われます。どうやら、コンテスト名の頭三文字と問題番号を組み合わせた計4文字のようです（`[ContestName;3][problem]`;ex.abca）。流石にしている暇はないので、`sd`関数を用意しています。`sd`では提出後に自動で次のディレクトリに移動するようにしてあります。`.zshrc`にはもう一つ、`test`の結果が`[AC]`（＝返り値が`0`）の時のみ提出するコマンド（`sf`）を用意しています。

```shell:.zshrc(only_one_part)
# submit
sd () {
  problem=`pwd | xargs basename`
  contest=`echo $(basename $(pwd | xargs dirname)) | tr '[:upper:]' '[:lower:]'`
  echo "${contest:0:3}$problem" | acc s
  source cd ../`echo $(printf "\x$(printf "%x" $(($(printf "%d" \'$problem)+1)))")`
  vs
}
alias sf='if test ; then sd ; fi'
```

提出作業が進むと自動でブラウザが開きます。
他の問題への移動もエイリアスにしました。一応自動で動きますが、これで前／先の問題に動くときはカレントディレクトリの移動とVSCodeのファイル操作は`a`・`b`・`c`...だけでOKですね。

```shell:.zshrc(only_one_part)
alias a='cd ../a ; vs'
alias b='cd ../b ; vs'
alias c='cd ../c ; vs'
alias d='cd ../d ; vs' # ...
```

これで完璧ですね！！

# おまけ（詰まったところ）
`.zshrc`を試行錯誤しながら編集していて、即反映させたいとき（普通に反映させるにはターミナルを再起動）

```terminal
$ source ~/.zshrc
```

なぜか関数が使えないとき

```shell:.zshrc
function func () {
  # do_something
}
alias func=func
```

シェルスクリプトは関数定義時の`function`が省略できますが、同名のエイリアスを定義したいときは明示的に宣言しないと以下のようなエラーが出ます。

```terminal
$ source ~/.zshrc
/Users/xxx/.zshrc:19: defining function based on alias `func'
/Users/xxx/.zshrc:19: parse error near `()'
```

# AtCoderの仕様変更について（追記）
5月1日から提出ができないな〜と思ったらどうやら`oj`の問題っぽい。

```terminal
$ oj -v s [URL] main.py
```
```terminal
...
  File "/Users/kazuhiroserizawa/.pyenv/versions/3.8.0/lib/python3.8/site-packages/onlinejudge/service/atcoder.py", line 728, in _parse_available_languages
    languages += [Language(option.attrs['value'], option.string)]
KeyError: 'value'

[ERROR] 'value'
```
<img width="1372" alt="スクリーンショット 2020-05-01 13.16.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599184/4d296b34-c791-3fe0-8c6e-e7aa895e11e0.png">

728行目から狂ってますね。[GitHubにもプルリクがありました](https://github.com/kmyk/online-judge-tools/pull/739)。すぐ修正されると思いますが一応直しておきます。

```terminal
# Install-Location
$ pip show online-judge-tools
...
Location: /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages
...
# Open
$ cd /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages
$ open -a Atom onlinejudge/service/AtCoder.py
```
コードの修正内容は[こちら](https://github.com/kmyk/online-judge-tools/pull/739/commits/577b1d9d9a505cea7835ac1c0a98ef9377b8b919)。あとはこの通りに書き換えます。

# 参考文献
GitHubにはスターを送るのを忘れずに！

- [GitHub - atcoder-cli](https://github.com/Tatamo/atcoder-cli)
    - [atcoder-cliを公開しました](http://tatamo.81.la/blog/2018/12/07/atcoder-cli/)
- [GitHub - online-judge-tools](https://github.com/kmyk/online-judge-tools)
- [AtCoder初心者こそ環境構築をしよう！ - Qiita](https://qiita.com/Adaachill/items/3d4ddad56c5c2cc372cd)
- [コマンドラインからVS Codeでファイルやフォルダを開く](https://qiita.com/1natsu172/items/b951aa33451dad36bd7c)
- ShellScript・ターミナルなど
    - [Macのターミナルコマンド一覧（基本編） - Qiita](https://qiita.com/ryouzi/items/f9dee1540a04a0bfb9a3)
    - [Macでtreeコマンドを使 - Qiita](https://qiita.com/kanuma1984/items/c158162adfeb6b217973)
    - [Finderで開いているディレクトリにコマンドで移動する](https://www.rasukarusan.com/entry/2019/05/02/000000)
    - [cdコマンドの移動先の取得について](https://oshiete.goo.ne.jp/qa/446463９.html)
    - [Linuxコマンドを連続して使うには - Qiita](https://qiita.com/egawa_kun/items/714394609eef6be8e0bf)
    - [シェルスクリプト if文](https://eng-entrance.com/linux-shellscript-if#if0)
    - [bashで変数から部分文字列を取得する - Qiita](https://qiita.com/koara-local/items/04d3efd1031ea62d8db5)
    - [Bash $((算術式)) のすべて - A 基本編 - Qiita](https://qiita.com/akinomyoga/items/9761031c551d43307374)
    - [パイプで渡したパスに無理やりcdするシェルスクリプト - Qiita](https://qiita.com/catfist/items/0ebd4350cc2976a95105#comments)
    - [zshでちょっとハマったのでメモ](http://fushiroyama.hatenablog.com/entry/2018/01/26/145214)
    - [ASCIIコード / shell](https://eggtoothcroc.hatenadiary.org/entry/20110718/p1)
    - [bashで大文字/小文字の変換](https://qiita.com/myy/items/782deb8347ac58f0d6a1)

# 免責
AtCoderの利用規約では明示的に禁止されていませんが、サーバーに負荷をかけご迷惑をおかけするわけにはいかないので、テストケースの取得は一回にしています。良識の範囲内での利用を。

# おわりに
Unix系のコマンドが全くわからず検索しまくりました！