simple_cli

Architecture

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:

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"):

  1. Convert to lowercase: "v"
  2. Check flag_values.has("v") - direct match?
  3. If not, check flag_short_to_long.item("v") - get long name
  4. 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.