shrub-core 0.0.4

Reusable core of a POSIX/bash shell (used by shrub-shell)
Documentation
//! Example of implementing a custom builtin command for a shrub-core based
//! shell.
//!
//! This example demonstrates best practices for:
//! - Creating a custom builtin command using the `Command` trait
//! - Defining custom error types with `thiserror`
//! - Parsing command-line arguments with `clap`
//! - Implementing proper error handling and exit code conversion
//! - Using the execution context to interact with shell state and I/O streams
//!
//! Run this example with:
//! ```bash
//! cargo run --package shrub-core --example custom-builtin
//! ```

use std::io::Write;

use anyhow::Result;
use clap::Parser;
use shrub_core::{ExecutionResult, builtins};

//
// Step 1 (optional): Define a custom error type for your builtin
// ==============================================
// We recommend using `thiserror` to create descriptive error types that can be
// converted to appropriate exit codes.
//

#[derive(Debug, thiserror::Error)]
enum GreetError {
    /// The requested repeat count is beyond the supported range.
    #[error("repeat count out of range")]
    RepeatCountOutOfRange,

    /// A shell error occurred during execution; we transparently forward error
    /// display to the underlying error.
    #[error(transparent)]
    ShellError(#[from] shrub_core::Error),

    /// An I/O error occurred.
    #[error("I/O error occurred during greeting: {0}")]
    IoError(#[from] std::io::Error),
}

// Mark your error type as a builtin error. This is required to use this error
// type in your command implementation.
impl shrub_core::BuiltinError for GreetError {}

// If you define a custom error type, you must map each error variant to an
// appropriate exit code. This ensures the shell interpreter will translate a
// returned error to the appropriate code during execution.
impl From<&GreetError> for shrub_core::ExecutionExitCode {
    fn from(value: &GreetError) -> Self {
        match value {
            GreetError::RepeatCountOutOfRange => Self::InvalidUsage,
            GreetError::ShellError(e) => e.into(),
            GreetError::IoError(_) => Self::GeneralError,
        }
    }
}

//
// Step 2 (recommended): Define your builtin command arguments
// ==============================================
// We recommend using the `clap` crate and the derive-able `clap::Parser` to
// define command-line arguments and options. This will simplify the work you
// need to do to provide helpful usage information and auto-generated argument
// validation.
//

/// Greet the user with a friendly message.
#[derive(Parser)]
struct GreetCommand {
    /// Number of times to repeat the greeting.
    #[arg(short = 'n', long = "repeat", default_value_t = 1)]
    repeat_count: usize,
}

//
// Step 3: Implement the Command trait
// ==============================================
// The `Command` trait requires implementing the `execute` method.
//

impl builtins::Command for GreetCommand {
    // Specify the error type you will use; this will either be your custom type
    // or the default-provided `shrub_core::Error` type.
    type Error = GreetError;

    async fn execute<SE: shrub_core::ShellExtensions>(
        &self,
        context: shrub_core::ExecutionContext<'_, SE>,
    ) -> Result<ExecutionResult, Self::Error> {
        // Additional validation.
        if self.repeat_count == 0 || self.repeat_count > 10 {
            return Err(GreetError::RepeatCountOutOfRange);
        }

        // For demonstration, we expand a greeting string using shell variable
        // expansion. This is a bit contrived, but it shows how to wrap
        // errors coming back from `shrub_core`.
        let greeting = context
            .shell
            .basic_expand_string(&context.params, "Hello, ${USER}!")
            .await?;

        // Execute the greeting.
        for _ in 0..self.repeat_count {
            writeln!(context.stdout(), "{greeting}")?;
        }

        // Return success
        Ok(ExecutionResult::success())
    }
}

//
// Step 4: Integrate your builtin into a shell
// ==============================================
// This example shows how to register and use your custom builtin.
//

type SE = shrub_core::extensions::DefaultShellExtensions;

async fn run_example() -> Result<()> {
    // Create a shell instance with custom builtin registered.
    let mut shell = shrub_core::Shell::builder()
        .builtin("greet", shrub_core::builtins::builtin::<GreetCommand, SE>())
        .build()
        .await?;

    // Demonstrate basic usage.
    let result = shell
        .run_string(
            "greet -n 4",
            &shrub_core::SourceInfo::default(),
            &shell.default_exec_params(),
        )
        .await?;
    println!("Exit code: {}\n", u8::from(result.exit_code));

    Ok(())
}

fn main() -> Result<()> {
    // Construct a `tokio` runtime for async execution
    let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()?;

    rt.block_on(run_example())?;

    Ok(())
}