格式化輸出io:format是我接觸Erlang使用的第一個庫函數(io:format("Hello World")),隨着學習的深入,它也是我debug優先選擇最簡單直接的工具。
不過它除了簡單的輸出外,還有很多進階用法。甚至通過它,你就可以在命令行畫出精艷的圖表。比如:我在Visualize Erlang/Elixir Nodes On The Command Line observer_cli中繪制的與htop類似圖表。

同時這個API的選項特別多,卻又非常好用,你完全可以不必了解這些選項(默認值)完成一些簡單的需求,也可以使用選項來定制復雜的需求,設計者在擴展性強和易用性間的平衡做得非常到位,這也給我們自己設計API提供了一種參考。
API接口說明
format(Format) -> format(Format,[]).
format(Format, Data) -> format(group_leader(),Format,Data).
format(IoDevice, Format, Data) -> ok
IoDevice = device()
Format = format()
Data = [term()]
device()
I/O驅動,可以是標准的standard_io, standard_error,也可以是一個使用file:open/2打開處理I/O協議的pid(或register name),比如:
%% 在當前目錄下test.txt文件(沒有則創建)中寫入二進制的<<"good">>
{ok, IoDevice} = file:open("test.txt", [write,binary]),
io:format(IoDevice, <<"good">>, []),
ok = file:close(IoDevice).
format()
可以是atom, string, binary,但最終都會使用(atom_to_list/1,binary_to_list/1)轉化為list,所以最佳實踐是直接使用list。Format是要和Data搭配使用的,比如最常見的:
> io:format("this is a ~s from ~w~n", ["hello world", erlang]).
this is a hello world from erlang
就是把”hello world”,erlang依次填充到對應的占位符。接下來系統的了解一下這些占位符。
Format通常格式為:
~F.P.PadModC
其中F,P,PadMod都是可以缺省(采用默認值),所以靈活性非常高!
FField Width 輸出內容的總寬度,如果是負數,則左對齊;正數則右對齊;缺省(不指定)就是使用輸出內容的實際長度。如果指定的長度小於實際長度,整個輸出內容就使用*代替。比如:
io:format("|~w|~n", [1234567890]). %% 缺省輸出實際長度
|1234567890|
io:format("|~-20w|~n", [1234567890]). %% 負數為左對齊
|1234567890 |
io:format("|~20w|~n", [1234567890]). %% 正常為右對齊
| 1234567890|
io:format("|~9w|~n", [1234567890]). %% 指定寬度小於實際寬度為*
|*********|
PPrecision 精度,默認值為不指定,精度和輸出內容的控制符(C)密切有關。比如:
io:format("|~.1f|~n", [1234567890.123]). %%浮點數: 小數個數>=精度時四舍五入小數位個數
|1234567890.1|
io:format("|~.10f|~n", [1234567890.123]). %%浮點數: 小數個數<精度時補全小數個數
|1234567890.1229999065|
io:format("|~.1s|~n", ["abcd”]). %%字符串長度>=精度截斷字符串的長度。
|a|
io:format("|~.10s|~n", ["abcd"]). %%字符串長度<精度補全字符串的長度(默認使用空填充)。
|abcd |
PadPadding 填充字符,這個是用來填充F和P不夠位數時的填充內容,默認為空‘ ’ ,有且僅能指定一個字符。
io:format("|~25.10.xs|~n", ["abcd"]). %% 把P和F都填充為了x
|xxxxxxxxxxxxxxxabcdxxxxxx|
io:format("|~25.10.Af|~n", [1234567890.123]). %% 把P填充為了字符A
|AAAA1234567890.1229999065|
ModModifier 修飾詞,只能為一個字符(t為翻譯Unicode友好輸出,l是禁止p和P轉義可打印字符輸出內容,使用最原始的格式輸出。
io:format("~ts~n”, ["中國"]). %% unicode轉義
中國
io:format("~p~n”, ["中國"]). %% 原始輸出
[20013,22269]
io:format("~p~n”, [[65]]). %% 轉義ASCILL
"A"
io:format("~lp~n”, [[65]]). %% 原始輸出
[65]
CControl Sequences 控制序列,根據Data的類型,可用的C有以下幾種。

-
特殊字符
~ 波浪號: 因為~是format里面的轉義,所以當需要真正輸出~需要轉義。
n 換行: 這個不需要解釋,新啟一行。
i 忽略: ignore,忽略下一個參數。io:format("|~i|~n", [good]).輸出為||。 -
ASCILL code
~c,輸出字符只能小於225,當為unicode時使用~tc。精度為把字符重復輸出多少位。io:format("|~10.5c|~-10.5c|~5c|~n", [$a, $b, $c]). %% 不指定精度時,默認與F同精度。 | aaaaa|bbbbb |ccccc| io:format(“~tc~n",[1024]). \x{400} io:format(“~c~n",[1024]). ^@ -
浮點數
無Mod參數(無指定修飾符),為~F.PfF為總寬度,P為小數點位,默認P為6,且>=2io:format("~f~n", [10.1234567]). %%默認小數點6位且四舍五入 10.123457如果你只想規定小數位個數,而不限制總長度時(不限整數位),可以使用
io:format("~.2f~n", [10.1234567]). 10.12 -
科學記數法
無Mod(無指定修飾符),為~F.PeF為總寬度,P為小數點位,默認P為6,且>=2
特點注意的是P是小數點的位數+1(有一位為e占領了)io:format(“~e~n”, [10.1234567]). 1.01235e+1 -
浮點數與科學記數結合體
~g如果0.1=<Data<10000.0時使用f輸出,否則使用e輸出。io:format("|~22.4g|~n", [102222.1234567]). | 1.022e+5| -
字符串輸出
~s默認為精度就是實際寬度,~ts為unicode轉義輸出,與其它控制符不同的是,如果寬度超過指定的精度或寬度,不會輸出*,只會截斷字符串。io:format("|~8.5.as|~n", ["1234567890"]). %% 截斷為5位,總長為8位,使用a填充不足的3位 |aaa12345|如果使用
~s去轉義>255的字符,會報錯。需要指定為~ts,所以如果不能明確范圍,統一使用~tsio:format("~s~n",[[1024]]). ** exception error: bad argument in function io:format/3 called as io:format(<0.53.0>,"~s~n",[[1024]]) io:format("~ts~n",[[1024]]). Ѐ -
Erlang任意term()
-
~w使用標准語法輸出Erlang的term(),Atom如果有不可打印字符會加上單引號‘’,如果Atom字符大於255,會直接輸出,友好輸出請加上~tw,浮點數會輸出實際最短(可能會四舍五入),``` io:format("~w~n”, ['Ѐ']). '\x{400}' io:format("~tw~n”, ['Ѐ']). 'Ѐ' ``` -
~p同~w一樣,但是更強大,會使用多行輸出字符串,不支持左對齊,會嘗試把可打印的字符都轉成string
單行最大寬度默認為80,精度確定了最初始(第一行)的寬度。1> T = [{attributes,[[{id,age,1.50000},{mode,explicit}, {typename,"INTEGER"}], [{id,cho},{mode,explicit},{typename,'Cho'}]]}, {typename,'Person'},{tag,{'PRIVATE',3}},{mode,implicit}]. ... 2> io:format(“~w~n", [T]). [{attributes,[[{id,age,1.5},{mode,explicit},{typename, [73,78,84,69,71,69,82]}],[{id,cho},{mode,explicit},{typena me,'Cho'}]]},{typename,'Person'},{tag,{'PRIVATE',3}},{mode ,implicit}] ok 3> io:format(“~62p~n", [T]). [{attributes,[[{id,age,1.5}, {mode,explicit}, {typename,"INTEGER"}], [{id,cho},{mode,explicit},{typename,'Cho'}]]}, {typename,'Person'}, {tag,{'PRIVATE',3}}, {mode,implicit}] 4> io:format(“Here T = ~64p~n", [T]). Here T = [{attributes,[[{id,age,1.5}, {mode,explicit}, {typename,"INTEGER"}], [{id,cho}, {mode,explicit}, {typename,'Cho'}]]}, {typename,'Person'}, {tag,{'PRIVATE',3}}, {mode,implicit}] ok 5> io:format(“Here T = ~64.10p~n", [T]). Here T = [{attributes,[[{id,age,1.5}, {mode,explicit}, {typename,"INTEGER"}], [{id,cho}, {mode,explicit}, {typename,'Cho'}]]}, {typename,'Person'}, {tag,{'PRIVATE',3}}, {mode,implicit}]如果使用
~lp,就不會嘗試把printable character轉化。6> S = [{a,"a"}, {b, "b"}]. 7> io:format(“~15p~n", [S]). [{a,"a"}, {b,"b"}] ok 8> io:format(“~15lp~n", [S]). [{a,[97]}, {b,[98]}] ok -
大寫
W和小寫的w一樣,不過可以加一個額外的參數指定最大的深度,超過了就只打印出…9> io:format("~W~n", [T,4]). [{attributes,[...]},{typename,...},{...}|...] -
大寫
P和小寫的p一樣,不過可以加一個額外的參數指定最大的深度,超過了就只打印出…10> io:format("~P~n", [T,9]). [{attributes,[[{id,age,1.5},{mode,explicit},{typename,...}], [{id,cho},{mode,...},{...}]]}, {typename,'Person'}, {tag,{'PRIVATE',3}}, {mode,implicit}]
- 按2-36進制輸出整數
-
~B默認為10進制,精度P指定幾進制。小寫的b就是使用小寫字母輸出>io:format(“~.16B~n", [31]). 1F >io:format(“~.2B~n", [-19]). -10011 >io:format(“~.36B~n”, [6*36+35]). 6Z -
~X大寫的X,和B一樣,不過可以加額外的參數。
小寫的x就是使用小寫字母輸出> io:format(“~X~n", [31,"10#"]). 10#31 > io:format(“~.16X~n", [-31,"0x"]). -0x1F -
~#和B一樣,但是可以輸出進制的base
~+就是使用小寫字母輸出> io:format(“~.10#~n", [31]). 10#31 > io:format(“~.16#~n", [-31]). -16#1F >io:format(“~.16+~n", [-31]). -16#1f
擴展閱讀
- Group Leader
一般調試時都是直接調用io:format(Format, Data),缺省了IoDevice為group_leader(),這在本地調試時是可以正常工作的,如果我們使用rpc來操作遠程節點,就分2種情況。比如:
rpc調用的函數中明明有運行了io:format,卻不能在遠程節點上輸出內容。因為rpc:call新創建的進程的group_leader()為使用rpc:call的節點,所以打印內容會顯示在本節點上。
如果想在遠程節點打印,可以指定IoDevice為一特殊的進程user。
rpc:call(`remote@ip`, io, format, ["test~p~n", erlang:time()]). %%在本節點打印
rpc:call(`remote@ip`, io, format, [user, "test~p~n", erlang:time()]). %%在遠程節點打印
- ANSI colors
在Erlang Shell中我們可以用ANSI colors來讓我們的內容更加好看。Elixir的shell就是這樣的,對此還有一個專門的模塊來處理ANSI(IO.ANSI), 但是在Erlang就需要我們自己來定義,也可以使用這個第三方庫(非常簡單)erlang-color - 如何清屏或把輸出內容的起點移到最上面的特殊字符。
io:format("\e[H\e[J"). %% 清屏,實現linux clear命令的效果
io:format(\e[H"). %% 把輸出內容的起點移動最上面開始寫,不會清除舊的輸出,但是會覆蓋。
-
輸出友好的時間格式最佳實現
lager最開始是使用io_lib:localtime_ms() -> {_, _, Micro} = Now = os:timestamp(), {Date, {Hours, Minutes, Seconds}} = calendar:now_to_local_time(Now), {Date, {Hours, Minutes, Seconds, Micro div 1000 rem 1000}}. format_time() -> format_time(localtime_ms()). format_time({{Y, M, D}, {H, Mi, S, Ms}}) -> {io_lib:format("~b-~2..0b-~2..0b", [Y, M, D]), io_lib:format("~2..0b:~2..0b:~2..0b.~3..0b", [H, Mi, S, Ms])}; format_time({{Y, M, D}, {H, Mi, S}}) -> {io_lib:format("~b-~2..0b-~2..0b", [Y, M, D]), io_lib:format("~2..0b:~2..0b:~2..0b", [H, Mi, S])}.因為這個函數調用的頻次非常高,但
io_lib的速度不是很滿意,所以就優化為了以下:format_time({utc, {{Y, M, D}, {H, Mi, S, Ms}}}) -> {[integer_to_list(Y), $-, i2l(M), $-, i2l(D)], [i2l(H), $:, i2l(Mi), $:, i2l(S), $., i3l(Ms), $ , $U, $T, $C]}; format_time({{Y, M, D}, {H, Mi, S, Ms}}) -> {[integer_to_list(Y), $-, i2l(M), $-, i2l(D)], [i2l(H), $:, i2l(Mi), $:, i2l(S), $., i3l(Ms)]}; format_time({utc, {{Y, M, D}, {H, Mi, S}}}) -> {[integer_to_list(Y), $-, i2l(M), $-, i2l(D)], [i2l(H), $:, i2l(Mi), $:, i2l(S), $ , $U, $T, $C]}; format_time({{Y, M, D}, {H, Mi, S}}) -> {[integer_to_list(Y), $-, i2l(M), $-, i2l(D)], [i2l(H), $:, i2l(Mi), $:, i2l(S)]}. i2l(I) when I < 10 -> [$0, $0+I]; i2l(I) -> integer_to_list(I). i3l(I) when I < 100 -> [$0 | i2l(I)]; i3l(I) -> integer_to_list(I). -
幾個很有用的輔助
Marco類型 作用 ?MODULE 當前模塊 ?LINE 當前文件行數 ?FUNCTION 當前運行函數 ??VAR 當前輸出的變量名字 比如在module中定義:
-ifndef(PRINT). -define(PRINT(Var), io:format("DEBUG: ~p:~p - ~p=~p~n~n", [?MODULE, ?LINE, ??Var, Var])). -endif. MyValue = test_value, ?PRINT(MyValue). %%調用 DEBUG: ModuleName:FileLine - MyValue=test_value
所有形式的知識最終意味著自我的認知。--李小龍
