@@ -126,23 +126,14 @@ pub(crate) fn execute_builtin( |
| 126 | 126 | "exit" => { |
| 127 | 127 | let code = args.first() |
| 128 | 128 | .and_then(|s| s.parse::<i32>().ok()) |
| 129 | | - .unwrap_or(0); |
| 130 | | - std::process::exit(code); |
| 129 | + .unwrap_or(context.last_exit_status); |
| 130 | + context.exit_requested = Some(code); |
| 131 | + Some(exit_code_to_result(code)) |
| 131 | 132 | } |
| 132 | 133 | "true" => Some(success_result()), |
| 133 | 134 | "false" => Some(error_result()), |
| 134 | 135 | ":" => Some(success_result()), |
| 135 | | - "cd" => { |
| 136 | | - let default_home = env::var("HOME").unwrap_or_else(|_| "/".to_string()); |
| 137 | | - let dir = args.first() |
| 138 | | - .map(|s| s.as_str()) |
| 139 | | - .unwrap_or(&default_home); |
| 140 | | - |
| 141 | | - match env::set_current_dir(dir) { |
| 142 | | - Ok(_) => Some(success_result()), |
| 143 | | - Err(_) => Some(error_result()), |
| 144 | | - } |
| 145 | | - } |
| 136 | + "cd" => Some(builtin_cd(args, context)), |
| 146 | 137 | "pwd" => { |
| 147 | 138 | match env::current_dir() { |
| 148 | 139 | Ok(path) => { |
@@ -225,8 +216,13 @@ pub(crate) fn execute_builtin( |
| 225 | 216 | eprintln!("{}: job control not supported on this platform", command); |
| 226 | 217 | Some(error_result()) |
| 227 | 218 | } |
| 219 | + "echo" => Some(builtin_echo(args)), |
| 228 | 220 | "printf" => Some(builtin_printf(args, context)), |
| 229 | 221 | "mapfile" | "readarray" => Some(builtin_mapfile(args, context)), |
| 222 | + "complete" => Some(builtin_complete(args, context)), |
| 223 | + "pushd" => Some(builtin_pushd(args, context)), |
| 224 | + "popd" => Some(builtin_popd(args, context)), |
| 225 | + "dirs" => Some(builtin_dirs(args, context)), |
| 230 | 226 | _ => None, |
| 231 | 227 | } |
| 232 | 228 | } |
@@ -2267,6 +2263,364 @@ fn builtin_eval(args: &[String], context: &mut rush_expand::Context) -> Result<E |
| 2267 | 2263 | } |
| 2268 | 2264 | } |
| 2269 | 2265 | |
| 2266 | +/// cd builtin - change directory |
| 2267 | +/// Supports: cd, cd -, cd ~, cd ~user, cd /path |
| 2268 | +fn builtin_cd(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult { |
| 2269 | + let home = env::var("HOME").unwrap_or_else(|_| "/".to_string()); |
| 2270 | + |
| 2271 | + // Get target directory |
| 2272 | + let target = if args.is_empty() { |
| 2273 | + // cd with no args goes to $HOME |
| 2274 | + home.clone() |
| 2275 | + } else { |
| 2276 | + let arg = &args[0]; |
| 2277 | + if arg == "-" { |
| 2278 | + // cd - goes to $OLDPWD |
| 2279 | + match context.get_var("OLDPWD") { |
| 2280 | + Some(oldpwd) => { |
| 2281 | + println!("{}", oldpwd); |
| 2282 | + oldpwd.to_string() |
| 2283 | + } |
| 2284 | + None => { |
| 2285 | + eprintln!("cd: OLDPWD not set"); |
| 2286 | + return error_result(); |
| 2287 | + } |
| 2288 | + } |
| 2289 | + } else if arg == "~" || arg.is_empty() { |
| 2290 | + // cd ~ goes to $HOME |
| 2291 | + home.clone() |
| 2292 | + } else if arg.starts_with("~/") { |
| 2293 | + // cd ~/path goes to $HOME/path |
| 2294 | + format!("{}/{}", home, &arg[2..]) |
| 2295 | + } else if arg.starts_with('~') { |
| 2296 | + // cd ~user - lookup user's home directory |
| 2297 | + let (username, suffix) = if let Some(slash_pos) = arg.find('/') { |
| 2298 | + (&arg[1..slash_pos], &arg[slash_pos..]) |
| 2299 | + } else { |
| 2300 | + (&arg[1..], "") |
| 2301 | + }; |
| 2302 | + |
| 2303 | + #[cfg(unix)] |
| 2304 | + { |
| 2305 | + use nix::unistd::User; |
| 2306 | + match User::from_name(username) { |
| 2307 | + Ok(Some(user)) => { |
| 2308 | + let home = user.dir.to_string_lossy(); |
| 2309 | + if suffix.is_empty() { |
| 2310 | + home.to_string() |
| 2311 | + } else { |
| 2312 | + format!("{}{}", home, suffix) |
| 2313 | + } |
| 2314 | + } |
| 2315 | + Ok(None) => { |
| 2316 | + eprintln!("cd: ~{}: No such user", username); |
| 2317 | + return error_result(); |
| 2318 | + } |
| 2319 | + Err(e) => { |
| 2320 | + eprintln!("cd: ~{}: {}", username, e); |
| 2321 | + return error_result(); |
| 2322 | + } |
| 2323 | + } |
| 2324 | + } |
| 2325 | + #[cfg(not(unix))] |
| 2326 | + { |
| 2327 | + // On non-Unix platforms, just treat as literal path |
| 2328 | + arg.clone() |
| 2329 | + } |
| 2330 | + } else { |
| 2331 | + arg.clone() |
| 2332 | + } |
| 2333 | + }; |
| 2334 | + |
| 2335 | + // Save current directory as OLDPWD before changing |
| 2336 | + if let Ok(cwd) = env::current_dir() { |
| 2337 | + let _ = context.set_var("OLDPWD", cwd.to_string_lossy().to_string()); |
| 2338 | + } |
| 2339 | + |
| 2340 | + // Change directory |
| 2341 | + match env::set_current_dir(&target) { |
| 2342 | + Ok(_) => { |
| 2343 | + // Update PWD |
| 2344 | + if let Ok(new_cwd) = env::current_dir() { |
| 2345 | + let _ = context.set_var("PWD", new_cwd.to_string_lossy().to_string()); |
| 2346 | + } |
| 2347 | + success_result() |
| 2348 | + } |
| 2349 | + Err(e) => { |
| 2350 | + eprintln!("cd: {}: {}", target, e); |
| 2351 | + error_result() |
| 2352 | + } |
| 2353 | + } |
| 2354 | +} |
| 2355 | + |
| 2356 | +/// pushd builtin - push directory onto stack and cd to it |
| 2357 | +fn builtin_pushd(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult { |
| 2358 | + // Get current directory first |
| 2359 | + let cwd = match env::current_dir() { |
| 2360 | + Ok(p) => p.to_string_lossy().to_string(), |
| 2361 | + Err(e) => { |
| 2362 | + eprintln!("pushd: error getting current directory: {}", e); |
| 2363 | + return error_result(); |
| 2364 | + } |
| 2365 | + }; |
| 2366 | + |
| 2367 | + if args.is_empty() { |
| 2368 | + // pushd with no args swaps top two directories |
| 2369 | + if context.dir_stack.is_empty() { |
| 2370 | + eprintln!("pushd: no other directory"); |
| 2371 | + return error_result(); |
| 2372 | + } |
| 2373 | + |
| 2374 | + let top = context.dir_stack.remove(0); |
| 2375 | + context.dir_stack.insert(0, cwd.clone()); |
| 2376 | + |
| 2377 | + // Change to the popped directory |
| 2378 | + if let Err(e) = env::set_current_dir(&top) { |
| 2379 | + // Restore stack on failure |
| 2380 | + context.dir_stack.remove(0); |
| 2381 | + context.dir_stack.insert(0, top); |
| 2382 | + eprintln!("pushd: {}", e); |
| 2383 | + return error_result(); |
| 2384 | + } |
| 2385 | + |
| 2386 | + // Update OLDPWD and PWD |
| 2387 | + let _ = context.set_var("OLDPWD", cwd); |
| 2388 | + let _ = context.set_var("PWD", top.clone()); |
| 2389 | + |
| 2390 | + // Print the stack |
| 2391 | + print_dir_stack(context); |
| 2392 | + success_result() |
| 2393 | + } else { |
| 2394 | + let target = &args[0]; |
| 2395 | + |
| 2396 | + // Expand ~ in target |
| 2397 | + let expanded = if target == "~" { |
| 2398 | + env::var("HOME").unwrap_or_else(|_| "/".to_string()) |
| 2399 | + } else if target.starts_with("~/") { |
| 2400 | + let home = env::var("HOME").unwrap_or_else(|_| "/".to_string()); |
| 2401 | + format!("{}/{}", home, &target[2..]) |
| 2402 | + } else { |
| 2403 | + target.clone() |
| 2404 | + }; |
| 2405 | + |
| 2406 | + // Push current directory onto stack |
| 2407 | + context.dir_stack.insert(0, cwd.clone()); |
| 2408 | + |
| 2409 | + // Change to new directory |
| 2410 | + if let Err(e) = env::set_current_dir(&expanded) { |
| 2411 | + // Remove the directory we just pushed on failure |
| 2412 | + context.dir_stack.remove(0); |
| 2413 | + eprintln!("pushd: {}: {}", expanded, e); |
| 2414 | + return error_result(); |
| 2415 | + } |
| 2416 | + |
| 2417 | + // Update OLDPWD and PWD |
| 2418 | + let _ = context.set_var("OLDPWD", cwd); |
| 2419 | + if let Ok(new_cwd) = env::current_dir() { |
| 2420 | + let _ = context.set_var("PWD", new_cwd.to_string_lossy().to_string()); |
| 2421 | + } |
| 2422 | + |
| 2423 | + // Print the stack |
| 2424 | + print_dir_stack(context); |
| 2425 | + success_result() |
| 2426 | + } |
| 2427 | +} |
| 2428 | + |
| 2429 | +/// popd builtin - pop directory from stack and cd to it |
| 2430 | +fn builtin_popd(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult { |
| 2431 | + // Check for -n flag (don't change directory, just manipulate stack) |
| 2432 | + let no_cd = args.iter().any(|a| a == "-n"); |
| 2433 | + |
| 2434 | + if context.dir_stack.is_empty() { |
| 2435 | + eprintln!("popd: directory stack empty"); |
| 2436 | + return error_result(); |
| 2437 | + } |
| 2438 | + |
| 2439 | + let dir = context.dir_stack.remove(0); |
| 2440 | + |
| 2441 | + if !no_cd { |
| 2442 | + // Save current directory as OLDPWD |
| 2443 | + if let Ok(cwd) = env::current_dir() { |
| 2444 | + let _ = context.set_var("OLDPWD", cwd.to_string_lossy().to_string()); |
| 2445 | + } |
| 2446 | + |
| 2447 | + // Change to the popped directory |
| 2448 | + if let Err(e) = env::set_current_dir(&dir) { |
| 2449 | + // Re-add directory to stack on failure |
| 2450 | + context.dir_stack.insert(0, dir); |
| 2451 | + eprintln!("popd: {}", e); |
| 2452 | + return error_result(); |
| 2453 | + } |
| 2454 | + |
| 2455 | + // Update PWD |
| 2456 | + if let Ok(new_cwd) = env::current_dir() { |
| 2457 | + let _ = context.set_var("PWD", new_cwd.to_string_lossy().to_string()); |
| 2458 | + } |
| 2459 | + } |
| 2460 | + |
| 2461 | + // Print the stack |
| 2462 | + print_dir_stack(context); |
| 2463 | + success_result() |
| 2464 | +} |
| 2465 | + |
| 2466 | +/// dirs builtin - display directory stack |
| 2467 | +fn builtin_dirs(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult { |
| 2468 | + let clear = args.iter().any(|a| a == "-c"); |
| 2469 | + let print_index = args.iter().any(|a| a == "-v"); |
| 2470 | + let one_per_line = args.iter().any(|a| a == "-p"); |
| 2471 | + |
| 2472 | + if clear { |
| 2473 | + context.dir_stack.clear(); |
| 2474 | + return success_result(); |
| 2475 | + } |
| 2476 | + |
| 2477 | + // Get current directory |
| 2478 | + let cwd = env::current_dir() |
| 2479 | + .map(|p| p.to_string_lossy().to_string()) |
| 2480 | + .unwrap_or_else(|_| ".".to_string()); |
| 2481 | + |
| 2482 | + if print_index { |
| 2483 | + // Print with indices |
| 2484 | + println!(" 0 {}", cwd); |
| 2485 | + for (i, dir) in context.dir_stack.iter().enumerate() { |
| 2486 | + println!(" {} {}", i + 1, dir); |
| 2487 | + } |
| 2488 | + } else if one_per_line { |
| 2489 | + // One directory per line |
| 2490 | + println!("{}", cwd); |
| 2491 | + for dir in &context.dir_stack { |
| 2492 | + println!("{}", dir); |
| 2493 | + } |
| 2494 | + } else { |
| 2495 | + // Default: space-separated on one line |
| 2496 | + print_dir_stack(context); |
| 2497 | + } |
| 2498 | + |
| 2499 | + success_result() |
| 2500 | +} |
| 2501 | + |
| 2502 | +/// Helper to print the directory stack |
| 2503 | +fn print_dir_stack(context: &rush_expand::Context) { |
| 2504 | + let cwd = env::current_dir() |
| 2505 | + .map(|p| p.to_string_lossy().to_string()) |
| 2506 | + .unwrap_or_else(|_| ".".to_string()); |
| 2507 | + |
| 2508 | + let mut parts = vec![cwd]; |
| 2509 | + parts.extend(context.dir_stack.iter().cloned()); |
| 2510 | + println!("{}", parts.join(" ")); |
| 2511 | +} |
| 2512 | + |
| 2513 | +/// echo builtin - display a line of text |
| 2514 | +/// |
| 2515 | +/// Uses direct writes to stdout for reliable capture in command substitution |
| 2516 | +fn builtin_echo(args: &[String]) -> ExecutionResult { |
| 2517 | + use std::io::Write; |
| 2518 | + |
| 2519 | + let mut newline = true; |
| 2520 | + let mut interpret_escapes = false; |
| 2521 | + let mut args_iter = args.iter().peekable(); |
| 2522 | + |
| 2523 | + // Parse options (bash-style: only at the start, stop at first non-option) |
| 2524 | + while let Some(arg) = args_iter.peek() { |
| 2525 | + if arg.starts_with('-') && arg.len() > 1 && arg.chars().skip(1).all(|c| matches!(c, 'n' | 'e' | 'E')) { |
| 2526 | + let arg = args_iter.next().unwrap(); |
| 2527 | + for c in arg.chars().skip(1) { |
| 2528 | + match c { |
| 2529 | + 'n' => newline = false, |
| 2530 | + 'e' => interpret_escapes = true, |
| 2531 | + 'E' => interpret_escapes = false, |
| 2532 | + _ => {} |
| 2533 | + } |
| 2534 | + } |
| 2535 | + } else { |
| 2536 | + break; |
| 2537 | + } |
| 2538 | + } |
| 2539 | + |
| 2540 | + let remaining: Vec<&String> = args_iter.collect(); |
| 2541 | + let text = remaining.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" "); |
| 2542 | + |
| 2543 | + // Get stdout handle and lock it for the duration of the write |
| 2544 | + let stdout = std::io::stdout(); |
| 2545 | + let mut handle = stdout.lock(); |
| 2546 | + |
| 2547 | + if interpret_escapes { |
| 2548 | + let mut output = String::new(); |
| 2549 | + let mut chars = text.chars().peekable(); |
| 2550 | + while let Some(c) = chars.next() { |
| 2551 | + if c == '\\' { |
| 2552 | + match chars.next() { |
| 2553 | + Some('n') => output.push('\n'), |
| 2554 | + Some('t') => output.push('\t'), |
| 2555 | + Some('r') => output.push('\r'), |
| 2556 | + Some('\\') => output.push('\\'), |
| 2557 | + Some('a') => output.push('\x07'), |
| 2558 | + Some('b') => output.push('\x08'), |
| 2559 | + Some('f') => output.push('\x0C'), |
| 2560 | + Some('v') => output.push('\x0B'), |
| 2561 | + Some('0') => { |
| 2562 | + // Octal escape |
| 2563 | + let mut oct = String::new(); |
| 2564 | + for _ in 0..3 { |
| 2565 | + if let Some(&ch) = chars.peek() { |
| 2566 | + if ch >= '0' && ch <= '7' { |
| 2567 | + oct.push(chars.next().unwrap()); |
| 2568 | + } else { |
| 2569 | + break; |
| 2570 | + } |
| 2571 | + } |
| 2572 | + } |
| 2573 | + let val = u8::from_str_radix(&oct, 8).unwrap_or(0); |
| 2574 | + output.push(val as char); |
| 2575 | + } |
| 2576 | + Some('x') => { |
| 2577 | + // Hex escape |
| 2578 | + let mut hex = String::new(); |
| 2579 | + for _ in 0..2 { |
| 2580 | + if let Some(&ch) = chars.peek() { |
| 2581 | + if ch.is_ascii_hexdigit() { |
| 2582 | + hex.push(chars.next().unwrap()); |
| 2583 | + } else { |
| 2584 | + break; |
| 2585 | + } |
| 2586 | + } |
| 2587 | + } |
| 2588 | + if !hex.is_empty() { |
| 2589 | + let val = u8::from_str_radix(&hex, 16).unwrap_or(0); |
| 2590 | + output.push(val as char); |
| 2591 | + } else { |
| 2592 | + output.push_str("\\x"); |
| 2593 | + } |
| 2594 | + } |
| 2595 | + Some('c') => { |
| 2596 | + // Stop output (no newline either) |
| 2597 | + let _ = handle.write_all(output.as_bytes()); |
| 2598 | + let _ = handle.flush(); |
| 2599 | + return success_result(); |
| 2600 | + } |
| 2601 | + Some(other) => { |
| 2602 | + output.push('\\'); |
| 2603 | + output.push(other); |
| 2604 | + } |
| 2605 | + None => output.push('\\'), |
| 2606 | + } |
| 2607 | + } else { |
| 2608 | + output.push(c); |
| 2609 | + } |
| 2610 | + } |
| 2611 | + let _ = handle.write_all(output.as_bytes()); |
| 2612 | + } else { |
| 2613 | + let _ = handle.write_all(text.as_bytes()); |
| 2614 | + } |
| 2615 | + |
| 2616 | + if newline { |
| 2617 | + let _ = handle.write_all(b"\n"); |
| 2618 | + } |
| 2619 | + |
| 2620 | + let _ = handle.flush(); |
| 2621 | + success_result() |
| 2622 | +} |
| 2623 | + |
| 2270 | 2624 | /// printf builtin - formatted output |
| 2271 | 2625 | fn builtin_printf(args: &[String], _context: &mut rush_expand::Context) -> ExecutionResult { |
| 2272 | 2626 | if args.is_empty() { |
@@ -2681,7 +3035,7 @@ fn builtin_disown(args: &[String], context: &mut rush_expand::Context) -> Execut |
| 2681 | 3035 | } |
| 2682 | 3036 | |
| 2683 | 3037 | /// Helper to execute a parsed statement |
| 2684 | | -fn execute_statement( |
| 3038 | +pub fn execute_statement( |
| 2685 | 3039 | statement: &rush_parser::Statement, |
| 2686 | 3040 | context: &mut rush_expand::Context, |
| 2687 | 3041 | ) -> Result<ExecutionResult, String> { |
@@ -2705,6 +3059,234 @@ fn execute_statement( |
| 2705 | 3059 | } |
| 2706 | 3060 | } |
| 2707 | 3061 | |
| 3062 | +/// `complete` builtin - Define command-specific completions |
| 3063 | +/// |
| 3064 | +/// Usage: |
| 3065 | +/// complete -c COMMAND [-a COMPLETIONS] [-d DESC] [-s SHORT] [-l LONG] [-f] [-n COND] |
| 3066 | +/// complete -c COMMAND -e # Erase completions |
| 3067 | +/// complete -p # Print all completions |
| 3068 | +/// complete -c COMMAND -p # Print completions for COMMAND |
| 3069 | +/// |
| 3070 | +/// Options: |
| 3071 | +/// -c COMMAND The command to add completions for |
| 3072 | +/// -a ARGS Completions to add (space-separated, or command in parentheses) |
| 3073 | +/// -d DESC Description for the completion |
| 3074 | +/// -s SHORT Short option (e.g., -v) |
| 3075 | +/// -l LONG Long option (e.g., --verbose) |
| 3076 | +/// -f Disable file completion for this command |
| 3077 | +/// -n COND Condition for when this completion applies |
| 3078 | +/// -e Erase all completions for the command |
| 3079 | +/// -p Print completions |
| 3080 | +fn builtin_complete(args: &[String], _context: &rush_expand::Context) -> ExecutionResult { |
| 3081 | + use rush_interactive::{ |
| 3082 | + add_completion, remove_completions, with_registry, |
| 3083 | + CompletionSource, CompletionSpec, |
| 3084 | + }; |
| 3085 | + |
| 3086 | + let mut command: Option<String> = None; |
| 3087 | + let mut completions: Option<String> = None; |
| 3088 | + let mut description: Option<String> = None; |
| 3089 | + let mut short_opt: Option<char> = None; |
| 3090 | + let mut long_opt: Option<String> = None; |
| 3091 | + let mut no_files = false; |
| 3092 | + let mut condition: Option<String> = None; |
| 3093 | + let mut erase = false; |
| 3094 | + let mut print = false; |
| 3095 | + |
| 3096 | + // Parse arguments |
| 3097 | + let mut i = 0; |
| 3098 | + while i < args.len() { |
| 3099 | + let arg = &args[i]; |
| 3100 | + match arg.as_str() { |
| 3101 | + "-c" => { |
| 3102 | + i += 1; |
| 3103 | + if i < args.len() { |
| 3104 | + command = Some(args[i].clone()); |
| 3105 | + } else { |
| 3106 | + eprintln!("complete: -c requires an argument"); |
| 3107 | + return error_result(); |
| 3108 | + } |
| 3109 | + } |
| 3110 | + "-a" => { |
| 3111 | + i += 1; |
| 3112 | + if i < args.len() { |
| 3113 | + completions = Some(args[i].clone()); |
| 3114 | + } else { |
| 3115 | + eprintln!("complete: -a requires an argument"); |
| 3116 | + return error_result(); |
| 3117 | + } |
| 3118 | + } |
| 3119 | + "-d" => { |
| 3120 | + i += 1; |
| 3121 | + if i < args.len() { |
| 3122 | + description = Some(args[i].clone()); |
| 3123 | + } else { |
| 3124 | + eprintln!("complete: -d requires an argument"); |
| 3125 | + return error_result(); |
| 3126 | + } |
| 3127 | + } |
| 3128 | + "-s" => { |
| 3129 | + i += 1; |
| 3130 | + if i < args.len() { |
| 3131 | + short_opt = args[i].chars().next(); |
| 3132 | + } else { |
| 3133 | + eprintln!("complete: -s requires an argument"); |
| 3134 | + return error_result(); |
| 3135 | + } |
| 3136 | + } |
| 3137 | + "-l" => { |
| 3138 | + i += 1; |
| 3139 | + if i < args.len() { |
| 3140 | + long_opt = Some(args[i].clone()); |
| 3141 | + } else { |
| 3142 | + eprintln!("complete: -l requires an argument"); |
| 3143 | + return error_result(); |
| 3144 | + } |
| 3145 | + } |
| 3146 | + "-n" => { |
| 3147 | + i += 1; |
| 3148 | + if i < args.len() { |
| 3149 | + condition = Some(args[i].clone()); |
| 3150 | + } else { |
| 3151 | + eprintln!("complete: -n requires an argument"); |
| 3152 | + return error_result(); |
| 3153 | + } |
| 3154 | + } |
| 3155 | + "-f" => no_files = true, |
| 3156 | + "-e" => erase = true, |
| 3157 | + "-p" => print = true, |
| 3158 | + _ => { |
| 3159 | + eprintln!("complete: unknown option: {}", arg); |
| 3160 | + return error_result(); |
| 3161 | + } |
| 3162 | + } |
| 3163 | + i += 1; |
| 3164 | + } |
| 3165 | + |
| 3166 | + // Handle print mode |
| 3167 | + if print { |
| 3168 | + with_registry(|registry| { |
| 3169 | + if let Some(cmd) = &command { |
| 3170 | + // Print completions for specific command |
| 3171 | + if let Some(specs) = registry.get(cmd) { |
| 3172 | + for spec in specs { |
| 3173 | + print_completion_spec(spec); |
| 3174 | + } |
| 3175 | + } |
| 3176 | + } else { |
| 3177 | + // Print all completions |
| 3178 | + for (cmd, specs) in registry.all_specs() { |
| 3179 | + println!("# Completions for '{}'", cmd); |
| 3180 | + for spec in specs { |
| 3181 | + print_completion_spec(spec); |
| 3182 | + } |
| 3183 | + } |
| 3184 | + } |
| 3185 | + }); |
| 3186 | + return success_result(); |
| 3187 | + } |
| 3188 | + |
| 3189 | + // Handle erase mode |
| 3190 | + if erase { |
| 3191 | + if let Some(cmd) = command { |
| 3192 | + remove_completions(&cmd); |
| 3193 | + return success_result(); |
| 3194 | + } else { |
| 3195 | + eprintln!("complete: -e requires -c COMMAND"); |
| 3196 | + return error_result(); |
| 3197 | + } |
| 3198 | + } |
| 3199 | + |
| 3200 | + // Adding a completion requires a command |
| 3201 | + let cmd = match command { |
| 3202 | + Some(c) => c, |
| 3203 | + None => { |
| 3204 | + eprintln!("complete: -c COMMAND is required"); |
| 3205 | + return error_result(); |
| 3206 | + } |
| 3207 | + }; |
| 3208 | + |
| 3209 | + // Determine the completion source |
| 3210 | + let source = if let Some(comps) = completions { |
| 3211 | + // Check if it's a dynamic command (wrapped in parentheses) |
| 3212 | + if comps.starts_with('(') && comps.ends_with(')') { |
| 3213 | + CompletionSource::Dynamic(comps[1..comps.len()-1].to_string()) |
| 3214 | + } else { |
| 3215 | + // Static list of completions (space-separated) |
| 3216 | + CompletionSource::Static( |
| 3217 | + comps.split_whitespace().map(String::from).collect() |
| 3218 | + ) |
| 3219 | + } |
| 3220 | + } else if short_opt.is_some() || long_opt.is_some() { |
| 3221 | + CompletionSource::Option { |
| 3222 | + short: short_opt, |
| 3223 | + long: long_opt, |
| 3224 | + } |
| 3225 | + } else if no_files { |
| 3226 | + // Just marking no-files without adding completions |
| 3227 | + CompletionSource::Static(vec![]) |
| 3228 | + } else { |
| 3229 | + eprintln!("complete: need -a, -s, -l, or -f"); |
| 3230 | + return error_result(); |
| 3231 | + }; |
| 3232 | + |
| 3233 | + // Create and register the completion spec |
| 3234 | + let spec = CompletionSpec { |
| 3235 | + command: cmd, |
| 3236 | + condition, |
| 3237 | + source, |
| 3238 | + description, |
| 3239 | + no_files, |
| 3240 | + }; |
| 3241 | + |
| 3242 | + add_completion(spec); |
| 3243 | + success_result() |
| 3244 | +} |
| 3245 | + |
| 3246 | +fn print_completion_spec(spec: &rush_interactive::CompletionSpec) { |
| 3247 | + use rush_interactive::CompletionSource; |
| 3248 | + |
| 3249 | + let mut parts = vec![format!("complete -c {}", spec.command)]; |
| 3250 | + |
| 3251 | + if let Some(ref cond) = spec.condition { |
| 3252 | + parts.push(format!("-n \"{}\"", cond)); |
| 3253 | + } |
| 3254 | + |
| 3255 | + match &spec.source { |
| 3256 | + CompletionSource::Static(items) if !items.is_empty() => { |
| 3257 | + parts.push(format!("-a \"{}\"", items.join(" "))); |
| 3258 | + } |
| 3259 | + CompletionSource::Dynamic(cmd) => { |
| 3260 | + parts.push(format!("-a \"({})\"", cmd)); |
| 3261 | + } |
| 3262 | + CompletionSource::ShortOption(c) => { |
| 3263 | + parts.push(format!("-s {}", c)); |
| 3264 | + } |
| 3265 | + CompletionSource::LongOption(s) => { |
| 3266 | + parts.push(format!("-l {}", s)); |
| 3267 | + } |
| 3268 | + CompletionSource::Option { short, long } => { |
| 3269 | + if let Some(c) = short { |
| 3270 | + parts.push(format!("-s {}", c)); |
| 3271 | + } |
| 3272 | + if let Some(l) = long { |
| 3273 | + parts.push(format!("-l {}", l)); |
| 3274 | + } |
| 3275 | + } |
| 3276 | + _ => {} |
| 3277 | + } |
| 3278 | + |
| 3279 | + if let Some(ref desc) = spec.description { |
| 3280 | + parts.push(format!("-d \"{}\"", desc)); |
| 3281 | + } |
| 3282 | + |
| 3283 | + if spec.no_files { |
| 3284 | + parts.push("-f".to_string()); |
| 3285 | + } |
| 3286 | + |
| 3287 | + println!("{}", parts.join(" ")); |
| 3288 | +} |
| 3289 | + |
| 2708 | 3290 | #[cfg(test)] |
| 2709 | 3291 | mod tests { |
| 2710 | 3292 | use super::*; |