Skip to content

Add ConvertTo-Lua and ConvertFrom-Lua for PowerShell-Lua data conversion #2

@MariusStorhaug

Description

Context

Lua uses tables as its sole data-structuring mechanism — serving as
arrays, dictionaries, records, and objects. Configuration files for Lua-based applications (e.g., World of
Warcraft addons like ElvUI) store structured data as Lua table constructors. There is currently no PowerShell
module that provides native serialization of PowerShell objects into Lua table format or deserialization of Lua
table strings back into PowerShell objects.

Request

The module should provide two public functions — ConvertTo-Lua and ConvertFrom-Lua — that enable round-trip
data conversion between PowerShell objects and Lua table constructor
strings:

PowerShell objects → ConvertTo-Lua → Lua table constructor string → ConvertFrom-Lua → PowerShell objects

The functions are data serialization/deserialization tools — analogous to
ConvertTo-Json /
ConvertFrom-Json.
The parameter design follows those cmdlets where the Lua format permits, but Lua's own type system and
table constructor grammar take precedence
over JSON alignment.

Lua data types and PowerShell mapping

According to the Lua 5.4 reference manual §2.1, Lua has eight basic
types. Only five are representable as data literals in table constructors:

Lua type Lua literal examples PowerShell type Direction
table (key-value) { name = "x", size = 10 } PSCustomObject (default) or [ordered] hashtable both
table (sequence) { 1, 2, 3 } [object[]] both
table (mixed) { "a", name = "x" } PSCustomObject or [ordered] hashtable (sequential values get integer keys) both
string "hello", 'hello', [[hello]] [string] both
number (integer) 42, 0xFF, -7 [int] or [long] both
number (float) 3.14, 1e10, 0x1.fp10 [double] both
boolean true, false [bool] both
nil nil $null both

The remaining three Lua types — function, userdata, and thread — cannot be represented as data
literals and are out of scope. This module treats Lua as a data serialization format, not as a programming
language.

Lua table constructor grammar

Per §3.4.9, the grammar for table constructors is:

tableconstructor ::= '{' [fieldlist] '}'
fieldlist        ::= field {fieldsep field} [fieldsep]
field            ::= '[' exp ']' '=' exp | Name '=' exp | exp
fieldsep         ::= ',' | ';'

