はじめに
この記事では、Table をダンプする自作モジュールの、使い方とコードの解説します。
またこの記事は、Minecraft の Mod である OpenComputers(以降 OC)について解説する記事の一貫として作ったものです(執筆中)。
Lua 初心者が作ったモジュールなので、間違いや非効率なことをしている可能性が高いです(OC で初めて Lua に触りました)
ソースコード
ここで紹介しているプログラムは、私の GitHub で公開しています。
MIT ライセンスにしてあるので、基本的に自由に使っていただいて大丈夫です。
ここで紹介しているモジュール(が入っているディレクトリ)
OC 用も含めたトップページ(OC 用はまだ何も無いです)
解説文中の 変数 / 関数 の型表記
解説文中の、変数や関数の型表記方法は次のように書きます。
- 変数:
変数名:型
- 関数:
関数名(引数名:型[, 任意引数名:型 ...]):戻り値1の型, 戻り値2の型 or nil ...
また、ソースコード中の説明文も同様になっています。
0. 目次
1. モジュールの概要
テーブルを tostring()
すると table: 01234567abc
のようになってしまうので、中身を文字列にする(ダンプする)モジュールを作りました。
循環参照でも大丈夫です。
モジュール名: table_dumper
(通常版) / tdumper_mini
(簡易版)
使い方:
local tdumper = require("table_dumper") -- 通常版
local td_mini = require("tdumper_mini") -- 簡易版
local tbl = { ... } -- テーブルの中身は省略
print(tdumper:dump(tbl, "tbl_name")) -- 通常版は :dump()
print(td_mini.dump(tbl, "namaemae")) -- 簡易版は .dump()
※必須モジュール
table_dumper
(通常版)では私の作った以下のモジューを使用しています(全て公開されています)。
-
lazy_importer
: モジュールの遅延読み込みをサポートするユーティリティ -
string_builder
: 文字列を組み立てやすくするヘルパーモジュール -
simple_logger
: ロガー未設定時に使われるprint()
するだけのロガー
tdumper_mini
(簡易版)では他のモジュールを使用していません。
ダンプした時の表示例
次のコードを実行すると、表示例
のようにダンプされます。
※ table_dumper
(通常版) を使用しています
----------------------------
-- ダンプするテーブルを定義
----------------------------
local tbl = {
[123] = 123,
["456"] = "456",
multi_line = [[
1行目
2行目
3行目]],
child1 = {
func = function() end
},
}
-- tbl.child1.inner に循環参照を追加
tbl.child1.inner = tbl.child1
-- tbl.child2 に共有参照を追加
tbl.child2 = tbl.child1
-- tbl に __tostring を設定
setmetatable(tbl, {
__tostring = function(_)
return "tbl の説明\nです!"
end,
})
------------------
-- ダンプする部分
------------------
-- モジュールを読み込み
local tdumper = require("table_dumper") -- 通常版を使用
-- テーブルをダンプして表示
print(tdumper:dump(tbl, "table")) -- 第2引数は表示上のテーブル名
table = {
123 = 123,
"456" = "456",
"child1" = {
"func" = function: 0000000000123abc,
"inner" = {
* 既に表示済み(循環参照) -> table."child1"
},
},
"child2" = {
* 既に表示済み(共有参照) -> table."child1"
},
"multi_line" = "1行目
2行目
3行目",
<tostring()> = "tbl の説明
です!"
}
※ 数字と文字列は、"" がついているかどうかで判別できます。
(OC 用の情報)
OC 環境で動かす場合の OC コンピュータの必要スペックは以下の通りです。
※ 検証バージョン -> GTNH: v2.7.4 / OC: v1.10.30-GTNH
--------------
table_dumper
(通常版)
- Graphics Card: Tier 1
- CPU: Tier 1
- Memory: Tier1 * 2 または Tier1.5 * 1
- HDD: Tier 1
--------------
tdumper_mini
(簡易版)
- Graphics Card: Tier 1
- CPU: Tier 1
- Memory: Tier1 * 1
- HDD: Tier 1
2. コードの解説
table_dumper
モジュールは色々な機能を詰め込み過ぎて読みにくくなってしまったので、軽量 & 簡易版の tdumper_mini
も作りました。
この項目では簡易版でコードの解説をして、次の項目で通常版に追加される機能の紹介をします。
全体のソースコードを見たい方は、ソースコードのリンクから見てください。
※ この解説はかなり冗長です。大したことはしていないので、慣れている方はコードだけ読めば分かると思います(コメントもたくさん付けておきました)
まず再帰呼び出しの前までの、コードの前半部分を解説します。
-- 結果を返すための補助関数
-- テーブル t1 に テーブル t2 の要素を追加する
local table_concat_array = function(t1, t2)
for i = 1, #t2 do table.insert(t1, t2[i]) end
end
-- 本体
local TDumperMini
TDumperMini = {
-- テーブルを表示する際の、1階層ごとにつけるインデント
indent_unit = " ",
-- テーブルの中身を再帰的に表示する(循環参照も OK)
-- tbl:table > 中身を表示したいテーブル
-- [tbl_name:string] > テーブル名(省略可)
-- :string > テーブルの中身を表した文字列(戻り値)
dump = function(tbl, tbl_name)
local result = {} -- 出力を溜める配列
-- tbl が table型では無かった時は、エラーメッセージを表示する
if type(tbl) ~= "table" then
table.insert(result, "関数: TDumperMini.dump() でエラーが発生しました")
table.insert(result, "引数: tbl(" .. tostring(tbl_name) .. ") は table型が必要です(受け取った型: " .. type(tbl) .. ")")
return table.concat(result, "\n")
end
-- トップレベルの表記
-- tbl_name が nil か空文字の時はテーブル名を "<top_table>" にする
if not tbl_name or tbl_name == "" then
tbl_name = "<top_table>"
end
tbl_name = tostring(tbl_name) -- tbl_name が string でなくても安全なように
TDumperMini.indent_unit = tostring(TDumperMini.indent_unit) -- indent_unit も同様
-- 出力
table.insert(result, tbl_name .. " = {")
-- 実際にテーブルをダンプする処理(再帰関数)
table_concat_array(result, TDumperMini._inner_dump(tbl, tbl_name, TDumperMini.indent_unit))
table.insert(result, "}") -- 最後にテーブルを閉じる
return table.concat(result, "\n")
end,
(省略)
}
最初に local TDumperMini
とだけ定義してあるのは、関数内で TDumperMini
の要素を使いたい時に、先に定義していないとエラーになるからです。
おまけ(エラーになる理由)
<無駄話>
私は Lua にあまり詳しくないので間違っているかもしれませんが、エラーになる原因は以下の通りだと思います(読み飛ばして OK)
-
local a = "b"
とした時は、まず"b"
が初期化されてからa
が初期化され、a
に"b"
が代入される -
local tbl = { ... }
とした時も同様に、{ ... }
が初期化されてからtbl
に代入される -
{ ... }
の中にfunc = function()
が定義されていたら、この初期化の段階で関数が作成 & 初期化される - 関数内で
tbl.xxx
と書かれていると、まだtbl
は代入されていないので、グローバル変数のtbl
を参照する - その後
tbl
に{ ... }
が代入されるが、関数内の参照はグローバル変数のまま
(「a = 1
b = a
の後にa = 2
としても、b
は1
のまま」※という現象に似ている) - よって、関数実行時に
nil.xxx
にアクセスしようとして、エラーになる
※ この「定義時の変数を参照する」という性質を利用して、関数A
の中で 関数B
を作って返すと、本来は 関数A
の中でしか使えないローカル変数を、関数B
がずっと保持できる
(この仕組みをクロージャという)
</無駄話>
---------------------------------
TDumperMini.indent_unit
は、テーブルの子要素を表示する時に、1階層ごとにつけるインデントを文字列で指定します。
空白以外も設定できるので、"@@"
を設定すればインデントを目立たせることもできます(見にくい)。
---------------------------------
TDumperMini.dump(tbl:table[, tbl_name:string]):string
ダンプする時に使う関数です。
第一引数にダンプしたいテーブルを、第二引数に最初のテーブル名として表示する名前を渡します。
第二引数を省略した場合は、"<top_table>"
になります。
table_dumper
では tdumper:dump()
と :
を使って呼び出すのに対し、tdumper_mini
では tdumper.dump()
と .
で呼び出すので注意
---------------------------------
local result = {}
この変数に、出力する文字列を table.insert()
で追加し、最後に table.concat()
で連結して戻り値にしています。
---------------------------------
引数 tbl
の型チェックや、tbl_name
の初期値を設定した後、
table_concat_array(result, TDumperMini._inner_dump(...))
としているところが、実際にテーブルをダンプしている部分です。
まず前半の table_concat_array()
は最初に定義してありますが、第一引数のテーブルに第二引数のテーブルの要素を追加する関数です。
つまり result
に _inner_dump()
で返ってきたテーブルの要素を追加しています。
TDumperMini._inner_dump()
は次のコードになります。
TDumperMini = {
-- 実際にテーブルをダンプする関数(再帰関数)
-- tbl:table > ダンプするテーブル
-- key_path:string > 親から辿ってきたキー名を、全て "." で繋いだ文字列
-- indent:string > 現在のインデント
-- [visited:table] > 既にダンプしたテーブルの一覧(循環参照検出時に使う)(省略可)
-- :table > テーブルをダンプした文字列の配列(戻り値)
_inner_dump = function(tbl, key_path, indent, visited)
visited = visited or {}
local result = {} -- 出力を溜める配列
-- 循環参照検出時はその先の検索を中止
if visited[tbl] then
table.insert(result, indent .. "* 既に表示済み -> " .. visited[tbl])
return result
end
-- tbl をダンプ済みテーブルとしてマークする
visited[tbl] = key_path
-- テーブルに含まれる要素をチェックし、値の型によって動作を分ける
for k, v in pairs(tbl) do
local key_str = tostring(k) -- キー名を結合できるように string にする
-- キーが string の時は "" で囲む
if type(k) == "string" then
key_str = "\"" .. key_str .. "\""
end
local value_type = type(v)
if value_type == "table" then
-- 値がテーブルの時は、再帰的に子要素を検索
table.insert(result, indent .. key_str .. " = {")
-- 再帰的呼び出し
local new_key_path = key_path .. "." .. key_str
local new_indent = indent .. TDumperMini.indent_unit
table_concat_array(result, TDumperMini._inner_dump(v, new_key_path, new_indent, visited))
table.insert(result, indent .. "},")
elseif value_type == "string" then
-- 値が string の時は "" で囲んで出力
table.insert(result, indent .. key_str .. " = \"" .. v .. "\",")
else
-- 値がそれ以外の時は、そのまま string に変換して出力
table.insert(result, indent .. key_str .. " = " .. tostring(v) .. ",")
end
end
-- 最後に __tostring があるものは、それを追記する
local mt = getmetatable(tbl)
if mt and type(mt.__tostring) == "function" then -- メタテーブルに __tostring が設定されている場合
-- 出力
table.insert(result, indent .. "<tostring()> = \"" .. tostring(tbl) .. "\"")
end
return result
end,
}
まず関数の定義は、
_inner_dump(tbl:table, key_path:string, indent:string, visited:table):table
です。それぞれの引数は、
-
tbl
: ダンプする対象のテーブル -
key_path
: 親から辿ってきたキー名を、全て"."
で繋いだ文字列 -
indent
: 現在のインデントの文字列(階層が深くなるとインデントが増えます) -
visited
: キーが既にダンプした(ダンプ中も含めて)テーブルで、値がそのテーブルのkey_path
となっているテーブル(循環参照検出に使います)
この関数を再帰的に呼び出して、テーブルをダンプしていきます。
最初に dump()
から呼び出される時の引数は、
-
tbl
: dump() に渡されたテーブル -
key_path
: dump() に渡されたテーブル名(または"<top_table>"
) -
indent
:TDumperMini.indent_unit
(最初の子要素はインデント1つ分) -
visited
: 省略(nil
)
関数の中身を順番に解説します。
循環参照を検出する部分
-- 循環参照検出時はその先の検索を中止
if visited[tbl] then
table.insert(result, indent .. "* 既に表示済み -> " .. visited[tbl])
return result
end
visited[tbl]
が文字列(nil
ではない)の場合は既にダンプ済みのテーブルなので、メッセージを残してこのテーブルのダンプを終了します。
tdumper_mini
では、循環参照と共有参照の判別ができないので、"* 既に表示済み"
とだけ書きます。
visited[tbl]
はそのテーブルをダンプした時の key_path
が入っているので、どのテーブルへの循環参照があったのかが分かります。
---------------------------------
循環参照の検出が終わったら、確実にダンプを実行するので、visited[tbl] = key_path
を先に書いておきます。
実際にダンプする前に書くことで、子要素を検索中に自分を参照するテーブルがあった場合でも、循環参照を検出できます(というか循環参照の場合は必ずこのパターン)。
---------------------------------
その後は for
文でテーブルの要素を順番にダンプしていきます。
要素の値の型によってダンプする処理が違いますが、テーブル以外については文字列を連結して result
に追加してるだけなので、すぐ分かると思います。
※ tdumper_mini
では、改行を含む文字列に、改行後にインデントを挿入していませんが、文字列をコピーしたい時はこちらの方が便利です
テーブルの場合は、
table_concat_array(result, TDumperMini._inner_dump(...))
で再帰的に _inner_dump()
を呼び出して、子テーブルを全てダンプしていきます。
_inner_dump()
に渡す引数は以下の通りです。
-
tbl
: 子テーブル -
key_path
: 自分のkey_path
に、"."
と子テーブルのキー名を連結した文字列 -
indent
: 現在のインデントにTDumperMini.indent_unit
を連結した文字列(インデントを 1つ増やす) -
visited
: 自分のkey_path
を登録済みの、自分のvisited
(コピーなどはしないので、全員が同じテーブルを参照することになります)
これで子テーブルにも同じ処理が繰り返され、戻ってきた値を result
に追加します。
---------------------------------
最後に、メタテーブルに __tostring
が設定されている場合は、有益な情報が入っているかもしれないので、これも一緒に表示します。
メタテーブルに __tostring
が設定されているかどうかを判別するには、
mt = getmetatable(tbl)
でメタテーブルを取得した後、mt.__tostring
の型が "function"
かどうかで判定できます(mt
の nil
チェックも忘れずに)。
---------------------------------
_inner_dump()
の最後で、要素をダンプした文字列を順番に追加した配列 result
を返します。
返された値は、table_concat_array()
で親の要素が入った、親の result
に追加され、その後親のダンプ処理が再開します。
---------------------------------
そして、_inner_dump()
の処理が全て終わると、呼び出し元の dump()
に戻り、最後の
return table.concat(result, "\n")
で result
の要素を最初から順番に、文字列の間を "\n"
で繋いで 1つの文字列にします。
それが dump()
の戻り値になります。
3. 通常版の追加機能
※ 以下の定義をしたものとして解説します
local tdumper = require("table_dumper")
local td_mini = require("tdumper_mini")
追加機能概要
table_dumper
(通常版) は tdumper_mini
(簡易版) と比べて、多数のオプションで動作を切り替えられます。
また、オプション以外の違いとしては、以下の機能があります。
- 循環参照と共有参照を判別して出力する
(tdumper_mini
では「* 既に表示済み -> <キーのパス>」とだけ表示されます) -
print(tdumper)
とすると、モジュールで使用できるオプションと関数の説明を表示する - ロガーを使ってエラー出力、デバッグ出力をする
-
tdumper.new()
を使用してオブジェクトを作り、複数の設定を使い分けられる-
tdumper.new()
をしないで使うこともできます
-
table_dumper
では tdumper:dump()
と :
を使って呼び出すのに対し、tdumper_mini
では td_mini.dump()
と .
で呼び出すので注意
設定できるオプションと簡単な説明は以下の通りです。
-
logger:table
: エラー & デバッグログを出力するロガー。初期値はprint()
するだけのロガー -
verbose_level:number
: デバッグログの出力レベル。0 でログなし、1 で通常、2 で詳細なログを出力 -
indent_unit:string
: 子テーブル 1階層ごとに付けるインデント。tdumper_mini
にも同じオプションがあります -
insert_indent:boolean
: 改行を含む文字列を表示する時に、改行後にインデントを挿入するか
(tdumper_mini
ではインデントは挿入されません) -
insert_indent_tostring:boolean or 0
:tostring()
を表示する時に改行を含む場合、改行後にインデントを挿入するか
(tdumper_mini
ではインデントは挿入されません) -
max_depth:number
: ネストした子テーブルを表示する時の最大深度 -
max_items_per_table:number
: テーブルの要素を表示する時の最大要素数 -
show_tostring:boolean
: テーブルのtostring()
の値を表示するか
(tdumper_mini
では常に表示されます) -
show_metatable:boolean
: テーブルの各メタテーブルが設定済みかどうかだけ表示するフラグ -
ignore_key_types:table or nil
: 表示をスキップするキーの型リスト。{ function = true }
の様に書く -
ignore_value_types:table or nil
: 表示をスキップする値の型リスト。{ function = true }
の様に書く -
filter:function or nil
: 表示する要素のみtrue
を返す関数 -
post_filter:function or nil
: 表示する要素のみtrue
を返す関数(ダンプ後出力前に呼ばれます)
※dump()
の最後にも呼ばれます -
comparator:function or nil
: テーブルとメタテーブルをキーでソートする時に用いる関数
(tdumper_mini
ではソートしません) -
value_comparator:function or nil
: テーブルを値でソートする時に用いる関数
(tdumper_mini
ではソートしません) -
top_table_name:string
: テーブル名を省略した場合に、代わりに表示される名前(循環参照検出時のみ) -
strict_mode:boolean
:true
にすると、エラーログを出力する代わりにエラーを投げる -
on_value_dumped:function or nil
: 要素がダンプされる直前に呼ばれるフック関数
※dump()
の最後にも呼ばれます -
key_formatter:function or nil
: キーの表示を変更する関数
※dump()
に渡されたテーブル名に対しては呼ばれません -
value_formatter:function or nil
: 値の表示を変更する関数
※dump()
に渡されたテーブルをダンプした結果に対しても呼ばれます
詳しい解説
分かりにくそうなものだけ解説します。
まずはオプション以外のものについて。
- ロガーは
logger.error(meg:string)
logger.debug(msg:string)
でログ出力できるものを想定しています- お使いのロガーと関数名などが合わない場合は、ラッパーを作るなどして対応してください
- 最初に
tdumper.<オプション>
にアクセスすると、自動的にdefault_dumper
にnew()
が代入され、以降tdumper
経由でアクセスできるようになります
※ いきなりtdumper:dump()
としても大丈夫です -
tdumper.new()
をすると、「その時点でのデフォルト値(tdumper.<オプション>
の値)がオプションに設定されたオブジェクト」が戻り値になります- その後に
tdumper
の値を変更しても、それ以前にnew()
されたオブジェクトには影響がありません - ただし
default_dumper
だけはtdumper
の変更が反映されます(というかtdumper
の実体です) -
new()
の引数には、logger
とverbose_level
を指定できますが、省略可能です
- その後に
次にオプションについて。
-
logger
にnil
が設定されていると、dump()
実行時にオブジェクトのlogger
にデフォルトロガーが代入されます -
insert_indent_tostring
に0
が設定されていると、dump()
実行時のinsert_indent
の値を代わりに使用します(上書きはしません) -
max_depth
やmax_items_per_table
に-1
が設定されていると、無制限に表示します -
show_metatable
がtrue
の場合、以下のように表示されます(メタテーブルが何も設定されていない場合は、何も表示されません)※table = { <metatable> = { __index : 設定済み __tostring : 設定済み } }
<tostring()> = ""
の後と<metatable> = {}
の後に","
が無いのは仕様です(データではないため) -
filter
には、function(key:全ての型, value:全ての型):boolean
の形式の関数を設定します-
true
が返された要素のみ表示します -
nil
を設定した場合はフィルタリングをスキップします(全て表示)
-
-
post_filter
にはfunction(key:全ての型, value:全ての型, key_str:string, value_str:string):boolean
の形式の関数を設定します- 要素のダンプが終わった後、最終的に出力に含めるかを決定します
-
key
とvalue
は生のキーと値、key_str
とvalue_str
は表示予定のキーと値の文字列が渡されます -
true
が返された要素のみ表示します -
nil
を設定した場合はフィルタリングをスキップします(全て表示)
-
comparator
はテーブルとメタテーブルをキーでソートする時に用いる関数です-
function(key_a:全ての型, key_b:全ての型):boolean
の形式でkey_a
<key_b
の時にtrue
を返す関数です - 値でソートされなかった時のみ使用されます
-
nil
を設定すると、ソートをスキップします(毎回ランダムな並び順になります)
-
-
value_comparator
はテーブルを値でソートする時に用いる関数です-
function(value_a:全ての型, value_b:全ての型):boolean or nil
の形式でvalue_a
<value_b
の時にtrue
を返す関数です - キーのソートより優先されます
-
nil
を返すとソートをキーに任せます - 関数の代わりに
nil
を設定すると、ソートをスキップします(毎回ランダムな並び順になります)
-
-
top_table_name
の値は循環参照時の" -> <top_table_name>.child.・・・"
と表示する時に使用されます-
on_value_dumped
オプションに設定する関数にも、キーのリストとしてtop_table_name
の値が渡されます -
table_dumper
とtdumper_mini
で、テーブル名を省略した時の表示は以下の通りですtable_dumper{ * 既に表示済み(循環参照) -> <top_table> }
tdumper_mini<top_table> = { * 既に表示済み -> <top_table> }
-
-
on_value_dumped
には、function(key:全ての型, value:全ての型, key_str:string, value_str:string, key_array:table)
の形式の関数を設定します-
key
とvalue
は生のキーと値、key_str
とvalue_str
は実際に出力するキーと値の文字列、key_array
は親から辿ってきたキー名が親から順に入った配列が渡されます - ここで値を変更しても、出力には影響しません
-
-
key_formatter
とvalue_formatter
にはfunction(key_or_value:全ての型, key_or_value_str:string):string
の形式の関数を設定します-
key_or_value
は生のキーまたは値、key_or_value_str
は表示予定のキーまたは値の文字列が渡されます - 戻り値で返した文字列が実際に出力されます
-
key_or_value_str
をそのまま返せば、変更せずに出力できます - 関数の代わりに
nil
を設定すると、変更をスキップします
-
オプションの初期値
初期状態で各オプションに設定されている値は、次の通りです
-
logger:table
:nil
(実行時にデフォルトロガーが代入されます) -
verbose_level:number
:0
-
indent_unit:string
:" "
-
insert_indent:boolean
:true
-
insert_indent_tostring:boolean or 0
:0
(実行時のinsert_indent
の値を参照します) -
max_depth:number
:-1
-
max_items_per_table:number
:-1
-
show_tostring:boolean
:true
-
show_metatable:boolean
:false
-
ignore_key_types:table or nil
:{}
-
ignore_value_types:table or nil
:{}
-
filter:function or nil
:nil
-
post_filter:function or nil
:nil
-
comparator:function or nil
: ソースコードの上の方に定義してあるコンパレータ -
value_comparator:function or nil
:nil
-
top_table_name:string
:"<top_table>"
-
strict_mode:boolean
:false
-
on_value_dumped:function or nil
:nil
-
key_formatter:function or nil
:nil
-
value_formatter:function or nil
:nil
(おまけ)関数を指定するオプションの設定例
関数を指定するオプションは、どのような関数を設定すればいいかが分かりにくいと思ったので、設定例を載せておきます。
項目内ジャンプ
filter & post_filter
特定の要素のみ表示したい場合は、filter
と post_filter
に以下のような関数を設定します。
ここではキー名が name
または age
の要素のみ表示するようにします。
ネストしたテーブル内に要素があるかもしれないので、filter
は table型も OK にします。
まずは filter
のコード:
tdumper.filter = function(key, value)
return key == "name" or
key == "age" or
type(value) == "table"
end
このままでは、テーブルに name
や age
が入っていない場合、空のテーブルが表示されてしまうので、空のテーブルは表示しないように post_filter
で設定します。
post_filter
のコード:
tdumper.post_filter = function(key, value, key_str, value_str)
if type(value) == "table" then
local _, count = value_str:gsub("\n", "")
return count > 1 -- 改行が 2回以上なら要素がある
end
return true -- 他は全て表示
end
これで name
や age
が入っているテーブルのみ表示されます。
ちなみに、dump()
に渡したテーブルの表示がスキップされた場合は、次のように表示されます。
<テーブル名> -> post_filter によりスキップされました
comparator
付属のコンパレータでは、「数字に変換できるなら数字として判定」→「できなければ文字列で判定」→ ・・・ のように判定しています。
これだと、 "33. いちごミルク"
と "4. コーヒー牛乳"
は文字列で判定してしまうので、"33. いちごミルク"
の方が先に並んでしまいます。
最初が数字から始まっている場合は、その数字で判定する関数を設定してみます。
-- デフォルトのコンパレータを保存
local default_comparator = tdumper.comparator
tdumper.comparator = function(key_a, key_b)
local type_a, type_b = type(key_a), type(key_b)
if (type_a == "number" or type_a == "string") and
(type_b == "number" or type_b == "string") then
-- 数字または文字列同士の比較のみ変更
local num_a, str_a = tostring(key_a):match("^(%d+)(.*)$") -- 数字とそれ以外を分ける
local num_b, str_b = tostring(key_b):match("^(%d+)(.*)$")
num_a, num_b = tonumber(num_a), tonumber(num_b)
if num_a ~= nil then -- key_a の最初が数字の時
if num_b ~= nil then -- key_b の最初も数字の時
if num_a ~= num_b then -- 値が違う時
return num_a < num_b -- 値の大小で判定
elseif str_a ~= str_b then
return default_comparator(str_a, str_b) -- 同じ時は後ろ部分で判定
end
return default_comparator(key_a, key_b) -- 後ろ部分も同じ時は元の値で判定
else
return true -- key_b の最初が数字で無い時は、key_a を先に表示
end
else
-- key_a の最初が数字で無い時
if num_b ~= nil then -- key_b の最初が数字の時
return false -- key_b を先に表示
end
end
end
return default_comparator(key_a, key_b) -- それ以外はデフォルトコンパレータを使用
end
value_comparator
値がテーブルの場合に、そのテーブルの id
の値でソートする関数を設定します。
id
の値が同じ時(ダメ)はキー順でソートします
id
が無い時、またはテーブルでない時は、id
付きテーブルの後ろにキー順でソートします
設定する関数のコード:
tdumper.value_comparator = function(value_a, value_b)
local has_id_a = type(value_a) == "table" and type(value_a.id) == "number"
local has_id_b = type(value_b) == "table" and type(value_b.id) == "number"
if has_id_a then -- value_a に id がある時
if has_id_b then -- value_b にも id がある時
if value_a.id ~= value_b.id then -- id の値が違う時
return value_a.id < value_b.id -- id の値でソート
end
return nil -- 値が同じ時はソートをキーに任せる
else
return true -- value_b に id が無い時は value_a を先に表示
end
else
-- value_a に id が無い時
if has_id_b then -- value_b には id がある時
return false -- value_b を先に表示
end
end
return nil -- どちらにも id が無い時は、ソートをキーに任せる
end
on_value_dumped
ここで設定する関数は、色付き文字対応のコンソールに出力する時、値の型によって色を分けて出力する関数です。
(ソースコードを書き換えたほうが早いですが、例として載せます)
設定する関数の説明(長いので折りたたみ)
まず on_value_dumped
の仕様として、要素にテーブルがあった場合、そのテーブルの要素を全てダンプした後に、このフックが呼ばれます。
そのため、テーブル内の要素
→ テーブル
の順番でフックが呼ばれるので、単純に print()
すると以下のようになります。
"name" = "Lyrica",
"age" = 12,
author = {
"name" = "Lyrica",
"age" = 12,
},
table = {
author = {
"name" = "Lyrica",
"age" = 12,
},
}
このため、表示するためのデータを別に作り、最後に出力する必要があります。
(絶対自分でループを回したほうが早い・・・)
※ このコードでは、<tostring()> の情報と、<metatable> の情報は出力されません
また、ここではコンソールに色付き文字を出力する関数が以下であるものとして書きます。
console.cprint(msg:string, clr:color) -- 改行あり
console.cwrite(msg:string, clr:color) -- 改行なし
※ color は console.colors[<色名>] に入っているものとします
local colored_segments = {} -- 文字列と色のセットを入れる配列(ネストする)
local add_segment -- 上記に単体のデータを追加する関数の宣言
local add_table_segments -- 上記にテーブルのデータを追加する関数の宣言
local print_to_console -- 最後にコンソール出力をする関数の宣言
local type_colors = { -- 型ごとに何色で表示するか
-- Qiita の表示が崩れるので ["xxx"] の形式にしています
["table"] = "WHITE", -- 「{」と「}」の部分のみ
["number"] = "PINK",
["string"] = "ORANGE",
["function"] = "BLUE",
["other"] = "RED", -- 上記以外の型
["comment"] = "GRAY", -- 「* 既に表示済み」などの文字
["symbol"] = "PURPLE", -- 「 = 」と、行頭のスペースと、行末の「,」
["key"] = "RAINBOW", -- キー名
}
-- ※ 改行入り文字列に追加されるインデントは string の色になりますが、通常空白に色は付きません
-- 気になる方は insert_indent を false にして、自分でインデントを入れてください
-- table_dumper に追加するフック関数
tdumper.on_value_dumped = function(key, value, key_str, value_str, key_array)
local value_type = type(value) -- 値の型によって色を分ける
local nesting_level = #key_array -- 現在のネストの深さ
-- 現在の階層のテーブルがない場合は作る
if colored_segments[nesting_level] == nil then
colored_segments[nesting_level] = {}
end
-- 現在のインデント
local indent = string.rep(tdumper.indent_unit, nesting_level - 1)
if value_type == "table" then -- 値が table型の時
-- 循環参照が検出されたかチェック ※最初が * から始まるのは循環参照時のみ
-- 以下の条件に当てはまる部分を抜き出す
-- 最初 { 改行 空白0+ * 任意の文字1+ 改行 空白0+ } 最後
-- の * 任意の文字1+ の部分
local content, count = value_str:gsub("^%{\n *(%*.+)\n *%}$", "%1")
if count > 0 then -- 1回以上置換された場合は循環参照
-- 検出メッセージを登録
add_table_segments(nesting_level, key_str, content, indent)
else
-- 循環参照ではない場合
local child = colored_segments[nesting_level + 1] -- 子要素
-- テーブルのデータを登録
add_table_segments(nesting_level, key_str, child, indent)
-- 最後に子要素の情報をテーブルから削除
colored_segments[nesting_level + 1] = nil
end
-- nesting_level が 1、つまりダンプが終わったら画面に出力する
if nesting_level == 1 then
print_to_console(colored_segments[1])
end
else
-- table 以外の型は、色だけ変えてそのまま情報を追加
if value_type ~= "number" and
value_type ~= "string" and
value_type ~= "function" then
value_type = "other"
end
-- 出力用のテーブルに、テキストと色を登録
add_segment(nesting_level, "symbol", indent) -- インデント
add_segment(nesting_level, "key", key_str) -- キー名
add_segment(nesting_level, "symbol", " = ") -- 「 = 」の部分
add_segment(nesting_level, value_type, value_str) -- 値
add_segment(nesting_level, "symbol", ",", true) --「,」(改行)
end
end
-- 指定された階層の情報を保存しているテーブルに、引数のデータを追加
add_segment = function(nesting_level, type_name, data, newline)
local color = type_colors[type_name or ""]
table.insert(colored_segments[nesting_level], {
color = color, -- 色(文字列 or nil)
data = data, -- 表示する文字列 or 子要素のデータが入った配列
newline = newline, -- 改行するか
})
end
-- テーブルの情報を、colored_segments に追加する
add_table_segments = function(nesting_level, key_str, data, indent)
-- 出力用のテーブルに、テキストと色を登録
add_segment(nesting_level, "symbol", indent) -- インデント
add_segment(nesting_level, "key", key_str) -- キー名
add_segment(nesting_level, "symbol", " = ") -- 「 = 」の部分
add_segment(nesting_level, "table", "{", true) -- 「{」(改行)
-- テーブルの中身部分
if type(data) == "string" then
-- 循環参照の場合
local indent2 = indent .. tdumper.indent_unit -- 子要素のインデント
add_segment(nesting_level, "symbol", indent2) -- 子要素のインデントを追加
add_segment(nesting_level, "comment", data, true) -- 検出メッセージ(改行)
else
-- 循環参照でない場合は子要素の情報を、現在の階層に追加
if data ~= nil then
-- 子要素のデータが入ったテーブルをそのまま入れる
add_segment(nesting_level, nil, data) -- data の最後は必ず改行入り
end
end
add_segment(nesting_level, "symbol", indent) -- インデント
-- nesting_level が 1 (つまりトップのテーブル)には , を付けない
if nesting_level == 1 then
add_segment(nesting_level, "table", "}", true) -- 「}」(改行)
else
add_segment(nesting_level, "table", "}") -- 「}」
add_segment(nesting_level, "symbol", ",", true) -- 「,」(改行)
end
end
-- 実際に画面に色付き文字を表示する
-- 引数はデータが入った配列(最初の呼び出しは colored_segments[1])
print_to_console = function(segment_array)
-- 配列内のデータを順番に表示していく
for _, segment in ipairs(segment_array) do
if type(segment.data) == "string" then
-- ネストしたデータでない場合
if segment.newline then
-- 改行付きで表示
console.cprint(segment.data, console.colors[segment.color])
else
-- 改行なしで表示
console.cwrite(segment.data, console.colors[segment.color])
end
else
-- ネストしたデータの場合は再帰呼び出し
print_to_console(segment.data)
end
end
end
key_formatter
ここで設定する関数は、キー名の表示を [1] から [2] の形式に変更する関数です。
[1] 文字列キー: "キー名"
/ 数字キー: キー名
[2] 文字列キー: キー名
/ 数字キー: [キー名]
設定する関数のコード:
table_dumper.key_formatter = function(key, key_str)
local key_type = type(key)
if key_type == "string" then
return key -- string の場合はそのまま
elseif key_type == "number" then
return "[" .. tostring(key) .. "]" -- number は [] で囲む
end
return key_str -- 他は通常の形式
end
value_formatter
ここで設定する関数は、テーブル内に要素がない時の表示を [1] から [2] の形式に変更する関数です。
{
empty_table = {
},
}
{
empty_table = {},
}
設定する関数のコード:
table_dumper.value_formatter = function(value, value_str)
if type(value) == "table" then
local _, count = value_str:gsub("\n", "")
if count == 1 then -- 改行が 1回なら要素なし
return "{}" -- 要素がない時は {} を返す
end
end
return value_str -- それ以外はそのまま表示
end
4. おわりに
以上が、Lua のテーブをダンプするモジュールの解説でした。
全部読む人は多分いないですね・・・
複雑なテーブルの構造を、見やすく出力したい時の助けになれば幸いです。
こんな記事を読んでくださり、ありがとうございました!