lua-users wiki: String Interpolation (original) (raw)

lua-users home

When variables need to be interpolated in strings, the resultant quoting and unquoting can become slightly unwieldy:

print("Hello " .. name .. ", the value of key " .. k .. " is " .. v .. "!")

Compare to Perl, where variables can be embedded in strings:

print "Hello name,thevalueofkeyname, the value of key name,thevalueofkeyk is $b!\n";

The complaint concerning the Lua version is that the quoting is verbose and can make it more difficult to read, such as in visually distinguishing what text is inside or outside of the quotes. Besides using an editor with syntax highlighting, the latter issue might be improved with a bracketed quoting style:

print([[Hello ]] .. name .. [[, the value of key ]] .. k .. [[ is ]] .. v .. [[!]])

This might also be made more terse with string.format:

print(string.format("Hello %s, the value of key %s is %s", name, k, v))

possibly using a helper function:

function printf(...) print(string.format(...)) end

printf("Hello %s, the value of key %s is %s", name, k, v)

The new problem that this presents is that the variables are identified positionally, which presents readability and maintainability problems if the number of variables is large.

The following solutions show how to implement support for interpolating variables into strings in Lua to achieve a syntax somewhat like this:

printf("Hello %(name), the value of key %(k) is %(v)")

Solution: Named Parameters in Table

Here's one simple implementation (-- RiciLake):

function interp(s, tab) return (s:gsub('($%b{})', function(w) return tab[w:sub(3, -2)] or w end)) end print( interp("${name} is ${value}", {name = "foo", value = "bar"}) )

getmetatable("").__mod = interp print( "${name} is ${value}" % {name = "foo", value = "bar"} )

Solution: Named Parameters with Formatting Codes

Here's another implementation (-- RiciLake) supporting Pythonic formatting specifications (requires Lua 5.1 or greater):

function interp(s, tab) return (s:gsub('%%%((%a%w*)%)([-0-9%.]*[cdeEfgGiouxXsq])', function(k, fmt) return tab[k] and ("%"..fmt):format(tab[k]) or '%('..k..')'..fmt end)) end getmetatable("").__mod = interp print( "%(key)s is %(val)7.2f%" % {key = "concentration", val = 56.2795} )

Solution: Named Parameters and Format String in Same Table

Here's another Lua-only solution (-- MarkEdgar):

function replace_vars(str, vars)

if not vars then vars = str str = vars[1] end return (string_gsub(str, "({([^}]+)})", function(whole,i) return vars[i] or whole end)) end

output = replace_vars{ [[Hello {name}, welcome to {company}. ]], name = name, company = get_company_name() }

Solution: Ruby- and Python-like string formatting with % operator

Both Ruby and Python have a short form for string formatting, using the % operator.

The following snippet adds a similar use of the mod operator to lua:

getmetatable("").__mod = function(a, b) if not b then return a elseif type(b) == "table" then return string.format(a, unpack(b)) else return string.format(a, b) end end

Example usage:

print( "%5.2f" % math.pi )

print( "%-10.10s %04d" % { "test", 123 } )

You might like or dislike this notation, choose for yourself.

Hack: Using debug to Access Lexicals

Below is a more complex implementation (-- DavidManura). This makes use of the debug library (particularly debug.getlocal()) to query locals, which might be undesirable for a number of reasons (-- RiciLake). First, it can be used to break into things you shouldn't break into, so it's a bad idea if trusted code is being run. debug.getlocal() is also expensive since it needs to scan through the entire byte code to figure out which variables are in scope. It also does not capture closed variables.

Code:

local mynil_mt = {__tostring = function() return tostring(nil) end} local mynil = setmetatable({}, mynil_mt)

function get_locals(func) local n = 1 local locals = {} func = (type(func) == "number") and func + 1 or func while true do local lname, lvalue = debug.getlocal(func, n) if lname == nil then break end
if lvalue == nil then lvalue = mynil end
locals[lname] = lvalue n = n + 1 end return locals end

