seahorse

repository·master·Indexed 43 days ago

https://github.com/ksk001100/seahorse

A minimal CLI framework written in Rust (v2.2.0) for building command-line applications with typed flags, sub-commands, and structured error handling. It provides a builder-style interface via the App and Command structs to define application metadata, nested command hierarchies, and actions using closures.

Tokens
5.6K
Snippets
27
Records
31
Agent score
55%

What's inside seahorse

  1. Use the Context object to access parsed arguments and flags

    master

    The Context struct is passed to Action closures and serves as the primary interface for retrieving parsed command-line data. It separates positional arguments from flags.

    • args: A Vec<String> containing the remaining positional arguments after flags and their values have been removed.
    • flags: Internal storage for parsed flag names and their corresponding FlagValue or FlagError.
  2. How Command and subcommands work together

    master

    In seahorse, commands are organized in a tree structure. A top-level App contains Command objects. Each Command can optionally contain a list of subcommands.

    When a command is executed, the system attempts to match the provided arguments against registered subcommands first. If a match is found (either by name or by an alias), the execution descends into that subcommand. If no subcommand matches, the command's own action or action_with_result is executed. This hierarchy allows for both leaf-node actions and branching command structures.

    // Example of a hierarchical structure
    let sub = Command::new("list").action(|_| println!("listing..."));
    let parent = Command::new("db").command(sub);
    // Resulting CLI usage: `app db list`
  3. How App, Commands, and Actions work together

    master

    The seahorse::App acts as the central orchestrator for a CLI. The execution flow follows this hierarchy:

    1. Argument Parsing: run_with_result normalizes the provided Vec<String>.
    2. Command Selection: The app checks if the first argument matches a registered Command (by name or alias).
    3. Command Execution: If a command matches, its specific action is executed.
    4. Fallback to App Action: If no command matches, the app executes its own action or action_with_result.
    5. Help Handling: If -h or --help is present in the arguments, the application automatically displays its help text instead of executing the action.

    This structure allows you to build complex CLIs with nested subcommands or simple single-purpose tools using just the top-level action.

  4. Set a command action or action with result

    master

    A Command can perform an operation using either an Action or an ActionWithResult.

    • action: A closure that takes a &Context and returns nothing (()).
    • action_with_result: A closure that takes a &Context and returns a Result<(), Box<dyn Error>>.

    Note: You cannot set both. Attempting to do so will cause a panic.

    use seahorse::{Command, Context, Action, ActionWithResult};
    
    // Using .action()
    let cmd_action = Command::new("greet")
        .action(|c: &Context| println!("Hello!"));
    
    // Using .action_with_result()
    let cmd_result = Command::new("save")
        .action_with_result(|c: &Context| {
            // Perform logic that might fail
            Ok(())
        });
  5. Configure application metadata

    master

    The App struct provides a builder-style interface to set metadata for your CLI. Use the following methods:

    • .author(author: T): Sets the application author.
    • .description(description: T): Sets the application description.
    • .usage(usage: T): Sets the application usage string.
    • .version(version: T): Sets the application version.

    All these methods accept types that implement Into<String> and return Self to allow chaining.

    use seahorse::App;
    
    let app = App::new("cli")
        .author("John Doe")
        .description("A useful tool")
        .usage("cli [command] [args]")
        .version("1.0.0");
  6. Register commands in an application

    master

    Add subcommands to your application using the .command(command: Command) method.

    Note: You cannot register two commands with the same name; doing so will cause the application to panic at runtime.

    use seahorse::{App, Command};
    
    let command = Command::new("hello")
        .usage("cli hello [arg]")
        .action(|c| println!("{:?}", c.args));
    
    let app = App::new("cli")
        .command(command);
  7. Extract flag position and values from arguments

    master

    Once a Flag is defined, you can use it to parse a slice of strings (typically std::env::args()):

    1. option_index(&[String]): Returns the index of the flag in the argument vector. It checks both the primary --name and any configured aliases.
    2. value(Option<String>): Parses the string value into a FlagValue enum based on the flag's FlagType.

    Returns Err(FlagError::ArgumentError) if a required value is missing (for non-Bool types) or Err(FlagError::ValueTypeError) if parsing fails.

    use seahorse::{Flag, FlagType, FlagValue};
    
    let flag = Flag::new("age", FlagType::Int).alias("a");
    let args = vec!["cli".to_string(), "-a".to_string(), "25".to_string()];
    
    if let Some(idx) = flag.option_index(&args) { 
        // Note: value() usually takes the string following the flag index
        // In this implementation, you pass the actual value string
        let val_str = args.get(idx + 1).cloned();
        match flag.value(val_str) {
            Ok(FlagValue::Int(v)) => println!("Age is: {}", v),
            Err(e) => println!("Error: {:?}", e),
            _ => {}
        }
    }
  8. Retrieve integer flags with int_flag and int_flag_vec

    master

    Use int_flag to get the value of an integer flag as an isize. It returns Result<isize, FlagError>.

    Use int_flag_vec to get all values for a repeated integer flag. It returns Vec<Result<isize, FlagError>>.

    use seahorse::Context;
    
    fn action(c: &Context) {
        match c.int_flag("int") {
            Ok(i) => println!("{}", i),
            Err(e) => println!("{}", e),
        }
    }
    
    // For repeated flags
    fn action_repeated(c: &Context) {
        for i in c.int_flag_vec("int") {
           match i {
              Ok(val) => println!("{}", val),
              Err(e) => println!("{}", e),
           }
        }
    }
  9. Retrieve string flags with string_flag and string_flag_vec

    master

    Use string_flag to get the value of a string flag. It returns Result<String, FlagError>.

    Use string_flag_vec to get all values for a repeated string flag. It returns Vec<Result<String, FlagError>>.

    use seahorse::Context;
    
    fn action(c: &Context) {
        match c.string_flag("string") {
            Ok(s) => println!("{}", s),
            Err(e) => println!("{}", e),
        }
    }
    
    // For repeated flags
    fn action_repeated(c: &Context) {
       for s in c.string_flag_vec("string") {
          match s {
             Ok(val) => println!("{}", val),
             Err(e) => println!("{}", e),
          }
       }
    }
  10. Add flags to a command

    master

    You can attach flags to a command using the .flag() method. Flags are defined using the Flag struct and a FlagType (e.g., Bool, Int, Float, String).

    use seahorse::{Command, Flag, FlagType};
    
    let command = Command::new("config")
        .flag(Flag::new("port", FlagType::Int))
        .flag(Flag::new("debug", FlagType::Bool));
  11. Create nested subcommands

    master

    Commands can act as containers for other commands using the .command() method. This allows you to build complex CLI structures like git remote add.

    use seahorse::{Command, App};
    
    let sub_command = Command::new("world")
        .action(|_| println!("Hello world!"));
    
    let parent_command = Command::new("hello")
        .command(sub_command);
    
    let app = App::new("my_cli")
        .command(parent_command);