string.format like printf(1)

216 views
Skip to first unread message

Soni "They/Them" L.

unread,
Feb 2, 2025, 2:35:51 PMFeb 2
to Lua mailing list
bash function printf(1) takes a format string and a list of arguments
C function printf(3) takes a format string and a list of arguments

unlike C argument lists, bash argument lists have a known size, likewise
lua argument lists.
it would make sense for lua to loop the format string just like bash.
(but maybe, unlike bash, enforce multiples.)

thoughts?

Frank Dana (FeRD)

unread,
Feb 2, 2025, 5:21:00 PMFeb 2
to lua-l
On Sunday, February 2, 2025 at 2:35:51 PM UTC-5 Soni "They/Them" L. wrote:
bash function printf(1) takes a format string and a list of arguments
C function printf(3) takes a format string and a list of arguments

Python string %-substitution takes a format string and a sequence or mapping of positional or named arguments (respectively). Python method str.format() applies positional or named arguments to a given format string.

Python also has f-strings, which perform immediate interpolated substitution, like JavaScript backtick-strings and Perl strings by default.

unlike C argument lists, bash argument lists have a known size, likewise
lua argument lists.
it would make sense for lua to loop the format string just like bash.
(but maybe, unlike bash, enforce multiples.)

I disagree, mostly because I don't feel shell printf's weird format-string-reuse feature makes sense. It makes errors harder to catch.

If I write C code like this:

printf("%s: %d", label, x, y);

GCC will warn me, "warning: too many arguments for format".

If I write Python code like this:

print("%s: %d" % ("name", 1, 2))

I'll get a "TypeError: not all arguments converted during string formatting"

If I write it like this:

print("{0}: {1}".format("name", 1, 2))

...I'll get "name: 1" printed to stdout, with no mention of the fact that the third argument to .format() was unused. (This is also dumb, which is why str.format() is... not great.)

But in no cases will the format string be applied to arguments in turn until they are exhausted. Why would you want that?

I get why someone might want that in the shell because loops can be a pain to code, especially over multiple-variable lists. Being able to format a table as simply as this has some merit:

$ item_properties = (color red size L collar Roman)
$ printf "%10s | %s" $item_properties
     color | red
      size | L
    collar | Roman


But it has its down sides, including the loss of type validation of the arguments (not that shell printf type-validates the arguments even when it's not repeating the format):

$ item_properties=(color 3 size 45 collar Roman)
$ printf "%10s | %d\n" $item_properties        
     color | 3
      size | 45
    collar | 0

No other formatting system would let that go without complaint.

Plus, you're not likely to use string.format() like that in Lua because, like in C, you can't pass it multiple arguments via a list variable. This won't work:

item_properties = {"color", 3};
string.format("%10s | %d", item_properties);

You'd have to do this, even when the number of arguments IS the same as the format string:

string.format("%10s | %d", item_properties[1], item_properties[2]);

So you're not likely to want to pass even more arguments to that string.format(), than your format string has placeholders. If you want to repeat its use, you're going to do the sensible thing and use it in a loop:

item_properties = {color = 3, size = 45, collar = 2};
for k, v in pairs(item_properties) do
    io.write(string.format("%10s | %d\n", k, v));
end

That outputs just what we want:

     color | 3
      size | 45
    collar | 12


But despite that being easy to write in Lua, it's not so easy to write in the shell. There are no associative arrays / mappings, multi-variable loops are a pain, and there's no type checking. So to compensate, its printf has a quick, dirty, and unsafe looping feature that you wouldn't want (and don't find) anywhere else.

Frank Dana (FeRD)

unread,
Feb 2, 2025, 5:26:28 PMFeb 2
to lua-l
I'll just point out my own typos to save anyone else the trouble:

On Sunday, February 2, 2025 at 5:21:00 PM UTC-5 Frank Dana (FeRD) wrote:
I get why someone might want that in the shell because loops can be a pain to code, especially over multiple-variable lists. Being able to format a table as simply as this has some merit:

$ item_properties = (color red size L collar Roman)
$ printf "%10s | %s" $item_properties
     color | red
      size | L
    collar | Roman

I left out the \n at the end of the format string there.
 
item_properties = {color = 3, size = 45, collar = 2};
for k, v in pairs(item_properties) do
    io.write(string.format("%10s | %d\n", k, v));
end

That outputs just what we want:

     color | 3
      size | 45
    collar | 12

No, it outputs this, of course:

     color | 3
      size | 45
    collar | 2
 

Soni "They/Them" L.

unread,
Feb 3, 2025, 7:04:23 AMFeb 3
to lu...@googlegroups.com


On 2025-02-02 19:20, Frank Dana (FeRD) wrote:

