0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Lua】Table をダンプするモジュールの解説

Last updated at Posted at 2025-04-21

はじめに

この記事では、Table をダンプする自作モジュールの、使い方とコードの解説します。

またこの記事は、Minecraft の Mod である OpenComputers(以降 OC)について解説する記事の一貫として作ったものです(執筆中)。

Lua 初心者が作ったモジュールなので、間違いや非効率なことをしている可能性が高いです(OC で初めて Lua に触りました)

ソースコード

ここで紹介しているプログラムは、私の GitHub で公開しています。
MIT ライセンスにしてあるので、基本的に自由に使っていただいて大丈夫です。
ここで紹介しているモジュール(が入っているディレクトリ)
OC 用も含めたトップページ(OC 用はまだ何も無いです)

解説文中の 変数 / 関数 の型表記

解説文中の、変数や関数の型表記方法は次のように書きます。

  • 変数: 変数名:型
  • 関数: 関数名(引数名:型[, 任意引数名:型 ...]):戻り値1の型, 戻り値2の型 or nil ...

また、ソースコード中の説明文も同様になっています。

0. 目次

  1. モジュールの概要
  2. コードの解説
  3. 通常版の追加機能
    3.1 追加機能概要
    3.2 詳しい解説
    3.3 オプションの初期値
    3.4 (おまけ)関数を指定するオプションの設定例
  4. おわりに

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 (通常版) を使用しています

ダンプする.lua
----------------------------
-- ダンプするテーブルを定義
----------------------------
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 も作りました。
この項目では簡易版でコードの解説をして、次の項目で通常版に追加される機能の紹介をします。
全体のソースコードを見たい方は、ソースコードのリンクから見てください。

※ この解説はかなり冗長です。大したことはしていないので、慣れている方はコードだけ読めば分かると思います(コメントもたくさん付けておきました)


まず再帰呼び出しの前までの、コードの前半部分を解説します。

tdumper_mini.lua
-- 結果を返すための補助関数
-- テーブル 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)

  1. local a = "b" とした時は、まず "b" が初期化されてから a が初期化され、 a"b" が代入される
  2. local tbl = { ... } とした時も同様に、{ ... } が初期化されてから tbl に代入される
  3. { ... } の中に func = function() が定義されていたら、この初期化の段階で関数が作成 & 初期化される
  4. 関数内で tbl.xxx と書かれていると、まだ tbl は代入されていないので、グローバル変数の tbl を参照する
  5. その後 tbl{ ... } が代入されるが、関数内の参照はグローバル変数のまま
    (「a = 1 b = a の後に a = 2 としても、b1 のまま」※という現象に似ている)
  6. よって、関数実行時に 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() は次のコードになります。

tdumper_mini.lua
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" かどうかで判定できます(mtnil チェックも忘れずに)。

---------------------------------
_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_dumpernew() が代入され、以降 tdumper 経由でアクセスできるようになります
    ※ いきなり tdumper:dump() としても大丈夫です
  • tdumper.new() をすると、「その時点でのデフォルト値(tdumper.<オプション> の値)がオプションに設定されたオブジェクト」が戻り値になります
    • その後に tdumper の値を変更しても、それ以前に new() されたオブジェクトには影響がありません
    • ただし default_dumper だけは tdumper の変更が反映されます(というか tdumper の実体です)
    • new() の引数には、loggerverbose_level を指定できますが、省略可能です

次にオプションについて。

  • loggernil が設定されていると、dump() 実行時にオブジェクトの logger にデフォルトロガーが代入されます
  • insert_indent_tostring0 が設定されていると、dump() 実行時の insert_indent の値を代わりに使用します(上書きはしません)
  • max_depthmax_items_per_table-1 が設定されていると、無制限に表示します
  • show_metatabletrue の場合、以下のように表示されます(メタテーブルが何も設定されていない場合は、何も表示されません)
    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 の形式の関数を設定します
    • 要素のダンプが終わった後、最終的に出力に含めるかを決定します
    • keyvalue は生のキーと値、key_strvalue_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_dumpertdumper_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) の形式の関数を設定します
    • keyvalue は生のキーと値、key_strvalue_str は実際に出力するキーと値の文字列、key_array は親から辿ってきたキー名が親から順に入った配列が渡されます
    • ここで値を変更しても、出力には影響しません
  • key_formattervalue_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

特定の要素のみ表示したい場合は、filterpost_filter に以下のような関数を設定します。
ここではキー名が name または age の要素のみ表示するようにします。
ネストしたテーブル内に要素があるかもしれないので、filter は table型も OK にします。

まずは filter のコード:

filter.lua
tdumper.filter = function(key, value)
  return key == "name" or
         key == "age" or
         type(value) == "table"
end

このままでは、テーブルに nameage が入っていない場合、空のテーブルが表示されてしまうので、空のテーブルは表示しないように post_filter で設定します。

post_filter のコード:

post_filter.lua
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

これで nameage が入っているテーブルのみ表示されます。
ちなみに、dump() に渡したテーブルの表示がスキップされた場合は、次のように表示されます。
<テーブル名> -> post_filter によりスキップされました

comparator

付属のコンパレータでは、「数字に変換できるなら数字として判定」→「できなければ文字列で判定」→ ・・・ のように判定しています。
これだと、 "33. いちごミルク""4. コーヒー牛乳" は文字列で判定してしまうので、"33. いちごミルク" の方が先に並んでしまいます。
最初が数字から始まっている場合は、その数字で判定する関数を設定してみます。

comparator.lua
-- デフォルトのコンパレータを保存
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 付きテーブルの後ろにキー順でソートします

設定する関数のコード:

value_comparator.lua
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[<色名>] に入っているものとします
on_value_dumped.lua
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] 文字列キー: キー名 / 数字キー: [キー名]

設定する関数のコード:

key_formatter.lua
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] の形式に変更する関数です。

[1] 通常の表示
{
  empty_table = {
  },
}
[2] 変更後の表示
{
  empty_table = {},
}

設定する関数のコード:

value_formatter.lua
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 のテーブをダンプするモジュールの解説でした。
全部読む人は多分いないですね・・・
複雑なテーブルの構造を、見やすく出力したい時の助けになれば幸いです。

こんな記事を読んでくださり、ありがとうございました!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?