Parsing command-line arguments differently

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