Rust · 111474 bytes Raw Blame History
1 use std::env;
2 use std::path::PathBuf;
3 use std::process::{Command, ExitStatus};
4 use thiserror::Error;
5 use rush_interactive::ErrorHints;
6
7 #[cfg(unix)]
8 use std::os::unix::process::ExitStatusExt;
9
10 #[derive(Error, Debug)]
11 pub enum ExecutionError {
12 #[error("{0}")]
13 CommandNotFound(String),
14
15 #[error("I/O error: {0}")]
16 IoError(#[from] std::io::Error),
17
18 #[error("Empty command")]
19 EmptyCommand,
20 }
21
22 pub struct ExecutionResult {
23 pub exit_status: ExitStatus,
24 /// Job control information (Unix only)
25 #[cfg(unix)]
26 pub job_control: Option<JobControlInfo>,
27 }
28
29 #[cfg(unix)]
30 #[derive(Debug, Clone)]
31 pub struct JobControlInfo {
32 /// Process ID
33 pub pid: nix::unistd::Pid,
34 /// Process group ID
35 pub pgid: nix::unistd::Pid,
36 /// Whether the job was stopped (Ctrl-Z)
37 pub stopped: bool,
38 }
39
40 impl ExecutionResult {
41 pub fn success_code() -> i32 {
42 0
43 }
44
45 pub fn exit_code(&self) -> i32 {
46 self.exit_status.code().unwrap_or(1)
47 }
48
49 pub fn success(&self) -> bool {
50 self.exit_status.success()
51 }
52
53 #[cfg(unix)]
54 pub fn is_stopped(&self) -> bool {
55 self.job_control.as_ref().map_or(false, |jc| jc.stopped)
56 }
57
58 #[cfg(not(unix))]
59 pub fn is_stopped(&self) -> bool {
60 false
61 }
62 }
63
64 /// Execute a simple command (external program)
65 ///
66 /// In interactive mode, this sets up proper process groups and terminal control.
67 /// In non-interactive mode, it runs the command normally.
68 pub fn execute_command(
69 command: &str,
70 args: &[String],
71 interactive: bool,
72 context: &mut rush_expand::Context,
73 ) -> Result<ExecutionResult, ExecutionError> {
74 if command.is_empty() {
75 return Err(ExecutionError::EmptyCommand);
76 }
77
78 // Expand aliases (only for the command name, not args)
79 let (actual_command, actual_args): (String, Vec<String>) = if let Some(alias_value) = context.aliases.get(command).cloned() {
80 // Parse the alias value to get command and its args
81 let parts: Vec<String> = alias_value.split_whitespace().map(|s| s.to_string()).collect();
82 if parts.is_empty() {
83 (command.to_string(), args.to_vec())
84 } else {
85 let cmd = parts[0].clone();
86 let mut new_args: Vec<String> = parts[1..].to_vec();
87 new_args.extend_from_slice(args);
88 (cmd, new_args)
89 }
90 } else {
91 (command.to_string(), args.to_vec())
92 };
93
94 // Check if it's a built-in command
95 if let Some(result) = execute_builtin(&actual_command, &actual_args, context) {
96 return Ok(result);
97 }
98
99 // Try to find the command in PATH
100 let program_path = find_in_path(&actual_command)
101 .ok_or_else(|| ExecutionError::CommandNotFound(ErrorHints::command_not_found(&actual_command)))?;
102
103 // Build the command
104 let mut cmd = Command::new(program_path);
105 cmd.args(&actual_args);
106
107 // Execute with proper terminal handling
108 #[cfg(unix)]
109 {
110 crate::terminal::unix::execute_with_terminal_control(cmd, interactive)
111 }
112
113 #[cfg(not(unix))]
114 {
115 crate::terminal::non_unix::execute_with_terminal_control(cmd, interactive)
116 }
117 }
118
119 /// Execute built-in commands
120 pub(crate) fn execute_builtin(
121 command: &str,
122 args: &[String],
123 context: &mut rush_expand::Context,
124 ) -> Option<ExecutionResult> {
125 match command {
126 "exit" => {
127 let code = args.first()
128 .and_then(|s| s.parse::<i32>().ok())
129 .unwrap_or(context.last_exit_status);
130 context.exit_requested = Some(code);
131 Some(exit_code_to_result(code))
132 }
133 "true" => Some(success_result()),
134 "false" => Some(error_result()),
135 ":" => Some(success_result()),
136 "cd" => Some(builtin_cd(args, context)),
137 "pwd" => {
138 match env::current_dir() {
139 Ok(path) => {
140 println!("{}", path.display());
141 Some(success_result())
142 }
143 Err(_) => Some(error_result()),
144 }
145 }
146 "test" | "[" => {
147 let exit_code = crate::test_builtin::execute_test(args);
148 Some(exit_code_to_result(exit_code))
149 }
150 "source" | "." => {
151 match builtin_source(args, context) {
152 Ok(result) => Some(result),
153 Err(err) => {
154 eprintln!("{}: {}", command, err);
155 Some(error_result())
156 }
157 }
158 }
159 "eval" => {
160 match builtin_eval(args, context) {
161 Ok(result) => Some(result),
162 Err(err) => {
163 eprintln!("eval: {}", err);
164 Some(error_result())
165 }
166 }
167 }
168 "alias" => Some(builtin_alias(args, context)),
169 "unalias" => Some(builtin_unalias(args, context)),
170 "trap" => Some(builtin_trap(args, context)),
171 "set" => Some(builtin_set(args, context)),
172 "shopt" => Some(builtin_shopt(args, context)),
173 "export" => Some(builtin_export(args, context)),
174 "unset" => Some(builtin_unset(args, context)),
175 "readonly" => Some(builtin_readonly(args, context)),
176 "declare" | "typeset" => Some(builtin_declare(args, context)),
177 "local" => Some(builtin_local(args, context)),
178 "read" => Some(builtin_read(args, context)),
179 "shift" => Some(builtin_shift(args, context)),
180 "wait" => Some(builtin_wait(args, context)),
181 "kill" => Some(builtin_kill(args, context)),
182 "times" => Some(builtin_times(args, context)),
183 "umask" => Some(builtin_umask(args, context)),
184 "hash" => Some(builtin_hash(args, context)),
185 "getopts" => Some(builtin_getopts(args, context)),
186 "exec" => {
187 match builtin_exec(args, context) {
188 Ok(result) => Some(result),
189 Err(err) => {
190 eprintln!("exec: {}", err);
191 Some(error_result())
192 }
193 }
194 }
195 "command" => {
196 match builtin_command(args, context) {
197 Ok(result) => Some(result),
198 Err(err) => {
199 eprintln!("command: {}", err);
200 Some(error_result())
201 }
202 }
203 }
204 #[cfg(unix)]
205 "jobs" => Some(builtin_jobs(context)),
206 #[cfg(unix)]
207 "fg" => Some(builtin_fg(args, context)),
208 #[cfg(unix)]
209 "bg" => Some(builtin_bg(args, context)),
210 #[cfg(unix)]
211 "coproc" => Some(builtin_coproc(args, context)),
212 #[cfg(unix)]
213 "disown" => Some(builtin_disown(args, context)),
214 #[cfg(not(unix))]
215 "jobs" | "fg" | "bg" | "coproc" | "disown" => {
216 eprintln!("{}: job control not supported on this platform", command);
217 Some(error_result())
218 }
219 "echo" => Some(builtin_echo(args)),
220 "printf" => Some(builtin_printf(args, context)),
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)),
226 _ => None,
227 }
228 }
229
230 pub(crate) fn exit_code_to_result(code: i32) -> ExecutionResult {
231 #[cfg(unix)]
232 {
233 ExecutionResult {
234 exit_status: std::process::ExitStatus::from_raw(code << 8),
235 job_control: None,
236 }
237 }
238
239 #[cfg(not(unix))]
240 {
241 // On non-Unix, we can't easily create an ExitStatus with a specific code
242 if code == 0 {
243 success_result()
244 } else {
245 error_result()
246 }
247 }
248 }
249
250 /// alias builtin - Manage command aliases
251 fn builtin_alias(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
252 // No arguments: list all aliases
253 if args.is_empty() {
254 let mut aliases: Vec<_> = context.aliases.iter().collect();
255 aliases.sort_by_key(|(name, _)| *name);
256 for (name, value) in aliases {
257 println!("alias {}='{}'", name, value);
258 }
259 return success_result();
260 }
261
262 // Process each argument
263 for arg in args {
264 if let Some(eq_pos) = arg.find('=') {
265 // Define alias: name=value
266 let name = &arg[..eq_pos];
267 let value = &arg[eq_pos + 1..];
268 context.aliases.insert(name.to_string(), value.to_string());
269 } else {
270 // Display specific alias
271 match context.aliases.get(arg) {
272 Some(value) => println!("alias {}='{}'", arg, value),
273 None => {
274 eprintln!("alias: {}: not found", arg);
275 return error_result();
276 }
277 }
278 }
279 }
280
281 success_result()
282 }
283
284 /// unalias builtin - Remove command aliases
285 fn builtin_unalias(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
286 if args.is_empty() {
287 eprintln!("unalias: usage: unalias [-a] name [name ...]");
288 return error_result();
289 }
290
291 // Check for -a flag (remove all aliases)
292 if args[0] == "-a" {
293 context.aliases.clear();
294 return success_result();
295 }
296
297 // Remove specified aliases
298 let mut had_error = false;
299 for name in args {
300 if context.aliases.remove(name).is_none() {
301 eprintln!("unalias: {}: not found", name);
302 had_error = true;
303 }
304 }
305
306 if had_error {
307 error_result()
308 } else {
309 success_result()
310 }
311 }
312
313 /// Normalize signal name (SIGINT, INT, 2 all -> INT)
314 fn normalize_signal_name(sig: &str) -> Option<String> {
315 // Special trap signals
316 match sig.to_uppercase().as_str() {
317 "EXIT" | "0" => return Some("EXIT".to_string()),
318 "ERR" => return Some("ERR".to_string()),
319 "DEBUG" => return Some("DEBUG".to_string()),
320 "RETURN" => return Some("RETURN".to_string()),
321 _ => {}
322 }
323
324 // Regular signals - strip SIG prefix if present
325 let name = sig.to_uppercase();
326 let name = name.strip_prefix("SIG").unwrap_or(&name);
327
328 // Map common signal names/numbers
329 match name {
330 "HUP" | "1" => Some("HUP".to_string()),
331 "INT" | "2" => Some("INT".to_string()),
332 "QUIT" | "3" => Some("QUIT".to_string()),
333 "ABRT" | "6" => Some("ABRT".to_string()),
334 "KILL" | "9" => Some("KILL".to_string()),
335 "ALRM" | "14" => Some("ALRM".to_string()),
336 "TERM" | "15" => Some("TERM".to_string()),
337 "USR1" | "10" => Some("USR1".to_string()),
338 "USR2" | "12" => Some("USR2".to_string()),
339 "CHLD" | "CHILD" | "17" => Some("CHLD".to_string()),
340 "CONT" | "18" => Some("CONT".to_string()),
341 "STOP" | "19" => Some("STOP".to_string()),
342 "TSTP" | "20" => Some("TSTP".to_string()),
343 "TTIN" | "21" => Some("TTIN".to_string()),
344 "TTOU" | "22" => Some("TTOU".to_string()),
345 _ => None,
346 }
347 }
348
349 /// trap builtin - Set or display signal handlers
350 fn builtin_trap(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
351 // No arguments: list all traps
352 if args.is_empty() {
353 let mut traps: Vec<_> = context.traps.iter().collect();
354 traps.sort_by_key(|(sig, _)| *sig);
355 for (signal, command) in traps {
356 if command.is_empty() {
357 println!("trap -- '' {}", signal);
358 } else {
359 println!("trap -- '{}' {}", command, signal);
360 }
361 }
362 return success_result();
363 }
364
365 // Handle -p flag (print traps)
366 if args[0] == "-p" {
367 if args.len() == 1 {
368 // Print all traps
369 let mut traps: Vec<_> = context.traps.iter().collect();
370 traps.sort_by_key(|(sig, _)| *sig);
371 for (signal, command) in traps {
372 if command.is_empty() {
373 println!("trap -- '' {}", signal);
374 } else {
375 println!("trap -- '{}' {}", command, signal);
376 }
377 }
378 } else {
379 // Print specific traps
380 for sig in &args[1..] {
381 if let Some(normalized) = normalize_signal_name(sig) {
382 if let Some(command) = context.traps.get(&normalized) {
383 if command.is_empty() {
384 println!("trap -- '' {}", normalized);
385 } else {
386 println!("trap -- '{}' {}", command, normalized);
387 }
388 }
389 }
390 }
391 }
392 return success_result();
393 }
394
395 // Handle -l flag (list signal names)
396 if args[0] == "-l" {
397 println!(" 1) HUP\t 2) INT\t 3) QUIT\t 6) ABRT\t 9) KILL");
398 println!("10) USR1\t12) USR2\t14) ALRM\t15) TERM\t17) CHLD");
399 println!("18) CONT\t19) STOP\t20) TSTP\t21) TTIN\t22) TTOU");
400 return success_result();
401 }
402
403 // trap COMMAND SIGNAL...
404 let command = &args[0];
405 let signals = &args[1..];
406
407 if signals.is_empty() {
408 eprintln!("trap: usage: trap [-lp] [[arg] signal_spec ...]");
409 return error_result();
410 }
411
412 // Check if we're clearing traps (trap - SIGNAL)
413 let clearing = command == "-";
414
415 let mut had_error = false;
416 for sig in signals {
417 if let Some(normalized) = normalize_signal_name(sig) {
418 if clearing {
419 context.traps.remove(&normalized);
420 } else {
421 context.traps.insert(normalized, command.clone());
422 }
423 } else {
424 eprintln!("trap: {}: invalid signal specification", sig);
425 had_error = true;
426 }
427 }
428
429 if had_error {
430 error_result()
431 } else {
432 success_result()
433 }
434 }
435
436 /// set builtin - Set or display shell options and positional parameters
437 fn builtin_set(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
438 // No arguments: display all variables
439 if args.is_empty() {
440 let mut vars: Vec<_> = context.all_vars().iter().collect();
441 vars.sort_by_key(|(name, _)| *name);
442 for (name, value) in vars {
443 println!("{}={}", name, value);
444 }
445 return success_result();
446 }
447
448 // Process options
449 let mut i = 0;
450 while i < args.len() {
451 let arg = &args[i];
452
453 if arg == "--" {
454 // End of options marker - remaining args become positional parameters
455 context.positional_params = args[i + 1..].to_vec();
456 return success_result();
457 } else if arg == "-" {
458 // Single dash: unset positional parameters and turn off -x and -v
459 context.positional_params.clear();
460 context.options.xtrace = false;
461 return success_result();
462 } else if arg.starts_with('-') || arg.starts_with('+') {
463 let enable = arg.starts_with('-');
464 let opts = &arg[1..];
465
466 for ch in opts.chars() {
467 match ch {
468 'e' => context.options.errexit = enable,
469 'x' => context.options.xtrace = enable,
470 'u' => context.options.nounset = enable,
471 'f' => context.options.noglob = enable,
472 'o' => {
473 // -o option_name format (next arg is the option)
474 // For simplicity, we'll handle this separately if needed
475 eprintln!("set: -o option requires an argument");
476 return error_result();
477 }
478 _ => {
479 eprintln!("set: -{}: invalid option", ch);
480 return error_result();
481 }
482 }
483 }
484 } else {
485 // Non-option argument: all remaining args become positional parameters
486 context.positional_params = args[i..].to_vec();
487 return success_result();
488 }
489 i += 1;
490 }
491
492 success_result()
493 }
494
495 /// shopt builtin - Set or display bash-specific shell options
496 fn builtin_shopt(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
497 let mut print_format = false;
498 let mut set_option = false;
499 let mut unset_option = false;
500 let mut quiet = false;
501 let mut options_to_process = Vec::new();
502
503 // Parse arguments
504 let mut i = 0;
505 while i < args.len() {
506 let arg = &args[i];
507 if arg == "-p" {
508 print_format = true;
509 } else if arg == "-s" {
510 set_option = true;
511 } else if arg == "-u" {
512 unset_option = true;
513 } else if arg == "-q" {
514 quiet = true;
515 } else if arg.starts_with('-') {
516 eprintln!("shopt: {}: invalid option", arg);
517 return error_result();
518 } else {
519 options_to_process.push(arg.clone());
520 }
521 i += 1;
522 }
523
524 // No arguments: list all options
525 if options_to_process.is_empty() && !print_format {
526 println!("nullglob\t{}", if context.options.nullglob { "on" } else { "off" });
527 println!("dotglob\t\t{}", if context.options.dotglob { "on" } else { "off" });
528 println!("extglob\t\t{}", if context.options.extglob { "on" } else { "off" });
529 return success_result();
530 }
531
532 // -p: print in reusable format
533 if print_format && options_to_process.is_empty() {
534 println!("shopt -{} nullglob", if context.options.nullglob { "s" } else { "u" });
535 println!("shopt -{} dotglob", if context.options.dotglob { "s" } else { "u" });
536 println!("shopt -{} extglob", if context.options.extglob { "s" } else { "u" });
537 return success_result();
538 }
539
540 // Process specific options
541 let mut had_error = false;
542 for opt_name in &options_to_process {
543 match opt_name.as_str() {
544 "nullglob" => {
545 if set_option {
546 context.options.nullglob = true;
547 } else if unset_option {
548 context.options.nullglob = false;
549 } else if print_format {
550 println!("shopt -{} nullglob", if context.options.nullglob { "s" } else { "u" });
551 } else if !quiet {
552 println!("nullglob\t{}", if context.options.nullglob { "on" } else { "off" });
553 }
554 }
555 "dotglob" => {
556 if set_option {
557 context.options.dotglob = true;
558 } else if unset_option {
559 context.options.dotglob = false;
560 } else if print_format {
561 println!("shopt -{} dotglob", if context.options.dotglob { "s" } else { "u" });
562 } else if !quiet {
563 println!("dotglob\t\t{}", if context.options.dotglob { "on" } else { "off" });
564 }
565 }
566 "extglob" => {
567 if set_option {
568 context.options.extglob = true;
569 } else if unset_option {
570 context.options.extglob = false;
571 } else if print_format {
572 println!("shopt -{} extglob", if context.options.extglob { "s" } else { "u" });
573 } else if !quiet {
574 println!("extglob\t\t{}", if context.options.extglob { "on" } else { "off" });
575 }
576 }
577 _ => {
578 if !quiet {
579 eprintln!("shopt: {}: invalid shell option name", opt_name);
580 }
581 had_error = true;
582 }
583 }
584 }
585
586 if had_error {
587 error_result()
588 } else {
589 success_result()
590 }
591 }
592
593 /// export builtin - Mark variables for export to child processes
594 fn builtin_export(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
595 let mut print_format = false;
596 let mut unexport = false;
597 let mut vars_to_process = Vec::new();
598
599 // Parse arguments
600 for arg in args {
601 if arg == "-p" {
602 print_format = true;
603 } else if arg == "-n" {
604 unexport = true;
605 } else if arg.starts_with('-') {
606 eprintln!("export: {}: invalid option", arg);
607 return error_result();
608 } else {
609 vars_to_process.push(arg.clone());
610 }
611 }
612
613 // No arguments or -p: list all exported variables
614 if vars_to_process.is_empty() {
615 let mut exported: Vec<_> = context.exported_vars().iter().collect();
616 exported.sort_by_key(|(name, _)| *name);
617 for (name, value) in exported {
618 if print_format {
619 println!("export {}={}", name, value);
620 } else {
621 println!("export {}={}", name, value);
622 }
623 }
624 return success_result();
625 }
626
627 // Process each variable
628 for var in &vars_to_process {
629 if let Some(eq_pos) = var.find('=') {
630 // VAR=value format
631 let name = &var[..eq_pos];
632 let value = &var[eq_pos + 1..];
633
634 if unexport {
635 eprintln!("export: -n: cannot assign value and unexport");
636 return error_result();
637 }
638
639 context.export_var(name, value);
640 } else {
641 // Just VAR (no value)
642 if unexport {
643 // Remove from exported (but keep in variables)
644 context.unexport_var(var);
645 } else {
646 // Export existing variable
647 if let Some(value) = context.get_var(var).map(|s| s.to_string()) {
648 context.export_var(var, value);
649 } else {
650 // Variable doesn't exist, export with empty value
651 context.export_var(var, "");
652 }
653 }
654 }
655 }
656
657 success_result()
658 }
659
660 /// unset builtin - Unset variables or functions
661 fn builtin_unset(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
662 let mut unset_vars = true;
663 let mut unset_funcs = true;
664 let mut names_to_unset = Vec::new();
665
666 // Parse arguments
667 for arg in args {
668 if arg == "-v" {
669 unset_vars = true;
670 unset_funcs = false;
671 } else if arg == "-f" {
672 unset_vars = false;
673 unset_funcs = true;
674 } else if arg.starts_with('-') {
675 eprintln!("unset: {}: invalid option", arg);
676 return error_result();
677 } else {
678 names_to_unset.push(arg.clone());
679 }
680 }
681
682 if names_to_unset.is_empty() {
683 // No error, just do nothing (POSIX behavior)
684 return success_result();
685 }
686
687 // Unset each name
688 for name in &names_to_unset {
689 if unset_vars {
690 context.unset_var(name);
691 }
692 if unset_funcs {
693 context.functions.remove(name);
694 }
695 }
696
697 success_result()
698 }
699
700 /// readonly builtin - Mark variables as readonly
701 fn builtin_readonly(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
702 let mut _print_format = false; // TODO: implement -p format output
703 let mut vars_to_process = Vec::new();
704
705 // Parse arguments
706 for arg in args {
707 if arg == "-p" {
708 _print_format = true;
709 } else if arg == "-f" {
710 // Readonly functions not yet supported
711 eprintln!("readonly: -f: readonly functions not yet supported");
712 return error_result();
713 } else if arg.starts_with('-') {
714 eprintln!("readonly: {}: invalid option", arg);
715 return error_result();
716 } else {
717 vars_to_process.push(arg.clone());
718 }
719 }
720
721 // No arguments or -p: list all readonly variables
722 if vars_to_process.is_empty() {
723 let mut readonly_list: Vec<_> = context.readonly_vars().iter().collect();
724 readonly_list.sort();
725 for name in readonly_list {
726 if let Some(value) = context.get_var(name) {
727 println!("readonly {}={}", name, value);
728 } else {
729 println!("readonly {}", name);
730 }
731 }
732 return success_result();
733 }
734
735 // Process each variable
736 for var in &vars_to_process {
737 if let Some(eq_pos) = var.find('=') {
738 // VAR=value format
739 let name = &var[..eq_pos];
740 let value = &var[eq_pos + 1..];
741
742 // Check if already readonly
743 if context.is_readonly(name) {
744 eprintln!("readonly: {}: readonly variable", name);
745 return error_result();
746 }
747
748 // Set value and mark readonly
749 // Ignore result since we already checked readonly above
750 let _ = context.set_var(name, value);
751 context.mark_readonly(name);
752 } else {
753 // Just VAR (no value) - mark existing variable as readonly
754 if !context.is_readonly(var) {
755 // If variable doesn't exist, create it with empty value
756 if context.get_var(var).is_none() {
757 let _ = context.set_var(var, "");
758 }
759 context.mark_readonly(var);
760 }
761 }
762 }
763
764 success_result()
765 }
766
767 /// declare/typeset builtin - Declare variables and arrays with attributes
768 fn builtin_declare(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
769 let mut print_mode = false;
770 let mut indexed_array = false;
771 let mut associative_array = false;
772 let mut readonly = false;
773 let mut export = false;
774 let mut vars_to_process = Vec::new();
775
776 // Parse arguments
777 for arg in args {
778 if arg.starts_with('-') {
779 for ch in arg.chars().skip(1) {
780 match ch {
781 'a' => indexed_array = true,
782 'A' => associative_array = true,
783 'r' => readonly = true,
784 'x' => export = true,
785 'p' => print_mode = true,
786 _ => {
787 eprintln!("declare: -{}: invalid option", ch);
788 return error_result();
789 }
790 }
791 }
792 } else {
793 vars_to_process.push(arg.clone());
794 }
795 }
796
797 // Check for conflicting flags
798 if indexed_array && associative_array {
799 eprintln!("declare: cannot use -a and -A together");
800 return error_result();
801 }
802
803 // Print mode
804 if print_mode {
805 if vars_to_process.is_empty() {
806 // Print all variables
807 let mut all_vars: Vec<_> = context.all_vars().iter().collect();
808 all_vars.sort_by_key(|(name, _)| *name);
809 for (name, value) in all_vars {
810 let mut attrs = String::new();
811 if context.is_readonly(name) {
812 attrs.push_str("r");
813 }
814 if context.is_exported(name) {
815 attrs.push_str("x");
816 }
817 if context.is_associative_array(name) {
818 attrs.push_str("A");
819 } else if context.arrays.contains_key(name) {
820 attrs.push_str("a");
821 }
822
823 if attrs.is_empty() {
824 println!("declare -- {}={}", name, value);
825 } else {
826 println!("declare -{} {}={}", attrs, name, value);
827 }
828 }
829 return success_result();
830 } else {
831 // Print specific variables
832 for var in &vars_to_process {
833 // Check if it's an array
834 if let Some(array) = context.arrays.get(var) {
835 let mut attrs = String::new();
836 if context.is_readonly(var) {
837 attrs.push_str("r");
838 }
839 if context.is_exported(var) {
840 attrs.push_str("x");
841 }
842
843 match array {
844 rush_expand::context::ArrayType::Indexed(vec) => {
845 attrs.push_str("a");
846 // Print array elements
847 let elements: Vec<String> = vec.iter()
848 .enumerate()
849 .map(|(i, v)| format!("[{}]=\"{}\"", i, v))
850 .collect();
851 if attrs.is_empty() {
852 println!("declare -- {}=({})", var, elements.join(" "));
853 } else {
854 println!("declare -{} {}=({})", attrs, var, elements.join(" "));
855 }
856 }
857 rush_expand::context::ArrayType::Associative(map) => {
858 attrs.push_str("A");
859 // Print associative array elements
860 let mut elements: Vec<String> = map.iter()
861 .map(|(k, v)| format!("[{}]=\"{}\"", k, v))
862 .collect();
863 elements.sort();
864 if attrs.is_empty() {
865 println!("declare -- {}=({})", var, elements.join(" "));
866 } else {
867 println!("declare -{} {}=({})", attrs, var, elements.join(" "));
868 }
869 }
870 }
871 } else if let Some(value) = context.get_var(var) {
872 // Regular variable
873 let mut attrs = String::new();
874 if context.is_readonly(var) {
875 attrs.push_str("r");
876 }
877 if context.is_exported(var) {
878 attrs.push_str("x");
879 }
880
881 if attrs.is_empty() {
882 println!("declare -- {}=\"{}\"", var, value);
883 } else {
884 println!("declare -{} {}=\"{}\"", attrs, var, value);
885 }
886 }
887 }
888 return success_result();
889 }
890 }
891
892 // Process each variable
893 for var in &vars_to_process {
894 if let Some(eq_pos) = var.find('=') {
895 // VAR=value format
896 let name = &var[..eq_pos];
897 let value = &var[eq_pos + 1..];
898
899 // Check if readonly
900 if context.is_readonly(name) {
901 eprintln!("declare: {}: readonly variable", name);
902 return error_result();
903 }
904
905 // Handle array declarations
906 if associative_array {
907 // Create associative array
908 context.create_assoc_array(name.to_string());
909 // TODO: Parse compound assignments like arr=([key1]=val1 [key2]=val2)
910 // For now, just create empty array
911 } else if indexed_array {
912 // Create indexed array
913 // TODO: Parse array literal assignments like arr=(one two three)
914 // For now, set as regular variable
915 let _ = context.set_var(name, value);
916 } else {
917 // Regular variable
918 let _ = context.set_var(name, value);
919 }
920
921 // Apply attributes
922 if readonly {
923 context.mark_readonly(name);
924 }
925 if export {
926 // Clone the value to avoid borrow checker issues
927 let val = context.get_var(name).map(|s| s.to_string());
928 if let Some(v) = val {
929 context.export_var(name, v);
930 }
931 }
932 } else {
933 // Just VAR (no value) - declare without assignment
934
935 // Create array if -a or -A specified
936 if associative_array {
937 if !context.is_readonly(var) {
938 context.create_assoc_array(var.to_string());
939 } else {
940 eprintln!("declare: {}: readonly variable", var);
941 return error_result();
942 }
943 } else if indexed_array {
944 if !context.is_readonly(var) {
945 context.create_indexed_array(var.to_string());
946 } else {
947 eprintln!("declare: {}: readonly variable", var);
948 return error_result();
949 }
950 } else if context.get_var(var).is_none() {
951 // Create variable with empty value if it doesn't exist
952 let _ = context.set_var(var, "");
953 }
954
955 // Apply attributes
956 if readonly {
957 context.mark_readonly(var);
958 }
959 if export {
960 // Clone the value to avoid borrow checker issues
961 let val = context.get_var(var).map(|s| s.to_string());
962 if let Some(v) = val {
963 context.export_var(var, v);
964 } else {
965 // Export with empty value
966 context.export_var(var, "");
967 }
968 }
969 }
970 }
971
972 success_result()
973 }
974
975 /// local builtin - Create function-local variables
976 fn builtin_local(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
977 if args.is_empty() {
978 // No arguments - success (bash behavior)
979 return success_result();
980 }
981
982 // Process each variable assignment
983 for arg in args {
984 if arg.starts_with('-') {
985 eprintln!("local: {}: invalid option", arg);
986 return error_result();
987 }
988
989 if let Some(eq_pos) = arg.find('=') {
990 // VAR=value format
991 let name = &arg[..eq_pos];
992 let value = &arg[eq_pos + 1..];
993
994 // Set as local variable in current function scope
995 if let Err(var_name) = context.set_local_var(name, value) {
996 eprintln!("local: {}: readonly variable", var_name);
997 return error_result();
998 }
999 } else {
1000 // Just VAR (no value) - create with empty value
1001 if let Err(var_name) = context.set_local_var(arg, "") {
1002 eprintln!("local: {}: readonly variable", var_name);
1003 return error_result();
1004 }
1005 }
1006 }
1007
1008 success_result()
1009 }
1010
1011 /// read builtin - Read a line from stdin into variables
1012 fn builtin_read(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
1013 use std::io::{self, BufRead, Write};
1014
1015 let mut raw_mode = false;
1016 let mut prompt = String::new();
1017 let mut var_names = Vec::new();
1018
1019 // Parse arguments
1020 let mut i = 0;
1021 while i < args.len() {
1022 let arg = &args[i];
1023 if arg == "-r" {
1024 raw_mode = true;
1025 } else if arg == "-p" {
1026 // Next argument is the prompt
1027 i += 1;
1028 if i >= args.len() {
1029 eprintln!("read: -p: option requires an argument");
1030 return error_result();
1031 }
1032 prompt = args[i].clone();
1033 } else if arg.starts_with('-') {
1034 eprintln!("read: {}: invalid option", arg);
1035 return error_result();
1036 } else {
1037 var_names.push(arg.clone());
1038 }
1039 i += 1;
1040 }
1041
1042 // Default to REPLY if no variable names given
1043 if var_names.is_empty() {
1044 var_names.push("REPLY".to_string());
1045 }
1046
1047 // Display prompt if provided
1048 if !prompt.is_empty() {
1049 print!("{}", prompt);
1050 let _ = io::stdout().flush();
1051 }
1052
1053 // Read line from stdin
1054 let stdin = io::stdin();
1055 let mut line = String::new();
1056 match stdin.lock().read_line(&mut line) {
1057 Ok(0) => {
1058 // EOF
1059 return error_result();
1060 }
1061 Ok(_) => {
1062 // Remove trailing newline
1063 if line.ends_with('\n') {
1064 line.pop();
1065 if line.ends_with('\r') {
1066 line.pop();
1067 }
1068 }
1069 }
1070 Err(_) => {
1071 return error_result();
1072 }
1073 }
1074
1075 // Handle backslash continuation (unless in raw mode)
1076 if !raw_mode {
1077 while line.ends_with('\\') {
1078 line.pop(); // Remove backslash
1079 let mut continuation = String::new();
1080 match stdin.lock().read_line(&mut continuation) {
1081 Ok(0) => break, // EOF
1082 Ok(_) => {
1083 if continuation.ends_with('\n') {
1084 continuation.pop();
1085 if continuation.ends_with('\r') {
1086 continuation.pop();
1087 }
1088 }
1089 line.push_str(&continuation);
1090 }
1091 Err(_) => break,
1092 }
1093 }
1094 }
1095
1096 // Get IFS for splitting (default to whitespace)
1097 let ifs = context.get_var("IFS").unwrap_or(" \t\n");
1098 let ifs_chars: Vec<char> = ifs.chars().collect();
1099
1100 // Split the line and assign to variables
1101 if var_names.len() == 1 {
1102 // Single variable gets entire line
1103 let _ = context.set_var(&var_names[0], line);
1104 } else {
1105 // Multiple variables: split by IFS
1106 let words: Vec<&str> = if ifs_chars.is_empty() {
1107 // Empty IFS means split every character
1108 vec![line.as_str()]
1109 } else {
1110 // Split by IFS characters
1111 line.split(|c: char| ifs_chars.contains(&c))
1112 .filter(|s| !s.is_empty())
1113 .collect()
1114 };
1115
1116 // Assign words to variables
1117 for (i, var_name) in var_names.iter().enumerate() {
1118 if i < words.len() {
1119 if i == var_names.len() - 1 {
1120 // Last variable gets all remaining words
1121 let remaining = words[i..].join(" ");
1122 let _ = context.set_var(var_name, remaining);
1123 } else {
1124 let _ = context.set_var(var_name, words[i]);
1125 }
1126 } else {
1127 // No more input, set to empty
1128 let _ = context.set_var(var_name, "");
1129 }
1130 }
1131 }
1132
1133 success_result()
1134 }
1135
1136 /// shift builtin - Shift positional parameters
1137 fn builtin_shift(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
1138 // Parse the count (default 1)
1139 let count = if args.is_empty() {
1140 1
1141 } else {
1142 match args[0].parse::<usize>() {
1143 Ok(n) => n,
1144 Err(_) => {
1145 eprintln!("shift: {}: numeric argument required", args[0]);
1146 return error_result();
1147 }
1148 }
1149 };
1150
1151 // Check if we have enough parameters to shift
1152 if count > context.positional_params.len() {
1153 eprintln!("shift: shift count ({}) exceeds number of positional parameters ({})",
1154 count, context.positional_params.len());
1155 return error_result();
1156 }
1157
1158 // Shift the parameters
1159 context.positional_params.drain(0..count);
1160
1161 success_result()
1162 }
1163
1164 /// command builtin - Execute command bypassing functions and aliases
1165 fn builtin_command(args: &[String], context: &mut rush_expand::Context) -> Result<ExecutionResult, String> {
1166 let mut verbose = false;
1167 let mut very_verbose = false;
1168 let mut _use_default_path = false; // TODO: implement -p to use default PATH
1169 let mut cmd_args = Vec::new();
1170
1171 // Parse arguments
1172 let mut i = 0;
1173 while i < args.len() {
1174 let arg = &args[i];
1175 if arg == "-v" {
1176 verbose = true;
1177 } else if arg == "-V" {
1178 very_verbose = true;
1179 } else if arg == "-p" {
1180 _use_default_path = true;
1181 } else if arg == "--" {
1182 // End of options
1183 i += 1;
1184 cmd_args.extend_from_slice(&args[i..]);
1185 break;
1186 } else if arg.starts_with('-') {
1187 return Err(format!("{}: invalid option", arg));
1188 } else {
1189 cmd_args.extend_from_slice(&args[i..]);
1190 break;
1191 }
1192 i += 1;
1193 }
1194
1195 if cmd_args.is_empty() {
1196 return Err("usage: command [-pvV] command [arg ...]".to_string());
1197 }
1198
1199 let cmd_name = &cmd_args[0];
1200
1201 // -v: print path to command
1202 if verbose {
1203 // Check if it's a builtin
1204 if execute_builtin(cmd_name, &[], context).is_some() {
1205 println!("{}", cmd_name);
1206 return Ok(success_result());
1207 }
1208
1209 // Check if it's in PATH
1210 if let Some(path) = find_in_path(cmd_name) {
1211 println!("{}", path.display());
1212 return Ok(success_result());
1213 }
1214
1215 return Ok(error_result());
1216 }
1217
1218 // -V: verbose description
1219 if very_verbose {
1220 // Check if it's a builtin
1221 if execute_builtin(cmd_name, &[], context).is_some() {
1222 println!("{} is a shell builtin", cmd_name);
1223 return Ok(success_result());
1224 }
1225
1226 // Check if it's a function
1227 if context.functions.contains_key(cmd_name) {
1228 println!("{} is a function", cmd_name);
1229 return Ok(success_result());
1230 }
1231
1232 // Check if it's an alias
1233 if context.aliases.contains_key(cmd_name) {
1234 if let Some(alias_val) = context.aliases.get(cmd_name) {
1235 println!("{} is aliased to `{}'", cmd_name, alias_val);
1236 }
1237 return Ok(success_result());
1238 }
1239
1240 // Check if it's in PATH
1241 if let Some(path) = find_in_path(cmd_name) {
1242 println!("{} is {}", cmd_name, path.display());
1243 return Ok(success_result());
1244 }
1245
1246 return Err(format!("{}: not found", cmd_name));
1247 }
1248
1249 // Execute the command, bypassing functions and aliases
1250 // First check if it's a builtin
1251 if let Some(result) = execute_builtin(cmd_name, &cmd_args[1..], context) {
1252 return Ok(result);
1253 }
1254
1255 // Find in PATH and execute
1256 let program_path = find_in_path(cmd_name)
1257 .ok_or_else(|| format!("{}: command not found", cmd_name))?;
1258
1259 let mut command = std::process::Command::new(program_path);
1260 command.args(&cmd_args[1..]);
1261
1262 // Execute
1263 #[cfg(unix)]
1264 {
1265 crate::terminal::unix::execute_with_terminal_control(command, false)
1266 .map_err(|e| e.to_string())
1267 }
1268
1269 #[cfg(not(unix))]
1270 {
1271 crate::terminal::non_unix::execute_with_terminal_control(command, false)
1272 .map_err(|e| e.to_string())
1273 }
1274 }
1275
1276 /// wait builtin - Wait for background jobs to complete
1277 fn builtin_wait(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
1278 #[cfg(unix)]
1279 {
1280 use nix::sys::wait::{waitpid, WaitPidFlag};
1281 use nix::unistd::Pid;
1282
1283 // If no arguments, wait for all background jobs
1284 if args.is_empty() {
1285 let mut last_status = 0;
1286
1287 // Get all job PIDs
1288 let job_pids: Vec<Pid> = context.job_list.jobs()
1289 .map(|job| job.pgid)
1290 .collect();
1291
1292 for pgid in job_pids {
1293 match waitpid(pgid, Some(WaitPidFlag::empty())) {
1294 Ok(status) => {
1295 use nix::sys::wait::WaitStatus;
1296 match status {
1297 WaitStatus::Exited(_, code) => last_status = code,
1298 WaitStatus::Signaled(_, sig, _) => last_status = 128 + sig as i32,
1299 _ => {}
1300 }
1301 }
1302 Err(_) => {}
1303 }
1304 }
1305
1306 return exit_code_to_result(last_status);
1307 }
1308
1309 // Wait for specific PIDs
1310 let mut last_status = 0;
1311 for arg in args {
1312 let pid = match arg.parse::<i32>() {
1313 Ok(p) => Pid::from_raw(p),
1314 Err(_) => {
1315 eprintln!("wait: {}: not a valid process ID", arg);
1316 return error_result();
1317 }
1318 };
1319
1320 match waitpid(pid, Some(WaitPidFlag::empty())) {
1321 Ok(status) => {
1322 use nix::sys::wait::WaitStatus;
1323 match status {
1324 WaitStatus::Exited(_, code) => last_status = code,
1325 WaitStatus::Signaled(_, sig, _) => last_status = 128 + sig as i32,
1326 _ => {}
1327 }
1328 }
1329 Err(_) => {
1330 eprintln!("wait: pid {}: no such job", pid);
1331 return error_result();
1332 }
1333 }
1334 }
1335
1336 exit_code_to_result(last_status)
1337 }
1338
1339 #[cfg(not(unix))]
1340 {
1341 eprintln!("wait: job control not supported on this platform");
1342 error_result()
1343 }
1344 }
1345
1346 /// kill builtin - Send signals to processes
1347 fn builtin_kill(args: &[String], _context: &mut rush_expand::Context) -> ExecutionResult {
1348 #[cfg(unix)]
1349 {
1350 use nix::sys::signal::{kill, Signal};
1351 use nix::unistd::Pid;
1352
1353 let mut signal = Signal::SIGTERM; // Default signal
1354 let mut pids = Vec::new();
1355 let mut list_signals = false;
1356
1357 // Parse arguments
1358 let mut i = 0;
1359 while i < args.len() {
1360 let arg = &args[i];
1361
1362 if arg == "-l" {
1363 list_signals = true;
1364 i += 1;
1365 continue;
1366 }
1367
1368 if arg.starts_with('-') && arg.len() > 1 {
1369 // Parse signal
1370 let sig_str = &arg[1..];
1371
1372 // Try to parse as signal number
1373 if let Ok(num) = sig_str.parse::<i32>() {
1374 signal = match Signal::try_from(num) {
1375 Ok(sig) => sig,
1376 Err(_) => {
1377 eprintln!("kill: {}: invalid signal specification", num);
1378 return error_result();
1379 }
1380 };
1381 } else {
1382 // Try to parse as signal name
1383 let sig_upper = sig_str.to_uppercase();
1384 let sig_name = sig_upper.strip_prefix("SIG").unwrap_or(&sig_upper);
1385
1386 signal = match sig_name {
1387 "HUP" | "1" => Signal::SIGHUP,
1388 "INT" | "2" => Signal::SIGINT,
1389 "QUIT" | "3" => Signal::SIGQUIT,
1390 "ABRT" | "6" => Signal::SIGABRT,
1391 "KILL" | "9" => Signal::SIGKILL,
1392 "ALRM" | "14" => Signal::SIGALRM,
1393 "TERM" | "15" => Signal::SIGTERM,
1394 "USR1" | "10" => Signal::SIGUSR1,
1395 "USR2" | "12" => Signal::SIGUSR2,
1396 "CONT" | "18" => Signal::SIGCONT,
1397 "STOP" | "19" => Signal::SIGSTOP,
1398 "TSTP" | "20" => Signal::SIGTSTP,
1399 _ => {
1400 eprintln!("kill: {}: invalid signal specification", sig_str);
1401 return error_result();
1402 }
1403 };
1404 }
1405 i += 1;
1406 continue;
1407 }
1408
1409 // It's a PID
1410 pids.push(arg.clone());
1411 i += 1;
1412 }
1413
1414 // Handle -l (list signals)
1415 if list_signals {
1416 println!(" 1) HUP\t 2) INT\t 3) QUIT\t 6) ABRT\t 9) KILL");
1417 println!("10) USR1\t12) USR2\t14) ALRM\t15) TERM");
1418 println!("18) CONT\t19) STOP\t20) TSTP");
1419 return success_result();
1420 }
1421
1422 // Must have at least one PID
1423 if pids.is_empty() {
1424 eprintln!("kill: usage: kill [-s sigspec | -signum] pid ...");
1425 return error_result();
1426 }
1427
1428 // Send signal to each PID
1429 let mut had_error = false;
1430 for pid_str in &pids {
1431 let pid = match pid_str.parse::<i32>() {
1432 Ok(p) => Pid::from_raw(p),
1433 Err(_) => {
1434 eprintln!("kill: {}: arguments must be process or job IDs", pid_str);
1435 had_error = true;
1436 continue;
1437 }
1438 };
1439
1440 if let Err(e) = kill(pid, signal) {
1441 eprintln!("kill: ({}): {}", pid, e);
1442 had_error = true;
1443 }
1444 }
1445
1446 if had_error {
1447 error_result()
1448 } else {
1449 success_result()
1450 }
1451 }
1452
1453 #[cfg(not(unix))]
1454 {
1455 eprintln!("kill: not supported on this platform");
1456 error_result()
1457 }
1458 }
1459
1460 /// exec builtin - Replace shell with command
1461 fn builtin_exec(args: &[String], _context: &mut rush_expand::Context) -> Result<ExecutionResult, String> {
1462 if args.is_empty() {
1463 return Err("usage: exec command [args...]".to_string());
1464 }
1465
1466 let cmd_name = &args[0];
1467 let cmd_args = &args[1..];
1468
1469 // Find the command in PATH
1470 let program_path = find_in_path(cmd_name)
1471 .ok_or_else(|| format!("{}: command not found", cmd_name))?;
1472
1473 #[cfg(unix)]
1474 {
1475 use std::ffi::CString;
1476 use std::os::unix::ffi::OsStrExt;
1477
1478 // Convert path to CString
1479 let path_cstr = CString::new(program_path.as_os_str().as_bytes())
1480 .map_err(|e| format!("invalid path: {}", e))?;
1481
1482 // Convert args to CStrings
1483 let mut exec_args = vec![path_cstr.clone()];
1484 for arg in cmd_args {
1485 let arg_cstr = CString::new(arg.as_bytes())
1486 .map_err(|e| format!("invalid argument: {}", e))?;
1487 exec_args.push(arg_cstr);
1488 }
1489
1490 // Execute - this replaces the current process
1491 // If exec succeeds, this function never returns
1492 nix::unistd::execv(&path_cstr, &exec_args)
1493 .map_err(|e| format!("exec failed: {}", e))?;
1494
1495 // This should never be reached
1496 unreachable!()
1497 }
1498
1499 #[cfg(not(unix))]
1500 {
1501 Err("exec: not supported on this platform".to_string())
1502 }
1503 }
1504
1505 /// times builtin - Display process times
1506 fn builtin_times(_args: &[String], _context: &mut rush_expand::Context) -> ExecutionResult {
1507 #[cfg(unix)]
1508 {
1509 use nix::sys::resource::{getrusage, UsageWho};
1510
1511 // Get rusage for self (the shell)
1512 match getrusage(UsageWho::RUSAGE_SELF) {
1513 Ok(self_usage) => {
1514 let user_sec = self_usage.user_time().tv_sec();
1515 let user_usec = self_usage.user_time().tv_usec();
1516 let sys_sec = self_usage.system_time().tv_sec();
1517 let sys_usec = self_usage.system_time().tv_usec();
1518
1519 // Get rusage for children
1520 match getrusage(UsageWho::RUSAGE_CHILDREN) {
1521 Ok(child_usage) => {
1522 let child_user_sec = child_usage.user_time().tv_sec();
1523 let child_user_usec = child_usage.user_time().tv_usec();
1524 let child_sys_sec = child_usage.system_time().tv_sec();
1525 let child_sys_usec = child_usage.system_time().tv_usec();
1526
1527 println!("{}m{:.3}s {}m{:.3}s",
1528 user_sec / 60,
1529 (user_sec % 60) as f64 + user_usec as f64 / 1_000_000.0,
1530 sys_sec / 60,
1531 (sys_sec % 60) as f64 + sys_usec as f64 / 1_000_000.0);
1532 println!("{}m{:.3}s {}m{:.3}s",
1533 child_user_sec / 60,
1534 (child_user_sec % 60) as f64 + child_user_usec as f64 / 1_000_000.0,
1535 child_sys_sec / 60,
1536 (child_sys_sec % 60) as f64 + child_sys_usec as f64 / 1_000_000.0);
1537 }
1538 Err(_) => {
1539 eprintln!("times: failed to get child process times");
1540 return error_result();
1541 }
1542 }
1543 }
1544 Err(_) => {
1545 eprintln!("times: failed to get process times");
1546 return error_result();
1547 }
1548 }
1549
1550 success_result()
1551 }
1552
1553 #[cfg(not(unix))]
1554 {
1555 eprintln!("times: not supported on this platform");
1556 error_result()
1557 }
1558 }
1559
1560 /// umask builtin - Set or display file creation mask
1561 fn builtin_umask(args: &[String], _context: &mut rush_expand::Context) -> ExecutionResult {
1562 #[cfg(unix)]
1563 {
1564 use nix::sys::stat::{umask, Mode};
1565
1566 let mut symbolic = false;
1567
1568 // Parse options
1569 let mut mask_arg = None;
1570 for arg in args {
1571 if arg == "-S" {
1572 symbolic = true;
1573 } else if arg.starts_with('-') {
1574 eprintln!("umask: {}: invalid option", arg);
1575 return error_result();
1576 } else {
1577 mask_arg = Some(arg);
1578 }
1579 }
1580
1581 if let Some(mask_str) = mask_arg {
1582 // Set new mask
1583 let new_mask = if mask_str.starts_with('0') {
1584 // Octal format
1585 match u32::from_str_radix(mask_str, 8) {
1586 Ok(m) => Mode::from_bits_truncate(m),
1587 Err(_) => {
1588 eprintln!("umask: {}: invalid octal number", mask_str);
1589 return error_result();
1590 }
1591 }
1592 } else {
1593 // Decimal format (also support)
1594 match mask_str.parse::<u32>() {
1595 Ok(m) => Mode::from_bits_truncate(m),
1596 Err(_) => {
1597 eprintln!("umask: {}: invalid number", mask_str);
1598 return error_result();
1599 }
1600 }
1601 };
1602
1603 umask(new_mask);
1604 } else {
1605 // Display current mask
1606 // We need to set it twice to read the current value
1607 let current = umask(Mode::empty());
1608 umask(current);
1609
1610 if symbolic {
1611 // Symbolic format: u=rwx,g=rwx,o=rwx
1612 let mode = current.bits();
1613 let u_r = if mode & 0o400 == 0 { 'r' } else { '-' };
1614 let u_w = if mode & 0o200 == 0 { 'w' } else { '-' };
1615 let u_x = if mode & 0o100 == 0 { 'x' } else { '-' };
1616 let g_r = if mode & 0o040 == 0 { 'r' } else { '-' };
1617 let g_w = if mode & 0o020 == 0 { 'w' } else { '-' };
1618 let g_x = if mode & 0o010 == 0 { 'x' } else { '-' };
1619 let o_r = if mode & 0o004 == 0 { 'r' } else { '-' };
1620 let o_w = if mode & 0o002 == 0 { 'w' } else { '-' };
1621 let o_x = if mode & 0o001 == 0 { 'x' } else { '-' };
1622
1623 println!("u={}{}{},g={}{}{},o={}{}{}",
1624 u_r, u_w, u_x, g_r, g_w, g_x, o_r, o_w, o_x);
1625 } else {
1626 // Octal format
1627 println!("{:04o}", current.bits());
1628 }
1629 }
1630
1631 success_result()
1632 }
1633
1634 #[cfg(not(unix))]
1635 {
1636 eprintln!("umask: not supported on this platform");
1637 error_result()
1638 }
1639 }
1640
1641 /// hash builtin - Remember or report command locations
1642 fn builtin_hash(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
1643 let mut clear_hash = false;
1644 let mut print_all = false;
1645 let mut commands = Vec::new();
1646
1647 // Parse arguments
1648 for arg in args {
1649 if arg == "-r" {
1650 clear_hash = true;
1651 } else if arg == "-l" {
1652 print_all = true;
1653 } else if arg.starts_with('-') {
1654 eprintln!("hash: {}: invalid option", arg);
1655 return error_result();
1656 } else {
1657 commands.push(arg.clone());
1658 }
1659 }
1660
1661 // Clear hash table
1662 if clear_hash {
1663 context.command_hash.clear();
1664 return success_result();
1665 }
1666
1667 // No arguments: display hash table
1668 if commands.is_empty() {
1669 if context.command_hash.is_empty() {
1670 // Nothing to display
1671 return success_result();
1672 }
1673
1674 if print_all {
1675 // List format: path
1676 for (name, path) in &context.command_hash {
1677 println!("builtin hash -p {} {}", path, name);
1678 }
1679 } else {
1680 // Table format: hits command
1681 for (name, path) in &context.command_hash {
1682 println!("{}\t{}", name, path);
1683 }
1684 }
1685 return success_result();
1686 }
1687
1688 // Add commands to hash table
1689 let mut had_error = false;
1690 for cmd_name in &commands {
1691 if let Some(path) = find_in_path(cmd_name) {
1692 context.command_hash.insert(cmd_name.clone(), path.display().to_string());
1693 } else {
1694 eprintln!("hash: {}: not found", cmd_name);
1695 had_error = true;
1696 }
1697 }
1698
1699 if had_error {
1700 error_result()
1701 } else {
1702 success_result()
1703 }
1704 }
1705
1706 /// getopts builtin - Parse utility options
1707 fn builtin_getopts(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
1708 if args.len() < 2 {
1709 eprintln!("getopts: usage: getopts optstring name [args...]");
1710 return error_result();
1711 }
1712
1713 let optstring = &args[0];
1714 let var_name = &args[1];
1715
1716 // Get arguments to parse (either from args[2..] or positional parameters)
1717 let parse_args: Vec<String> = if args.len() > 2 {
1718 args[2..].to_vec()
1719 } else {
1720 context.positional_params.clone()
1721 };
1722
1723 // Check if we're done parsing
1724 if context.optind > parse_args.len() {
1725 // Reset OPTIND for next getopts call
1726 context.optind = 1;
1727 let _ = context.set_var("OPTIND", "1");
1728 return error_result(); // Return 1 to indicate done
1729 }
1730
1731 let current_arg = &parse_args[context.optind - 1];
1732
1733 // Check if this is an option
1734 if !current_arg.starts_with('-') || current_arg == "-" {
1735 // Not an option, we're done
1736 context.optind = 1;
1737 let _ = context.set_var("OPTIND", "1");
1738 return error_result();
1739 }
1740
1741 // Check for end of options marker "--"
1742 if current_arg == "--" {
1743 context.optind += 1;
1744 let _ = context.set_var("OPTIND", context.optind.to_string());
1745 context.optind = 1;
1746 return error_result();
1747 }
1748
1749 // Parse the option
1750 // For simplicity, we'll just handle single character options
1751 let opt_chars: Vec<char> = current_arg.chars().skip(1).collect();
1752 if opt_chars.is_empty() {
1753 context.optind += 1;
1754 let _ = context.set_var("OPTIND", context.optind.to_string());
1755 return error_result();
1756 }
1757
1758 let opt_char = opt_chars[0];
1759
1760 // Check if option is in optstring
1761 if let Some(pos) = optstring.find(opt_char) {
1762 // Set the variable to the option character
1763 let _ = context.set_var(var_name, opt_char.to_string());
1764
1765 // Check if option requires an argument
1766 if pos + 1 < optstring.len() && optstring.chars().nth(pos + 1) == Some(':') {
1767 // Option requires an argument
1768 if opt_chars.len() > 1 {
1769 // Argument is in same word: -oARG
1770 let arg_value: String = opt_chars[1..].iter().collect();
1771 let _ = context.set_var("OPTARG", arg_value);
1772 } else if context.optind < parse_args.len() {
1773 // Argument is next word: -o ARG
1774 context.optind += 1;
1775 let arg_value = &parse_args[context.optind - 1];
1776 let _ = context.set_var("OPTARG", arg_value);
1777 } else {
1778 // Missing required argument
1779 if optstring.starts_with(':') {
1780 // Silent mode: set var to ':', OPTARG to option char
1781 let _ = context.set_var(var_name, ":");
1782 let _ = context.set_var("OPTARG", opt_char.to_string());
1783 } else {
1784 eprintln!("getopts: option requires an argument -- {}", opt_char);
1785 let _ = context.set_var(var_name, "?");
1786 let _ = context.set_var("OPTARG", opt_char.to_string());
1787 }
1788 context.optind += 1;
1789 let _ = context.set_var("OPTIND", context.optind.to_string());
1790 return success_result();
1791 }
1792 }
1793
1794 context.optind += 1;
1795 let _ = context.set_var("OPTIND", context.optind.to_string());
1796 success_result()
1797 } else {
1798 // Invalid option
1799 if optstring.starts_with(':') {
1800 // Silent mode: set var to '?'
1801 let _ = context.set_var(var_name, "?");
1802 let _ = context.set_var("OPTARG", opt_char.to_string());
1803 } else {
1804 eprintln!("getopts: illegal option -- {}", opt_char);
1805 let _ = context.set_var(var_name, "?");
1806 let _ = context.set_var("OPTARG", opt_char.to_string());
1807 }
1808 context.optind += 1;
1809 let _ = context.set_var("OPTIND", context.optind.to_string());
1810 success_result()
1811 }
1812 }
1813
1814 #[cfg(unix)]
1815 pub(crate) fn success_result() -> ExecutionResult {
1816 ExecutionResult {
1817 exit_status: std::process::ExitStatus::from_raw(0),
1818 job_control: None,
1819 }
1820 }
1821
1822 #[cfg(unix)]
1823 fn error_result() -> ExecutionResult {
1824 ExecutionResult {
1825 exit_status: std::process::ExitStatus::from_raw(1 << 8),
1826 job_control: None,
1827 }
1828 }
1829
1830 #[cfg(not(unix))]
1831 pub(crate) fn success_result() -> ExecutionResult {
1832 ExecutionResult {
1833 exit_status: std::process::ExitStatus::default(),
1834 }
1835 }
1836
1837 #[cfg(not(unix))]
1838 fn error_result() -> ExecutionResult {
1839 // On non-Unix, we can't easily create a failed ExitStatus
1840 // This is a limitation for now
1841 ExecutionResult {
1842 exit_status: std::process::ExitStatus::default(),
1843 }
1844 }
1845
1846 /// Find a command in PATH
1847 pub(crate) fn find_in_path(command: &str) -> Option<PathBuf> {
1848 // If the command contains a slash, treat it as a path
1849 if command.contains('/') {
1850 let path = PathBuf::from(command);
1851 if path.exists() && is_executable(&path) {
1852 return Some(path);
1853 }
1854 return None;
1855 }
1856
1857 // Search in PATH
1858 let path_var = env::var_os("PATH")?;
1859 env::split_paths(&path_var)
1860 .map(|dir| dir.join(command))
1861 .find(|path| path.exists() && is_executable(path))
1862 }
1863
1864 /// Check if a file is executable
1865 #[cfg(unix)]
1866 fn is_executable(path: &PathBuf) -> bool {
1867 use std::os::unix::fs::PermissionsExt;
1868 path.metadata()
1869 .map(|m| m.permissions().mode() & 0o111 != 0)
1870 .unwrap_or(false)
1871 }
1872
1873 #[cfg(not(unix))]
1874 fn is_executable(_path: &PathBuf) -> bool {
1875 // On non-Unix systems, assume existence is enough
1876 true
1877 }
1878
1879 // Job control builtins (Unix only)
1880
1881 #[cfg(unix)]
1882 fn builtin_jobs(context: &mut rush_expand::Context) -> ExecutionResult {
1883 // List all jobs in sorted order
1884 for job in context.job_list.jobs_sorted() {
1885 println!("[{}] {} {}", job.id, job.status_string(), job.command);
1886 }
1887
1888 success_result()
1889 }
1890
1891 #[cfg(unix)]
1892 fn builtin_fg(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
1893 use nix::sys::signal::{kill, Signal};
1894 use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
1895 use rush_job::{give_terminal_to, JobState};
1896
1897 // Parse job ID argument (default to most recent job)
1898 let job_id = if let Some(arg) = args.first() {
1899 match arg.parse::<u32>() {
1900 Ok(id) => id,
1901 Err(_) => {
1902 eprintln!("fg: invalid job id: {}", arg);
1903 return error_result();
1904 }
1905 }
1906 } else {
1907 // Get most recent job
1908 match context.job_list.current_job() {
1909 Some(job) => job.id,
1910 None => {
1911 eprintln!("fg: no current job");
1912 return error_result();
1913 }
1914 }
1915 };
1916
1917 // Get the job's pgid before we mutate the job
1918 let (pgid, command, is_stopped) = {
1919 let job = match context.job_list.get_job(job_id) {
1920 Some(job) => job,
1921 None => {
1922 eprintln!("fg: job {} not found", job_id);
1923 return error_result();
1924 }
1925 };
1926 (job.pgid, job.command.clone(), job.is_stopped())
1927 };
1928
1929 // If the job is stopped, send SIGCONT to resume it
1930 if is_stopped {
1931 if let Err(e) = kill(pgid, Signal::SIGCONT) {
1932 eprintln!("fg: failed to continue job: {}", e);
1933 return error_result();
1934 }
1935 }
1936
1937 // Give terminal control to the job
1938 if let Err(e) = give_terminal_to(pgid) {
1939 eprintln!("fg: failed to give terminal control: {}", e);
1940 return error_result();
1941 }
1942
1943 // Update job state
1944 if let Some(job) = context.job_list.get_job_mut(job_id) {
1945 job.state = JobState::Running;
1946 }
1947 println!("{}", command);
1948
1949 // Wait for the job to complete or stop (wait on the process group leader)
1950 loop {
1951 match waitpid(pgid, Some(WaitPidFlag::WUNTRACED)) {
1952 Ok(WaitStatus::Exited(_, code)) => {
1953 if let Some(job) = context.job_list.get_job_mut(job_id) {
1954 job.state = JobState::Done(code);
1955 }
1956
1957 // Restore terminal to shell
1958 if let Err(e) = rush_job::restore_shell_terminal(nix::unistd::getpgrp()) {
1959 eprintln!("fg: failed to restore terminal: {}", e);
1960 }
1961
1962 return exit_code_to_result(code);
1963 }
1964 Ok(WaitStatus::Signaled(_, sig, _)) => {
1965 let exit_code = 128 + sig as i32;
1966 if let Some(job) = context.job_list.get_job_mut(job_id) {
1967 job.state = JobState::Done(exit_code);
1968 }
1969
1970 // Restore terminal to shell
1971 if let Err(e) = rush_job::restore_shell_terminal(nix::unistd::getpgrp()) {
1972 eprintln!("fg: failed to restore terminal: {}", e);
1973 }
1974
1975 return exit_code_to_result(exit_code);
1976 }
1977 Ok(WaitStatus::Stopped(_, _)) => {
1978 if let Some(job) = context.job_list.get_job_mut(job_id) {
1979 job.state = JobState::Stopped;
1980 }
1981
1982 // Restore terminal to shell
1983 if let Err(e) = rush_job::restore_shell_terminal(nix::unistd::getpgrp()) {
1984 eprintln!("fg: failed to restore terminal: {}", e);
1985 }
1986
1987 return success_result();
1988 }
1989 Err(e) => {
1990 eprintln!("fg: wait failed: {}", e);
1991
1992 // Restore terminal to shell
1993 if let Err(e) = rush_job::restore_shell_terminal(nix::unistd::getpgrp()) {
1994 eprintln!("fg: failed to restore terminal: {}", e);
1995 }
1996
1997 return error_result();
1998 }
1999 _ => continue,
2000 }
2001 }
2002 }
2003
2004 #[cfg(unix)]
2005 fn builtin_bg(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
2006 use nix::sys::signal::{kill, Signal};
2007 use rush_job::JobState;
2008
2009 // Parse job ID argument (default to most recent stopped job)
2010 let job_id = if let Some(arg) = args.first() {
2011 match arg.parse::<u32>() {
2012 Ok(id) => id,
2013 Err(_) => {
2014 eprintln!("bg: invalid job id: {}", arg);
2015 return error_result();
2016 }
2017 }
2018 } else {
2019 // Get most recent stopped job
2020 match context
2021 .job_list
2022 .jobs()
2023 .filter(|j| j.is_stopped())
2024 .max_by_key(|j| j.id)
2025 {
2026 Some(job) => job.id,
2027 None => {
2028 eprintln!("bg: no stopped job");
2029 return error_result();
2030 }
2031 }
2032 };
2033
2034 // Get the job
2035 let job = match context.job_list.get_job_mut(job_id) {
2036 Some(job) => job,
2037 None => {
2038 eprintln!("bg: job {} not found", job_id);
2039 return error_result();
2040 }
2041 };
2042
2043 // Job must be stopped
2044 if !job.is_stopped() {
2045 eprintln!("bg: job {} is not stopped", job_id);
2046 return error_result();
2047 }
2048
2049 // Send SIGCONT to resume the job in the background
2050 let pgid = job.pgid;
2051 if let Err(e) = kill(pgid, Signal::SIGCONT) {
2052 eprintln!("bg: failed to continue job: {}", e);
2053 return error_result();
2054 }
2055
2056 // Update job state
2057 job.state = JobState::Running;
2058 println!("[{}] {}", job.id, job.command);
2059
2060 success_result()
2061 }
2062
2063 #[cfg(unix)]
2064 fn builtin_coproc(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
2065 use nix::unistd::{fork, pipe, close, dup2, setpgid, ForkResult};
2066 use std::ffi::CString;
2067 use std::os::unix::ffi::OsStrExt;
2068 use std::os::unix::io::IntoRawFd;
2069
2070 // Parse arguments: coproc [NAME] command [args...]
2071 let (name, command_args) = if args.is_empty() {
2072 eprintln!("coproc: usage: coproc [NAME] command [args...]");
2073 return error_result();
2074 } else if args.len() == 1 {
2075 // Single arg: could be just command with default name
2076 ("COPROC".to_string(), args.to_vec())
2077 } else {
2078 // Check if first arg is a valid name (starts with letter or underscore)
2079 let first = &args[0];
2080 if first.chars().next().map(|c| c.is_alphabetic() || c == '_').unwrap_or(false)
2081 && first.chars().all(|c| c.is_alphanumeric() || c == '_')
2082 && args.len() > 1
2083 {
2084 // First arg is name, rest is command
2085 (first.clone(), args[1..].to_vec())
2086 } else {
2087 // First arg is part of command, use default name
2088 ("COPROC".to_string(), args.to_vec())
2089 }
2090 };
2091
2092 // Close any existing coproc
2093 context.clear_coproc();
2094
2095 // Create two pipes: one for reading from coproc stdout, one for writing to coproc stdin
2096 let (read_fd_parent, write_fd_child) = match pipe() {
2097 Ok((r, w)) => (r.into_raw_fd(), w.into_raw_fd()),
2098 Err(e) => {
2099 eprintln!("coproc: failed to create pipe: {}", e);
2100 return error_result();
2101 }
2102 };
2103
2104 let (read_fd_child, write_fd_parent) = match pipe() {
2105 Ok((r, w)) => (r.into_raw_fd(), w.into_raw_fd()),
2106 Err(e) => {
2107 eprintln!("coproc: failed to create pipe: {}", e);
2108 // close() is already unsafe, no need for unsafe block
2109 close(read_fd_parent).ok();
2110 close(write_fd_child).ok();
2111 return error_result();
2112 }
2113 };
2114
2115 // Fork the child process
2116 match unsafe { fork() } {
2117 Ok(ForkResult::Child) => {
2118 // Child process: set up pipes and exec command
2119
2120 // Close parent ends of pipes
2121 close(read_fd_parent).ok();
2122 close(write_fd_parent).ok();
2123
2124 // Redirect stdin from read_fd_child
2125 if let Err(e) = dup2(read_fd_child, 0) {
2126 eprintln!("coproc: failed to dup2 stdin: {}", e);
2127 std::process::exit(1);
2128 }
2129 close(read_fd_child).ok();
2130
2131 // Redirect stdout to write_fd_child
2132 if let Err(e) = dup2(write_fd_child, 1) {
2133 eprintln!("coproc: failed to dup2 stdout: {}", e);
2134 std::process::exit(1);
2135 }
2136 close(write_fd_child).ok();
2137
2138 // Find and execute the command
2139 let command_name = &command_args[0];
2140 let program_path = match find_in_path(command_name) {
2141 Some(path) => path,
2142 None => {
2143 eprintln!("coproc: {}: command not found", command_name);
2144 std::process::exit(127);
2145 }
2146 };
2147
2148 // Build argument list for execvp
2149 let program_cstring = match CString::new(program_path.as_os_str().as_bytes()) {
2150 Ok(s) => s,
2151 Err(_) => std::process::exit(1),
2152 };
2153
2154 let arg_cstrings: Vec<CString> = command_args.iter()
2155 .filter_map(|arg| CString::new(arg.as_bytes()).ok())
2156 .collect();
2157
2158 let arg_ptrs: Vec<*const i8> = std::iter::once(program_cstring.as_ptr())
2159 .chain(arg_cstrings.iter().map(|s| s.as_ptr()))
2160 .chain(std::iter::once(std::ptr::null()))
2161 .collect();
2162
2163 // Exec the command
2164 unsafe {
2165 nix::libc::execvp(program_cstring.as_ptr(), arg_ptrs.as_ptr());
2166 }
2167
2168 // If execvp returns, it failed
2169 eprintln!("coproc: failed to exec {}", command_name);
2170 std::process::exit(127);
2171 }
2172 Ok(ForkResult::Parent { child }) => {
2173 // Parent process: close child ends of pipes and set up coproc state
2174
2175 // Close child ends
2176 close(read_fd_child).ok();
2177 close(write_fd_child).ok();
2178
2179 let pid = child;
2180 let pgid = pid; // Use PID as PGID (process becomes group leader)
2181
2182 // Put child in its own process group
2183 if let Err(e) = setpgid(pid, pgid) {
2184 eprintln!("coproc: warning: failed to set process group: {}", e);
2185 }
2186
2187 // Build command string for display
2188 let command_string = command_args.join(" ");
2189
2190 // Store coproc state
2191 context.coproc = Some(rush_expand::CoprocState {
2192 name: name.clone(),
2193 read_fd: read_fd_parent,
2194 write_fd: write_fd_parent,
2195 pid,
2196 pgid,
2197 });
2198
2199 // Set array variables: NAME[0] = read_fd, NAME[1] = write_fd
2200 context.set_coproc_vars(&name, read_fd_parent, write_fd_parent);
2201
2202 // Add to job list
2203 let job_id = context.job_list.add_job(
2204 pgid,
2205 command_string.clone(),
2206 vec![pid],
2207 false, // not foreground
2208 );
2209
2210 // Print job notification
2211 println!("[{}] {}", job_id, pid);
2212
2213 success_result()
2214 }
2215 Err(e) => {
2216 eprintln!("coproc: fork failed: {}", e);
2217 // Clean up pipes
2218 close(read_fd_parent).ok();
2219 close(write_fd_parent).ok();
2220 close(read_fd_child).ok();
2221 close(write_fd_child).ok();
2222 error_result()
2223 }
2224 }
2225 }
2226
2227 /// source/. builtin - Execute commands from a file in the current shell context
2228 fn builtin_source(args: &[String], context: &mut rush_expand::Context) -> Result<ExecutionResult, String> {
2229 if args.is_empty() {
2230 return Err("filename required".to_string());
2231 }
2232
2233 let filename = &args[0];
2234
2235 // Read the file
2236 let content = std::fs::read_to_string(filename)
2237 .map_err(|e| format!("{}: {}", filename, e))?;
2238
2239 // Parse the entire content as a single unit to handle multiline constructs
2240 use rush_parser::parse_line;
2241
2242 match parse_line(&content) {
2243 Ok(statement) => {
2244 execute_statement(&statement, context)
2245 .map_err(|e| format!("{}: {}", filename, e))
2246 }
2247 Err(e) => {
2248 Err(format!("{}: parse error: {}", filename, e))
2249 }
2250 }
2251 }
2252
2253 /// eval builtin - Evaluate arguments as a shell command
2254 fn builtin_eval(args: &[String], context: &mut rush_expand::Context) -> Result<ExecutionResult, String> {
2255 if args.is_empty() {
2256 return Ok(success_result());
2257 }
2258
2259 // Join all arguments into a single command string
2260 let command = args.join(" ");
2261
2262 // Parse and execute
2263 use rush_parser::parse_line;
2264
2265 match parse_line(&command) {
2266 Ok(statement) => {
2267 execute_statement(&statement, context)
2268 .map_err(|e| format!("{}", e))
2269 }
2270 Err(e) => {
2271 Err(format!("parse error: {}", e))
2272 }
2273 }
2274 }
2275
2276 /// cd builtin - change directory
2277 /// Supports: cd, cd -, cd ~, cd ~user, cd /path
2278 fn builtin_cd(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
2279 let home = env::var("HOME").unwrap_or_else(|_| "/".to_string());
2280
2281 // Get target directory
2282 let target = if args.is_empty() {
2283 // cd with no args goes to $HOME
2284 home.clone()
2285 } else {
2286 let arg = &args[0];
2287 if arg == "-" {
2288 // cd - goes to $OLDPWD
2289 match context.get_var("OLDPWD") {
2290 Some(oldpwd) => {
2291 println!("{}", oldpwd);
2292 oldpwd.to_string()
2293 }
2294 None => {
2295 eprintln!("cd: OLDPWD not set");
2296 return error_result();
2297 }
2298 }
2299 } else if arg == "~" || arg.is_empty() {
2300 // cd ~ goes to $HOME
2301 home.clone()
2302 } else if arg.starts_with("~/") {
2303 // cd ~/path goes to $HOME/path
2304 format!("{}/{}", home, &arg[2..])
2305 } else if arg.starts_with('~') {
2306 // cd ~user - lookup user's home directory
2307 let (username, suffix) = if let Some(slash_pos) = arg.find('/') {
2308 (&arg[1..slash_pos], &arg[slash_pos..])
2309 } else {
2310 (&arg[1..], "")
2311 };
2312
2313 #[cfg(unix)]
2314 {
2315 use nix::unistd::User;
2316 match User::from_name(username) {
2317 Ok(Some(user)) => {
2318 let home = user.dir.to_string_lossy();
2319 if suffix.is_empty() {
2320 home.to_string()
2321 } else {
2322 format!("{}{}", home, suffix)
2323 }
2324 }
2325 Ok(None) => {
2326 eprintln!("cd: ~{}: No such user", username);
2327 return error_result();
2328 }
2329 Err(e) => {
2330 eprintln!("cd: ~{}: {}", username, e);
2331 return error_result();
2332 }
2333 }
2334 }
2335 #[cfg(not(unix))]
2336 {
2337 // On non-Unix platforms, just treat as literal path
2338 arg.clone()
2339 }
2340 } else {
2341 arg.clone()
2342 }
2343 };
2344
2345 // Save current directory as OLDPWD before changing
2346 if let Ok(cwd) = env::current_dir() {
2347 let _ = context.set_var("OLDPWD", cwd.to_string_lossy().to_string());
2348 }
2349
2350 // Change directory
2351 match env::set_current_dir(&target) {
2352 Ok(_) => {
2353 // Update PWD
2354 if let Ok(new_cwd) = env::current_dir() {
2355 let _ = context.set_var("PWD", new_cwd.to_string_lossy().to_string());
2356 }
2357 success_result()
2358 }
2359 Err(e) => {
2360 eprintln!("cd: {}: {}", target, e);
2361 error_result()
2362 }
2363 }
2364 }
2365
2366 /// pushd builtin - push directory onto stack and cd to it
2367 fn builtin_pushd(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
2368 // Get current directory first
2369 let cwd = match env::current_dir() {
2370 Ok(p) => p.to_string_lossy().to_string(),
2371 Err(e) => {
2372 eprintln!("pushd: error getting current directory: {}", e);
2373 return error_result();
2374 }
2375 };
2376
2377 if args.is_empty() {
2378 // pushd with no args swaps top two directories
2379 if context.dir_stack.is_empty() {
2380 eprintln!("pushd: no other directory");
2381 return error_result();
2382 }
2383
2384 let top = context.dir_stack.remove(0);
2385 context.dir_stack.insert(0, cwd.clone());
2386
2387 // Change to the popped directory
2388 if let Err(e) = env::set_current_dir(&top) {
2389 // Restore stack on failure
2390 context.dir_stack.remove(0);
2391 context.dir_stack.insert(0, top);
2392 eprintln!("pushd: {}", e);
2393 return error_result();
2394 }
2395
2396 // Update OLDPWD and PWD
2397 let _ = context.set_var("OLDPWD", cwd);
2398 let _ = context.set_var("PWD", top.clone());
2399
2400 // Print the stack
2401 print_dir_stack(context);
2402 success_result()
2403 } else {
2404 let target = &args[0];
2405
2406 // Expand ~ in target
2407 let expanded = if target == "~" {
2408 env::var("HOME").unwrap_or_else(|_| "/".to_string())
2409 } else if target.starts_with("~/") {
2410 let home = env::var("HOME").unwrap_or_else(|_| "/".to_string());
2411 format!("{}/{}", home, &target[2..])
2412 } else {
2413 target.clone()
2414 };
2415
2416 // Push current directory onto stack
2417 context.dir_stack.insert(0, cwd.clone());
2418
2419 // Change to new directory
2420 if let Err(e) = env::set_current_dir(&expanded) {
2421 // Remove the directory we just pushed on failure
2422 context.dir_stack.remove(0);
2423 eprintln!("pushd: {}: {}", expanded, e);
2424 return error_result();
2425 }
2426
2427 // Update OLDPWD and PWD
2428 let _ = context.set_var("OLDPWD", cwd);
2429 if let Ok(new_cwd) = env::current_dir() {
2430 let _ = context.set_var("PWD", new_cwd.to_string_lossy().to_string());
2431 }
2432
2433 // Print the stack
2434 print_dir_stack(context);
2435 success_result()
2436 }
2437 }
2438
2439 /// popd builtin - pop directory from stack and cd to it
2440 fn builtin_popd(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
2441 // Check for -n flag (don't change directory, just manipulate stack)
2442 let no_cd = args.iter().any(|a| a == "-n");
2443
2444 if context.dir_stack.is_empty() {
2445 eprintln!("popd: directory stack empty");
2446 return error_result();
2447 }
2448
2449 let dir = context.dir_stack.remove(0);
2450
2451 if !no_cd {
2452 // Save current directory as OLDPWD
2453 if let Ok(cwd) = env::current_dir() {
2454 let _ = context.set_var("OLDPWD", cwd.to_string_lossy().to_string());
2455 }
2456
2457 // Change to the popped directory
2458 if let Err(e) = env::set_current_dir(&dir) {
2459 // Re-add directory to stack on failure
2460 context.dir_stack.insert(0, dir);
2461 eprintln!("popd: {}", e);
2462 return error_result();
2463 }
2464
2465 // Update PWD
2466 if let Ok(new_cwd) = env::current_dir() {
2467 let _ = context.set_var("PWD", new_cwd.to_string_lossy().to_string());
2468 }
2469 }
2470
2471 // Print the stack
2472 print_dir_stack(context);
2473 success_result()
2474 }
2475
2476 /// dirs builtin - display directory stack
2477 fn builtin_dirs(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
2478 let clear = args.iter().any(|a| a == "-c");
2479 let print_index = args.iter().any(|a| a == "-v");
2480 let one_per_line = args.iter().any(|a| a == "-p");
2481
2482 if clear {
2483 context.dir_stack.clear();
2484 return success_result();
2485 }
2486
2487 // Get current directory
2488 let cwd = env::current_dir()
2489 .map(|p| p.to_string_lossy().to_string())
2490 .unwrap_or_else(|_| ".".to_string());
2491
2492 if print_index {
2493 // Print with indices
2494 println!(" 0 {}", cwd);
2495 for (i, dir) in context.dir_stack.iter().enumerate() {
2496 println!(" {} {}", i + 1, dir);
2497 }
2498 } else if one_per_line {
2499 // One directory per line
2500 println!("{}", cwd);
2501 for dir in &context.dir_stack {
2502 println!("{}", dir);
2503 }
2504 } else {
2505 // Default: space-separated on one line
2506 print_dir_stack(context);
2507 }
2508
2509 success_result()
2510 }
2511
2512 /// Helper to print the directory stack
2513 fn print_dir_stack(context: &rush_expand::Context) {
2514 let cwd = env::current_dir()
2515 .map(|p| p.to_string_lossy().to_string())
2516 .unwrap_or_else(|_| ".".to_string());
2517
2518 let mut parts = vec![cwd];
2519 parts.extend(context.dir_stack.iter().cloned());
2520 println!("{}", parts.join(" "));
2521 }
2522
2523 /// echo builtin - display a line of text
2524 ///
2525 /// Uses direct writes to stdout for reliable capture in command substitution
2526 fn builtin_echo(args: &[String]) -> ExecutionResult {
2527 use std::io::Write;
2528
2529 let mut newline = true;
2530 let mut interpret_escapes = false;
2531 let mut args_iter = args.iter().peekable();
2532
2533 // Parse options (bash-style: only at the start, stop at first non-option)
2534 while let Some(arg) = args_iter.peek() {
2535 if arg.starts_with('-') && arg.len() > 1 && arg.chars().skip(1).all(|c| matches!(c, 'n' | 'e' | 'E')) {
2536 let arg = args_iter.next().unwrap();
2537 for c in arg.chars().skip(1) {
2538 match c {
2539 'n' => newline = false,
2540 'e' => interpret_escapes = true,
2541 'E' => interpret_escapes = false,
2542 _ => {}
2543 }
2544 }
2545 } else {
2546 break;
2547 }
2548 }
2549
2550 let remaining: Vec<&String> = args_iter.collect();
2551 let text = remaining.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" ");
2552
2553 // Get stdout handle and lock it for the duration of the write
2554 let stdout = std::io::stdout();
2555 let mut handle = stdout.lock();
2556
2557 if interpret_escapes {
2558 let mut output = String::new();
2559 let mut chars = text.chars().peekable();
2560 while let Some(c) = chars.next() {
2561 if c == '\\' {
2562 match chars.next() {
2563 Some('n') => output.push('\n'),
2564 Some('t') => output.push('\t'),
2565 Some('r') => output.push('\r'),
2566 Some('\\') => output.push('\\'),
2567 Some('a') => output.push('\x07'),
2568 Some('b') => output.push('\x08'),
2569 Some('f') => output.push('\x0C'),
2570 Some('v') => output.push('\x0B'),
2571 Some('0') => {
2572 // Octal escape
2573 let mut oct = String::new();
2574 for _ in 0..3 {
2575 if let Some(&ch) = chars.peek() {
2576 if ch >= '0' && ch <= '7' {
2577 oct.push(chars.next().unwrap());
2578 } else {
2579 break;
2580 }
2581 }
2582 }
2583 let val = u8::from_str_radix(&oct, 8).unwrap_or(0);
2584 output.push(val as char);
2585 }
2586 Some('x') => {
2587 // Hex escape
2588 let mut hex = String::new();
2589 for _ in 0..2 {
2590 if let Some(&ch) = chars.peek() {
2591 if ch.is_ascii_hexdigit() {
2592 hex.push(chars.next().unwrap());
2593 } else {
2594 break;
2595 }
2596 }
2597 }
2598 if !hex.is_empty() {
2599 let val = u8::from_str_radix(&hex, 16).unwrap_or(0);
2600 output.push(val as char);
2601 } else {
2602 output.push_str("\\x");
2603 }
2604 }
2605 Some('c') => {
2606 // Stop output (no newline either)
2607 let _ = handle.write_all(output.as_bytes());
2608 let _ = handle.flush();
2609 return success_result();
2610 }
2611 Some(other) => {
2612 output.push('\\');
2613 output.push(other);
2614 }
2615 None => output.push('\\'),
2616 }
2617 } else {
2618 output.push(c);
2619 }
2620 }
2621 let _ = handle.write_all(output.as_bytes());
2622 } else {
2623 let _ = handle.write_all(text.as_bytes());
2624 }
2625
2626 if newline {
2627 let _ = handle.write_all(b"\n");
2628 }
2629
2630 let _ = handle.flush();
2631 success_result()
2632 }
2633
2634 /// printf builtin - formatted output
2635 fn builtin_printf(args: &[String], _context: &mut rush_expand::Context) -> ExecutionResult {
2636 if args.is_empty() {
2637 eprintln!("printf: usage: printf format [arguments]");
2638 return error_result();
2639 }
2640
2641 let format = &args[0];
2642 let arguments = &args[1..];
2643 let mut arg_index = 0;
2644
2645 let mut output = String::new();
2646 let mut chars = format.chars().peekable();
2647
2648 while let Some(c) = chars.next() {
2649 if c == '\\' {
2650 // Handle escape sequences
2651 match chars.next() {
2652 Some('n') => output.push('\n'),
2653 Some('t') => output.push('\t'),
2654 Some('r') => output.push('\r'),
2655 Some('\\') => output.push('\\'),
2656 Some('"') => output.push('"'),
2657 Some('\'') => output.push('\''),
2658 Some('a') => output.push('\x07'), // bell
2659 Some('b') => output.push('\x08'), // backspace
2660 Some('f') => output.push('\x0C'), // form feed
2661 Some('v') => output.push('\x0B'), // vertical tab
2662 Some('0') => {
2663 // Octal escape \0nnn
2664 let mut oct = String::new();
2665 for _ in 0..3 {
2666 if let Some(&ch) = chars.peek() {
2667 if ch >= '0' && ch <= '7' {
2668 oct.push(chars.next().unwrap());
2669 } else {
2670 break;
2671 }
2672 }
2673 }
2674 if oct.is_empty() {
2675 output.push('\0');
2676 } else {
2677 let val = u8::from_str_radix(&oct, 8).unwrap_or(0);
2678 output.push(val as char);
2679 }
2680 }
2681 Some('x') => {
2682 // Hex escape \xHH
2683 let mut hex = String::new();
2684 for _ in 0..2 {
2685 if let Some(&ch) = chars.peek() {
2686 if ch.is_ascii_hexdigit() {
2687 hex.push(chars.next().unwrap());
2688 } else {
2689 break;
2690 }
2691 }
2692 }
2693 if !hex.is_empty() {
2694 let val = u8::from_str_radix(&hex, 16).unwrap_or(0);
2695 output.push(val as char);
2696 }
2697 }
2698 Some(other) => {
2699 output.push('\\');
2700 output.push(other);
2701 }
2702 None => output.push('\\'),
2703 }
2704 } else if c == '%' {
2705 // Handle format specifiers
2706 match chars.peek() {
2707 Some('%') => {
2708 chars.next();
2709 output.push('%');
2710 }
2711 _ => {
2712 // Parse format specifier: %[flags][width][.precision]specifier
2713 let mut spec = String::from('%');
2714
2715 // Flags
2716 while let Some(&ch) = chars.peek() {
2717 if ch == '-' || ch == '+' || ch == ' ' || ch == '#' || ch == '0' {
2718 spec.push(chars.next().unwrap());
2719 } else {
2720 break;
2721 }
2722 }
2723
2724 // Width
2725 while let Some(&ch) = chars.peek() {
2726 if ch.is_ascii_digit() {
2727 spec.push(chars.next().unwrap());
2728 } else {
2729 break;
2730 }
2731 }
2732
2733 // Precision
2734 if chars.peek() == Some(&'.') {
2735 spec.push(chars.next().unwrap());
2736 while let Some(&ch) = chars.peek() {
2737 if ch.is_ascii_digit() {
2738 spec.push(chars.next().unwrap());
2739 } else {
2740 break;
2741 }
2742 }
2743 }
2744
2745 // Specifier
2746 let specifier = chars.next().unwrap_or('s');
2747 let arg = arguments.get(arg_index).map(|s| s.as_str()).unwrap_or("");
2748 arg_index += 1;
2749
2750 match specifier {
2751 's' => {
2752 // String - handle width/precision
2753 output.push_str(arg);
2754 }
2755 'd' | 'i' => {
2756 let val: i64 = arg.parse().unwrap_or(0);
2757 output.push_str(&val.to_string());
2758 }
2759 'u' => {
2760 let val: u64 = arg.parse().unwrap_or(0);
2761 output.push_str(&val.to_string());
2762 }
2763 'o' => {
2764 let val: u64 = arg.parse().unwrap_or(0);
2765 output.push_str(&format!("{:o}", val));
2766 }
2767 'x' => {
2768 let val: u64 = arg.parse().unwrap_or(0);
2769 output.push_str(&format!("{:x}", val));
2770 }
2771 'X' => {
2772 let val: u64 = arg.parse().unwrap_or(0);
2773 output.push_str(&format!("{:X}", val));
2774 }
2775 'f' | 'F' | 'e' | 'E' | 'g' | 'G' => {
2776 let val: f64 = arg.parse().unwrap_or(0.0);
2777 output.push_str(&format!("{}", val));
2778 }
2779 'c' => {
2780 if let Some(ch) = arg.chars().next() {
2781 output.push(ch);
2782 }
2783 }
2784 'b' => {
2785 // %b - interpret backslash escapes in the argument
2786 let mut arg_chars = arg.chars().peekable();
2787 while let Some(ac) = arg_chars.next() {
2788 if ac == '\\' {
2789 match arg_chars.next() {
2790 Some('n') => output.push('\n'),
2791 Some('t') => output.push('\t'),
2792 Some('r') => output.push('\r'),
2793 Some('\\') => output.push('\\'),
2794 Some(other) => {
2795 output.push('\\');
2796 output.push(other);
2797 }
2798 None => output.push('\\'),
2799 }
2800 } else {
2801 output.push(ac);
2802 }
2803 }
2804 }
2805 'q' => {
2806 // %q - quote the argument for shell reuse
2807 output.push('\'');
2808 for ch in arg.chars() {
2809 if ch == '\'' {
2810 output.push_str("'\\''");
2811 } else {
2812 output.push(ch);
2813 }
2814 }
2815 output.push('\'');
2816 }
2817 _ => {
2818 // Unknown specifier, output as-is
2819 output.push('%');
2820 output.push(specifier);
2821 }
2822 }
2823 }
2824 }
2825 } else {
2826 output.push(c);
2827 }
2828 }
2829
2830 print!("{}", output);
2831 success_result()
2832 }
2833
2834 /// mapfile/readarray builtin - read lines into an array
2835 fn builtin_mapfile(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
2836 use std::io::{self, BufRead, Read};
2837
2838 let mut delimiter = '\n';
2839 let mut count: Option<usize> = None;
2840 let mut origin = 0usize;
2841 let mut skip = 0usize;
2842 let mut remove_delimiter = false;
2843 let mut array_name = String::from("MAPFILE");
2844
2845 // Parse options
2846 let mut i = 0;
2847 while i < args.len() {
2848 match args[i].as_str() {
2849 "-d" => {
2850 i += 1;
2851 if i < args.len() {
2852 delimiter = args[i].chars().next().unwrap_or('\n');
2853 }
2854 }
2855 "-n" => {
2856 i += 1;
2857 if i < args.len() {
2858 count = args[i].parse().ok();
2859 }
2860 }
2861 "-O" => {
2862 i += 1;
2863 if i < args.len() {
2864 origin = args[i].parse().unwrap_or(0);
2865 }
2866 }
2867 "-s" => {
2868 i += 1;
2869 if i < args.len() {
2870 skip = args[i].parse().unwrap_or(0);
2871 }
2872 }
2873 "-t" => {
2874 remove_delimiter = true;
2875 }
2876 arg if !arg.starts_with('-') => {
2877 array_name = arg.to_string();
2878 }
2879 _ => {
2880 // Unknown option, skip
2881 }
2882 }
2883 i += 1;
2884 }
2885
2886 let stdin = io::stdin();
2887 let reader = stdin.lock();
2888 let mut lines: Vec<String> = Vec::new();
2889 let mut lines_read = 0usize;
2890
2891 if delimiter == '\n' {
2892 for line_result in reader.lines() {
2893 match line_result {
2894 Ok(line) => {
2895 if lines_read < skip {
2896 lines_read += 1;
2897 continue;
2898 }
2899
2900 if let Some(max) = count {
2901 if lines.len() >= max {
2902 break;
2903 }
2904 }
2905
2906 let content = if remove_delimiter {
2907 line
2908 } else {
2909 format!("{}\n", line)
2910 };
2911 lines.push(content);
2912 lines_read += 1;
2913 }
2914 Err(_) => break,
2915 }
2916 }
2917 } else {
2918 // Custom delimiter
2919 let mut buffer = String::new();
2920 for byte_result in reader.bytes() {
2921 match byte_result {
2922 Ok(byte) => {
2923 let ch = byte as char;
2924 if ch == delimiter {
2925 if lines_read < skip {
2926 buffer.clear();
2927 lines_read += 1;
2928 continue;
2929 }
2930
2931 if let Some(max) = count {
2932 if lines.len() >= max {
2933 break;
2934 }
2935 }
2936
2937 let content = if remove_delimiter {
2938 buffer.clone()
2939 } else {
2940 format!("{}{}", buffer, delimiter)
2941 };
2942 lines.push(content);
2943 buffer.clear();
2944 lines_read += 1;
2945 } else {
2946 buffer.push(ch);
2947 }
2948 }
2949 Err(_) => break,
2950 }
2951 }
2952 // Handle remaining content
2953 if !buffer.is_empty() && (count.is_none() || lines.len() < count.unwrap()) {
2954 if lines_read >= skip {
2955 lines.push(buffer);
2956 }
2957 }
2958 }
2959
2960 // Build array starting at origin index
2961 let mut array_values: Vec<String> = Vec::new();
2962
2963 // Preserve existing elements if origin > 0
2964 if origin > 0 {
2965 if let Some(rush_expand::context::ArrayType::Indexed(existing)) = context.arrays.get(&array_name) {
2966 for i in 0..origin {
2967 array_values.push(existing.get(i).cloned().unwrap_or_default());
2968 }
2969 } else {
2970 for _ in 0..origin {
2971 array_values.push(String::new());
2972 }
2973 }
2974 }
2975
2976 array_values.extend(lines);
2977
2978 context.arrays.insert(array_name, rush_expand::context::ArrayType::Indexed(array_values));
2979
2980 success_result()
2981 }
2982
2983 /// disown builtin - remove jobs from job table
2984 #[cfg(unix)]
2985 fn builtin_disown(args: &[String], context: &mut rush_expand::Context) -> ExecutionResult {
2986 let mut _mark_no_sighup = false; // TODO: implement -h to prevent SIGHUP
2987 let mut all_jobs = false;
2988 let mut running_only = false;
2989 let mut job_specs: Vec<String> = Vec::new();
2990
2991 // Parse options
2992 for arg in args {
2993 match arg.as_str() {
2994 "-h" => _mark_no_sighup = true,
2995 "-a" => all_jobs = true,
2996 "-r" => running_only = true,
2997 _ if arg.starts_with('%') || arg.parse::<u32>().is_ok() => {
2998 job_specs.push(arg.clone());
2999 }
3000 _ => {
3001 eprintln!("disown: {}: invalid option", arg);
3002 return error_result();
3003 }
3004 }
3005 }
3006
3007 // If -a flag, disown all jobs
3008 if all_jobs {
3009 context.job_list.disown_all(running_only);
3010 return success_result();
3011 }
3012
3013 // If no job specs and no -a, disown current job
3014 if job_specs.is_empty() {
3015 if let Some(current_job) = context.job_list.current_job_id() {
3016 context.job_list.disown_job(current_job);
3017 }
3018 return success_result();
3019 }
3020
3021 // Disown specific jobs
3022 for spec in job_specs {
3023 let job_id = if spec.starts_with('%') {
3024 // Job ID format: %n or %+, %-, etc.
3025 let id_str = &spec[1..];
3026 if id_str == "+" || id_str == "%" {
3027 context.job_list.current_job_id()
3028 } else if id_str == "-" {
3029 context.job_list.previous_job()
3030 } else {
3031 id_str.parse::<u32>().ok()
3032 }
3033 } else {
3034 spec.parse::<u32>().ok()
3035 };
3036
3037 if let Some(id) = job_id {
3038 context.job_list.disown_job(id);
3039 } else {
3040 eprintln!("disown: {}: no such job", spec);
3041 }
3042 }
3043
3044 success_result()
3045 }
3046
3047 /// Helper to execute a parsed statement
3048 pub fn execute_statement(
3049 statement: &rush_parser::Statement,
3050 context: &mut rush_expand::Context,
3051 ) -> Result<ExecutionResult, String> {
3052 use rush_parser::Statement;
3053
3054 match statement {
3055 Statement::Empty => Ok(success_result()),
3056 Statement::Complete(cmd) => {
3057 crate::control_flow::execute_complete_command(cmd, context)
3058 .map_err(|e| e.to_string())
3059 }
3060 Statement::Script(commands) => {
3061 let mut last_result = success_result();
3062 for cmd in commands {
3063 last_result = crate::control_flow::execute_complete_command(cmd, context)
3064 .map_err(|e| e.to_string())?;
3065 context.set_exit_status(last_result.exit_code());
3066 }
3067 Ok(last_result)
3068 }
3069 }
3070 }
3071
3072 /// `complete` builtin - Define command-specific completions
3073 ///
3074 /// Usage:
3075 /// complete -c COMMAND [-a COMPLETIONS] [-d DESC] [-s SHORT] [-l LONG] [-f] [-n COND]
3076 /// complete -c COMMAND -e # Erase completions
3077 /// complete -p # Print all completions
3078 /// complete -c COMMAND -p # Print completions for COMMAND
3079 ///
3080 /// Options:
3081 /// -c COMMAND The command to add completions for
3082 /// -a ARGS Completions to add (space-separated, or command in parentheses)
3083 /// -d DESC Description for the completion
3084 /// -s SHORT Short option (e.g., -v)
3085 /// -l LONG Long option (e.g., --verbose)
3086 /// -f Disable file completion for this command
3087 /// -n COND Condition for when this completion applies
3088 /// -e Erase all completions for the command
3089 /// -p Print completions
3090 fn builtin_complete(args: &[String], _context: &rush_expand::Context) -> ExecutionResult {
3091 use rush_interactive::{
3092 add_completion, remove_completions, with_registry,
3093 CompletionSource, CompletionSpec,
3094 };
3095
3096 let mut command: Option<String> = None;
3097 let mut completions: Option<String> = None;
3098 let mut description: Option<String> = None;
3099 let mut short_opt: Option<char> = None;
3100 let mut long_opt: Option<String> = None;
3101 let mut no_files = false;
3102 let mut condition: Option<String> = None;
3103 let mut erase = false;
3104 let mut print = false;
3105
3106 // Parse arguments
3107 let mut i = 0;
3108 while i < args.len() {
3109 let arg = &args[i];
3110 match arg.as_str() {
3111 "-c" => {
3112 i += 1;
3113 if i < args.len() {
3114 command = Some(args[i].clone());
3115 } else {
3116 eprintln!("complete: -c requires an argument");
3117 return error_result();
3118 }
3119 }
3120 "-a" => {
3121 i += 1;
3122 if i < args.len() {
3123 completions = Some(args[i].clone());
3124 } else {
3125 eprintln!("complete: -a requires an argument");
3126 return error_result();
3127 }
3128 }
3129 "-d" => {
3130 i += 1;
3131 if i < args.len() {
3132 description = Some(args[i].clone());
3133 } else {
3134 eprintln!("complete: -d requires an argument");
3135 return error_result();
3136 }
3137 }
3138 "-s" => {
3139 i += 1;
3140 if i < args.len() {
3141 short_opt = args[i].chars().next();
3142 } else {
3143 eprintln!("complete: -s requires an argument");
3144 return error_result();
3145 }
3146 }
3147 "-l" => {
3148 i += 1;
3149 if i < args.len() {
3150 long_opt = Some(args[i].clone());
3151 } else {
3152 eprintln!("complete: -l requires an argument");
3153 return error_result();
3154 }
3155 }
3156 "-n" => {
3157 i += 1;
3158 if i < args.len() {
3159 condition = Some(args[i].clone());
3160 } else {
3161 eprintln!("complete: -n requires an argument");
3162 return error_result();
3163 }
3164 }
3165 "-f" => no_files = true,
3166 "-e" => erase = true,
3167 "-p" => print = true,
3168 _ => {
3169 eprintln!("complete: unknown option: {}", arg);
3170 return error_result();
3171 }
3172 }
3173 i += 1;
3174 }
3175
3176 // Handle print mode
3177 if print {
3178 with_registry(|registry| {
3179 if let Some(cmd) = &command {
3180 // Print completions for specific command
3181 if let Some(specs) = registry.get(cmd) {
3182 for spec in specs {
3183 print_completion_spec(spec);
3184 }
3185 }
3186 } else {
3187 // Print all completions
3188 for (cmd, specs) in registry.all_specs() {
3189 println!("# Completions for '{}'", cmd);
3190 for spec in specs {
3191 print_completion_spec(spec);
3192 }
3193 }
3194 }
3195 });
3196 return success_result();
3197 }
3198
3199 // Handle erase mode
3200 if erase {
3201 if let Some(cmd) = command {
3202 remove_completions(&cmd);
3203 return success_result();
3204 } else {
3205 eprintln!("complete: -e requires -c COMMAND");
3206 return error_result();
3207 }
3208 }
3209
3210 // Adding a completion requires a command
3211 let cmd = match command {
3212 Some(c) => c,
3213 None => {
3214 eprintln!("complete: -c COMMAND is required");
3215 return error_result();
3216 }
3217 };
3218
3219 // Determine the completion source
3220 let source = if let Some(comps) = completions {
3221 // Check if it's a dynamic command (wrapped in parentheses)
3222 if comps.starts_with('(') && comps.ends_with(')') {
3223 CompletionSource::Dynamic(comps[1..comps.len()-1].to_string())
3224 } else {
3225 // Static list of completions (space-separated)
3226 CompletionSource::Static(
3227 comps.split_whitespace().map(String::from).collect()
3228 )
3229 }
3230 } else if short_opt.is_some() || long_opt.is_some() {
3231 CompletionSource::Option {
3232 short: short_opt,
3233 long: long_opt,
3234 }
3235 } else if no_files {
3236 // Just marking no-files without adding completions
3237 CompletionSource::Static(vec![])
3238 } else {
3239 eprintln!("complete: need -a, -s, -l, or -f");
3240 return error_result();
3241 };
3242
3243 // Create and register the completion spec
3244 let spec = CompletionSpec {
3245 command: cmd,
3246 condition,
3247 source,
3248 description,
3249 no_files,
3250 };
3251
3252 add_completion(spec);
3253 success_result()
3254 }
3255
3256 fn print_completion_spec(spec: &rush_interactive::CompletionSpec) {
3257 use rush_interactive::CompletionSource;
3258
3259 let mut parts = vec![format!("complete -c {}", spec.command)];
3260
3261 if let Some(ref cond) = spec.condition {
3262 parts.push(format!("-n \"{}\"", cond));
3263 }
3264
3265 match &spec.source {
3266 CompletionSource::Static(items) if !items.is_empty() => {
3267 parts.push(format!("-a \"{}\"", items.join(" ")));
3268 }
3269 CompletionSource::Dynamic(cmd) => {
3270 parts.push(format!("-a \"({})\"", cmd));
3271 }
3272 CompletionSource::ShortOption(c) => {
3273 parts.push(format!("-s {}", c));
3274 }
3275 CompletionSource::LongOption(s) => {
3276 parts.push(format!("-l {}", s));
3277 }
3278 CompletionSource::Option { short, long } => {
3279 if let Some(c) = short {
3280 parts.push(format!("-s {}", c));
3281 }
3282 if let Some(l) = long {
3283 parts.push(format!("-l {}", l));
3284 }
3285 }
3286 _ => {}
3287 }
3288
3289 if let Some(ref desc) = spec.description {
3290 parts.push(format!("-d \"{}\"", desc));
3291 }
3292
3293 if spec.no_files {
3294 parts.push("-f".to_string());
3295 }
3296
3297 println!("{}", parts.join(" "));
3298 }
3299
3300 #[cfg(test)]
3301 mod tests {
3302 use super::*;
3303
3304 #[test]
3305 fn test_find_in_path() {
3306 // ls should exist on most Unix systems
3307 let result = find_in_path("ls");
3308 assert!(result.is_some());
3309 }
3310
3311 #[test]
3312 fn test_command_not_found() {
3313 let mut context = rush_expand::Context::empty();
3314 let result = execute_command("nonexistent_command_12345", &[], false, &mut context);
3315 assert!(result.is_err());
3316 }
3317 }
3318