Key behaviors from the spec:

  • Name = exp is syntactic sugar for ["Name"] = exp
  • Fields of the form exp (no key) receive sequential integer keys starting at 1
  • Both , and ; are valid field separators — the parser must accept either
  • A trailing separator is allowed
  • Key order in non-sequence parts is not specified by Lua ("the order of the assignments in a
    constructor is undefined")

ConvertTo-Lua — serialization

Parameter Type Default Analogous JSON param Notes
-InputObject [object] -InputObject Mandatory, Position 0, pipeline. Any PowerShell object
-Depth [int] 2 -Depth Max recursion depth for nested object serialization. Range 0–100. Emits a warning when input exceeds this depth, matching ConvertTo-Json behavior
-Compress [switch] -Compress Omit whitespace and indentation
-EnumsAsStrings [switch] -EnumsAsStrings Serialize PowerShell enum values as their string name instead of numeric value
-AsArray [switch] -AsArray Always wrap output in a Lua sequence table { ... }, even for a single value

Not applicable from ConvertTo-Json:

Serialization rules grounded in the Lua spec:

  • Lua identifiers ([a-zA-Z_][a-zA-Z0-9_]*) that are not reserved words
    are emitted as bare keys: name = "value". All other keys use bracket-quote notation: ["key with spaces"] = "value".
    This follows the Name = exp vs [exp] = exp distinction in the grammar.
  • Lua makes no distinction between arrays and dictionaries — both are tables. PowerShell arrays serialize as
    sequence tables ({ 1, 2, 3 }); hashtables and PSCustomObject as key-value tables.
  • Per §2.1, Lua numbers have two subtypes: integer and float.
    PowerShell integers serialize as Lua integers; PowerShell floats/doubles serialize as Lua floats. Numbers
    always use invariant culture formatting.
  • $null serializes as nil. Per the Lua spec, "any key associated to the value nil is not considered part
    of the table" — so properties with $null values are omitted from the output, consistent with how Lua
    tables actually work.
  • Booleans serialize as true/false (lowercase, per Lua keywords).
  • Strings are double-quoted with escape sequences applied per §3.1.
  • Empty collections serialize as {}.
  • Uses 4-space indentation (Lua community convention). Not configurable — ConvertTo-Json also does not
    expose indent control.

ConvertFrom-Lua — deserialization

Parameter Type Default Analogous JSON param Notes
-InputObject [string] -InputObject Mandatory, Position 0, pipeline. A Lua table constructor string
-AsHashtable [switch] -AsHashtable Output [ordered] hashtable instead of the default PSCustomObject. Matches ConvertFrom-Json -AsHashtable (which returns OrderedHashtable since PS 7.3)
-Depth [int] 1024 -Depth Max nesting depth allowed in input. Protects against excessively deep or crafted input. Throws a terminating error when exceeded
-NoEnumerate [switch] -NoEnumerate Output arrays as a single [object[]] instead of enumerating elements through the pipeline. Required for round-trip fidelity of Lua sequences

Not applicable from ConvertFrom-Json:

  • -DateKind — Lua has no native date/time type (§2.1).
    Date-like strings remain strings.

Deserialization rules grounded in the Lua spec:

  • Default output is PSCustomObject (matching ConvertFrom-Json). Use -AsHashtable for ordered hashtable
    output.
  • Tables with only sequential values (the exp field form) deserialize as [object[]] arrays.
  • Tables with only key-value fields (Name = exp or [exp] = exp) deserialize as PSCustomObject
    (or ordered hashtable with -AsHashtable).
  • Mixed tables (both sequential values and named keys) deserialize as PSCustomObject or ordered hashtable,
    with sequential values assigned integer keys starting at 1 — matching the Lua spec behavior for the exp
    field form.
  • Lua comments (-- single-line, --[[ ]] multi-line)
    are ignored during parsing, matching how ConvertFrom-Json ignores JSON comments since PowerShell 6.
  • String parsing supports all three Lua string literal forms: double-quoted ("..."), single-quoted
    ('...'), and long strings ([[...]]), with escape sequences per §3.1.
  • Number parsing supports integers, floats, hexadecimal (0x/0X), and scientific notation (both e/E
    for decimal and p/P for hex floats), per the numeral grammar in §3.1.
    Integer values that fit in [int] return [int]; larger values return [long]; floats return [double].
  • nil deserializes as $null.
  • true/false deserialize as [bool].
  • Bare identifiers that are not true, false, or nil (i.e., variable references) are not valid in a
    data-only context and should produce a parse error.

Example usage

# PowerShell object to Lua table constructor
@{ name = "ElvUI"; version = "13.74"; enabled = $true } | ConvertTo-Lua
# Output:
# {
#     enabled = true,
#     name = "ElvUI",
#     version = "13.74"
# }

# Compressed output
@(1, 2, 3) | ConvertTo-Lua -Compress
# Output: {1,2,3}

# Depth-limited serialization
$deep = @{ a = @{ b = @{ c = "deep" } } }
$deep | ConvertTo-Lua -Depth 1
# Serializes only one level deep, warning about truncation

# Force array wrapping for single value
"hello" | ConvertTo-Lua -AsArray
# Output:
# {
#     "hello"
# }

# Lua table constructor to PSCustomObject (default)
'{ server = "localhost", port = 8080 }' | ConvertFrom-Lua
# Output: PSCustomObject with .server and .port properties

# Lua table constructor to ordered hashtable
'{ name = "ElvUI", enabled = true }' | ConvertFrom-Lua -AsHashtable
# Output: ordered hashtable

# Parsing Lua sequences
'{1, 2, 3}' | ConvertFrom-Lua
# Output: 1, 2, 3 (enumerated through pipeline)

# Round-trip fidelity with -NoEnumerate
'{1, 2, 3}' | ConvertFrom-Lua -NoEnumerate | ConvertTo-Lua -Compress
# Output: {1,2,3}

# Lua comments are ignored
'{ -- config
  name = "test", --[[ block comment ]] enabled = true
}' | ConvertFrom-Lua
# Output: PSCustomObject with .name and .enabled properties

