Skip to content

The Error Pipeline: From Parsing Failures to Compiler-Style Diagnostics

When a command-line user inputs an incorrect argument, a missing parameter, or an invalid value, clish does not just crash or output a generic usage string. Instead, it generates a beautiful, color-coded, compiler-style error report pointing directly to the problematic token.

This page explains how errors are represented, passed across the runtime boundary, and rendered to the terminal.


The Error Registry (ErrorKind)

Every parsing and validation failure maps to a specific variant of the ErrorKind enum:

pub enum ErrorKind {
    UnknownCommand { name: String },
    UnknownOption { token: String },
    MissingValue { name: String },
    MissingArgument { name: String },
    InvalidValue { token: String, expected: String },
    InvalidChoice { token: String, expected: String },
    Conflict { name: String, other: String },
    Requires { name: String, requires: String },
    OneshotWithOtherCommands,
}

Each variant is responsible for capturing the context of the error: * Token-based errors (UnknownOption, InvalidValue, InvalidChoice) capture the exact string the user typed. * Structural errors (Conflict, Requires) capture the long names of the parameters whose constraints were violated.


The Boundary Round-Trip Design

An interesting architectural aspect of clish is the string round-trip boundary between the generated macro closure and the runner:

graph TD
    subgraph closure ["Run Closure (macro-expanded)"]
        ErrEnum[ErrorKind] -->|"impl From#60;ErrorKind#62; for String"| ErrStr[String Representation]
    end

    ErrStr -->|Result::Err| Boundary((Closure Boundary))

    subgraph apprun ["App::run (clish-core)"]
        Boundary --> Catch[Catch Err#40;String#41;]
        Catch -->|parse_error_string| Recover[Reconstruct ErrorKind]
        Recover -->|print_error| StdErr[Format and print to stderr]
    end

Why use a string boundary?

  1. Crate Decoupling: The runtime closure pointer registered in the static command registry uses a clean, generic Rust signature: run: fn(&[String]) -> Result<(), String>. Using basic types prevents unnecessary exposure of the internal ErrorKind enum in the function pointer type.
  2. User Errors Compatibility: Returning a Result<(), String> leaves the door open to support custom errors returned from user functions, allowing them to propagate through the same error handling loop.

Reconstructing the Error

Inside App::run(), if the command execution closure returns an Err(String), clish-core runs a parsing routine called parse_error_string to reconstruct the original ErrorKind. It does this by checking prefixes like "unknown option: ", "missing value for option: ", or "invalid value '". Once reconstructed, the typed error is passed to the diagnostic printer.


Diagnostic Output Rendering (print_error)

Once the ErrorKind is recovered, the print_error function creates a diagnostic display that mimics the Rust compiler's error output. It prints three distinct sections: 1. Error Label & Message: e.g., error: unknown argument --verbose. 2. Source Code Context (print_source_line): Shows the exact CLI invocation with an underline pointing at the bad argument. 3. Actionable Hint: e.g., = hint: run 'myapp deploy --help' for more information.


How Pointing works (print_source_line)

To underline the exact error location, clish reconstructs the user's input line using the application name and the argument slice:

let line = format!("{} {}", app.name, args.join(" "));
The printer then searches the argument string for the problematic token. * If found, it computes the character column offset. * It prints a vertical separator line (|). * It pads the line with spaces up to the column offset and prints a caret highlight sequence (^^^^) matching the length of the invalid token.

Example Outputs

1. Unknown Command

If the user runs myapp deplooy --force, clish detects deplooy as an invalid command:

error: unknown command 'deplooy'
  |
1 | myapp deplooy --force
  |       ^^^^^^^
  |
  = hint: run 'myapp --help' for available commands

2. Missing Named Option Value

If the user passes --port without a value:

error: missing value for --port
  |
1 | myapp deploy --port
  |              ^^^^^^
  |
  = note: --port expects a value

3. Prerequisite Constraint Violated

If --port requires --host to be configured, but the user only supplied --port 80:

error: missing required argument: --port requires --host
  |
1 | myapp deploy --port 80
  |              ^^^^^^
  |
  = hint: run 'myapp deploy --help' for more information


Next, let's explore Help Rendering to see how parameter metadata is formatted into aligned CLI reference screens.