EmacsでAtCoder環境を整えた


はじめに

Emacs + Ruby で競プロをやるという時点でほぼ需要はないと思いますが備忘録を兼ねて紹介します。

個々のコマンドの意味はググるとたくさん出てくるのでこちらはシェルに流せば済むような簡潔さを意識して書いています。

のちのち Rust でも解きたいのでおまけで Rust 用の環境も作っています。

Ruby 用

atcoder-cli の導入・テスト・提出の手順

pip3 install online-judge-tools
yarn global add atcoder-cli
acc check-oj
acc login
acc session
acc config default-test-dirname-format test
acc config default-task-choice all
acc config default-template ruby

cd ~/Library/Preferences/atcoder-cli-nodejs
mkdir ruby
cd ruby
emacsclient template.json
emacsclient main.rb

mkdir -p ~/src/procon/ruby
cd ~/src/procon/ruby
acc new math-and-algorithm
cd math-and-algorithm/001
echo "p gets.to_i + 5" > main.rb
oj t -c "ruby main.rb"
acc s main.rb -- -w 0 -y
template.json
{
  "task":{
    "program": ["main.rb"],
    "submit": "main.rb"
  }
}
main.rb
$stdin = DATA
n = gets.to_i
p gets.split.take(n).collect(&:to_i)
__END__
3
10 20 30
  • atcoder-cli の実際のコマンド名が acc
    • acc は AtCoder Command line interface の略と思われる
  • atcoder-cli は Ruby に依存しているわけではない
    • Python C C++ もいける
    • Rust もいけるけど、外部クレートに依存せず rustc だけでコンパイルできるシンプルなコードに限る
      • cargo を使わずに外部クレートと合わせてビルドできないのかな?
  • acc config default-test-dirname-format test が超重要
    • これをやっておかないと oj t -c "ruby main.rb" が動かなくてはまる、というかはまった
    • oj t -c "ruby main.rb" -d tests とすれば動くことがあとでわかったが、とにかくすげーはまった
    • なので最初から test で作るようにしておけば -d tests が不要になり、ひっかかることはなくなる
    • これは atcoder-cli のテストディレクトリ名のこだわりが影響している
  • template.json のなかに main.rb を提出すると書いてあるのに acc s -- -w 0 -y とは書けないのは謎
    • なので template.json の submit の指定は意味ない

Rust 用

cargo-compete の導入・テスト・提出の手順

mkdir -p ~/src/procon/rust
cd ~/src/procon/rust

cargo install cargo-compete
cargo compete init atcoder
cargo compete login atcoder
rustup install 1.42.0

cargo compete new math-and-algorithm
cd math-and-algorithm/bin
cargo compete open --bin 001
cat <<'EOF' > 001.rs
use proconio::input;
fn main() {
    input! { n: isize, }
    println!("{}", n + 5);
}
EOF
cargo compete test 001
cargo compete submit --no-test --no-watch 001
  • cargo-atcoder が以前は主流だったようだが、基本的な部分が引き継がれ、導入のしやすさや使い勝手が cargo-compete の方がさらによくなっている
  • コンテスト math-and-algorithmcargo compete open するとブラウザで 104 個のタブが開かれるので注意
    • たぶん5問ぐらいのコンテストしか想定してないんだと思う
    • --bin 001 とすれば問題 001 だけ開ける
  • eshell で cargo compete submit すると表がめちゃくちゃになるので --no-watch している
  • atcoder-cli とはディレクトリ構成が大きく異なる
    • どうしても依存 crate を cargo で管理しないといけないので構成も cargo 依存になっている
    • acc ではディレクトリ名が問題インデックス名になっていたが、こちらはファイル名が問題インデックス名になっている

Emacs の設定

CLIで何でもできるようになったとはいえ、そんなコマンドは半日で忘れるので、次のようにメニューを出して1文字打てば実行できるようにします。

これを行うには magit で活用されている transient.el がぴったりです。

簡単な使い方
(transient-define-prefix my-procon-launcher ()
  [
    ("a" "説明1" (lambda () (interactive) (message "A")))
    ("b" "説明2" (lambda () (interactive) (message "B")))
  ]
 )
  • a を押すと "A" が表示されます。
  • b を押すと "B" が表示されます。

