A universal Lua/Luau code beautifier. Supports Lua 5.1, 5.2, 5.3, 5.4, Luau (Roblox), and all dialects in between.
- Multi-dialect: works with Lua 5.1 through 5.4, Luau, and Roblox Lua
- Idempotent: running the beautifier twice produces the same output
- Comment-preserving: shebangs, single-line and multi-line comments are kept intact, including inline trailing comments
- Blank-line aware: keeps intentional blank lines between statements, capped at a configurable maximum
- Configurable: indent width, tabs vs spaces, spacing around operators, and more
- Constant folding: simplifies constant arithmetic and propagates constant locals (on by default)
- Dead code elimination: removes branches whose conditions fold to a constant (on by default)
- String escapes: decode
\dddescapes to readable text or re-encode them - Check mode: verify formatting without modifying files (
--check) - Recursive: process entire directory trees (
--recursive) - No dependencies: zero external libraries
- CLI and library: use from the command line or embed in your own tools
BeautyAndTheLua [options] <file|directory>
| Option | Description |
|---|---|
-i, --indent <n> |
Indent width (default: 4) |
-t, --tabs |
Use tabs for indentation |
-w, --width <n> |
Max line length (default: 120) |
-c, --check |
Check formatting without modifying files |
-e, --solve-expressions |
Fold constant arithmetic and propagate constant locals (on by default) |
--no-solve-expressions |
Disable constant folding and propagation |
-d, --decode-strings |
Decode string escapes to readable characters |
-E, --encode-strings |
Encode strings as \ddd decimal escapes |
--no-dead-code |
Disable dead code elimination |
-o, --output <file> |
Write to file instead of in-place |
--stdin |
Read source from stdin |
-r, --recursive |
Process directories recursively |
-v, --version |
Show version |
-h, --help |
Show help |
# Format a single file in-place
java -jar BeautyAndTheLua.jar script.lua
# Check formatting without modifying
java -jar BeautyAndTheLua.jar --check script.lua
# Process all .lua files in a directory tree
java -jar BeautyAndTheLua.jar --recursive src/
# Use 2-space indentation
java -jar BeautyAndTheLua.jar --indent 2 script.lua
# Fold constant expressions while formatting
java -jar BeautyAndTheLua.jar -e script.lua
# Format without folding constant expressions
java -jar BeautyAndTheLua.jar --no-solve-expressions script.lua
# Decode escaped strings into readable text
java -jar BeautyAndTheLua.jar -d script.lua
# Encode strings as decimal escapes
java -jar BeautyAndTheLua.jar -E script.lua
# Read from stdin, write to stdout
cat script.lua | java -jar BeautyAndTheLua.jar --stdin > formatted.luamvn packageThe JAR will be in target/BeautyAndTheLua-1.0.0.jar.
import com.beautyandthelua.BeautyAndTheLua;
import com.beautyandthelua.Config;
BeautyAndTheLua beautifier = new BeautyAndTheLua();
String formatted = beautifier.beautify(source);
// With custom config
Config config = new Config()
.indentWidth(2)
.useTabs(false)
.spacesAroundOperators(true);
beautifier = new BeautyAndTheLua(config);
formatted = beautifier.beautify(source, "script.lua");
// Check mode
boolean isFormatted = beautifier.check(source, "script.lua");| Before | After |
|---|---|
local x=1 |
local x = 1 |
if x>0 then |
if x > 0 then |
for i,v in pairs(t) do |
for i, v in pairs(t) do |
local t={1,2,3} |
local t = {1, 2, 3} |
print(i,v) |
print(i, v) |
return{1,2} |
return {1, 2} |
x=-1 |
x = -1 |
x=n-1 |
x = n - 1 |
With -e enabled:
| Before | After |
|---|---|
local y = 2 + 3 * 4 |
local y = 14 |
local z = 2 ^ 3 ^ 2 |
local z = 512 |
local x = 5; local y = x + 3 |
local y = 8 |
Constant folding and propagation run by default. Pass --no-solve-expressions
to turn them off, or -e to state the intent explicitly. Two passes run before
formatting:
- ExpressionSolver folds constant sub-expressions. It parses each expression
respecting Lua operator precedence and associativity, so
2 + 3 * 4becomes14and2 ^ 3 ^ 2becomes512. A result is only substituted when it is an exact integer or a boolean, so folding never changes program behavior. Hex floats and anything non-constant are left untouched. - ConstantPropagator replaces references to constant locals with their values.
A local is propagated only when it is effectively final: declared once as
local n = <literal>and never reassigned in the region where it is visible. Scopes are tracked properly, so alocalinside a nested block or function never leaks into the enclosing scope.
Both passes are conservative by design: when in doubt, they leave the code as-is.
Layout-level cleanup is always on and is controlled by the Config object:
- Blank lines: runs of blank lines between statements are preserved but capped
at
maxBlankLines(default 2). Leading and trailing blank lines inside a block are removed. SetpreserveBlankLinestofalseto collapse all blank lines. - Inline comments: a comment that sits on the same source line as the code
before it stays on that line (
local x = 1 -- note). This also applies to block headers, such asfunction f() -- ...,if cond then -- ..., andfor i = 1, n do -- .... SetkeepInlineCommentstofalseto push every comment onto its own line.
After constant folding, branch conditions that reduce to a constant are resolved
and unreachable code is dropped. Runs by default; pass --no-dead-code to keep
every branch.
if false then ... endis removed entirely.if true then X endbecomesdo X end, preserving the scope of any locals.- Dead
elseifclauses are dropped, and the first clause that is always taken turns the rest of the chain into a plain block. while false do ... endis removed.
Conditions are only resolved when they fold to a single literal, so anything depending on runtime values is left untouched.
Two optional, opposite transforms rewrite short-string literals before parsing:
- Decode (
-d/decodeStringEscapes): turns escape sequences back into readable characters where it is safe."\056\051\052"becomes"834". Control characters, quotes and backslashes stay escaped; bytes outside printable ASCII are kept as\ddd. - Encode (
-E/encodeStringEscapes): renders every byte as a\ddddecimal escape, the form many obfuscators emit."Hi"becomes"\72\105".
Both decode the literal to its raw bytes first, so each direction is idempotent
and the two can be chained. Long strings ([[...]]) and literals containing an
unsupported escape (unicode or \z) are left untouched.
- All Lua 5.1 to 5.4 keywords and operators (
::,//,<<,>>,&,|,~) - Luau extensions (
continue,type,export type,typeof) - Long strings and comments (
[[...]],[=[...]=]) - Shebang lines (
#!/usr/bin/env lua) - Goto labels (
::label::) - Numeric and generic
forloops repeat...until,while...do,if...then...elseif...else...end- Nested block structures
MIT