function interp(str, table, level) local use_locals = (table == nil) table = table or getfenv(2) if use_locals then level = level or 1 local locals = get_locals(level + 1) table = setmetatable(locals, {__index = table}) end local out = string.gsub(str, '$(%b{})', function(w) local variable_name = string.sub(w, 2, -2) local variable_value = table[variable_name] if variable_value == mynil then variable_value = nil end return tostring(variable_value) end ) return out end

function printi(str) print(interp(str, nil, 2)) end

getmetatable("").__mod = interp

Tests:

x=123 assert(interp "x = ${x}" == "x = 123")

assert(interp("x = ${x}", {x = 234}) == "x = 234")

do local x = 3 assert(interp "x = ${x}" == "x = 3") end

function test() assert(interp "y = ${y}" == "y = 123") end local env = {y = 123} setmetatable(env, {__index = _G}) setfenv(test, env) test()

do local z = 1 local z = 2 assert(interp "z = ${z}" == "z = 2") local z = 3 end

do z = 2 local z = 1 local z = nil assert(interp "z = ${z}" == "z = nil") end

x = 123 for k, v in ipairs {3,4} do printi("${x} - The value of key kis{k} is kis{v}") end

assert("x = ${x}" % {x = 2} == "x = 2")

Various enhancements could be made. For example,

v = {x = 2} print(interp "v.x = ${v.x}")

Patch to Lua

One of the features I loved in Ruby and PHP was the ability to include variables inside strings, example print "Hello ${Name}" The following patch does the same thing but only for the doc string type, strings starting with [[ and ending with ]]. It uses the "|" character to represent the open and close braces.

To add variables inline example :

output = [[Hello |name|, welcome to |get_company_name()|. ]]

What the patch does is quite literally convert the above to:

output = [[Hello ]]..name..[[, welcome to ]]..get_company_name()..[[. ]]

The following functions are updated in the llex.c file.

Important Note: Somehow, I needed another character as a means to represent the closing brace inside the code, and I have arbitarily chosen '�' , what this means if somehow you have that character in your string (specially when you are using foreign language encoding) you will get a syntax error. I don't know if there is the solution to this problem as yet.

int luaX_lex (LexState *LS, SemInfo *seminfo) { for (;;) { switch (LS->current) {

  case '\n': {
    inclinenumber(LS);
    continue;
  }
  case '-': {
    next(LS);
    if (LS->current != '-') return '-';
    /* else is a comment */
    next(LS);
    if (LS->current == '[' && (next(LS), LS->current == '['))
      read_long_string(LS, NULL);  /* long comment */
    else  /* short comment */
      while (LS->current != '\n' && LS->current != EOZ)
        next(LS);
    continue;
  }
  case '[': {
    next(LS);
    if (LS->current != '[') return '[';
    else {
      read_long_string(LS, seminfo);
      return TK_STRING;
    }
  }
  case '=': {
    next(LS);
    if (LS->current != '=') return '=';
    else { next(LS); return TK_EQ; }
  }
  case '<': {
    next(LS);
    if (LS->current != '=') return '<';
    else { next(LS); return TK_LE; }
  }
  case '>': {
    next(LS);
    if (LS->current != '=') return '>';
    else { next(LS); return TK_GE; }
  }
  case '~': {
    next(LS);
    if (LS->current != '=') return '~';
    else { next(LS); return TK_NE; }
  }
  case '"':
  case '\'': {
    read_string(LS, LS->current, seminfo);
    return TK_STRING;
  }

// added!!!
    //------------------------------
  case '|': {
 LS->current = '�';
 return TK_CONCAT;
  }

  case '�': {
read_long_string(LS, seminfo);
return TK_STRING;
}
    //------------------------------

  case '.': {
    next(LS);
    if (LS->current == '.') {
      next(LS);
      if (LS->current == '.') {
        next(LS);
        return TK_DOTS;   /* ... */
      }
      else return TK_CONCAT;   /* .. */
    }

    else if (!isdigit(LS->current)) return '.';
    else {
      read_numeral(LS, 1, seminfo);
      return TK_NUMBER;
    }
  }
  case EOZ: {
    return TK_EOS;
  }
  default: {
    if (isspace(LS->current)) {
      next(LS);
      continue;
    }
    else if (isdigit(LS->current)) {
      read_numeral(LS, 0, seminfo);
      return TK_NUMBER;
    }
    else if (isalpha(LS->current) || LS->current == '_') {
      /* identifier or reserved word */
      size_t l = readname(LS);
      TString *ts = luaS_newlstr(LS->L, luaZ_buffer(LS->buff), l);
      if (ts->tsv.reserved > 0)  /* reserved word? */
        return ts->tsv.reserved - 1 + FIRST_RESERVED;
      seminfo->ts = ts;
      return TK_NAME;
    }
    else {
      int c = LS->current;
      if (iscntrl(c))
        luaX_error(LS, "invalid control char",
                       luaO_pushfstring(LS->L, "char(%d)", c));
      next(LS);
      return c;  /* single-char tokens (+ - / ...) */
    }
  }
}

} }