I get why someone might want that in the shell because loops can be a pain to code, especially over multiple-variable lists. Being able to format a table as simply as this has some merit:

$ item_properties = (color red size L collar Roman)
$ printf "%10s | %s" $item_properties
     color | red
      size | L
    collar | Roman


But it has its down sides, including the loss of type validation of the arguments (not that shell printf type-validates the arguments even when it's not repeating the format):

$ item_properties=(color 3 size 45 collar Roman)
$ printf "%10s | %d\n" $item_properties        
     color | 3
      size | 45
    collar | 0

No other formatting system would let that go without complaint.

Plus, you're not likely to use string.format() like that in Lua because, like in C, you can't pass it multiple arguments via a list variable. This won't work:

item_properties = {"color", 3};
string.format("%10s | %d", item_properties);

You'd have to do this, even when the number of arguments IS the same as the format string:

string.format("%10s | %d", item_properties[1], item_properties[2]);

first, you forget Lua has table.unpack:

string.format("%s %d\n", table.unpack(t))

second, Lua also has vararg functions:

function(...) end

third, we can opt to check for multiples:

function custom_slow_and_memory_hungry_string_format_that_looks_at_varargs(f, ...)
  local parts = {}
  local n = select('#', ...)
  local k = select(2, string.gsub(f, "%%.", "%0")) - select(2, string.gsub(f, "%%%%", "%0")) -- (is this even correct? probably not, but anyway)
  for i=1,n,k do
    table.insert(parts, string.format(f, select(i, ...))) -- will automatically error if n doesn't split into k
  end
  return table.concat(parts)
end

(this is actually *more likely to error* than lua's own string.format, believe it or not. but it is slow and memory-hungry, look at all those individually allocated parts instead of allocating it all in one go.)

Xavier Wang

unread,
Feb 3, 2025, 8:16:02 AMFeb 3
to lu...@googlegroups.com
> Plus, you're not likely to use string.format() like that in Lua because, like in C, you can't pass it multiple arguments via a list variable. This won't work:

> item_properties = {"color", 3};
> string.format("%10s | %d", item_properties);

> You'd have to do this, even when the number of arguments IS the same as the format string:

> string.format("%10s | %d", item_properties[1], item_properties[2]);

I made a `lua-fmt` module before (`luarocks install lua-fmt`), you could:

```lua
print(fmt("{1.1:10} | {1.2}", item_properties)) --> outputs: color | 3
```

You could even:

```lua
local obj = {color = "red", num = 3}
print(fmt("{color:10} | {num}", obj)) --> outputs: read | 3
```

--
regards,
Xavier Wang.

Frank Dana (FeRD)

unread,
Feb 6, 2025, 5:53:32 AMFeb 6
to lua-l
On Monday, February 3, 2025 at 8:16:02 AM UTC-5 Xavier Wang wrote:
I made a `lua-fmt` module before (`luarocks install lua-fmt`), you could:

```lua
print(fmt("{1.1:10} | {1.2}", item_properties)) --> outputs: color | 3
```

You could even:

```lua
local obj = {color = "red", num = 3}
print(fmt("{color:10} | {num}", obj)) --> outputs: read | 3
```

See, now THAT is useful — and very cool!

(Though IMHO using bare {} as the replacement tags — omitting the leading $ that JS backtick-strings, for instance, use — makes the same mistake that Python did with their .format() / f-strings. It seems fine... until you try to use it to output JSON, and keep having to remember to double all of your opening and closing braces.) 

Xavier Wang

unread,
Feb 6, 2025, 7:55:03 AMFeb 6
to lu...@googlegroups.com

> (Though IMHO using bare {} as the replacement tags — omitting the leading $ that JS backtick-strings, for instance, use — makes the same mistake that Python did with their .format() / f-strings. It seems fine... until you try to use it to output JSON, and keep having to remember to double all of your opening and closing braces.) 

But it already goes into the C++ standard, that’s what I followed. This module is implemented after the spec of the C++ fmt library. 

Lars Müller

unread,
Feb 6, 2025, 9:41:24 AMFeb 6
to lu...@googlegroups.com

Using a barebones fmt module to output JSON seems like a mistake.

Instead you should build the appropriate Lua table and pass that to a JSON library.

That way you essentially get syntax checking at Lua load time (because the Lua table syntax is checked)
and you can't make mistakes such as poorly formatting numbers or not escaping strings.

--
You received this message because you are subscribed to the Google Groups "lua-l" group.
To unsubscribe from this group and stop receiving emails from it, send an email to lua-l+un...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/lua-l/e3c33503-850b-4966-bd62-efc33b88dfe3n%40googlegroups.com.

Denis Dos Santos Silva

unread,
Feb 9, 2025, 9:58:34 PMFeb 9
to lua-l
Reply all
Reply to author
Forward
0 new messages