parse_args

A small command-line flag parser built on os.args().

Oxigen
introduce {Parser} from parse_args

Import the struct by name. A struct is not constructible through a module namespace, so parse_args.Parser does not work — introduce {Parser} from parse_args does.

ItemSignaturePurpose
Parserstructholds the normalized args and the flag map
define_flagparser.define_flag(verbose_name, alias?)declare a flag
handle_argsparser.handle_args()parse and populate in one call
parse_argsparser.parse_args()read and normalize os.args()
process_argsparser.process_args()fill args_map from the normalized args
normalize_arraynormalize_array(arr)join --k v pairs into --k=v
k_v_pairk_v_pair(s)split "k=v" into a tuple
double_dash / single_dashpatternsflag-shape tests

#Quick start

Oxigen
introduce {Parser} from parse_args

main {
    p <Parser>
    p.define_flag(verbose_name="name", alias="n")
    p.define_flag(verbose_name="count")
    p.handle_args()

    println(p.args_map.name)
    println(p.args_map.count)
}
$ oxigen script.oxi --name=Alice --count 5
Alice
5

$ oxigen script.oxi -n Bob
Bob

Both --flag=value and --flag value are accepted, and a one-character alias may be given with a single dash.


#Struct: Parser

FieldTypeContents
argsarraythe normalized argument list, every flag as key=value
args_mapmapdeclared flag → value; "" for a flag that was not passed
aliasesmapalias → long name, filled by define_flag

Declare one with no arguments — both fields start empty:

Oxigen
p <Parser>

#parser.define_flag(verbose_name, alias?)

Declare a flag. verbose_name is the long form (used as --name), alias an optional one-character short form (used as -n).

Oxigen
p.define_flag(verbose_name="name", alias="n")
p.define_flag(verbose_name="count")

Both names are keys in args_map. A value given through the alias fills both, so after -n x args_map["name"] and args_map["n"] are both x; after --name=x only args_map["name"] is. Read the long name:

Oxigen
name := p.args_map.name

Errors:

ConditionError
verbose_name is empty<Error<too_short>> "Verbose name is too short…"
alias longer than 1 character<Error<too_long>> "Alias name is too long…"
alias is an empty string<Error<too_short>> "Alias name is empty…"

#parser.handle_args()

Run parse_args then process_args. This is the one to call.

Oxigen
p.handle_args()

#parser.parse_args()

Read os.args(), normalize it, and store the result in self.args. Fails with "Length of args = 0. Expected at least 1." when the script was given no arguments at all — so a script whose flags are all optional should guard:

Oxigen
p.handle_args() unless len(os.args()) == 0

#parser.process_args()

Walk self.args and copy each recognised flag's value into args_map. An unrecognised flag is skipped with a log line, not an error:

2026-08-06 16:24:41: Key: --unknown is undefined. Skipping.

Positional arguments (anything with no -) are ignored entirely — read them from os.args() yourself.


#Module functions

#normalize_array(arr)

Turn a raw argument list into one where every flag carries its value: ["--name", "Alice"] becomes ["--name=Alice"]. An argument already containing =, one at the end of the list, and one followed by another flag are all left alone. A negative number (-5, -2.5) is a value, not a flag, so --offset -5 becomes --offset=-5.

Oxigen
introduce parse_args

println(parse_args.normalize_array(["--name", "Alice", "-v"]))
// [--name=Alice, -v]

#k_v_pair(s)

Split "key=value" into a (key, value) tuple, at the first = only, so a value may contain = itself.

Oxigen
println(parse_args.k_v_pair("--name=Alice"))           // (--name, Alice)
println(parse_args.k_v_pair("--url=http://x/?a=b"))    // (--url, http://x/?a=b)

#Patterns

PatternMatches
double_dash(s)s contains --
single_dash(s)s contains -

Used by process_args in a choose block. They test for a containing dash, not a prefix, so a value with a dash in it can match — the parser strips leading dashes from the key either way.


#Worked example

Oxigen
introduce {Parser} from parse_args
introduce os

fun usage() {
    println("usage: greet.oxi --name=NAME [--greeting=TEXT]")
    os.exit(2)
}

main {
    p <Parser>
    p.define_flag(verbose_name="name", alias="n")
    p.define_flag(verbose_name="greeting", alias="g")

    usage() when len(os.args()) == 0
    p.handle_args()

    name := option {
        p.args_map.name != "" -> { p.args_map.name }
        p.args_map.n != "" -> { p.args_map.n }
        { usage() }
    }
    greeting := option {
        p.args_map.greeting != "" -> { p.args_map.greeting }
        p.args_map.g != "" -> { p.args_map.g }
        { "Hello" }
    }

    println("{greeting}, {name}!")
}
$ oxigen greet.oxi --name=Ada
Hello, Ada!

$ oxigen greet.oxi -n Ada -g Hi
Hi, Ada!

#Limits

Worth knowing before you build a large CLI on it:

  • No --help generation, no required-flag checking, no type conversion — every value is a string.
  • No boolean flags: --verbose with no value stores "", which is indistinguishable from "not passed".
  • Long and short forms are separate keys, not aliases of one value.
  • No -- end-of-flags marker, and no subcommands.

For anything past that, read os.args() directly.


See also: os for args() and exit(), env and toml for configuration that does not come from the command line.