static void read_long_string (LexState *LS, SemInfo seminfo) { int cont = 0; size_t l = 0; checkbuffer(LS, l); save(LS, '[', l); / save first [' */ save_and_next(LS, l); /* pass the second [' / if (LS->current == '\n') / string starts with a newline? / inclinenumber(LS); / skip it / for (;;) { checkbuffer(LS, l); switch (LS->current) { case EOZ: save(LS, '\0', l); luaX_lexerror(LS, (seminfo) ? "unfinished long string" : "unfinished long comment", TK_EOS); break; / to avoid warnings */ case '[': save_and_next(LS, l); if (LS->current == '[') { cont++; save_and_next(LS, l); } continue; case ']': save_and_next(LS, l); if (LS->current == ']') { if (cont == 0) goto endloop; cont--; save_and_next(LS, l); } continue;

// added //------------------------------ case '|': save(LS, ']', l);

    LS->lookahead.token = TK_CONCAT;
    goto endloop;
    continue;

//------------------------------

  case '\n':
    save(LS, '\n', l);
    inclinenumber(LS);
    if (!seminfo) l = 0;  /* reset buffer to avoid wasting space */
    continue;
  default:
    save_and_next(LS, l);
}

} endloop: save_and_next(LS, l); /* skip the second `]' */ save(LS, '\0', l); if (seminfo) seminfo->ts = luaS_newlstr(LS->L, luaZ_buffer(LS->buff) + 2, l - 5); }

--Sam Lie

Note: the above patch is broken in 5.1. Make sure that f [[Hello |name|, welcome to |get_company_name()|. ]] translates into f([[Hello ]]..name..[[, welcome to ]]..get_company_name()..[[. ]]). Alternately translate it to f([[Hello ]], name, [[, welcome to ]], get_company_name(), [[. ]]). Perhaps use [[ ]] rather than | | to break out of the string because nested [[ ]] are deprecated in Lua 5.1 and by default raise an error, so we are free to redefine its semantics. --DavidManura

Metalua

For a MetaLua implementation, see "String Interpolation" in MetaLuaRecipes.

Var Expand

VarExpand - Advanced version of bash-like inline variable expanding.

Custom searcher that preprocesses

See [gist1338609] (--DavidManura), which installs a custom searcher function that preprocesses modules being loaded. The example preprocessor given does string interpolation:

local M = {}

local function printf(s, ...) local vals = {...} local i = 0 s = s:gsub('\0[^\0]*\0', function() i = i + 1 return tostring(vals[i]) end) print(s) end

function M.test() local x = 16 printf("value is $(math.sqrt(x)) ") end

return M

Other Ideas

Other Possible Applications

Embedding expressions inside strings can have these applications:


RecentChanges · preferences
edit · history
Last edited January 20, 2023 6:57 pm GMT (diff)