Architecture Overview
simple_cli is a single-class library that provides fluent argument parsing. It inherits from ARGUMENTS_32 for access to command-line arguments and provides a declarative API for defining options.
+------------------+
| SIMPLE_CLI |
+------------------+
| inherits |
| ARGUMENTS_32 |
+------------------+
|
+------------------+-------------------+-------------------+
| Definition | Parsing | Access |
+------------------+-------------------+-------------------+
| set_app_info() | parse() | has_flag() |
| add_flag() | handle_long_arg() | option_value() |
| add_option() | handle_short_arg()| integer_option() |
| add_required() | validate() | arguments() |
+------------------+-------------------+-------------------+
|
v
+------------------+
| Internal State |
+------------------+
| flag_values |
| option_values |
| arguments_list |
| errors_list |
+------------------+
Processing Phases
Phase 1: Definition
The application defines flags and options before parsing:
cli.set_app_info ("myapp", "Description", "1.0")
cli.add_flag ("v|verbose", "Enable verbose output")
cli.add_option ("o|output", "Output file", "FILE")
During definition, the library builds internal mappings:
flag_descriptions: Long name -> descriptionflag_short_to_long: Short name -> long nameoption_descriptions: Long name -> descriptionoption_short_to_long: Short name -> long nameoption_defaults: Long name -> default valuerequired_options: Long name -> True
Phase 2: Parsing
The parse feature processes command-line arguments:
parse
local
i: INTEGER
l_arg: STRING_32
do
from i := 1
until i > argument_count
loop
l_arg := argument (i)
if l_arg.starts_with ("--") then
i := handle_long_argument (l_arg, i)
elseif l_arg.starts_with ("-") then
i := handle_short_argument (l_arg, i)
else
arguments_list.extend (l_arg)
end
i := i + 1
end
validate_required_options
end
Phase 3: Access
After parsing, the application queries results:
if cli.has_flag ("verbose") then ... end
if attached cli.option_value ("output") as f then ... end
Argument Handling
Long Arguments (--name)
handle_long_argument (a_arg: STRING_32; a_index: INTEGER): INTEGER
local
l_name, l_value: STRING
l_eq_pos: INTEGER
do
Result := a_index
l_eq_pos := a_arg.index_of ('=', 1)
if l_eq_pos > 0 then
-- Format: --name=value
l_name := a_arg.substring (3, l_eq_pos - 1).as_lower
l_value := a_arg.substring (l_eq_pos + 1, a_arg.count)
option_values.force (l_value, l_name)
else
-- Format: --name or --name value
l_name := a_arg.substring (3, a_arg.count).as_lower
if is_flag (l_name) then
flag_values.force (True, l_name)
elseif is_option (l_name) then
-- Next argument is the value
l_value := argument (a_index + 1)
option_values.force (l_value, l_name)
Result := a_index + 1
end
end
end
Short Arguments (-x)
handle_short_argument (a_arg: STRING_32; a_index: INTEGER): INTEGER
-- Handle -v, -vd (combined), -ofile (attached value)
local
i: INTEGER
l_char: CHARACTER_32
l_short: STRING
do
Result := a_index
-- Iterate through each character after the dash
from i := 2
until i > a_arg.count
loop
l_char := a_arg.item (i)
l_short := l_char.out.as_lower
if is_short_flag (l_short) then
-- Set the flag
flag_values.force (True, long_for_short_flag (l_short))
elseif is_short_option (l_short) then
if i < a_arg.count then
-- Value is attached: -ofile.txt
l_value := a_arg.substring (i + 1, a_arg.count)
i := a_arg.count -- Exit loop
else
-- Value is next argument: -o file.txt
l_value := argument (a_index + 1)
Result := a_index + 1
end
option_values.force (l_value, long_for_short_option (l_short))
end
i := i + 1
end
end
Internal Data Structures
Definition Storage
| Attribute | Type | Purpose |
|---|---|---|
flag_descriptions |
HASH_TABLE [STRING, STRING] | Long name -> description |
flag_short_to_long |
HASH_TABLE [STRING, STRING] | Short name -> long name |
option_descriptions |
HASH_TABLE [STRING, STRING] | Long name -> description |
option_short_to_long |
HASH_TABLE [STRING, STRING] | Short name -> long name |
option_arg_names |
HASH_TABLE [STRING, STRING] | Long name -> placeholder (FILE, PATH) |
option_defaults |
HASH_TABLE [STRING, STRING] | Long name -> default value |
required_options |
HASH_TABLE [BOOLEAN, STRING] | Long name -> True |
Parse Results
| Attribute | Type | Purpose |
|---|---|---|
flag_values |
HASH_TABLE [BOOLEAN, STRING] | Long name -> True (if set) |
option_values |
HASH_TABLE [STRING, STRING] | Long name -> value |
arguments_list |
ARRAYED_LIST [STRING] | Positional arguments |
errors_list |
ARRAYED_LIST [STRING] | Error messages |
Name Resolution
All names are stored in lowercase for case-insensitive matching:
-- When adding a flag
add_flag (a_names, a_description: STRING)
local
l_parts: LIST [STRING]
l_short, l_long: STRING
do
l_parts := a_names.split ('|')
if l_parts.count >= 2 then
l_short := l_parts.first
l_long := l_parts.i_th (2)
-- Store mapping: short -> long (both lowercase)
flag_short_to_long.force (l_long.as_lower, l_short.as_lower)
else
l_long := a_names
end
-- Store description with lowercase key
flag_descriptions.force (a_description, l_long.as_lower)
end
Lookup Chain
When checking has_flag("v"):
- Convert to lowercase: "v"
- Check
flag_values.has("v")- direct match? - If not, check
flag_short_to_long.item("v")- get long name - Check
flag_values.has(long_name)
Help Text Generation
The help_text feature generates formatted help output:
help_text: STRING
do
create Result.make (500)
-- Header
Result.append (app_name + " v" + app_version + "%N")
Result.append (app_description + "%N")
-- Usage line
Result.append ("%NUsage: " + app_name + " [OPTIONS] [COMMAND] [ARGS...]%N")
-- Options section
Result.append ("%NOptions:%N")
-- Built-in flags
Result.append (" -h, --help%TShow this help message%N")
Result.append (" -V, --version%TShow version information%N")
-- User-defined flags
across all_flag_names as name loop
format_flag (name, Result)
end
-- User-defined options
across all_option_names as name loop
format_option (name, Result)
end
end
Output Format
myapp v1.0.0
My application description
Usage: myapp [OPTIONS] [COMMAND] [ARGS...]
Options:
-h, --help Show this help message
-V, --version Show version information
-v, --verbose Enable verbose output
-o, --output=FILE Output file path
-f, --format=FMT Output format (default: json)
-i, --input=FILE Input file [required]
Validation
Required Options
validate_required_options
do
across required_options as req loop
if not option_values.has (req.key) then
if not option_defaults.has (req.key) then
errors_list.extend ("Required option missing: --" + req.key)
end
end
end
end
Unknown Options
Errors are added when an option isn't recognized:
if not flag_descriptions.has (l_name) and
not option_descriptions.has (l_name) then
errors_list.extend ("Unknown option: --" + l_name)
end
Inheritance from ARGUMENTS_32
SIMPLE_CLI inherits from ARGUMENTS_32 to access command-line arguments:
| Inherited Feature | Purpose |
|---|---|
argument_count |
Number of arguments |
argument (i) |
Get argument at position i |
This avoids the need to pass arguments explicitly to the parser.
Design Decisions
Single Class
Unlike complex argument parsers with separate Flag, Option, and Command classes, simple_cli uses a single class. This simplifies usage while still supporting common CLI patterns.
Fluent API
Definition methods don't return self, but could be chained via local variable assignment. The linear definition style is clearer for CLI setup.
Built-in Help/Version
Most CLI applications need help and version flags. Providing them by default reduces boilerplate while allowing opt-out via disable_help_flag.
Case Insensitive
All option/flag names are stored lowercase. This matches user expectations (-V and -v are distinguished only by the uppercase rule for version).
Error Collection
Errors are collected rather than raising exceptions. This allows showing all errors at once rather than stopping at the first.