aboutsummaryrefslogtreecommitdiffstats
path: root/util/format.lua
diff options
context:
space:
mode:
Diffstat (limited to 'util/format.lua')
-rw-r--r--util/format.lua115
1 files changed, 98 insertions, 17 deletions
diff --git a/util/format.lua b/util/format.lua
index c5e513fa..1cd8d81b 100644
--- a/util/format.lua
+++ b/util/format.lua
@@ -3,12 +3,40 @@
--
local tostring = tostring;
-local select = select;
local unpack = table.unpack or unpack; -- luacheck: ignore 113/unpack
+local pack = require "util.table".pack; -- TODO table.pack in 5.2+
+local valid_utf8 = require "util.encodings".utf8.valid;
local type = type;
+local dump = require "util.serialization".new("debug");
+local num_type = math.type or function (n)
+ return n % 1 == 0 and n <= 9007199254740992 and n >= -9007199254740992 and "integer" or "float";
+end
+
+-- In Lua 5.3+ these formats throw an error if given a float
+local expects_integer = { c = true, d = true, i = true, o = true, u = true, X = true, x = true, };
+-- In Lua 5.2 these throw an error given a negative number
+local expects_positive = { o = true; u = true; x = true; X = true };
+-- Printable Unicode replacements for control characters
+local control_symbols = {
+ -- 0x00 .. 0x1F --> U+2400 .. U+241F, 0x7F --> U+2421
+ ["\000"] = "\226\144\128", ["\001"] = "\226\144\129", ["\002"] = "\226\144\130",
+ ["\003"] = "\226\144\131", ["\004"] = "\226\144\132", ["\005"] = "\226\144\133",
+ ["\006"] = "\226\144\134", ["\007"] = "\226\144\135", ["\008"] = "\226\144\136",
+ ["\009"] = "\226\144\137", ["\010"] = "\226\144\138", ["\011"] = "\226\144\139",
+ ["\012"] = "\226\144\140", ["\013"] = "\226\144\141", ["\014"] = "\226\144\142",
+ ["\015"] = "\226\144\143", ["\016"] = "\226\144\144", ["\017"] = "\226\144\145",
+ ["\018"] = "\226\144\146", ["\019"] = "\226\144\147", ["\020"] = "\226\144\148",
+ ["\021"] = "\226\144\149", ["\022"] = "\226\144\150", ["\023"] = "\226\144\151",
+ ["\024"] = "\226\144\152", ["\025"] = "\226\144\153", ["\026"] = "\226\144\154",
+ ["\027"] = "\226\144\155", ["\028"] = "\226\144\156", ["\029"] = "\226\144\157",
+ ["\030"] = "\226\144\158", ["\031"] = "\226\144\159", ["\127"] = "\226\144\161",
+};
+local supports_p = pcall(string.format, "%p", ""); -- >= Lua 5.4
+local supports_a = pcall(string.format, "%a", 0.0); -- > Lua 5.1
local function format(formatstring, ...)
- local args, args_length = { ... }, select('#', ...);
+ local args = pack(...);
+ local args_length = args.n;
-- format specifier spec:
-- 1. Start: '%%'
@@ -20,28 +48,81 @@ local function format(formatstring, ...)
-- The options c, d, E, e, f, g, G, i, o, u, X, and x all expect a number as argument, whereas q and s expect a string.
-- This function does not accept string values containing embedded zeros, except as arguments to the q option.
-- a and A are only in Lua 5.2+
+ -- Lua 5.4 adds a p format that produces a pointer
-- process each format specifier
local i = 0;
- formatstring = formatstring:gsub("%%[^cdiouxXaAeEfgGqs%%]*[cdiouxXaAeEfgGqs%%]", function(spec)
- if spec ~= "%%" then
- i = i + 1;
- local arg = args[i];
- if arg == nil then -- special handling for nil
- arg = "<nil>"
- args[i] = "<nil>";
- end
+ formatstring = formatstring:gsub("%%[^cdiouxXaAeEfgGpqs%%]*[cdiouxXaAeEfgGpqs%%]", function(spec)
+ if spec == "%%" then return end
+ i = i + 1;
+ local arg = args[i];
- local option = spec:sub(-1);
- if option == "q" or option == "s" then -- arg should be string
+ if arg == nil then
+ args[i] = "nil";
+ return "(%s)";
+ end
+
+ local option = spec:sub(-1);
+ local t = type(arg);
+
+ if option == "s" and t == "string" and not arg:find("[%z\1-\31\128-\255]") then
+ -- No UTF-8 or control characters, assumed to be the common case.
+ return
+ elseif t == "number" then
+ if option == "g" or (option == "d" and num_type(arg) == "integer") then return end
+ elseif option == "s" and t ~= "string" then
+ arg = tostring(arg);
+ t = "string";
+ end
+
+ if option ~= "s" and option ~= "q" and option ~= "p" then
+ -- all other options expect numbers
+ if t ~= "number" then
+ -- arg isn't number as expected?
+ arg = tostring(arg);
+ option = "s";
+ spec = "[%s]";
+ t = "string";
+ elseif expects_integer[option] and num_type(arg) ~= "integer" then
args[i] = tostring(arg);
- elseif type(arg) ~= "number" then -- arg isn't number as expected?
+ return "[%s]";
+ elseif expects_positive[option] and arg < 0 then
args[i] = tostring(arg);
- spec = "[%s]";
+ return "[%s]";
+ elseif (option == "a" or option == "A") and not supports_a then
+ return "%x";
+ else
+ return -- acceptable number
+ end
+ end
+
+
+ if option == "p" and not supports_p then
+ arg = tostring(arg);
+ option = "s";
+ spec = "[%s]";
+ t = "string";
+ end
+
+ if t == "string" and option ~= "p" then
+ if not valid_utf8(arg) then
+ option = "q";
+ else
+ args[i] = arg:gsub("[%z\1-\8\11-\31\127]", control_symbols):gsub("\n\t?", "\n\t");
+ return spec;
end
end
- return spec;
+
+ if option == "q" then
+ args[i] = dump(arg);
+ return "%s";
+ end
+
+ if option == "p" and (t == "boolean" or t == "number") then
+ args[i] = tostring(arg);
+ return "[%s]";
+ end
end);
-- process extra args
@@ -49,9 +130,9 @@ local function format(formatstring, ...)
i = i + 1;
local arg = args[i];
if arg == nil then
- args[i] = "<nil>";
+ args[i] = "(nil)";
else
- args[i] = tostring(arg);
+ args[i] = tostring(arg):gsub("[%z\1-\8\11-\31\127]", control_symbols):gsub("\n\t?", "\n\t");
end
formatstring = formatstring .. " [%s]"
end