# Bracket-key notation
'{ ["key with spaces"] = "value" }' | ConvertFrom-Lua
# Output: PSCustomObject with a "key with spaces" property

Acceptance criteria

  • ConvertTo-Lua serializes hashtables, ordered dictionaries, PSCustomObjects, arrays, and primitives into
    valid Lua table constructor strings per §3.4.9
  • ConvertFrom-Lua parses Lua table constructor strings into PSCustomObject by default, with -AsHashtable
    for ordered hashtable output
  • All Lua data literal types are supported: tables, strings (all three forms), numbers (integer, float, hex,
    scientific notation), booleans, and nil
  • Lua comments (single-line and multi-line) are correctly ignored during parsing
  • -Depth on ConvertTo-Lua controls max recursion (default 2, range 0–100, warns on truncation)
  • -Depth on ConvertFrom-Lua limits max nesting (default 1024, throws on violation)
  • -Compress, -EnumsAsStrings, -AsArray, and -NoEnumerate behave consistently with their
    ConvertTo-Json/ConvertFrom-Json counterparts
  • Properties with $null values are omitted from serialized output (per Lua's nil-means-absent semantics)
  • Keys use bare identifiers when valid per Lua grammar; bracket-quote notation otherwise
  • Round-trip conversion preserves data: $obj | ConvertTo-Lua | ConvertFrom-Lua returns equivalent data
  • Both functions support pipeline input
  • Unicode characters are preserved
  • Bare identifier references (variable names) in input produce a parse error

Technical decisions

Design principles:

  1. The Lua 5.4 reference manual is the authoritative specification
    for serialization/deserialization behavior — type mapping, string escaping, number formats, table
    constructor grammar, and nil semantics.
  2. Parameter names, defaults, and behavioral semantics follow
    ConvertTo-Json /
    ConvertFrom-Json
    where those do not conflict with Lua's own rules — to lower cognitive burden for PowerShell developers.

Implementation approach: Pure PowerShell recursive-descent parser — no external DLL or .NET assembly
dependency. This keeps the module dependency-free and auditable.

Default output of ConvertFrom-LuaPSCustomObject. Matching ConvertFrom-Json default behavior. Lua
tables are fundamentally associative arrays, but PSCustomObject provides idiomatic PowerShell dot-notation
access to fields. -AsHashtable returns [ordered]@{} for cases requiring duplicate-key tolerance or
case-sensitive keys (same rationale as ConvertFrom-Json -AsHashtable).

$null / nil handling. Per §2.1: "any key associated to the
value nil is not considered part of the table." Properties with $null values are omitted from serialized
output. On deserialization, explicit nil values in Lua input are preserved as $null in the PowerShell
object (since the user intentionally put them there), even though Lua itself would treat them as absent.

Serializer key formatting. Per §3.1, Lua identifiers are
[a-zA-Z_][a-zA-Z0-9_]* and must not be reserved words (and, break, do, else, elseif, end,
false, for, function, goto, if, in, local, nil, not, or, repeat, return, then,
true, until, while). Keys matching this pattern use bare notation; all others use ["..."].

Number serialization. Lua 5.4 distinguishes integer and float subtypes
(§2.1). PowerShell [int], [long], and other integer types
serialize as Lua integers; [float], [double], and [decimal] serialize as Lua floats with invariant
culture formatting.

Indentation. Fixed 4-space indent (Lua community convention). ConvertTo-Json uses fixed 2-space indent
without exposing a parameter — the same approach is used here.

Function placement:

  • Public: src/functions/public/Lua/ConvertTo-Lua.ps1 and src/functions/public/Lua/ConvertFrom-Lua.ps1
  • Private: src/functions/private/ConvertTo-LuaTable.ps1, src/functions/private/ConvertFrom-LuaTable.ps1,
    src/functions/private/Format-LuaKey.ps1

Parser architecture: ConvertFrom-LuaTable.ps1 contains the main parser entry point plus internal helper
functions that form a recursive-descent parser using script-scoped state. The parser tracks current nesting
depth and throws when -Depth is exceeded.

Serializer architecture: ConvertTo-LuaTable.ps1 recursively walks PowerShell object graphs with
type-specific formatting. Format-LuaKey.ps1 determines whether a key is a valid bare Lua identifier or
needs bracket-quote notation (checking both the identifier pattern and the reserved word list).

Test approach: Pester tests covering:

  • Primitive type serialization/deserialization for every Lua data type
  • All three Lua string forms (double-quoted, single-quoted, long strings) with escape sequences
  • Number variants (integer, float, negative, hex, scientific notation, hex float)
  • Table structures (sequence, key-value, mixed, nested, empty)
  • Comment handling (single-line --, multi-line --[[ ]])
  • Lua reserved words as keys (must use bracket notation)
  • $null/nil omission behavior
  • -Depth, -Compress, -EnumsAsStrings, -AsArray, -AsHashtable, -NoEnumerate
  • Round-trip conversion fidelity
  • Error cases (bare variable references, exceeded depth, malformed input)
  • Test data files with complex structures

Implementation plan

Core functions — ConvertTo-Lua

  • Add ConvertTo-Lua public function in src/functions/public/Lua/ConvertTo-Lua.ps1 with parameters: -InputObject (mandatory, pipeline), -Depth (int, default 2, range 0–100), -Compress (switch), -EnumsAsStrings (switch), -AsArray (switch)
  • Add ConvertTo-LuaTable private function in src/functions/private/ConvertTo-LuaTable.ps1 — recursive serializer with depth tracking and 4-space indent
  • Add Format-LuaKey private function in src/functions/private/Format-LuaKey.ps1 — validate keys against Lua identifier pattern and reserved word list
  • Implement $null omission — skip properties with $null values per Lua nil-means-absent semantics
  • Implement -Depth — emit warning when input object nesting exceeds specified depth
  • Implement -EnumsAsStrings — serialize enum values as string names instead of numeric values
  • Implement -AsArray — wrap single-object output in Lua sequence table

Core functions — ConvertFrom-Lua

  • Add ConvertFrom-Lua public function in src/functions/public/Lua/ConvertFrom-Lua.ps1 with parameters: -InputObject (mandatory, pipeline), -AsHashtable (switch), -Depth (int, default 1024), -NoEnumerate (switch)
  • Add ConvertFrom-LuaTable private function in src/functions/private/ConvertFrom-LuaTable.ps1 — recursive-descent parser with depth tracking
  • Implement default PSCustomObject output; -AsHashtable for [ordered]@{} output
  • Implement full Lua string parsing — double-quoted, single-quoted, long strings ([[...]]), all escape sequences per §3.1
  • Implement full Lua number parsing — integer, float, hex (0x), scientific notation (e/E), hex float exponent (p/P)
  • Implement Lua comment skipping — single-line (--) and multi-line (--[[ ]])
  • Implement mixed table handling — sequential values get integer keys starting at 1
  • Implement -Depth — throw terminating error when nesting exceeds allowed depth
  • Implement -NoEnumerate — output arrays as single [object[]] without pipeline enumeration
  • Implement parse error for bare identifier references (not true/false/nil)

Tests

  • Add tests/Lua.Tests.ps1 with comprehensive test coverage
  • Add test data files in tests/data/ (Lua/JSON pairs for complex structure validation)
  • Test all Lua data types: strings (3 forms + escapes), numbers (int, float, hex, scientific, hex float), booleans, nil, tables (sequence, key-value, mixed, nested, empty)
  • Test Lua comment handling (single-line, multi-line, nested in tables)
  • Test reserved words as keys (bracket notation required)
  • Test $null omission in serialization
  • Test -Depth behavior on both functions (warning on ConvertTo-Lua, error on ConvertFrom-Lua)
  • Test -Compress, -EnumsAsStrings, -AsArray, -AsHashtable, -NoEnumerate
  • Test round-trip fidelity
  • Test error cases (bare variable references, malformed input, exceeded depth)

Documentation

  • Write README.md with module description, installation, and usage examples
  • Write examples/General.ps1 with ConvertTo-Lua and ConvertFrom-Lua examples
  • Update src/manifest.psd1 as needed

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions