Windowsでescriptするときにちょっぴり幸せになれる Tips


1.escript ってなに?

ご存じ赤丸急上昇中の Elixirと言えば、WEBフレームワークの Phoenix、インタラクティブUIの LiveView、そして異色のIOTシステム Nervesと、比較的規模の大きなガチな面々が有名で、ライトな用途には向かないよーな印象がある……少なくとも私にはそう見えていた

ところがどっこい、Elixirでもライトな CLI(Command Line Interface)アプリを作ることが出来るのだ。後ほど詳しく触れるが、Elixirプロジェクトのディレクトリで、

mix escript.build

とすれば、CLIアプリのコマンドが作成できる(以下escriptと呼ぶ)。もちろん、shellで起動すればちゃんと動く。

そう、ここまでは良かった。だが、この escriptは Windowsのコマンドプロンプトでは実行できないのだ。残念、Windowsファンは指を咥えて我慢するしかないのか?

2.escriptを実行するカラクリ

できないとなると、俄然何とかならないものかと悪あがきをしたくなるのが、天邪鬼な技術屋の性分である。

取り敢えず、escriptを実行するカラクリを調べてみよう。えいやっと、escript - helloとする - のバイナリ・ダンプを取ってみると、下図のようになっていた。なるほど、un*xシェルの shebang "#! /usr/bin/env escript"を応用している訳ね[*1]。となれば脈はある

[*1]shebang: un*xシェルは、<ファイル>の先頭に "#! cmd"のパターンを見つけると、代わりに "cmd <ファイル>"を実行する。上の helloでは、実際には "/usr/bin/env escript hello"が実行される。

3."mix escript.build"にバッチファイルを作らせる

では始めよう。

最初に、いつもの通りElixirプロジェクトを用意しよう。
プロジェクト名は "hello"、実行するとコンソールに"Hello world!"と表示する escriptを作る。

C:\home\Elixir>mix new hello
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/hello.ex
* creating test
* creating test/test_helper.exs
* creating test/hello_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd hello
    mix test

Run "mix help" for more commands.

escriptのプロジェクトでは、コマンドのエントリーポイントとなる "main(args \ [])"関数を用意する必要がある(下記)。

hello.ex
defmodule Hello do
  @moduledoc """
  Documentation for `Hello`.
  """

  @doc """
  Hello world.
  """
  def main(args \\ []) do
    IO.puts "Hello world!"
  end
end

そして、mix.exsの project()に 属性escriptを追加し、コマンド・エントリーポイントのモジュール"Hello"を指定する。

mix.exs(抜粋)
  def project do
    [
      app: :hello,
      version: "0.1.0",
      elixir: "~> 1.10",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      escript: escript()
    ]
  end

  defp escript do
    app_name = "hello"
    [
      main_module: Hello,
    ]
  end

ここまでが、普通の escriptを作成するための設定だ。

ここからひと捻りして、Windowsのバッチファイルが作られるように改造しよう。
属性escriptには、main_moduleの他に shebangの文字列(:shebang)や escriptファイル名(:path)を指定できるサブ属性がある。これらを利用するのだ。

サブ属性pathは、作成されるescriptファイル名が "hello.bat"となるように設定する。

一方、サブ属性shebangには下記の文字列を設定する。これが手品の種だ。
"%~f0"の部分はバッチファイルのコマンド引数表記で、自分自身のバッチファイル名の絶対PATHに置き換わる表記だ。したがって本例では、Windowsのコマンドプロンプトによって「#! escript "C:\home\Elixir\hello\hello.bat"」と解釈され、そして実行されるのだ。その通り、コマンド "#!" に引数 "escript"と "C:\home\Elixir\hello\hello.bat"を与えて実行せよだ

shebang: "#! escript \"%~f0\"\n"

そして、もう一つの手品の種がこれ。中身が下記で名前が "#!"のバッチファイルを作成し、PATHが通ったディレクトリに置いておく。

#!.bat
@echo off
%*

結果、mix escript.buildで作成された hello.batを実行すると、下記の様にバッチファイルの連鎖が起こり、無事escriptが実行されるのだ。めでたしめでたし。

hello.bat
=> #! escript "C:\home\Elixir\hello\hello.bat"
=> escript "C:\home\Elixir\hello\hello.bat"

mix.exsの修正箇所をまとめると下記の通り。

mix.exs(修正)
  defp escript do
    app_name = "hello"
    [
      main_module: Hello,
      shebang: "#! escript \"%~f0\"\n",
      path: "#{app_name}.bat"
    ]
  end

では、試しにやってみる。
ほぼ期待通りだ。途中で echo出力(下から2行目)が出てしまうのはご愛嬌かな

C:\home\Elixir\hello>mix escript.build
Compiling 1 file (.ex)
warning: variable "args" is unused (if the variable is not meant to be used, prefix it with an underscore)
  lib/hello.ex:15: Hello.main/1

Generated hello app
Generated escript hello.bat with MIX_ENV=dev

C:\home\shozo\Elixir\hello>hello.bat

C:\home\shozo\Elixir\hello>#! escript "C:\home\shozo\Elixir\hello\hello.bat"
Hello world!

4.エピローグ

「あきらめない、あきらめない、あきらめない ‥‥」

Windowsでも気楽に escriptを書いてみようかなという気持ちが湧いてきた。
ちょっぴり幸せになったかも