これをどんどん拡張して半自動化したいことを追加していきます。

my-procon.el
(require 'transient)
(require 'f)

(defun my-procon-eshell-send (command)
  "eshellを現在のディレクトリに移動して指定のコマンドを実行"
  (interactive)
  (let ((dir default-directory))
    (eshell)
    (goto-char (point-max))
    (eshell-bol)
    (unless (eobp)
      (kill-line))
    (insert (format "pushd '%s'" dir))
    (eshell-send-input)
    (insert command)
    (eshell-send-input)))

(transient-define-prefix my-procon-launcher ()
  "競プロ用ランチャー"
  ["Ruby"
   ("i" "問題情報入力"
    (lambda ()
      (interactive)
      (beginning-of-buffer)
      (insert (shell-command-to-string (format "ruby -e \"
require 'pathname'
require 'json'
file = Pathname('../contest.acc.json')
info = JSON.parse(file.read, symbolize_names: true)[:tasks].find { |e| e[:label].casecmp?(ARGV[0]) }
puts '# [' + info[:label] + '] ' + info[:title]
puts '# ' + info[:url]
\" %s" (f-filename (f-dirname (buffer-file-name))))))))
   ("o" "問題URLをブラウザで開く"
    (lambda ()
      (interactive)
      (beginning-of-buffer)
      (shell-command (format "ruby -e \"
require 'pathname'
require 'json'
file = Pathname('../contest.acc.json')
info = JSON.parse(file.read, symbolize_names: true)[:tasks].find { |e| e[:label].casecmp?(ARGV[0]) }
system 'open ' + info[:url]
\" %s" (f-filename (f-dirname (buffer-file-name)))))))
   ("1" "単体実行"
    (lambda ()
      (interactive)
      (set (make-local-variable 'compile-command) "ruby main.rb")
      (call-interactively 'compile)))
   ("t" "テスト"
    (lambda ()
      (interactive)
      (set (make-local-variable 'compile-command) "oj t -c 'ruby main.rb'")
      (call-interactively 'compile)))
   ("s" "提出"
    (lambda ()
      (interactive)
      (my-procon-eshell-send "acc s main.rb -- -w 0 -y")))
   ("l" "問題一覧の確認"
    (lambda ()
      (interactive)
      (my-procon-eshell-send "acc tasks")))
   ("c" "コンテスト名の確認"
    (lambda ()
      (interactive)
      (my-procon-eshell-send "acc contest")))
   ("n" "次の問題を取得"
    (lambda ()
      (interactive)
      (my-procon-eshell-send "acc add -c next")))
   ("e" "テンプレート編集"
    (lambda ()
      (interactive)
      (find-file "~/Library/Preferences/atcoder-cli-nodejs/ruby/main.rb")))
   ("z" "設定"
    (lambda ()
      (interactive)
      (dired "~/Library/Preferences/atcoder-cli-nodejs")))
   ]
  ["Rust"
   ("rt" "テスト (cargo compete test)"
    (lambda ()
      (interactive)
      (set (make-local-variable 'compile-command) (format "cargo compete t %s" (f-base (buffer-file-name))))
      (call-interactively 'compile)))
   ("rs" "提出 (cargo compete submit)"
    (lambda ()
      (interactive)
      (my-procon-eshell-send (format "cargo compete s --no-test --no-watch %s" (f-base (buffer-file-name))))))
   ]
  )

;; (my-procon-launcher)
;; (global-set-key (kbd "C-j p") 'my-procon-launcher)
  • 説明文はユニークにすること
    • 同じだと同じコマンドと見なされてしまう

おわり

自分の場合は次の手順で進めていくことが多いです。

  1. i → 問題情報入力
  2. o → 問題URLをブラウザで開く
  3. 1 → 単体実行
  4. t → テスト
  5. s → 提出
  6. n → 次の問題を取得

CLIで半自動化した操作を Emacs でさらに簡単にするまでの紹介でした。

参照

http://tatamo.81.la/blog/2018/12/07/atcoder-cli/
https://github.com/online-judge-tools/oj
https://github.com/qryxip/cargo-compete
https://github.com/magit/transient