I just came up with a novel way to parse command-line arguments. Well, it's not new as such. I've just never seen it used for this purpose before.
You know recursive descent parsers? They work like this: start at a known point; check what comes next; decide where to go from there, and repeat. You're always looking at the following token: it's very natural when using shift in shell script for example. Even better, you need very little code to do it. Let's try in Lua:
local ArgMatch = {}
ArgMatch.__index = ArgMatch
function ArgMatch.new(args)
local self = setmetatable({}, ArgMatch)
self.args = args
self.cursor = 0
self.token = ""
return self
end
function ArgMatch:has_more()
return self.cursor < #self.args
end
function ArgMatch:match(text)
if self.args[self.cursor + 1] == text then
self.cursor = self.cursor + 1
self.token = self.args[self.cursor]
return self.token
else
return nil
end
end
function ArgMatch:match_string()
if self:has_more() then
self.cursor = self.cursor + 1
self.token = self.args[self.cursor]
return self.token
else
return nil
end
end
Surprisingly, that's enough to cover the basics, though you might want some extra methods for parsing numbers and booleans:
function ArgMatch:match_number()
local token = self.args[self.cursor + 1]
local value = tonumber(token)
if value then
self.cursor = self.cursor + 1
self.token = token
self.value = value
return token, value
else
return nil
end
end
function ArgMatch:match_boolean()
local token = self.args[self.cursor + 1]
if token == "true" or token == "yes" or token == "on" then
self.cursor = self.cursor + 1
self.token = token
self.value = true
return token, true
elseif token == "false" or token == "no" or token == "off" then
self.cursor = self.cursor + 1
self.token = token
self.value = false
return token, false
else
return nil
end
end
All in all, just enough code to fill a page of printer paper. As for how to use it:
local parser = ArgMatch.new(arg)
local end_args = false
local rev = false
local limit = 0
local files = {}
while parser:has_more() do
if end_args then
table.insert(files, parser:match_string())
elseif parser:match("--") then
end_args = true
elseif parser:match("-v") or parser:match("--version") then
print "Argument parser test version 2025-08-31"
os.exit()
elseif parser:match("-r") or parser:match("--reverse") then
if parser:match_boolean() then
rev = parser.value
else
rev = true
end
elseif parser:match("-c") or parser:match("--config") then
if parser:match_string() then
print "(pretending to run config file)" -- dofile(parser.token)
else
io.stderr:write("Missing config file name\n")
os.exit(1)
end
elseif parser:match("--limit") then
if parser:match_number() then
limit = parser.value
else
io.stderr:write("Bad limit argument\n")
os.exit(1)
end
else
table.insert(files, parser:match_string())
end
end
This is to prove that my little class can imitate GNU-style options, complete with mixing in positional arguments anywhere:
lua argmatch.lua --limit 10 a b -c config.lua c --reverse
But it can be much more flexible, up to a little language of sorts. Plus, the code is small enough to reuse via copy-paste. The only downside is that you have to write your own help text. But that's hardly unusual as such things go.
Try doing things differently one of these days, just for fun.
Similar posts as of 2025-11-01