Elixir 工具篇
原理篇
学习一门新的语言之前,有必要先了解下它提供的工具。通过这些工具我们才能编译和运行代码,这俗话说的好,纸上得来终觉浅,绝知此事,它要躬行呐!实践永远是学习的不二法门。
相比于 erlang,elixir 提供的工具要更加现代化。安装完 elixir 之后,你会得到四个工具,它们都在 elixir 安装目录的 bin
目录下,分别是 elixir
, elixirc
, iex
和 mix
。打开看一下你就会发现它们其实都是脚本。没错,都是脚本!
这些文件中,不带扩展名的是 Linux 脚本, .bat
Windows 命令行脚本, .ps1
是 Windows power shell 脚本。因为在 Windows 环境,我们以 bat 脚本为离说明,功能都是一样的。
从易到难,我们先看 mix.bat
。它的内容如下:
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
call "%~dp0\elixir.bat" "%~dp0\mix" %*
第一行其实相当于 @echo off
用来关闭 bat 脚本的命令回显,属于写 bat 脚本的基操,只不过这里可以通过 ELIXIR_CLI_ECHO
环境变量开启命令回显。
第二行才是核心, %dp0
是命令所在目录,也就是 mix.bat
所在目录,所以它其实是在调用 elixir.bat
脚本,并且给了两个参数。实际上, iex
和 elixirc
都只是在调用 elixir.bat
脚本而已,只不过大家传的参数不一样。
接下来我们来看看 iex.bat
的内容:
@echo off
setlocal
if /I ""%1""==""--help"" goto documentation
if /I ""%1""==""-h"" goto documentation
if /I ""%1""==""/h"" goto documentation
if ""%1""==""/?"" goto documentation
goto run
:documentation
echo Usage: %~nx0 [options] [.exs file] [data]
echo.
echo The following options are exclusive to IEx:
echo.
echo --dbg pry Sets the backend for Kernel.dbg/2 to IEx.pry/0
echo --dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environment.
echo Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists.
echo If FILE is empty, then no file will be loaded.
echo --remsh NAME Connects to a node using a remote shell
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo.
echo Set the IEX_WITH_WERL environment variable to always use werl.
echo It accepts all other options listed by "elixir --help".
goto end
:run
if defined IEX_WITH_WERL (set __ELIXIR_IEX_FLAGS=--werl) else (set __ELIXIR_IEX_FLAGS=)
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %__ELIXIR_IEX_FLAGS% %*
:end
endlocal
虽然内容变多了一些,但是真正有用的还是只有 :run
和 :end
之间的两行, :documentation
和 :run
之间的是命名帮助文档。仔细看 :run
和 :end
之间的第二行代码就会发现,它实际上也是在调用 elixir.bat
脚本,但是这次参数比较多,注意倒数第二个参数 +iex %__ELIXIR_IEX_FLAGS%
, %__ELIXIR_IEX_FLAGS%
的值来自于上一行代码,根据 IEX_WITH_WERL
环境变量的设置与否,它要么是 --werl
,要么是空。
那么 --werl
参数是干嘛的呢?它其实和 erlang 有关,在 Windows 上安装 erlang 之后,我们有两种方式启动 erlang shell,一种是直接在命令行输入 erl
回车,一种是在开始菜单搜索 erlang。后一种打开的其实就是一个叫 werl.exe
的程序,这是 erlang 自带的交互式命令窗口。那么加了 --werl
参数之后,意思就是要打开 werl.exe
程序,而不是一个 Windows 命令行。其实这也很好理解,因为 elixir 程序最终还是编译成 beam 文件了,要运行 elixir 程序,我们就需要 erlang 虚拟机,还是回到 erlang 上了。后面我们会再次证实这一点。
如果你在开始菜单中搜索 elixir,也会找到一个程序,但实际上它只是一个快捷方式。右键选择“打开文件位置”,你会在 C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Elixir
目录下看到一个叫 Elixir 的快捷方式,右键选择“属性”打开属性窗口,在“快捷方式”选项卡找到“目标”栏,可以看到它指向的程序其实就是 werl.exe
。当然这个快捷方式在后续版本中也被删除了,1.14还有,升级到1.17就没有了。
"D:\program\Erlang OTP\bin\werl.exe" -env ERL_LIBS "D:\program\Elixir\lib" -user Elixir.IEx.CLI -extra --no-halt
看到这里,熟悉 erlang 的话,应该会露出会心一笑。
好了,我们继续来看 elixirc.bat
脚本,它的内容如下:
@echo off
setlocal
set argc=0
for %%A in (%*) do (
if /I "%%A"=="--help" goto documentation
if /I "%%A"=="-h" goto documentation
if /I "%%A"=="/h" goto documentation
if "%%A"=="/?" goto documentation
set /A argc+=1
)
if %argc%==0 goto documentation
goto run
:documentation
echo Usage: %~nx0 [elixir switches] [compiler switches] [.ex files]
echo.
echo -h, --help Prints this message and exits
echo -o The directory to output compiled files
echo -v, --version Prints Elixir version and exits (standalone)
echo.
echo --ignore-module-conflict Does not emit warnings if a module was previously defined
echo --no-debug-info Does not attach debug info to compiled modules
echo --no-docs Does not attach documentation to compiled modules
echo --profile time Profile the time to compile modules
echo --verbose Prints compilation status
echo --warnings-as-errors Treats warnings as errors and returns non-zero exit status
echo.
echo ** Options given after -- are passed down to the executed code
echo ** Options can be passed to the Erlang runtime using ELIXIR_ERL_OPTIONS
echo ** Options can be passed to the Erlang compiler using ERL_COMPILER_OPTIONS
goto end
:run
call "%~dp0\elixir.bat" +elixirc %*
:end
endlocal
elixirc
是 elixir 的编译工具,它更简单,真正有用的只有一行,就是 :run
和 :end
之间的那行,
上面都是命令的帮助文档。不出意外,它依然是在调用 elixir.bat
脚本。
最后让我们来看看重头戏 elixir.bat
脚本,它的内容如下,由于内容比较多我们省略了帮助文档部分。
@echo off
set ELIXIR_VERSION=1.17.3
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
if /I ""%1""==""-h"" if ""%2""=="""" goto documentation
if /I ""%1""==""/h"" if ""%2""=="""" goto documentation
if ""%1""==""/?"" if ""%2""=="""" goto documentation
if /I ""%1""==""--short-version"" if ""%2""=="""" goto shortversion
goto parseopts
:documentation
... ...
goto end
:shortversion
echo %ELIXIR_VERSION%
goto end
:parseopts
setlocal enabledelayedexpansion
rem Parameters for Erlang
set parsErlang=
rem Optional parameters before the "-extra" parameter
set beforeExtra=
rem Option which determines whether the loop is over
set endLoop=0
rem Designates the path to the current script
set SCRIPT_PATH=%~dp0
rem Designates the path to the ERTS system
set ERTS_BIN=
set ERTS_BIN=!ERTS_BIN!
rem Recursive loop called for each parameter that parses the cmd line parameters
:startloop
set "par=%~1"
if "!par!"=="" (
rem skip if no parameter
goto run
)
shift
set par="!par:"=\"!"
rem ******* EXECUTION OPTIONS **********************
if !par!=="--werl" (set useWerl=1 && goto startloop)
if !par!=="+iex" (set useIEx=1 && goto startloop)
if !par!=="+elixirc" (goto startloop)
rem ******* ELIXIR PARAMETERS **********************
if ""==!par:-e=! (shift && goto startloop)
if ""==!par:--eval=! (shift && goto startloop)
if ""==!par:--rpc-eval=! (shift && shift && goto startloop)
if ""==!par:-r=! (shift && goto startloop)
if ""==!par:-pr=! (shift && goto startloop)
if ""==!par:-pa=! (shift && goto startloop)
if ""==!par:-pz=! (shift && goto startloop)
if ""==!par:-v=! (goto startloop)
if ""==!par:--version=! (goto startloop)
if ""==!par:--no-halt=! (goto startloop)
if ""==!par:--remsh=! (shift && goto startloop)
if ""==!par:--dot-iex=! (shift && goto startloop)
if ""==!par:--dbg=! (shift && goto startloop)
rem ******* ERLANG PARAMETERS **********************
if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot "%~1"" && shift && goto startloop)
if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var "%~1" "%~2"" && shift && shift && goto startloop)
if ""==!par:--cookie=! (set "parsErlang=!parsErlang! -setcookie "%~1"" && shift && goto startloop)
if ""==!par:--hidden=! (set "parsErlang=!parsErlang! -hidden" && goto startloop)
if ""==!par:--erl-config=! (set "parsErlang=!parsErlang! -config "%~1"" && shift && goto startloop)
if ""==!par:--logger-otp-reports=! (set "parsErlang=!parsErlang! -logger handle_otp_reports %1" && shift && goto startloop)
if ""==!par:--logger-sasl-reports=! (set "parsErlang=!parsErlang! -logger handle_sasl_reports %1" && shift && goto startloop)
if ""==!par:--name=! (set "parsErlang=!parsErlang! -name "%~1"" && shift && goto startloop)
if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname "%~1"" && shift && goto startloop)
if ""==!par:--vm-args=! (set "parsErlang=!parsErlang! -args_file "%~1"" && shift && goto startloop)
if ""==!par:--erl=! (set "beforeExtra=!beforeExtra! %~1" && shift && goto startloop)
if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on Windows && goto end)
:run
setlocal disabledelayedexpansion
reg query HKCU\Console /v VirtualTerminalLevel 2>nul | findstr /e "0x1" >nul 2>nul
if %errorlevel% == 0 (
set beforeExtra=-elixir ansi_enabled true %beforeExtra%
)
if not defined useIEx (
set beforeExtra=-s elixir start_cli %beforeExtra%
)
set beforeExtra=-noshell -elixir_root "%SCRIPT_PATH%..\lib" -pa "%SCRIPT_PATH%..\lib\elixir\ebin" %beforeExtra%
if defined ELIXIR_CLI_DRY_RUN (
if defined useWerl (
echo start "" "%ERTS_BIN%werl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
) else (
echo "%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
)
) else (
if defined useWerl (
start "" "%ERTS_BIN%werl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
) else (
"%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
)
)
exit /B %ERRORLEVEL%
:end
endlocal
虽然内容比较长,但是能看出来三大部分:文档,参数处理和命令调用。文档就不说了,参数稍后再看,命令调用也是两行代码,要么调用 werl.exe
要么调用 erl.exe
,最终我们还是回到了 erlang。
:startloop
和 :run
之间循环处理传递给脚本的每个参数, shift
指令会将参数列表整体左移一位。当然它也可以使用 shift \n
左移多位。此外 set par="!par:"=\"!"
会将参数中的 "
全部替换成 \"
,也就是会加个转义。
这里的逻辑也并不复杂,只是在做参数过滤和拼接。如果你感兴趣的话,可以配置一个 ELIXIR_CLI_DRY_RUN
环境变量,然后 elixir 就会打印出最终的命令而不会执行。比如我们可以打开一个 cmd 窗口,然后 set 一个临时环境变量来查看效果:
在运行 erl.exe
或 werl.exe
时,通过 -pa
选项将 elixir 的库提前加载了,所以我们能使用 elixir 的库函数,熟悉 erlang 的朋友应该不会对此感到陌生。原理讲完了,接下来介绍下这些命令怎么使用。
使用篇
mix
首先我们看 mix
命令,它是 elixir 的工程管理工具,包括工程的新建,编译,测试,打包等,而且还可以由用户去实现自定义的命令。在命令行运行 mix help
可以查看 mix
的全部命令列表,每个人打印出来的命令列表可能会不太一样,比如我安装了 phoenix,所以会由 phx.xxx
等命令。我们来看几个比较常用的。
mix new app_name
创建应用mix format
格式化代码mix deps.get
获取依赖mix test
运行测试mix compile
编译工程mix run
运行工程mix escript.build
编译成“可执行”文件
这里重点对最后两个命令做一点说明。首先 mix run
运行工程的话必须是在“应用模式”下。所谓工程其实可以简单理解为根目录下有 mix.exs
文件。那么“应用模式”又是什么呢?
这里的“应用”就是 erlang 中 application 的概念,或者理解为一个 OTP 应用。在 elixir 中我们需要做两件事:
-
在
mix.exs
文件中定义 application。def application do [mod: {MyApp, []}] end
-
实现
Application
行为以及它的start/2
回调。defmodule MyApp do use Application def start(_type, _args) do children = [] Supervisor.start_link(children, strategy: :one_for_one) end end
深入学习可以参考官方文档👉点击直达👈。
使用 mix run
运行应用时,应用运行起来后 mix 就退出了,有时候会来不及看到应用的结果,所以通常我们会加上 --no-halt
选项不让它退出: mix run --no-halt
。虽然 mix
本身默认就是执行的 mix run
任务,但是 mix --no-halt
是不行的。
除了运行应用 mix run
也能直接运行 elixir 表达式或 .exs
文件,如:
mix run -e "IO.puts(:hello)"
mix run hello.exs
-e
是 --eval
的简短形式。 mix
毕竟是工程管理命令,它会去检查 mix.exs
文件的存在,如果不存在,上面的命令就会报错,这时我们需要加上 --no-mix-exs
选项跳过检查。
mix run -e "IO.puts(:hello)" --no-mix-exs
mix run hello.exs --no-mix-exs
完整的文档可以通过 mix help run
查看。 mix help task
可以用来查看 mix 任务的帮助文档。
Elixir 代码编译之后是 .beam
文件,需要加载到 erlang 虚拟机中才能运行,不能像 go 那样直接编译成可执行文件。erlang 通过 escript 机制也能实现类似的效果,将代码编译成一个文件,然后通过 escript
来运行。 mix escript.build
命令就是将工程编译成这样的”可执行”文件。
那么代价是什么呢?还是两件事情:
-
编写一个包含
main/1
函数的模块。defmodule My.CLI do def main(argv) do IO.puts(argv) end end
-
在
mix.exs
的 project 配置中增加 escript 配置,配置main_module
为包含main/1
函数的模块。defmodule My.MixProject do use Mix.Project def project do [ app: :my, escript: escript_config(), version: "0.1.0", elixir: "~> 1.14", ] end ... defp escript_config do [ main_module: My.CLI ] end end
运行 mix escript.build
会编译出一个和工程同名的没有扩展名的文件,我们可以运行命令 escript xxx
来运行程序,命令行参数会通过 argv
传递给 main
函数。
当然在开发时,我们可能更常用的是使用 iex -S mix
在交互式命令行中来运行项目代码,如果你去查看 mix help
打印出来的命令列表就会发现,列表的最后一条命令就是 iex -S mix
。
iex
iex
是 elixir 的交互式解释器,我们可以在命令行直接输入 iex
进入,然后就可以输入 elixir 表达式查看结果了,在 iex
中可以使用 v
来引用上一个表达式的结果,或者 v()
也是可以的。
在 elixir 工程目录下,可以使用 iex -S mix
在进入 iex 之前编译项目并将其加载进虚拟机。如果是单独的 .ex
或 .exs
文件,也可以通过 iex xxx.ex
或 iex xxx.exs
将其加载进虚拟机。
在进入 iex 之后,也可以通过 c "xxx.ex"
或 c "xxx.exs"
将代码加载进虚拟机。
在代码更改之后,我们有两种方式去重新编译和加载代码:
recomplie
它会重新编译和加载整个 mix 工程。r ModuleName
或r [Module1, Module2]
它只会重新加载指定的模块。
iex 还有一些有用的函数,当然这些都是 erlang 提供的,谁叫他站在了巨人的肩膀上呢。
pwd
打印当前目录cd
切换目录ls
列出当前目录下的文件和目录
如果要在 iex 中查看帮助的话,可以使用 h(xxx)
。
elixirc
elixirc
是 elixir 的编译工具,用来编译 .ex
文件,生成 .beam
文件。但实际上它也能把 .exs
文件也能编译成 .beam
文件,如果 .exs
文件中有模块定义的话。如果 .exs
文件中都是一些命令,如 IO.puts("xxx")
,那么 elixirc xxx.exs
会直接运行整个脚本,不会编译出 .beam
文件。
elixir
直接使用 elixir
命令的话一般用来求值 elixir 表达式或者运行 .exs
脚本。
elixir -e "IO.puts(:hello)"
elixir xxx.exs
Elixir 源代码文件有两种扩展名, .ex
是源码文件,需要编译后运行, .exs
是脚本文件,可以直接解释运行。
总的来说,我们用到 mix
和 iex
的概率会大很多, elixirc
很少会直接使用它来编译, elixir
会在运行 elixir 脚本的情况下用到。