はじめに
最近個人的に注目しているプログラミング言語にCrystalがあります。
そのCrystalが今年になって1、ようやくWindows (x86-64)向けのコンパイラが(まだunsupported扱いですが)公式からリリースされるようになりました。
筆者は普段の業務でWindowsを使用しており、業務で度々使用する各種便利スクリプトをPythonで書いていました。
なので、これを機にそれらのPythonスクリプトをCrystalで書き直してみたくなりました。
そこでまず手始めに、毎日使用している日報ファイル作成スクリプトをCrystalで(再)実装してみることにしました。
Crystalとは
Crystalの特徴という名の個人的な注目ポイントです。
- Rubyの強い影響を受けた、読みやすく書きやすい構文を持つスクリプト言語
- ただしRubyと違い、静的型付き言語
- 優秀な型推論があるため、あまり型を意識することなくコーディングが可能
- 型安全・null安全2
- バックエンドにLLVMを用いており、高速なネイティブコードにコンパイル可能
- シングルバイナリを生成できるため、配布や実行環境の構築がとても簡単
- 標準ライブラリが充実しており、かゆいところに手が届く
今回実装したいスクリプトの動作
- テンプレートファイル
template.md
を読み込む - テンプレートファイルの中の特定の文字列を今日の日付や曜日に変換する
- それを指定のパス
./年/月/daily_年月日.md
(e.g../2022/03/daily_20220304.md
)に保存する - 生成された
.md
ファイルを、関連付けられたテキストエディタで開く
実際の実装
環境
実行環境はWindows 10 Pro (64 bit)、
使用したCrystalのバージョンは
Crystal 1.3.2 [932f193ae] (2022-01-18)
LLVM: 10.0.0
Default target: x86_64-pc-windows-msvc
です。
スクリプト本体
require "file_utils"
require "json"
def get_day_of_week_name(time : Time, config : JSON::Any) : String
case time.day_of_week
in Time::DayOfWeek::Monday then config["dayOfWeek"]["monday"].to_s
in Time::DayOfWeek::Tuesday then config["dayOfWeek"]["tuesday"].to_s
in Time::DayOfWeek::Wednesday then config["dayOfWeek"]["wednesday"].to_s
in Time::DayOfWeek::Thursday then config["dayOfWeek"]["thursday"].to_s
in Time::DayOfWeek::Friday then config["dayOfWeek"]["friday"].to_s
in Time::DayOfWeek::Saturday then config["dayOfWeek"]["saturday"].to_s
in Time::DayOfWeek::Sunday then config["dayOfWeek"]["sunday"].to_s
end
end
def format(str : String, time : Time, config : JSON::Any) : String
time.to_s(str.gsub("%-a", get_day_of_week_name(time, config)))
end
config = File.open("config.json") do |f|
JSON.parse(f)
end
current_time = Time.local
template_file = Path[config["templateFile"].to_s]
dir_name = Path[format(config["outputDir"].to_s, current_time, config)]
file_name = Path[dir_name, format(config["outputFile"].to_s, current_time, config)]
unless File.exists?(file_name)
FileUtils.mkdir_p(dir_name)
File.open(file_name, "w") do |f|
f.puts format(File.read(template_file), current_time, config)
end
end
Process.new("cmd.exe", ["/c", "start", file_name.expand.to_s])
exit
設定ファイル
{
"templateFile": "template.md",
"outputDir": "./%Y/%m",
"outputFile": "daily_%Y%m%d.md",
"dayOfWeek": {
"monday": "月",
"tuesday": "火",
"wednesday": "水",
"thursday": "木",
"friday": "金",
"saturday": "土",
"sunday": "日"
}
}
テンプレートファイル
# %Y年%m月%d日(%-a)日報
## 今日やったこと
-
## 明日やること
-
## 連絡事項とか
-
使い方
上記の3ファイルを同一のディレクトリに保存してやり、PowerShell等のお好きなシェルで
crystal run open_daily_report.cr
として実行してあげる3と、設定されたパスに下記のような日報ファイルが生成され、.md
が関連付けられたテキストエディタで開かれます。
# 2022年03月04日(金)日報
(略)
後は開かれたテキストエディタ上で日報を書いて、決められた提出先に提出するだけです。
使用できるフォーマット/ディレクティブについて
設定ファイルやテンプレートには、%Y
、%m
、%d
などTime::Format
で定められたディレクティブ(指示文字列)を使用できます。
CrystalのディレクティブはCのそれやPythonのそれとほぼ互換性があるようですが、一部独自拡張があるようです。
また、Crystalでは現状で曜日等にロケールを使用できない(i.e. 日月火……みたいな日本語の曜日名を使用できない)ようだったので、%-a
という独自拡張を作り、それを設定ファイルで設定した曜日名に置換するようにしました。
既知の不具合
現時点(Crystal 1.3.2)において、Windows環境ではTime.local
が常にUTCでの時刻を返すようです。
そのため、本スクリプトもUTCでの日時が適用されます。
例えばタイムゾーンが日本に設定されたWindows環境で上記のテンプレートを使用した場合、08:59以前はディレクティブが前日の日付に置換されます4。
シングルバイナリにビルドする
想定どおり動作することが確かめられたので、実行ファイルを生成してみましょう。
お好みのシェルで
crystal build open_daily_report.cr --release
としてやると、同じディレクトリにopen_dialy_report.exe
が生成されます。
次回からはこのexeを実行するだけで、日報ファイルが生成されるようになります。
また、他のPC等に配布する際も、このexeとテンプレート、設定ファイルの3つを配るだけでOKになりました。
自分が書いたスクリプトを他人に使ってもらうのに、いちいち「あらかじめ○○っていう言語の処理系をインストールしてパスを通しておいてください」みたいな事前準備が要らなくなるのは嬉しいですね。
おわりに
これだけ短いスクリプトを書いただけでも、Crystalは
- 簡単に習得できる
- 型安全・null安全なコードをサクッと書ける
- 強力な標準ライブラリによって目的を簡単に実現できる
- 高速なネイティブコードを出力・実行可能
と、とても便利なLightweight5 Languageだということを実感できました。
一方で、現状では
- 特にWindows環境ではまだ不具合も多い
- まだまだ公式のリファレンス以外に情報が少ない(日本語・英語とも)
- 公式のリファレンスも情報が少なかったりする(特に標準ライブラリ周り)ので、結局ソースコードから実装を読み解く羽目になることもある
といった課題も感じました。
Crystalの今後のさらなる発展と普及に期待しつつ、他にも色々とスクリプトを書いてみようと思いました。