Search code examples
lualua-tablelua-5.1

Pre-process ... input - bad argument #2 to 'format' (no value)


I've got a scenario where I want to take input to a printf implementation and pre-process the arguments. My goal is to take the input, process the variable arguments (...) if necessary, and feed that into a string.format. The issue I'm encountering is the data gets "lost" when passing it around. I'm finding nil values in ... are not passed in. For example, if the normal run passes nil, false, false, subsequent downline functions in the call path only see false, false. What am I missing here to get the keep the data preserved when converted to an internal table ({...}) for processing?

EDIT: I was incorrect in my initial assessment. nil is never preserved (even in printf). If ... is printed, the table can be empty (for all nil) or only have indexed values in the places that contain "real" data (e.g. TablePrint gives { [2] = false, [3] = false })

This is running inside a game, but I can replicate the behavior without using any of the game data.

local function TablePrint (data, indent)
    if not indent then indent = 0 end

    local output = string.rep(" ", indent) .. "{\r\n"
    indent = indent + 2
    
    for k, v in pairs(data) do
        output = output .. string.rep(" ", indent)
        
        if (type(k) == "number") then
            output = output .. "[" .. k .. "] = "
        elseif (type(k) == "string") then
            output = output  .. k ..  "= "   
        end
        
        if (v == nil) then
            output = output .. 'nil' .. ",\r\n"
        elseif (v == NULL) then
            output = output .. 'NULL' .. ",\r\n"
        elseif (type(v) == "number") then
            output = output .. v .. ",\r\n"
        elseif (type(v) == "string") then
            output = output .. "\"" .. v .. "\",\r\n"
        elseif (type(v) == "table") then
            output = output .. TablePrint(v, indent + 2) .. ",\r\n"
        else
            output = output .. "\"" .. tostring(v) .. "\",\r\n"
        end
    end
    
    output = output .. string.rep(" ", indent-2) .. "}"
    
    return output
end

local function NormalizeValue(v)
    local output

    if (v == nil) then
        output = 'nil'
    elseif (v == NULL) then
        output = 'NULL'
    elseif (type(v) == "number" or type(v) == "string") then
        output = v
    elseif (type(v) == "table") then
        output = TablePrint(v)
    else
        output = tostring(v)
    end

    return output
end

local function NormalizeArgs(...)
    local args = {...}
    --print(TablePrint(args))
    local normalizedArgs = {}

    for _,v in ipairs(args) do
        table.insert(normalizedArgs, NormalizeValue(v))
    end

    return unpack(normalizedArgs)
end

local function printf(message, ...)
    --print(TablePrint({...}))
    local output

    if (type(message) == "number") then
        output = message
    elseif (type(message) == "string") then
        output = string.format(message, NormalizeArgs(...))
        --this call to string.format succeeds if used
        --output = string.format(message, ...)
    elseif (type(message) == "table") then
        output = TablePrint(message)
    else
        output = tostring(message)
    end

    print(output)
end

--this line fails since nil isn't preserved across call boundaries
--printf('initial conditions, cursor: %s, confirm window: %s, isfinished: %s', nil, nil, nil)
--this line fails since nil isn't preserved across call boundaries
printf('initial conditions, cursor: %s, confirm window: %s, isfinished: %s', nil, false, false)
--this will succeed
printf('initial conditions, cursor: %s, confirm window: %s, isfinished: %s', false, false, false)
--in the normal impl the final parameter is supplied by a call to a local function
printf('initial conditions, cursor: %s, confirm window: %s, isfinished: %s', gamelib.Cursor.ID(), gamelib.Window('Confirm').Open(), nil)

Solution

  • You can use select to fully examine .... It's probably easier to return multiple values recursively than to create a table just to unpack it:

    local function NormalizeArgs(...)
        if select('#', ...) > 0 then
            return NormalizeValue(select(1, ...)), NormalizeArgs(select(2, ...))
        end
    end