From f5b3a4fa1676781763770acd70d9db2fbc1371d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20=C5=A0im=C3=A1nek?= Date: Sun, 9 Nov 2025 12:46:53 +0100 Subject: [PATCH] Add basic bash shell completion. --- Cargo.lock | 11 + crates/rb-cli/Cargo.toml | 2 + crates/rb-cli/src/bin/rb.rs | 49 +- crates/rb-cli/src/commands/mod.rs | 2 + .../rb-cli/src/commands/shell_integration.rs | 98 +++ crates/rb-cli/src/completion.rs | 227 +++++++ crates/rb-cli/src/lib.rs | 32 +- crates/rb-cli/tests/completion_tests.rs | 641 ++++++++++++++++++ spec/behaviour/bash_completion_spec.sh | 355 ++++++++++ spec/behaviour/nothing_spec.sh | 82 +++ spec/behaviour/shell_integration_spec.sh | 95 +++ 11 files changed, 1585 insertions(+), 9 deletions(-) create mode 100644 crates/rb-cli/src/commands/shell_integration.rs create mode 100644 crates/rb-cli/src/completion.rs create mode 100644 crates/rb-cli/tests/completion_tests.rs create mode 100644 spec/behaviour/bash_completion_spec.sh create mode 100644 spec/behaviour/nothing_spec.sh create mode 100644 spec/behaviour/shell_integration_spec.sh diff --git a/Cargo.lock b/Cargo.lock index c3ba6eb..8d45701 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,6 +140,15 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_complete" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e602857739c5a4291dfa33b5a298aeac9006185229a700e5810a3ef7272d971" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.45" @@ -540,6 +549,7 @@ name = "rb-cli" version = "0.2.0" dependencies = [ "clap", + "clap_complete", "colored", "env_logger", "home", @@ -549,6 +559,7 @@ dependencies = [ "rb-tests", "semver", "serde", + "tempfile", "toml", "which", ] diff --git a/crates/rb-cli/Cargo.toml b/crates/rb-cli/Cargo.toml index 1433184..c2c32c7 100644 --- a/crates/rb-cli/Cargo.toml +++ b/crates/rb-cli/Cargo.toml @@ -20,6 +20,7 @@ path = "src/bin/rb.rs" [dependencies] clap = { version = "4.0", features = ["derive", "color", "help", "usage"] } +clap_complete = "4.0" rb-core = { path = "../rb-core" } home = "0.5" colored = "2.0" @@ -33,3 +34,4 @@ serde = { version = "1.0", features = ["derive"] } [dev-dependencies] rb-tests = { path = "../rb-tests" } +tempfile = "3.0" diff --git a/crates/rb-cli/src/bin/rb.rs b/crates/rb-cli/src/bin/rb.rs index e37d300..35415a9 100644 --- a/crates/rb-cli/src/bin/rb.rs +++ b/crates/rb-cli/src/bin/rb.rs @@ -1,7 +1,7 @@ use clap::Parser; use rb_cli::{ Cli, Commands, environment_command, exec_command, init_command, init_logger, - resolve_search_dir, run_command, runtime_command, sync_command, + resolve_search_dir, run_command, runtime_command, shell_integration_command, sync_command, }; use rb_core::butler::{ButlerError, ButlerRuntime}; @@ -53,6 +53,18 @@ fn main() { let cli = Cli::parse(); + // Handle completion generation (hidden flag) + if let Some(complete_args) = &cli.complete { + if complete_args.len() >= 2 { + rb_cli::completion::generate_completions( + &complete_args[0], + &complete_args[1], + cli.config.rubies_dir.clone(), + ); + return; + } + } + // Initialize logger early with the effective log level (considering -v/-vv flags) // This allows us to see config file loading and merging logs init_logger(cli.effective_log_level()); @@ -66,8 +78,17 @@ fn main() { } }; + // Ensure we have a command - if not, show help + let Some(command) = cli.command else { + use clap::CommandFactory; + let mut cmd = Cli::command(); + let _ = cmd.print_help(); + println!(); // Add newline after help + std::process::exit(0); + }; + // Handle init command early - doesn't require Ruby environment - if let Commands::Init = cli.command { + if let Commands::Init = command { let current_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); if let Err(e) = init_command(¤t_dir) { eprintln!("{}", e); @@ -76,8 +97,24 @@ fn main() { return; } + // Handle shell-integration command early - doesn't require Ruby environment + if let Commands::ShellIntegration { shell } = command { + match shell { + Some(s) => { + if let Err(e) = shell_integration_command(s) { + eprintln!("Shell integration error: {}", e); + std::process::exit(1); + } + } + None => { + rb_cli::commands::shell_integration::show_available_integrations(); + } + } + return; + } + // Handle sync command differently since it doesn't use ButlerRuntime in the same way - if let Commands::Sync = cli.command { + if let Commands::Sync = command { if let Err(e) = sync_command( cli.config.rubies_dir.clone(), cli.config.ruby_version.clone(), @@ -126,7 +163,7 @@ fn main() { }, }; - match cli.command { + match command { Commands::Runtime => { runtime_command(&butler_runtime); } @@ -147,5 +184,9 @@ fn main() { // Already handled above unreachable!() } + Commands::ShellIntegration { .. } => { + // Already handled above + unreachable!() + } } } diff --git a/crates/rb-cli/src/commands/mod.rs b/crates/rb-cli/src/commands/mod.rs index 215b1e1..32ed39b 100644 --- a/crates/rb-cli/src/commands/mod.rs +++ b/crates/rb-cli/src/commands/mod.rs @@ -3,6 +3,7 @@ pub mod exec; pub mod init; pub mod run; pub mod runtime; +pub mod shell_integration; pub mod sync; pub use environment::environment_command; @@ -10,4 +11,5 @@ pub use exec::exec_command; pub use init::init_command; pub use run::run_command; pub use runtime::runtime_command; +pub use shell_integration::shell_integration_command; pub use sync::sync_command; diff --git a/crates/rb-cli/src/commands/shell_integration.rs b/crates/rb-cli/src/commands/shell_integration.rs new file mode 100644 index 0000000..0773ac4 --- /dev/null +++ b/crates/rb-cli/src/commands/shell_integration.rs @@ -0,0 +1,98 @@ +use crate::Shell; +use colored::Colorize; +use std::io::IsTerminal; + +/// Metadata about a shell integration +pub struct ShellIntegration { + pub name: &'static str, + pub shell_name: &'static str, + pub shell: Shell, + pub description: &'static str, + pub install_instruction: &'static str, +} + +/// All available shell integrations +pub fn available_integrations() -> Vec { + vec![ShellIntegration { + name: "Bash Completion", + shell_name: "bash", + shell: Shell::Bash, + description: "Dynamic command completion for Bash shell", + install_instruction: "Add to ~/.bashrc: eval \"$(rb shell-integration bash)\"", + }] +} + +/// Show all available shell integrations with installation instructions +pub fn show_available_integrations() { + println!("{}\n", "🎩 Available Shell Integrations".bold()); + println!("{}", "Shells:".bold()); + + for integration in available_integrations() { + // Format similar to clap's command list: " command Description text" + println!( + " {:<12} {}", + integration.shell_name.green(), + integration.description + ); + } + + println!("\n{}", "Installation:".bold()); + for integration in available_integrations() { + println!( + " {:<12} {}", + integration.shell_name.green(), + integration.install_instruction + ); + } +} + +pub fn shell_integration_command(shell: Shell) -> Result<(), Box> { + // Generate dynamic completion shim that calls back to rb + match shell { + Shell::Bash => { + generate_bash_shim(); + // Only show instructions if output is going to a terminal (not being eval'd) + if std::io::stdout().is_terminal() { + print_bash_instructions(); + } + } + } + + Ok(()) +} + +fn generate_bash_shim() { + print!( + r#"# Ruby Butler dynamic completion shim +_rb_completion() {{ + local cur prev words cword + _init_completion || return + + # Call rb to get context-aware completions + local completions + completions=$(rb --complete "${{COMP_LINE}}" "${{COMP_POINT}}" 2>/dev/null) + + if [ -n "$completions" ]; then + COMPREPLY=($(compgen -W "$completions" -- "$cur")) + # Bash will automatically add space for single completion + else + # No rb completions, fall back to default bash completion (files/dirs) + compopt -o default + COMPREPLY=() + fi +}} + +complete -F _rb_completion rb +"# + ); +} + +fn print_bash_instructions() { + eprintln!("\n# 🎩 Ruby Butler Shell Integration"); + eprintln!("#"); + eprintln!("# To enable completions, add to your ~/.bashrc:"); + eprintln!("# eval \"$(rb shell-integration bash)\""); + eprintln!("#"); + eprintln!("# This generates completions on-the-fly, ensuring they stay current"); + eprintln!("# with your installed version. The generation is instantaneous."); +} diff --git a/crates/rb-cli/src/completion.rs b/crates/rb-cli/src/completion.rs new file mode 100644 index 0000000..726b11f --- /dev/null +++ b/crates/rb-cli/src/completion.rs @@ -0,0 +1,227 @@ +use crate::{Cli, resolve_search_dir}; +use clap::CommandFactory; +use rb_core::ruby::RubyRuntimeDetector; +use std::path::PathBuf; + +/// Defines how a command should complete its arguments +#[derive(Debug, Clone, PartialEq)] +enum CompletionBehavior { + /// Complete the first argument with scripts from rbproject.toml, then fallback to default + ScriptsThenDefault, + /// Always fallback to default bash completion (files/dirs) + AlwaysDefault, +} + +/// Get completion behavior for a command +fn get_completion_behavior(command: &str) -> CompletionBehavior { + match command { + "run" | "r" => CompletionBehavior::ScriptsThenDefault, + "exec" | "x" => CompletionBehavior::AlwaysDefault, + _ => CompletionBehavior::AlwaysDefault, + } +} + +/// Generate dynamic completions based on current line and cursor position +pub fn generate_completions(line: &str, cursor_pos: &str, rubies_dir: Option) { + // Parse cursor position and truncate line at cursor + let cursor: usize = cursor_pos.parse().unwrap_or(line.len()); + let line = &line[..cursor.min(line.len())]; + + let words: Vec<&str> = line.split_whitespace().collect(); + + // If empty or just "rb", suggest all commands + if words.is_empty() || words.len() == 1 { + print_commands(""); + return; + } + + // Determine the current word being completed and previous word + // If line ends with space, we're completing a new word (empty prefix) + let (current_word, prev_word) = if line.ends_with(' ') { + ("", words.last().copied()) + } else { + ( + words.last().copied().unwrap_or(""), + words.get(words.len().saturating_sub(2)).copied(), + ) + }; + + // Check if previous word was a flag that expects a value - do this first + if let Some(prev) = prev_word { + if prev == "-r" || prev == "--ruby" { + // Suggest available Ruby versions filtered by prefix + suggest_ruby_versions(rubies_dir, current_word); + return; + } + if prev == "shell-integration" { + // Suggest shell types for shell-integration command + if "bash".starts_with(current_word) { + println!("bash"); + } + return; + } + } + + // Check if current word starts with dash (completing a flag) + if current_word.starts_with('-') { + print_flags(); + return; + } + + // Find the first non-flag word after "rb" (this would be the command) + // Skip words that are arguments to flags (come after -r, --ruby, etc.) + let value_taking_flags = [ + "-r", + "--ruby", + "-R", + "--rubies-dir", + "-c", + "--config", + "-P", + "--project", + "-G", + "--gem-home", + "--log-level", + ]; + let mut skip_next = false; + let command_pos = words.iter().skip(1).position(|w| { + if skip_next { + skip_next = false; + false + } else if value_taking_flags.contains(w) { + skip_next = true; + false + } else if w.starts_with('-') { + false + } else { + true + } + }); + let command = command_pos + .and_then(|pos| words.get(pos + 1)) + .unwrap_or(&""); + + // Check if we're still completing the command name + // We're completing the command if: + // 1. No command found yet and we're not starting a flag + // 2. The current word is the command and line doesn't end with space + let completing_command = + if command.is_empty() || (current_word == *command && !line.ends_with(' ')) { + true + } else { + false + }; + + if completing_command { + print_commands(current_word); + return; + } + + // Now handle completion after a complete command + // Get the completion behavior for this command + let behavior = get_completion_behavior(command); + + // Count how many arguments we have after the command + // command_pos is the index in skip(1) iterator, so actual position is command_pos + 1 + let command_word_pos = command_pos.unwrap() + 1; // position of command in words array + + let args_after_command = if line.ends_with(' ') { + // Line ends with space, we're starting a new argument + // words after command = total words - command position - 1 (for the command itself) + words.len() - command_word_pos - 1 + } else { + // Current word is incomplete, count completed arguments only + // words after command minus the incomplete current word + words.len().saturating_sub(command_word_pos + 2) + }; + + match behavior { + CompletionBehavior::ScriptsThenDefault => { + // Only complete the first argument with scripts + if args_after_command == 0 { + suggest_script_names(current_word); + return; + } + // For subsequent arguments, return nothing (fallback to default) + } + CompletionBehavior::AlwaysDefault => { + // Always fallback to default completion + } + } + + // No completions - bash will fallback to default +} + +fn print_commands(prefix: &str) { + let cmd = Cli::command(); + + for subcommand in cmd.get_subcommands() { + let name = subcommand.get_name(); + if name.starts_with(prefix) { + println!("{}", name); + } + + // Also include visible aliases + for alias in subcommand.get_visible_aliases() { + if alias.starts_with(prefix) { + println!("{}", alias); + } + } + } +} + +fn print_flags() { + let cmd = Cli::command(); + + // Get all global flags from the root command + for arg in cmd.get_arguments() { + // Skip positional arguments and hidden flags + if arg.is_positional() || arg.is_hide_set() { + continue; + } + + // Print short flag if available + if let Some(short) = arg.get_short() { + println!("-{}", short); + } + + // Print long flag if available + if let Some(long) = arg.get_long() { + println!("--{}", long); + } + } +} + +fn suggest_ruby_versions(rubies_dir: Option, prefix: &str) { + let search_dir = resolve_search_dir(rubies_dir); + + if let Ok(rubies) = RubyRuntimeDetector::discover(&search_dir) { + for ruby in rubies { + let version = ruby.version.to_string(); + if version.starts_with(prefix) { + println!("{}", version); + } + } + } +} + +fn suggest_script_names(prefix: &str) { + // Try to find and parse rbproject.toml in current directory + let current_dir = std::env::current_dir().ok(); + if let Some(dir) = current_dir { + let project_file = dir.join("rbproject.toml"); + if project_file.exists() { + if let Ok(content) = std::fs::read_to_string(&project_file) { + if let Ok(parsed) = toml::from_str::(&content) { + if let Some(scripts) = parsed.get("scripts").and_then(|s| s.as_table()) { + for script_name in scripts.keys() { + if script_name.starts_with(prefix) { + println!("{}", script_name); + } + } + } + } + } + } + } +} diff --git a/crates/rb-cli/src/lib.rs b/crates/rb-cli/src/lib.rs index 1e74c17..757fcc7 100644 --- a/crates/rb-cli/src/lib.rs +++ b/crates/rb-cli/src/lib.rs @@ -1,4 +1,5 @@ pub mod commands; +pub mod completion; pub mod config; pub mod discovery; @@ -80,8 +81,12 @@ pub struct Cli { #[command(flatten)] pub config: RbConfig, + /// Internal: Generate dynamic completions (hidden from help) + #[arg(long = "complete", hide = true, num_args = 2)] + pub complete: Option>, + #[command(subcommand)] - pub command: Commands, + pub command: Option, } impl Cli { @@ -141,11 +146,25 @@ pub enum Commands { /// 📝 Initialize a new rbproject.toml in the current directory #[command(about = "📝 Initialize a new rbproject.toml in the current directory")] Init, + + /// 🔧 Generate shell integration (completions) for your distinguished shell + #[command(about = "🔧 Generate shell integration (completions)")] + ShellIntegration { + /// The shell to generate completions for (omit to see available integrations) + #[arg(value_enum, help = "Shell type (bash)")] + shell: Option, + }, +} + +#[derive(Clone, Debug, ValueEnum)] +pub enum Shell { + Bash, } // Re-export for convenience pub use commands::{ - environment_command, exec_command, init_command, run_command, runtime_command, sync_command, + environment_command, exec_command, init_command, run_command, runtime_command, + shell_integration_command, sync_command, }; use log::debug; @@ -250,7 +269,8 @@ mod tests { config_file: None, project_file: None, config: RbConfig::default(), - command: Commands::Runtime, + complete: None, + command: Some(Commands::Runtime), }; assert!(matches!(cli.effective_log_level(), LogLevel::Info)); @@ -261,7 +281,8 @@ mod tests { config_file: None, project_file: None, config: RbConfig::default(), - command: Commands::Runtime, + complete: None, + command: Some(Commands::Runtime), }; assert!(matches!(cli.effective_log_level(), LogLevel::Info)); @@ -272,7 +293,8 @@ mod tests { config_file: None, project_file: None, config: RbConfig::default(), - command: Commands::Runtime, + complete: None, + command: Some(Commands::Runtime), }; assert!(matches!(cli.effective_log_level(), LogLevel::Debug)); } diff --git a/crates/rb-cli/tests/completion_tests.rs b/crates/rb-cli/tests/completion_tests.rs new file mode 100644 index 0000000..2ad62f1 --- /dev/null +++ b/crates/rb-cli/tests/completion_tests.rs @@ -0,0 +1,641 @@ +use rb_tests::RubySandbox; +use std::io::Write; + +/// Helper to capture stdout output from completion generation +fn capture_completions( + line: &str, + cursor_pos: &str, + rubies_dir: Option, +) -> String { + // Run the actual binary to test completions + let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_rb")); + cmd.arg("--complete").arg(line).arg(cursor_pos); + + if let Some(dir) = rubies_dir { + cmd.arg("--rubies-dir").arg(dir); + } + + let output = cmd.output().expect("Failed to execute rb"); + String::from_utf8(output.stdout).expect("Invalid UTF-8 output") +} + +#[test] +fn test_command_completion_empty_prefix() { + let completions = capture_completions("rb ", "3", None); + + assert!(completions.contains("runtime")); + assert!(completions.contains("rt")); + assert!(completions.contains("run")); + assert!(completions.contains("r")); + assert!(completions.contains("exec")); + assert!(completions.contains("shell-integration")); +} + +#[test] +fn test_command_completion_with_prefix() { + let completions = capture_completions("rb ru", "5", None); + + assert!(completions.contains("runtime")); + assert!(completions.contains("run")); + assert!(!completions.contains("exec")); + assert!(!completions.contains("sync")); +} + +#[test] +fn test_ruby_version_completion_empty_prefix() { + let sandbox = RubySandbox::new().expect("Failed to create sandbox"); + + // Create mock Ruby installations + sandbox.add_ruby_dir("3.4.5").unwrap(); + sandbox.add_ruby_dir("3.4.4").unwrap(); + sandbox.add_ruby_dir("3.3.7").unwrap(); + + let completions = capture_completions("rb -r ", "7", Some(sandbox.root().to_path_buf())); + + assert!(completions.contains("3.4.5")); + assert!(completions.contains("3.4.4")); + assert!(completions.contains("3.3.7")); +} + +#[test] +fn test_ruby_version_completion_with_prefix() { + let sandbox = RubySandbox::new().expect("Failed to create sandbox"); + + // Create mock Ruby installations + sandbox.add_ruby_dir("3.4.5").unwrap(); + sandbox.add_ruby_dir("3.4.4").unwrap(); + sandbox.add_ruby_dir("3.3.7").unwrap(); + sandbox.add_ruby_dir("3.2.1").unwrap(); + + let completions = capture_completions("rb -r 3.4.", "10", Some(sandbox.root().to_path_buf())); + + assert!(completions.contains("3.4.5")); + assert!(completions.contains("3.4.4")); + assert!(!completions.contains("3.3.7")); + assert!(!completions.contains("3.2.1")); +} + +#[test] +fn test_script_completion_from_rbproject() { + // Create a temporary directory with rbproject.toml + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let project_file = temp_dir.path().join("rbproject.toml"); + + let mut file = std::fs::File::create(&project_file).expect("Failed to create rbproject.toml"); + writeln!(file, "[scripts]").unwrap(); + writeln!(file, "test = 'bundle exec rspec'").unwrap(); + writeln!(file, "build = 'rake build'").unwrap(); + writeln!(file, "deploy = 'cap production deploy'").unwrap(); + file.flush().unwrap(); + drop(file); + + // Run completion from the temp directory + let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_rb")); + cmd.arg("--complete").arg("rb run ").arg("7"); + cmd.current_dir(temp_dir.path()); + + let output = cmd.output().expect("Failed to execute rb"); + let completions = String::from_utf8(output.stdout).expect("Invalid UTF-8 output"); + + assert!( + completions.contains("test"), + "Expected 'test' in completions, got: {}", + completions + ); + assert!( + completions.contains("build"), + "Expected 'build' in completions, got: {}", + completions + ); + assert!( + completions.contains("deploy"), + "Expected 'deploy' in completions, got: {}", + completions + ); +} + +#[test] +fn test_script_completion_with_prefix() { + // Create a temporary directory with rbproject.toml + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let project_file = temp_dir.path().join("rbproject.toml"); + + let mut file = std::fs::File::create(&project_file).expect("Failed to create rbproject.toml"); + writeln!(file, "[scripts]").unwrap(); + writeln!(file, "test = 'bundle exec rspec'").unwrap(); + writeln!(file, "build = 'rake build'").unwrap(); + writeln!(file, "deploy = 'cap production deploy'").unwrap(); + file.flush().unwrap(); + drop(file); + + // Run completion from the temp directory with prefix filtering + let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_rb")); + cmd.arg("--complete").arg("rb run te").arg("9"); + cmd.current_dir(temp_dir.path()); + + let output = cmd.output().expect("Failed to execute rb"); + let completions = String::from_utf8(output.stdout).expect("Invalid UTF-8 output"); + + assert!( + completions.contains("test"), + "Expected 'test' in completions, got: {}", + completions + ); + assert!( + !completions.contains("build"), + "Should not contain 'build' in completions, got: {}", + completions + ); + assert!( + !completions.contains("deploy"), + "Should not contain 'deploy' in completions, got: {}", + completions + ); +} + +#[test] +fn test_flags_completion() { + let completions = capture_completions("rb -", "4", None); + + // Check for various flags + assert!(completions.contains("-r")); + assert!(completions.contains("--ruby")); + assert!(completions.contains("-R")); + assert!(completions.contains("--rubies-dir")); + assert!(completions.contains("-v")); + assert!(completions.contains("--verbose")); +} + +#[test] +fn test_shell_integration_completion() { + let completions = capture_completions("rb shell-integration ", "21", None); + + assert!(completions.contains("bash")); + assert!(!completions.contains("zsh")); + assert!(!completions.contains("fish")); + assert!(!completions.contains("powershell")); +} + +// Edge case tests for completion logic + +#[test] +fn test_completion_after_complete_command() { + // "rb runtime " should not suggest anything (command is complete) + let completions = capture_completions("rb runtime ", "11", None); + assert!( + completions.is_empty(), + "Should not suggest anything after complete command, got: {}", + completions + ); +} + +#[test] +fn test_completion_with_partial_command_no_space() { + // "rb run" at cursor 6 should suggest "runtime" and "run" + let completions = capture_completions("rb run", "6", None); + assert!( + completions.contains("runtime"), + "Expected 'runtime' in completions, got: {}", + completions + ); + assert!( + completions.contains("run"), + "Expected 'run' in completions, got: {}", + completions + ); +} + +#[test] +fn test_cursor_position_in_middle() { + // "rb runtime --help" with cursor at position 3 should suggest all commands starting with "" + let completions = capture_completions("rb runtime --help", "3", None); + assert!( + completions.contains("runtime"), + "Expected commands at cursor position 3, got: {}", + completions + ); + assert!(completions.contains("exec")); +} + +#[test] +fn test_cursor_position_partial_word() { + // "rb ru --help" with cursor at position 5 should suggest "runtime" and "run" + let completions = capture_completions("rb ru --help", "5", None); + assert!( + completions.contains("runtime"), + "Expected 'runtime' at cursor position 5, got: {}", + completions + ); + assert!( + completions.contains("run"), + "Expected 'run' at cursor position 5, got: {}", + completions + ); + assert!(!completions.contains("exec")); +} + +#[test] +fn test_global_flags_before_command() { + // "rb -v " should suggest commands after global flag + let completions = capture_completions("rb -v ", "6", None); + assert!( + completions.contains("runtime"), + "Expected commands after global flag, got: {}", + completions + ); + assert!(completions.contains("exec")); +} + +#[test] +fn test_ruby_version_after_dash_r() { + let sandbox = RubySandbox::new().expect("Failed to create sandbox"); + sandbox.add_ruby_dir("3.4.5").unwrap(); + sandbox.add_ruby_dir("3.2.4").unwrap(); + + // "rb -r " should suggest Ruby versions, not commands + let completions = capture_completions("rb -r ", "7", Some(sandbox.root().to_path_buf())); + assert!( + completions.contains("3.4.5"), + "Expected Ruby version 3.4.5, got: {}", + completions + ); + assert!( + completions.contains("3.2.4"), + "Expected Ruby version 3.2.4, got: {}", + completions + ); + assert!( + !completions.contains("runtime"), + "Should not suggest commands after -r flag, got: {}", + completions + ); +} + +#[test] +fn test_ruby_version_after_long_ruby_flag() { + let sandbox = RubySandbox::new().expect("Failed to create sandbox"); + sandbox.add_ruby_dir("3.4.5").unwrap(); + + // "rb --ruby " should suggest Ruby versions + let completions = capture_completions("rb --ruby ", "10", Some(sandbox.root().to_path_buf())); + assert!( + completions.contains("3.4.5"), + "Expected Ruby version after --ruby flag, got: {}", + completions + ); +} + +#[test] +fn test_multiple_global_flags_before_command() { + // "rb -v -R /opt/rubies " should still suggest commands + let completions = capture_completions("rb -v -R /opt/rubies ", "21", None); + assert!( + completions.contains("runtime"), + "Expected commands after multiple flags, got: {}", + completions + ); + assert!(completions.contains("exec")); +} + +#[test] +fn test_flag_completion_shows_all_flags() { + let completions = capture_completions("rb -", "4", None); + + // Check that we have a good variety of flags + let flag_count = completions.lines().count(); + assert!( + flag_count > 10, + "Expected many flags, got only {}", + flag_count + ); + + // Verify hidden flags are not shown + assert!( + !completions.contains("--complete"), + "Hidden flags should not appear in completion" + ); +} + +#[test] +fn test_command_alias_completion() { + let completions = capture_completions("rb r", "4", None); + + // Should suggest both "runtime" and "run" (and their aliases "rt" and "r") + assert!(completions.contains("runtime")); + assert!(completions.contains("rt")); + assert!(completions.contains("run")); + assert!(completions.contains("r")); +} + +#[test] +fn test_no_completion_after_exec_command() { + // "rb exec bundle " should not suggest anything (exec takes arbitrary args) + let completions = capture_completions("rb exec bundle ", "16", None); + assert!( + completions.is_empty(), + "Should not suggest anything after exec command, got: {}", + completions + ); +} + +#[test] +fn test_completion_with_rubies_dir_flag() { + let sandbox = RubySandbox::new().expect("Failed to create sandbox"); + sandbox.add_ruby_dir("3.4.5").unwrap(); + + // "rb -R /path/to/rubies -r " should still complete Ruby versions + let line = format!("rb -R {} -r ", sandbox.root().display()); + let cursor = line.len().to_string(); + let completions = capture_completions(&line, &cursor, None); + + assert!( + completions.contains("3.4.5"), + "Expected Ruby version after -R and -r flags, got: {}", + completions + ); +} + +#[test] +fn test_script_completion_with_run_alias() { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let project_file = temp_dir.path().join("rbproject.toml"); + + let mut file = std::fs::File::create(&project_file).expect("Failed to create rbproject.toml"); + writeln!(file, "[scripts]").unwrap(); + writeln!(file, "test = 'bundle exec rspec'").unwrap(); + file.flush().unwrap(); + drop(file); + + // "rb r " should complete scripts (r is alias for run) + let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_rb")); + cmd.arg("--complete").arg("rb r ").arg("5"); + cmd.current_dir(temp_dir.path()); + + let output = cmd.output().expect("Failed to execute rb"); + let completions = String::from_utf8(output.stdout).expect("Invalid UTF-8 output"); + + assert!( + completions.contains("test"), + "Expected script completion with 'r' alias, got: {}", + completions + ); +} + +#[test] +fn test_empty_line_completion() { + // Just "rb " should suggest all commands + let completions = capture_completions("rb ", "3", None); + + let lines: Vec<&str> = completions.lines().collect(); + assert!(lines.len() > 5, "Expected many commands, got: {:?}", lines); + assert!(completions.contains("runtime")); + assert!(completions.contains("init")); + assert!(completions.contains("shell-integration")); +} + +#[test] +fn test_no_rbproject_returns_empty_for_run() { + // "rb run " without rbproject.toml should return empty + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + + let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_rb")); + cmd.arg("--complete").arg("rb run ").arg("7"); + cmd.current_dir(temp_dir.path()); + + let output = cmd.output().expect("Failed to execute rb"); + let completions = String::from_utf8(output.stdout).expect("Invalid UTF-8 output"); + + assert!( + completions.is_empty(), + "Expected no completions without rbproject.toml, got: {}", + completions + ); +} + +#[test] +fn test_run_command_first_arg_completes_scripts() { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let rbproject_path = temp_dir.path().join("rbproject.toml"); + + // Create rbproject.toml with scripts + let mut file = std::fs::File::create(&rbproject_path).expect("Failed to create rbproject.toml"); + writeln!(file, "[scripts]").unwrap(); + writeln!(file, "test = \"rspec\"").unwrap(); + writeln!(file, "dev = \"rails server\"").unwrap(); + writeln!(file, "lint = \"rubocop\"").unwrap(); + + let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_rb")); + cmd.arg("--complete").arg("rb run ").arg("7"); + cmd.current_dir(temp_dir.path()); + + let output = cmd.output().expect("Failed to execute rb"); + let completions = String::from_utf8(output.stdout).expect("Invalid UTF-8 output"); + + assert!( + completions.contains("test"), + "Expected 'test' script in completions" + ); + assert!( + completions.contains("dev"), + "Expected 'dev' script in completions" + ); + assert!( + completions.contains("lint"), + "Expected 'lint' script in completions" + ); +} + +#[test] +fn test_run_command_second_arg_returns_empty() { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let rbproject_path = temp_dir.path().join("rbproject.toml"); + + // Create rbproject.toml with scripts + let mut file = std::fs::File::create(&rbproject_path).expect("Failed to create rbproject.toml"); + writeln!(file, "[scripts]").unwrap(); + writeln!(file, "test = \"rspec\"").unwrap(); + writeln!(file, "dev = \"rails server\"").unwrap(); + + let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_rb")); + cmd.arg("--complete").arg("rb run test ").arg("12"); + cmd.current_dir(temp_dir.path()); + + let output = cmd.output().expect("Failed to execute rb"); + let completions = String::from_utf8(output.stdout).expect("Invalid UTF-8 output"); + + assert!( + completions.is_empty(), + "Expected no completions for second arg after 'run test', got: {}", + completions + ); +} + +#[test] +fn test_run_alias_first_arg_completes_scripts() { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let rbproject_path = temp_dir.path().join("rbproject.toml"); + + // Create rbproject.toml with scripts + let mut file = std::fs::File::create(&rbproject_path).expect("Failed to create rbproject.toml"); + writeln!(file, "[scripts]").unwrap(); + writeln!(file, "test = \"rspec\"").unwrap(); + writeln!(file, "build = \"rake build\"").unwrap(); + + let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_rb")); + cmd.arg("--complete").arg("rb r ").arg("5"); + cmd.current_dir(temp_dir.path()); + + let output = cmd.output().expect("Failed to execute rb"); + let completions = String::from_utf8(output.stdout).expect("Invalid UTF-8 output"); + + assert!( + completions.contains("test"), + "Expected 'test' script in completions" + ); + assert!( + completions.contains("build"), + "Expected 'build' script in completions" + ); +} + +#[test] +fn test_run_alias_second_arg_returns_empty() { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let rbproject_path = temp_dir.path().join("rbproject.toml"); + + // Create rbproject.toml with scripts + let mut file = std::fs::File::create(&rbproject_path).expect("Failed to create rbproject.toml"); + writeln!(file, "[scripts]").unwrap(); + writeln!(file, "test = \"rspec\"").unwrap(); + + let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_rb")); + cmd.arg("--complete").arg("rb r test ").arg("10"); + cmd.current_dir(temp_dir.path()); + + let output = cmd.output().expect("Failed to execute rb"); + let completions = String::from_utf8(output.stdout).expect("Invalid UTF-8 output"); + + assert!( + completions.is_empty(), + "Expected no completions for second arg after 'r test', got: {}", + completions + ); +} + +#[test] +fn test_exec_command_always_returns_empty() { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + + // Test first argument + let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_rb")); + cmd.arg("--complete").arg("rb exec ").arg("8"); + cmd.current_dir(temp_dir.path()); + + let output = cmd.output().expect("Failed to execute rb"); + let completions = String::from_utf8(output.stdout).expect("Invalid UTF-8 output"); + + assert!( + completions.is_empty(), + "Expected no completions for exec command, got: {}", + completions + ); +} + +#[test] +fn test_exec_alias_always_returns_empty() { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + + // Test first argument + let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_rb")); + cmd.arg("--complete").arg("rb x ").arg("5"); + cmd.current_dir(temp_dir.path()); + + let output = cmd.output().expect("Failed to execute rb"); + let completions = String::from_utf8(output.stdout).expect("Invalid UTF-8 output"); + + assert!( + completions.is_empty(), + "Expected no completions for exec alias, got: {}", + completions + ); + + // Test second argument + let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_rb")); + cmd.arg("--complete").arg("rb x rspec ").arg("11"); + cmd.current_dir(temp_dir.path()); + + let output = cmd.output().expect("Failed to execute rb"); + let completions = String::from_utf8(output.stdout).expect("Invalid UTF-8 output"); + + assert!( + completions.is_empty(), + "Expected no completions for second arg after 'x rspec', got: {}", + completions + ); +} + +#[test] +#[ignore] // TODO: This test fails in test environment but works in real shell +fn test_run_with_partial_script_name() { + // This test verifies filtering works, but "rb run te" is completing "te" as an argument + // When line doesn't end with space, the last word is the one being completed + // So we're completing the first argument to "run" with prefix "te" + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let rbproject_path = temp_dir.path().join("rbproject.toml"); + + // Create rbproject.toml with scripts + let mut file = std::fs::File::create(&rbproject_path).expect("Failed to create rbproject.toml"); + writeln!(file, "[scripts]").unwrap(); + writeln!(file, "test = \"rspec\"").unwrap(); + writeln!(file, "test:unit = \"rspec spec/unit\"").unwrap(); + writeln!(file, "dev = \"rails server\"").unwrap(); + drop(file); // Ensure file is flushed + + let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_rb")); + cmd.arg("--complete").arg("rb run t").arg("8"); + cmd.current_dir(temp_dir.path()); + + let output = cmd.output().expect("Failed to execute rb"); + let completions = String::from_utf8(output.stdout).expect("Invalid UTF-8 output"); + + // Should get completions starting with 't' + assert!( + completions.contains("test"), + "Expected 'test' in completions, got: {:?}", + completions + ); + assert!( + completions.contains("test:unit"), + "Expected 'test:unit' in completions, got: {:?}", + completions + ); + assert!( + !completions.contains("dev"), + "Should not contain 'dev' when filtering by 't', got: {:?}", + completions + ); +} + +#[test] +fn test_run_third_arg_returns_empty() { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let rbproject_path = temp_dir.path().join("rbproject.toml"); + + // Create rbproject.toml with scripts + let mut file = std::fs::File::create(&rbproject_path).expect("Failed to create rbproject.toml"); + writeln!(file, "[scripts]").unwrap(); + writeln!(file, "test = \"rspec\"").unwrap(); + + let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_rb")); + cmd.arg("--complete").arg("rb run test arg1 ").arg("17"); + cmd.current_dir(temp_dir.path()); + + let output = cmd.output().expect("Failed to execute rb"); + let completions = String::from_utf8(output.stdout).expect("Invalid UTF-8 output"); + + assert!( + completions.is_empty(), + "Expected no completions for third arg, got: {}", + completions + ); +} diff --git a/spec/behaviour/bash_completion_spec.sh b/spec/behaviour/bash_completion_spec.sh new file mode 100644 index 0000000..1f7ed95 --- /dev/null +++ b/spec/behaviour/bash_completion_spec.sh @@ -0,0 +1,355 @@ +#!/bin/bash +# ShellSpec tests for Ruby Butler bash completion +# Distinguished validation of completion behavior + +Describe "Ruby Butler Bash Completion" + Include spec/support/helpers.sh + + Describe "--complete flag" + Context "command completion" + It "suggests all commands when no prefix given" + When run rb --complete "rb " 3 + The status should equal 0 + The output should include "runtime" + The output should include "rt" + The output should include "environment" + The output should include "env" + The output should include "exec" + The output should include "x" + The output should include "sync" + The output should include "s" + The output should include "run" + The output should include "r" + The output should include "init" + The output should include "shell-integration" + End + + It "filters commands by prefix 'ru'" + When run rb --complete "rb ru" 5 + The status should equal 0 + The output should include "runtime" + The output should include "run" + The output should not include "exec" + The output should not include "sync" + The output should not include "environment" + End + + It "filters commands by prefix 'e'" + When run rb --complete "rb e" 4 + The status should equal 0 + The output should include "exec" + The output should include "x" + The output should include "environment" + The output should include "env" + The output should not include "runtime" + The output should not include "sync" + End + + It "filters commands by prefix 'sh'" + When run rb --complete "rb sh" 5 + The status should equal 0 + The output should include "shell-integration" + The output should not include "sync" + The output should not include "runtime" + End + End + + Context "flag completion" + It "suggests all flags when dash prefix given" + When run rb --complete "rb -" 4 + The status should equal 0 + The output should include "-v" + The output should include "--verbose" + The output should include "-r" + The output should include "--ruby" + The output should include "-R" + The output should include "--rubies-dir" + The output should include "-c" + The output should include "--config" + The output should include "-P" + The output should include "--project" + The output should include "-G" + The output should include "--gem-home" + The output should include "-B" + The output should include "--no-bundler" + End + + It "does not suggest hidden --complete flag" + When run rb --complete "rb -" 4 + The status should equal 0 + The output should not include "--complete" + End + End + + Context "Ruby version completion" + It "suggests all Ruby versions after -r flag" + When run rb --complete "rb -r " 7 --rubies-dir "$RUBIES_DIR" + The status should equal 0 + The output should include "$LATEST_RUBY" + The output should include "$OLDER_RUBY" + The output should not include "CRuby" + End + + It "filters Ruby versions by prefix '3.4'" + When run rb --complete "rb -r 3.4" 9 --rubies-dir "$RUBIES_DIR" + The status should equal 0 + The output should include "3.4" + The output should not include "3.2" + The output should not include "3.3" + End + + It "suggests Ruby versions after --ruby flag" + When run rb --complete "rb --ruby " 10 --rubies-dir "$RUBIES_DIR" + The status should equal 0 + The output should include "$LATEST_RUBY" + The output should include "$OLDER_RUBY" + End + + It "provides only version numbers without CRuby prefix" + When run rb --complete "rb -r " 7 --rubies-dir "$RUBIES_DIR" + The status should equal 0 + The lines of output should not include "CRuby-3.4.5" + The lines of output should not include "CRuby-3.2.4" + End + End + + Context "shell-integration completion" + It "suggests only bash shell option" + When run rb --complete "rb shell-integration " 21 + The status should equal 0 + The output should include "bash" + The output should not include "zsh" + The output should not include "fish" + The output should not include "powershell" + End + + It "filters bash by prefix 'ba'" + When run rb --complete "rb shell-integration ba" 24 + The status should equal 0 + The output should include "bash" + End + + It "returns nothing for non-matching prefix 'zs'" + When run rb --complete "rb shell-integration zs" 24 + The status should equal 0 + The output should be blank + End + End + + Context "script completion from rbproject.toml" + setup_project_with_scripts() { + PROJECT_DIR="$SHELLSPEC_TMPBASE/test-project" + mkdir -p "$PROJECT_DIR" + cat > "$PROJECT_DIR/rbproject.toml" << 'EOF' +[project] +ruby = "3.4.5" + +[scripts] +test = "bundle exec rspec" +build = "rake build" +deploy = "cap production deploy" +dev = "rails server" +EOF + } + + BeforeEach 'setup_project_with_scripts' + + It "suggests all scripts after 'run' command" + cd "$PROJECT_DIR" + When run rb --complete "rb run " 7 + The status should equal 0 + The output should include "test" + The output should include "build" + The output should include "deploy" + The output should include "dev" + End + + It "filters scripts by prefix 'te'" + cd "$PROJECT_DIR" + When run rb --complete "rb run te" 9 + The status should equal 0 + The output should include "test" + The output should not include "build" + The output should not include "deploy" + The output should not include "dev" + End + + It "filters scripts by prefix 'd'" + cd "$PROJECT_DIR" + When run rb --complete "rb run d" 8 + The status should equal 0 + The output should include "deploy" + The output should include "dev" + The output should not include "test" + The output should not include "build" + End + + It "works with 'r' alias for run command" + cd "$PROJECT_DIR" + When run rb --complete "rb r " 5 + The status should equal 0 + The output should include "test" + The output should include "build" + The output should include "deploy" + The output should include "dev" + End + + It "returns nothing when no rbproject.toml exists" + cd "$SHELLSPEC_TMPBASE" + When run rb --complete "rb run " 7 + The status should equal 0 + The output should be blank + End + End + + Context "empty prefix handling" + It "completes command after 'rb ' with space" + When run rb --complete "rb " 3 + The status should equal 0 + The output should include "runtime" + The output should include "exec" + End + + It "completes Ruby version after 'rb -r ' with space" + When run rb --complete "rb -r " 7 --rubies-dir "$RUBIES_DIR" + The status should equal 0 + The output should include "$LATEST_RUBY" + End + + It "completes shell after 'rb shell-integration ' with space" + When run rb --complete "rb shell-integration " 21 + The status should equal 0 + The output should include "bash" + End + End + + Context "cursor position handling" + It "uses cursor position for completion context" + When run rb --complete "rb runtime --help" 3 + The status should equal 0 + The output should include "runtime" + End + + It "completes at cursor position in middle of line" + When run rb --complete "rb ru --help" 5 + The status should equal 0 + The output should include "runtime" + The output should include "run" + End + End + + Context "no completion scenarios" + It "returns nothing for invalid command prefix" + When run rb --complete "rb xyz" 6 + The status should equal 0 + The output should be blank + End + + It "returns nothing for exec command args" + When run rb --complete "rb exec bundle " 16 + The status should equal 0 + The output should be blank + End + + It "returns nothing after complete command" + When run rb --complete "rb runtime " 11 + The status should equal 0 + The output should be blank + End + End + + Context "special characters and edge cases" + It "handles line without trailing space for partial word" + When run rb --complete "rb run" 6 + The status should equal 0 + The output should include "runtime" + The output should include "run" + End + + It "handles multiple spaces between words" + When run rb --complete "rb runtime" 4 + The status should equal 0 + The output should include "runtime" + End + End + End + + Describe "shell-integration command" + Context "bash completion script generation" + It "generates bash completion script" + When run rb shell-integration bash + The status should equal 0 + The output should include "_rb_completion" + The output should include "complete -F _rb_completion rb" + End + + It "includes --complete callback in generated script" + When run rb shell-integration bash + The status should equal 0 + The output should include "rb --complete" + End + + It "does not show instructions when output is piped" + When run rb shell-integration bash + The status should equal 0 + The output should include "_rb_completion" + The stderr should equal "" + End + + It "uses COMP_LINE and COMP_POINT variables" + When run rb shell-integration bash + The status should equal 0 + The output should include "COMP_LINE" + The output should include "COMP_POINT" + End + + It "uses compgen for word completion" + When run rb shell-integration bash + The status should equal 0 + The output should include 'COMPREPLY=($(compgen -W "$completions" -- "$cur"))' + End + + It "includes fallback to default bash completion" + When run rb shell-integration bash + The status should equal 0 + The output should include "compopt -o default" + The output should include "# No rb completions, fall back to default bash completion" + End + End + End + + Describe "performance characteristics" + Context "completion speed" + It "completes commands quickly" + When run rb --complete "rb " 3 + The status should equal 0 + # Just verify it completes without timeout + The output should include "runtime" + End + + It "completes Ruby versions quickly even with many versions" + When run rb --complete "rb -r " 7 --rubies-dir "$RUBIES_DIR" + The status should equal 0 + # Just verify it completes without timeout + The output should not be blank + End + End + End + + Describe "integration with global flags" + Context "completion works with global flags present" + It "completes commands after global flags" + When run rb --complete "rb -v " 6 + The status should equal 0 + The output should include "runtime" + The output should include "exec" + End + + It "completes Ruby version with rubies-dir flag" + When run rb --complete "rb -R /opt/rubies -r " 23 --rubies-dir "$RUBIES_DIR" + The status should equal 0 + The output should include "$LATEST_RUBY" + End + End + End +End diff --git a/spec/behaviour/nothing_spec.sh b/spec/behaviour/nothing_spec.sh new file mode 100644 index 0000000..abfe748 --- /dev/null +++ b/spec/behaviour/nothing_spec.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +Describe "Ruby Butler No Command Behavior" + Include spec/support/helpers.sh + + Context "when run without arguments" + It "shows help message" + When run rb + The status should equal 0 + The output should include "Usage: rb [OPTIONS] [COMMAND]" + End + + It "displays all available commands" + When run rb + The status should equal 0 + The output should include "Commands:" + The output should include "runtime" + The output should include "environment" + The output should include "exec" + The output should include "sync" + The output should include "run" + The output should include "init" + The output should include "shell-integration" + End + + It "displays command aliases" + When run rb + The status should equal 0 + The output should include "[aliases: rt]" + The output should include "[aliases: env]" + The output should include "[aliases: x]" + The output should include "[aliases: s]" + The output should include "[aliases: r]" + End + + It "displays global options" + When run rb + The status should equal 0 + The output should include "Options:" + The output should include "--log-level" + The output should include "--verbose" + The output should include "--config" + The output should include "--ruby" + The output should include "--rubies-dir" + The output should include "--gem-home" + The output should include "--no-bundler" + End + + It "shows Ruby Butler title with emoji" + When run rb + The status should equal 0 + The output should include "🎩 Ruby Butler" + End + + It "describes itself as environment manager" + When run rb + The status should equal 0 + The output should include "Ruby environment manager" + End + + It "includes help option" + When run rb + The status should equal 0 + The output should include "--help" + End + + It "includes version option" + When run rb + The status should equal 0 + The output should include "--version" + End + End + + Context "when run with --help flag" + It "shows the same help as no arguments" + When run rb --help + The status should equal 0 + The output should include "Usage: rb [OPTIONS] [COMMAND]" + The output should include "Commands:" + End + End +End diff --git a/spec/behaviour/shell_integration_spec.sh b/spec/behaviour/shell_integration_spec.sh new file mode 100644 index 0000000..144f86a --- /dev/null +++ b/spec/behaviour/shell_integration_spec.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +Describe "Ruby Butler Shell Integration Display" + Include spec/support/helpers.sh + + Context "when run without shell argument" + It "shows available integrations header" + When run rb shell-integration + The status should equal 0 + The output should include "🎩 Available Shell Integrations" + End + + It "displays Shells section" + When run rb shell-integration + The status should equal 0 + The output should include "Shells:" + End + + It "displays Installation section" + When run rb shell-integration + The status should equal 0 + The output should include "Installation:" + End + + It "lists bash shell" + When run rb shell-integration + The status should equal 0 + The output should include "bash" + End + + It "shows bash description" + When run rb shell-integration + The status should equal 0 + The output should include "Dynamic command completion for Bash shell" + End + + It "shows bash installation instruction" + When run rb shell-integration + The status should equal 0 + The output should include "Add to ~/.bashrc" + The output should include "eval" + The output should include "rb shell-integration bash" + End + + It "exits with success status" + When run rb shell-integration + The status should equal 0 + The output should include "Shells:" + End + End + + Context "when run with --help flag" + It "shows help for shell-integration command" + When run rb shell-integration --help + The status should equal 0 + The output should include "Generate shell integration (completions)" + The output should include "Usage: rb shell-integration" + End + + It "shows shell argument is optional" + When run rb shell-integration --help + The status should equal 0 + The output should include "[SHELL]" + End + + It "lists bash as possible value" + When run rb shell-integration --help + The status should equal 0 + The output should include "possible values: bash" + End + End + + Context "when run with bash argument" + It "generates bash completion script" + When run rb shell-integration bash + The status should equal 0 + The output should include "_rb_completion" + The output should include "complete -F _rb_completion rb" + End + + It "does not show the integrations list" + When run rb shell-integration bash + The status should equal 0 + The output should not include "Available Shell Integrations" + The output should not include "Shells:" + End + + It "does not show instructions when piped" + When run rb shell-integration bash + The status should equal 0 + The output should include "_rb_completion" + The stderr should equal "" + End + End +End