1pub(crate) mod awk;
4pub(crate) mod compression;
5pub(crate) mod diff_cmd;
6pub(crate) mod exec_cmds;
7pub(crate) mod file_ops;
8pub(crate) mod jq_cmd;
9pub(crate) mod navigation;
10#[cfg(feature = "network")]
11pub(crate) mod net;
12pub(crate) mod regex_util;
13pub(crate) mod sed;
14pub(crate) mod test_cmd;
15pub(crate) mod text;
16pub(crate) mod utils;
17
18use crate::error::RustBashError;
19use crate::interpreter::ExecutionLimits;
20use crate::network::NetworkPolicy;
21use crate::vfs::VirtualFs;
22use std::collections::HashMap;
23
24#[derive(Debug, Clone, Default, PartialEq, Eq)]
26pub struct CommandResult {
27 pub stdout: String,
28 pub stderr: String,
29 pub exit_code: i32,
30 pub stdout_bytes: Option<Vec<u8>>,
33}
34
35pub type ExecCallback<'a> = &'a dyn Fn(&str) -> Result<CommandResult, RustBashError>;
37
38pub struct CommandContext<'a> {
40 pub fs: &'a dyn VirtualFs,
41 pub cwd: &'a str,
42 pub env: &'a HashMap<String, String>,
43 pub variables: Option<&'a HashMap<String, crate::interpreter::Variable>>,
45 pub stdin: &'a str,
46 pub stdin_bytes: Option<&'a [u8]>,
49 pub limits: &'a ExecutionLimits,
50 pub network_policy: &'a NetworkPolicy,
51 pub exec: Option<ExecCallback<'a>>,
52 pub shell_opts: Option<&'a crate::interpreter::ShellOpts>,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum FlagStatus {
59 Supported,
61 Stubbed,
63 Ignored,
65}
66
67#[derive(Debug, Clone)]
69pub struct FlagInfo {
70 pub flag: &'static str,
71 pub description: &'static str,
72 pub status: FlagStatus,
73}
74
75pub struct CommandMeta {
77 pub name: &'static str,
78 pub synopsis: &'static str,
79 pub description: &'static str,
80 pub options: &'static [(&'static str, &'static str)],
81 pub supports_help_flag: bool,
82 pub flags: &'static [FlagInfo],
83}
84
85pub fn format_help(meta: &CommandMeta) -> String {
87 let mut out = format!("Usage: {}\n\n{}\n", meta.synopsis, meta.description);
88 if !meta.options.is_empty() {
89 out.push_str("\nOptions:\n");
90 for (flag, desc) in meta.options {
91 out.push_str(&format!(" {:<20} {}\n", flag, desc));
92 }
93 }
94 if !meta.flags.is_empty() {
95 out.push_str("\nFlag support:\n");
96 for fi in meta.flags {
97 let status_label = match fi.status {
98 FlagStatus::Supported => "supported",
99 FlagStatus::Stubbed => "stubbed",
100 FlagStatus::Ignored => "ignored",
101 };
102 out.push_str(&format!(
103 " {:<20} {} [{}]\n",
104 fi.flag, fi.description, status_label
105 ));
106 }
107 }
108 out
109}
110
111pub fn unknown_option(cmd: &str, option: &str) -> CommandResult {
113 let msg = if option.starts_with("--") {
114 format!("{}: unrecognized option '{}'\n", cmd, option)
115 } else {
116 format!(
117 "{}: invalid option -- '{}'\n",
118 cmd,
119 option.trim_start_matches('-')
120 )
121 };
122 CommandResult {
123 stderr: msg,
124 exit_code: 2,
125 ..Default::default()
126 }
127}
128
129pub trait VirtualCommand: Send + Sync {
131 fn name(&self) -> &str;
132 fn execute(&self, args: &[String], ctx: &CommandContext) -> CommandResult;
133 fn meta(&self) -> Option<&'static CommandMeta> {
134 None
135 }
136}
137
138pub struct EchoCommand;
142
143static ECHO_META: CommandMeta = CommandMeta {
144 name: "echo",
145 synopsis: "echo [-neE] [string ...]",
146 description: "Write arguments to standard output.",
147 options: &[
148 ("-n", "do not output the trailing newline"),
149 ("-e", "enable interpretation of backslash escapes"),
150 ("-E", "disable interpretation of backslash escapes"),
151 ],
152 supports_help_flag: false,
153 flags: &[],
154};
155
156impl VirtualCommand for EchoCommand {
157 fn name(&self) -> &str {
158 "echo"
159 }
160
161 fn meta(&self) -> Option<&'static CommandMeta> {
162 Some(&ECHO_META)
163 }
164
165 fn execute(&self, args: &[String], _ctx: &CommandContext) -> CommandResult {
166 let mut no_newline = false;
167 let mut interpret_escapes = false;
168 let mut arg_start = 0;
169
170 for (i, arg) in args.iter().enumerate() {
171 if arg.starts_with('-')
172 && arg.len() > 1
173 && arg[1..].chars().all(|c| matches!(c, 'n' | 'e' | 'E'))
174 {
175 for c in arg[1..].chars() {
176 match c {
177 'n' => no_newline = true,
178 'e' => interpret_escapes = true,
179 'E' => interpret_escapes = false,
180 _ => unreachable!(),
181 }
182 }
183 arg_start = i + 1;
184 } else {
185 break;
186 }
187 }
188
189 let text = args[arg_start..].join(" ");
190 let (output, suppress_newline) = if interpret_escapes {
191 interpret_echo_escapes(&text)
192 } else {
193 (text, false)
194 };
195
196 let stdout = if no_newline || suppress_newline {
197 output
198 } else {
199 format!("{output}\n")
200 };
201
202 CommandResult {
203 stdout,
204 stderr: String::new(),
205 exit_code: 0,
206 stdout_bytes: None,
207 }
208 }
209}
210
211fn interpret_echo_escapes(s: &str) -> (String, bool) {
212 let mut result = String::with_capacity(s.len());
213 let chars: Vec<char> = s.chars().collect();
214 let mut i = 0;
215 while i < chars.len() {
216 if chars[i] == '\\' && i + 1 < chars.len() {
217 i += 1;
218 match chars[i] {
219 'n' => result.push('\n'),
220 't' => result.push('\t'),
221 '\\' => result.push('\\'),
222 'a' => result.push('\x07'),
223 'b' => result.push('\x08'),
224 'f' => result.push('\x0C'),
225 'r' => result.push('\r'),
226 'v' => result.push('\x0B'),
227 'e' | 'E' => result.push('\x1B'),
228 'c' => return (result, true),
229 '0' => {
230 let mut val: u32 = 0;
232 let mut count = 0;
233 while count < 3
234 && i + 1 < chars.len()
235 && chars[i + 1] >= '0'
236 && chars[i + 1] <= '7'
237 {
238 i += 1;
239 val = val * 8 + (chars[i] as u32 - '0' as u32);
240 count += 1;
241 }
242 if let Some(c) = char::from_u32(val) {
243 result.push(c);
244 }
245 }
246 'x' => {
247 let mut hex = String::new();
249 while hex.len() < 2 && i + 1 < chars.len() && chars[i + 1].is_ascii_hexdigit() {
250 i += 1;
251 hex.push(chars[i]);
252 }
253 if hex.is_empty() {
254 result.push('\\');
255 result.push('x');
256 } else if let Some(c) =
257 u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32)
258 {
259 result.push(c);
260 }
261 }
262 'u' => {
263 let mut hex = String::new();
265 while hex.len() < 4 && i + 1 < chars.len() && chars[i + 1].is_ascii_hexdigit() {
266 i += 1;
267 hex.push(chars[i]);
268 }
269 if hex.is_empty() {
270 result.push('\\');
271 result.push('u');
272 } else if let Some(c) =
273 u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32)
274 {
275 result.push(c);
276 }
277 }
278 'U' => {
279 let mut hex = String::new();
281 while hex.len() < 8 && i + 1 < chars.len() && chars[i + 1].is_ascii_hexdigit() {
282 i += 1;
283 hex.push(chars[i]);
284 }
285 if hex.is_empty() {
286 result.push('\\');
287 result.push('U');
288 } else if let Some(c) =
289 u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32)
290 {
291 result.push(c);
292 }
293 }
294 other => {
295 result.push('\\');
296 result.push(other);
297 }
298 }
299 } else {
300 result.push(chars[i]);
301 }
302 i += 1;
303 }
304 (result, false)
305}
306
307pub struct TrueCommand;
309
310static TRUE_META: CommandMeta = CommandMeta {
311 name: "true",
312 synopsis: "true",
313 description: "Do nothing, successfully.",
314 options: &[],
315 supports_help_flag: false,
316 flags: &[],
317};
318
319impl VirtualCommand for TrueCommand {
320 fn name(&self) -> &str {
321 "true"
322 }
323
324 fn meta(&self) -> Option<&'static CommandMeta> {
325 Some(&TRUE_META)
326 }
327
328 fn execute(&self, _args: &[String], _ctx: &CommandContext) -> CommandResult {
329 CommandResult::default()
330 }
331}
332
333pub struct FalseCommand;
335
336static FALSE_META: CommandMeta = CommandMeta {
337 name: "false",
338 synopsis: "false",
339 description: "Do nothing, unsuccessfully.",
340 options: &[],
341 supports_help_flag: false,
342 flags: &[],
343};
344
345impl VirtualCommand for FalseCommand {
346 fn name(&self) -> &str {
347 "false"
348 }
349
350 fn meta(&self) -> Option<&'static CommandMeta> {
351 Some(&FALSE_META)
352 }
353
354 fn execute(&self, _args: &[String], _ctx: &CommandContext) -> CommandResult {
355 CommandResult {
356 exit_code: 1,
357 ..CommandResult::default()
358 }
359 }
360}
361
362pub struct CatCommand;
364
365static CAT_META: CommandMeta = CommandMeta {
366 name: "cat",
367 synopsis: "cat [-n] [FILE ...]",
368 description: "Concatenate files and print on standard output.",
369 options: &[("-n, --number", "number all output lines")],
370 supports_help_flag: true,
371 flags: &[],
372};
373
374impl VirtualCommand for CatCommand {
375 fn name(&self) -> &str {
376 "cat"
377 }
378
379 fn meta(&self) -> Option<&'static CommandMeta> {
380 Some(&CAT_META)
381 }
382
383 fn execute(&self, args: &[String], ctx: &CommandContext) -> CommandResult {
384 let mut number_lines = false;
385 let mut files: Vec<&str> = Vec::new();
386
387 for arg in args {
388 if arg == "-n" || arg == "--number" {
389 number_lines = true;
390 } else if arg == "-" {
391 files.push("-");
392 } else if arg.starts_with('-') && arg.len() > 1 {
393 } else {
395 files.push(arg);
396 }
397 }
398
399 if files.is_empty() {
401 files.push("-");
402 }
403
404 let mut output = String::new();
405 let mut stderr = String::new();
406 let mut exit_code = 0;
407
408 for file in &files {
409 let content = if *file == "-" || *file == "/dev/stdin" {
410 ctx.stdin.to_string()
411 } else if *file == "/dev/null" || *file == "/dev/zero" || *file == "/dev/full" {
412 String::new()
413 } else {
414 let path = if file.starts_with('/') {
415 std::path::PathBuf::from(file)
416 } else {
417 std::path::PathBuf::from(ctx.cwd).join(file)
418 };
419 match ctx.fs.read_file(&path) {
420 Ok(bytes) => String::from_utf8_lossy(&bytes).to_string(),
421 Err(e) => {
422 stderr.push_str(&format!("cat: {file}: {e}\n"));
423 exit_code = 1;
424 continue;
425 }
426 }
427 };
428
429 if number_lines {
430 let lines: Vec<&str> = content.split('\n').collect();
431 let line_count = if content.ends_with('\n') && lines.last() == Some(&"") {
432 lines.len() - 1
433 } else {
434 lines.len()
435 };
436 for (i, line) in lines.iter().take(line_count).enumerate() {
437 output.push_str(&format!(" {}\t{}", i + 1, line));
438 if i < line_count - 1 || content.ends_with('\n') {
439 output.push('\n');
440 }
441 }
442 } else {
443 output.push_str(&content);
444 }
445 }
446
447 CommandResult {
448 stdout: output,
449 stderr,
450 exit_code,
451 stdout_bytes: None,
452 }
453 }
454}
455
456pub struct PwdCommand;
458
459static PWD_META: CommandMeta = CommandMeta {
460 name: "pwd",
461 synopsis: "pwd",
462 description: "Print the current working directory.",
463 options: &[],
464 supports_help_flag: true,
465 flags: &[],
466};
467
468impl VirtualCommand for PwdCommand {
469 fn name(&self) -> &str {
470 "pwd"
471 }
472
473 fn meta(&self) -> Option<&'static CommandMeta> {
474 Some(&PWD_META)
475 }
476
477 fn execute(&self, _args: &[String], ctx: &CommandContext) -> CommandResult {
478 CommandResult {
479 stdout: format!("{}\n", ctx.cwd),
480 stderr: String::new(),
481 exit_code: 0,
482 stdout_bytes: None,
483 }
484 }
485}
486
487pub struct TouchCommand;
489
490static TOUCH_META: CommandMeta = CommandMeta {
491 name: "touch",
492 synopsis: "touch FILE ...",
493 description: "Update file access and modification times, creating files if needed.",
494 options: &[],
495 supports_help_flag: true,
496 flags: &[],
497};
498
499impl VirtualCommand for TouchCommand {
500 fn name(&self) -> &str {
501 "touch"
502 }
503
504 fn meta(&self) -> Option<&'static CommandMeta> {
505 Some(&TOUCH_META)
506 }
507
508 fn execute(&self, args: &[String], ctx: &CommandContext) -> CommandResult {
509 let mut stderr = String::new();
510 let mut exit_code = 0;
511
512 let mut files: Vec<&str> = Vec::new();
514 let mut past_options = false;
515 for arg in args {
516 if past_options {
517 files.push(arg.as_str());
518 } else if arg == "--" {
519 past_options = true;
520 } else if arg.starts_with('-') && arg.len() > 1 {
521 continue;
523 } else {
524 files.push(arg.as_str());
525 }
526 }
527
528 if files.is_empty() {
529 return CommandResult {
530 stdout: String::new(),
531 stderr: "touch: missing file operand\n".to_string(),
532 exit_code: 1,
533 stdout_bytes: None,
534 };
535 }
536
537 for file in files {
538 let path = if file.starts_with('/') {
539 std::path::PathBuf::from(file)
540 } else {
541 std::path::PathBuf::from(ctx.cwd).join(file)
542 };
543
544 if ctx.fs.exists(&path) {
545 if let Err(e) = ctx.fs.utimes(&path, crate::platform::SystemTime::now()) {
547 stderr.push_str(&format!("touch: cannot touch '{}': {}\n", file, e));
548 exit_code = 1;
549 }
550 } else {
551 if let Err(e) = ctx.fs.write_file(&path, b"") {
553 stderr.push_str(&format!("touch: cannot touch '{}': {}\n", file, e));
554 exit_code = 1;
555 }
556 }
557 }
558
559 CommandResult {
560 stdout: String::new(),
561 stderr,
562 exit_code,
563 stdout_bytes: None,
564 }
565 }
566}
567
568pub struct MkdirCommand;
570
571static MKDIR_META: CommandMeta = CommandMeta {
572 name: "mkdir",
573 synopsis: "mkdir [-p] DIRECTORY ...",
574 description: "Create directories.",
575 options: &[("-p, --parents", "create parent directories as needed")],
576 supports_help_flag: true,
577 flags: &[],
578};
579
580impl VirtualCommand for MkdirCommand {
581 fn name(&self) -> &str {
582 "mkdir"
583 }
584
585 fn meta(&self) -> Option<&'static CommandMeta> {
586 Some(&MKDIR_META)
587 }
588
589 fn execute(&self, args: &[String], ctx: &CommandContext) -> CommandResult {
590 let mut parents = false;
591 let mut dirs: Vec<&str> = Vec::new();
592 let mut stderr = String::new();
593 let mut exit_code = 0;
594
595 for arg in args {
596 if arg == "-p" || arg == "--parents" {
597 parents = true;
598 } else if arg.starts_with('-') {
599 } else {
601 dirs.push(arg);
602 }
603 }
604
605 if dirs.is_empty() {
606 return CommandResult {
607 stdout: String::new(),
608 stderr: "mkdir: missing operand\n".to_string(),
609 exit_code: 1,
610 stdout_bytes: None,
611 };
612 }
613
614 for dir in dirs {
615 let path = if dir.starts_with('/') {
616 std::path::PathBuf::from(dir)
617 } else {
618 std::path::PathBuf::from(ctx.cwd).join(dir)
619 };
620
621 let result = if parents {
622 ctx.fs.mkdir_p(&path)
623 } else {
624 ctx.fs.mkdir(&path)
625 };
626
627 if let Err(e) = result {
628 stderr.push_str(&format!(
629 "mkdir: cannot create directory '{}': {}\n",
630 dir, e
631 ));
632 exit_code = 1;
633 }
634 }
635
636 CommandResult {
637 stdout: String::new(),
638 stderr,
639 exit_code,
640 stdout_bytes: None,
641 }
642 }
643}
644
645pub struct LsCommand;
647
648static LS_FLAGS: &[FlagInfo] = &[
649 FlagInfo {
650 flag: "-a",
651 description: "show hidden entries",
652 status: FlagStatus::Supported,
653 },
654 FlagInfo {
655 flag: "-l",
656 description: "long listing format",
657 status: FlagStatus::Supported,
658 },
659 FlagInfo {
660 flag: "-1",
661 description: "one entry per line",
662 status: FlagStatus::Supported,
663 },
664 FlagInfo {
665 flag: "-R",
666 description: "recursive listing",
667 status: FlagStatus::Supported,
668 },
669 FlagInfo {
670 flag: "-t",
671 description: "sort by modification time",
672 status: FlagStatus::Ignored,
673 },
674 FlagInfo {
675 flag: "-S",
676 description: "sort by file size",
677 status: FlagStatus::Ignored,
678 },
679 FlagInfo {
680 flag: "-h",
681 description: "human-readable sizes",
682 status: FlagStatus::Ignored,
683 },
684];
685
686static LS_META: CommandMeta = CommandMeta {
687 name: "ls",
688 synopsis: "ls [-alR1] [FILE ...]",
689 description: "List directory contents.",
690 options: &[
691 ("-a", "do not ignore entries starting with ."),
692 ("-l", "use a long listing format"),
693 ("-1", "list one file per line"),
694 ("-R", "list subdirectories recursively"),
695 ],
696 supports_help_flag: true,
697 flags: LS_FLAGS,
698};
699
700impl VirtualCommand for LsCommand {
701 fn name(&self) -> &str {
702 "ls"
703 }
704
705 fn meta(&self) -> Option<&'static CommandMeta> {
706 Some(&LS_META)
707 }
708
709 fn execute(&self, args: &[String], ctx: &CommandContext) -> CommandResult {
710 let mut show_all = false;
711 let mut long_format = false;
712 let mut one_per_line = false;
713 let mut recursive = false;
714 let mut targets: Vec<&str> = Vec::new();
715
716 for arg in args {
717 if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") {
718 for c in arg[1..].chars() {
719 match c {
720 'a' => show_all = true,
721 'l' => long_format = true,
722 '1' => one_per_line = true,
723 'R' => recursive = true,
724 _ => {}
725 }
726 }
727 } else {
728 targets.push(arg);
729 }
730 }
731
732 if targets.is_empty() {
733 targets.push(".");
734 }
735
736 let opts = LsOptions {
737 show_all,
738 long_format,
739 one_per_line,
740 recursive,
741 };
742 let mut out = LsOutput {
743 stdout: String::new(),
744 stderr: String::new(),
745 exit_code: 0,
746 };
747 let multi_target = targets.len() > 1 || recursive;
748
749 for (idx, target) in targets.iter().enumerate() {
750 let path = if *target == "." {
751 std::path::PathBuf::from(ctx.cwd)
752 } else if target.starts_with('/') {
753 std::path::PathBuf::from(target)
754 } else {
755 std::path::PathBuf::from(ctx.cwd).join(target)
756 };
757
758 if idx > 0 {
759 out.stdout.push('\n');
760 }
761
762 ls_dir(ctx, &path, target, &opts, multi_target, &mut out);
763 }
764
765 CommandResult {
766 stdout: out.stdout,
767 stderr: out.stderr,
768 exit_code: out.exit_code,
769 stdout_bytes: None,
770 }
771 }
772}
773
774struct LsOptions {
775 show_all: bool,
776 long_format: bool,
777 one_per_line: bool,
778 recursive: bool,
779}
780
781struct LsOutput {
782 stdout: String,
783 stderr: String,
784 exit_code: i32,
785}
786
787fn ls_dir(
788 ctx: &CommandContext,
789 path: &std::path::Path,
790 display_name: &str,
791 opts: &LsOptions,
792 show_header: bool,
793 out: &mut LsOutput,
794) {
795 let entries = match ctx.fs.readdir(path) {
796 Ok(e) => e,
797 Err(e) => {
798 out.stderr
799 .push_str(&format!("ls: cannot access '{}': {}\n", display_name, e));
800 out.exit_code = 2;
801 return;
802 }
803 };
804
805 if show_header {
806 out.stdout.push_str(&format!("{}:\n", display_name));
807 }
808
809 let mut names: Vec<(String, crate::vfs::NodeType)> = entries
810 .iter()
811 .filter(|e| opts.show_all || !e.name.starts_with('.'))
812 .map(|e| (e.name.clone(), e.node_type))
813 .collect();
814 names.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
815
816 if opts.long_format {
817 for (name, node_type) in &names {
818 let child_path = path.join(name);
819 let meta = ctx.fs.stat(&child_path);
820 let mode = match meta {
821 Ok(m) => m.mode,
822 Err(_) => 0o644,
823 };
824 let type_char = match node_type {
825 crate::vfs::NodeType::Directory => 'd',
826 crate::vfs::NodeType::Symlink => 'l',
827 crate::vfs::NodeType::File => '-',
828 };
829 out.stdout
830 .push_str(&format!("{}{} {}\n", type_char, format_mode(mode), name));
831 }
832 } else if opts.one_per_line {
833 for (name, _) in &names {
834 out.stdout.push_str(name);
835 out.stdout.push('\n');
836 }
837 } else {
838 let name_strs: Vec<&str> = names.iter().map(|(n, _)| n.as_str()).collect();
840 if !name_strs.is_empty() {
841 out.stdout.push_str(&name_strs.join(" "));
842 out.stdout.push('\n');
843 }
844 }
845
846 if opts.recursive {
847 let subdirs: Vec<(String, std::path::PathBuf)> = names
848 .iter()
849 .filter(|(_, t)| matches!(t, crate::vfs::NodeType::Directory))
850 .map(|(n, _)| (n.clone(), path.join(n)))
851 .collect();
852
853 for (name, subpath) in subdirs {
854 out.stdout.push('\n');
855 let sub_display = if display_name == "." {
856 format!("./{}", name)
857 } else {
858 format!("{}/{}", display_name, name)
859 };
860 ls_dir(ctx, &subpath, &sub_display, opts, true, out);
861 }
862 }
863}
864
865fn format_mode(mode: u32) -> String {
866 let mut s = String::with_capacity(9);
867 let flags = [
868 (0o400, 'r'),
869 (0o200, 'w'),
870 (0o100, 'x'),
871 (0o040, 'r'),
872 (0o020, 'w'),
873 (0o010, 'x'),
874 (0o004, 'r'),
875 (0o002, 'w'),
876 (0o001, 'x'),
877 ];
878 for (bit, ch) in flags {
879 s.push(if mode & bit != 0 { ch } else { '-' });
880 }
881 s
882}
883
884pub struct TestCommand;
886
887static TEST_META: CommandMeta = CommandMeta {
888 name: "test",
889 synopsis: "test EXPRESSION",
890 description: "Evaluate conditional expression.",
891 options: &[
892 ("-e FILE", "FILE exists"),
893 ("-f FILE", "FILE exists and is a regular file"),
894 ("-d FILE", "FILE exists and is a directory"),
895 ("-z STRING", "the length of STRING is zero"),
896 ("-n STRING", "the length of STRING is nonzero"),
897 ("s1 = s2", "the strings are equal"),
898 ("s1 != s2", "the strings are not equal"),
899 ("n1 -eq n2", "integers are equal"),
900 ("n1 -lt n2", "first integer is less than second"),
901 ],
902 supports_help_flag: false,
903 flags: &[],
904};
905
906impl VirtualCommand for TestCommand {
907 fn name(&self) -> &str {
908 "test"
909 }
910
911 fn meta(&self) -> Option<&'static CommandMeta> {
912 Some(&TEST_META)
913 }
914
915 fn execute(&self, args: &[String], ctx: &CommandContext) -> CommandResult {
916 test_cmd::evaluate_test_args(args, ctx)
917 }
918}
919
920pub struct BracketCommand;
922
923static BRACKET_META: CommandMeta = CommandMeta {
924 name: "[",
925 synopsis: "[ EXPRESSION ]",
926 description: "Evaluate conditional expression (synonym for test).",
927 options: &[],
928 supports_help_flag: false,
929 flags: &[],
930};
931
932impl VirtualCommand for BracketCommand {
933 fn name(&self) -> &str {
934 "["
935 }
936
937 fn meta(&self) -> Option<&'static CommandMeta> {
938 Some(&BRACKET_META)
939 }
940
941 fn execute(&self, args: &[String], ctx: &CommandContext) -> CommandResult {
942 if args.is_empty() || args.last().map(|s| s.as_str()) != Some("]") {
943 return CommandResult {
944 stderr: "[: missing ']'\n".to_string(),
945 exit_code: 2,
946 ..CommandResult::default()
947 };
948 }
949 test_cmd::evaluate_test_args(&args[..args.len() - 1], ctx)
951 }
952}
953
954pub struct FgrepCommand;
956
957static FGREP_META: CommandMeta = CommandMeta {
958 name: "fgrep",
959 synopsis: "fgrep [OPTIONS] PATTERN [FILE ...]",
960 description: "Equivalent to grep -F (fixed-string search).",
961 options: &[],
962 supports_help_flag: true,
963 flags: &[],
964};
965
966impl VirtualCommand for FgrepCommand {
967 fn name(&self) -> &str {
968 "fgrep"
969 }
970
971 fn meta(&self) -> Option<&'static CommandMeta> {
972 Some(&FGREP_META)
973 }
974
975 fn execute(&self, args: &[String], ctx: &CommandContext) -> CommandResult {
976 let mut new_args = vec!["-F".to_string()];
977 new_args.extend(args.iter().cloned());
978 text::GrepCommand.execute(&new_args, ctx)
979 }
980}
981
982pub struct EgrepCommand;
984
985static EGREP_META: CommandMeta = CommandMeta {
986 name: "egrep",
987 synopsis: "egrep [OPTIONS] PATTERN [FILE ...]",
988 description: "Equivalent to grep -E (extended regexp search).",
989 options: &[],
990 supports_help_flag: true,
991 flags: &[],
992};
993
994impl VirtualCommand for EgrepCommand {
995 fn name(&self) -> &str {
996 "egrep"
997 }
998
999 fn meta(&self) -> Option<&'static CommandMeta> {
1000 Some(&EGREP_META)
1001 }
1002
1003 fn execute(&self, args: &[String], ctx: &CommandContext) -> CommandResult {
1004 let mut new_args = vec!["-E".to_string()];
1005 new_args.extend(args.iter().cloned());
1006 text::GrepCommand.execute(&new_args, ctx)
1007 }
1008}
1009
1010struct ArgvPyCommand;
1014
1015static ARGV_PY_META: CommandMeta = CommandMeta {
1016 name: "argv.py",
1017 synopsis: "argv.py [arg ...]",
1018 description: "Print arguments as a Python list (Oils test helper).",
1019 options: &[],
1020 supports_help_flag: false,
1021 flags: &[],
1022};
1023
1024impl VirtualCommand for ArgvPyCommand {
1025 fn name(&self) -> &str {
1026 "argv.py"
1027 }
1028
1029 fn meta(&self) -> Option<&'static CommandMeta> {
1030 Some(&ARGV_PY_META)
1031 }
1032
1033 fn execute(&self, args: &[String], _ctx: &CommandContext) -> CommandResult {
1034 let parts: Vec<String> = args.iter().map(|a| python_repr_string(a)).collect();
1037 CommandResult {
1038 stdout: format!("[{}]\n", parts.join(", ")),
1039 ..Default::default()
1040 }
1041 }
1042}
1043
1044fn python_repr_string(s: &str) -> String {
1046 if s.contains('\'') && !s.contains('"') {
1047 let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
1049 format!("\"{escaped}\"")
1050 } else {
1051 let escaped = s.replace('\\', "\\\\").replace('\'', "\\'");
1053 format!("'{escaped}'")
1054 }
1055}
1056
1057struct PrintenvPyCommand;
1059
1060static PRINTENV_PY_META: CommandMeta = CommandMeta {
1061 name: "printenv.py",
1062 synopsis: "printenv.py [NAME ...]",
1063 description: "Print environment variables or 'None' (Oils test helper).",
1064 options: &[],
1065 supports_help_flag: false,
1066 flags: &[],
1067};
1068
1069impl VirtualCommand for PrintenvPyCommand {
1070 fn name(&self) -> &str {
1071 "printenv.py"
1072 }
1073
1074 fn meta(&self) -> Option<&'static CommandMeta> {
1075 Some(&PRINTENV_PY_META)
1076 }
1077
1078 fn execute(&self, args: &[String], ctx: &CommandContext) -> CommandResult {
1079 let mut out = String::new();
1080 for name in args {
1081 match ctx.env.get(name.as_str()) {
1082 Some(val) => out.push_str(&format!("{val}\n")),
1083 None => out.push_str("None\n"),
1084 }
1085 }
1086 CommandResult {
1087 stdout: out,
1088 ..Default::default()
1089 }
1090 }
1091}
1092
1093pub fn register_default_commands() -> HashMap<String, Box<dyn VirtualCommand>> {
1095 let mut commands: HashMap<String, Box<dyn VirtualCommand>> = HashMap::new();
1096 let defaults: Vec<Box<dyn VirtualCommand>> = vec![
1097 Box::new(EchoCommand),
1098 Box::new(TrueCommand),
1099 Box::new(FalseCommand),
1100 Box::new(CatCommand),
1101 Box::new(PwdCommand),
1102 Box::new(TouchCommand),
1103 Box::new(MkdirCommand),
1104 Box::new(LsCommand),
1105 Box::new(TestCommand),
1106 Box::new(BracketCommand),
1107 Box::new(file_ops::CpCommand),
1109 Box::new(file_ops::MvCommand),
1110 Box::new(file_ops::RmCommand),
1111 Box::new(file_ops::TeeCommand),
1112 Box::new(file_ops::StatCommand),
1113 Box::new(file_ops::ChmodCommand),
1114 Box::new(file_ops::LnCommand),
1115 Box::new(text::GrepCommand),
1117 Box::new(text::SortCommand),
1118 Box::new(text::UniqCommand),
1119 Box::new(text::CutCommand),
1120 Box::new(text::HeadCommand),
1121 Box::new(text::TailCommand),
1122 Box::new(text::WcCommand),
1123 Box::new(text::TrCommand),
1124 Box::new(text::RevCommand),
1125 Box::new(text::FoldCommand),
1126 Box::new(text::NlCommand),
1127 Box::new(text::PrintfCommand),
1128 Box::new(text::PasteCommand),
1129 Box::new(text::OdCommand),
1130 Box::new(text::TacCommand),
1132 Box::new(text::CommCommand),
1133 Box::new(text::JoinCommand),
1134 Box::new(text::FmtCommand),
1135 Box::new(text::ColumnCommand),
1136 Box::new(text::ExpandCommand),
1137 Box::new(text::UnexpandCommand),
1138 Box::new(navigation::RealpathCommand),
1140 Box::new(navigation::BasenameCommand),
1141 Box::new(navigation::DirnameCommand),
1142 Box::new(navigation::TreeCommand),
1143 Box::new(utils::ExprCommand),
1145 Box::new(utils::DateCommand),
1146 Box::new(utils::SleepCommand),
1147 Box::new(utils::SeqCommand),
1148 Box::new(utils::EnvCommand),
1149 Box::new(utils::PrintenvCommand),
1150 Box::new(utils::WhichCommand),
1151 Box::new(utils::Base64Command),
1152 Box::new(utils::Md5sumCommand),
1153 Box::new(utils::Sha256sumCommand),
1154 Box::new(utils::WhoamiCommand),
1155 Box::new(utils::HostnameCommand),
1156 Box::new(utils::UnameCommand),
1157 Box::new(utils::YesCommand),
1158 Box::new(exec_cmds::XargsCommand),
1160 Box::new(exec_cmds::FindCommand),
1161 Box::new(diff_cmd::DiffCommand),
1163 Box::new(sed::SedCommand),
1165 Box::new(jq_cmd::JqCommand),
1167 Box::new(awk::AwkCommand),
1169 Box::new(utils::Sha1sumCommand),
1171 Box::new(utils::TimeoutCommand),
1172 Box::new(utils::FileCommand),
1173 Box::new(utils::BcCommand),
1174 Box::new(utils::ClearCommand),
1175 Box::new(FgrepCommand),
1176 Box::new(EgrepCommand),
1177 Box::new(text::StringsCommand),
1179 Box::new(text::RgCommand),
1181 Box::new(file_ops::ReadlinkCommand),
1183 Box::new(file_ops::RmdirCommand),
1184 Box::new(file_ops::DuCommand),
1185 Box::new(file_ops::SplitCommand),
1186 Box::new(compression::GzipCommand),
1188 Box::new(compression::GunzipCommand),
1189 Box::new(compression::ZcatCommand),
1190 Box::new(compression::TarCommand),
1191 Box::new(ArgvPyCommand),
1193 Box::new(PrintenvPyCommand),
1194 ];
1195 for cmd in defaults {
1196 commands.insert(cmd.name().to_string(), cmd);
1197 }
1198 #[cfg(feature = "network")]
1200 {
1201 commands.insert("curl".to_string(), Box::new(net::CurlCommand));
1202 }
1203 commands
1204}
1205
1206#[cfg(test)]
1207mod tests {
1208 use super::*;
1209 use crate::network::NetworkPolicy;
1210 use crate::vfs::InMemoryFs;
1211 use std::sync::Arc;
1212
1213 fn test_ctx() -> (
1214 Arc<InMemoryFs>,
1215 HashMap<String, String>,
1216 ExecutionLimits,
1217 NetworkPolicy,
1218 ) {
1219 (
1220 Arc::new(InMemoryFs::new()),
1221 HashMap::new(),
1222 ExecutionLimits::default(),
1223 NetworkPolicy::default(),
1224 )
1225 }
1226
1227 #[test]
1228 fn echo_no_args() {
1229 let (fs, env, limits, np) = test_ctx();
1230 let ctx = CommandContext {
1231 fs: &*fs,
1232 cwd: "/",
1233 env: &env,
1234 variables: None,
1235 stdin: "",
1236 stdin_bytes: None,
1237 limits: &limits,
1238 network_policy: &np,
1239 exec: None,
1240 shell_opts: None,
1241 };
1242 let result = EchoCommand.execute(&[], &ctx);
1243 assert_eq!(result.stdout, "\n");
1244 assert_eq!(result.exit_code, 0);
1245 }
1246
1247 #[test]
1248 fn echo_simple_text() {
1249 let (fs, env, limits, np) = test_ctx();
1250 let ctx = CommandContext {
1251 fs: &*fs,
1252 cwd: "/",
1253 env: &env,
1254 variables: None,
1255 stdin: "",
1256 stdin_bytes: None,
1257 limits: &limits,
1258 network_policy: &np,
1259 exec: None,
1260 shell_opts: None,
1261 };
1262 let result = EchoCommand.execute(&["hello".into(), "world".into()], &ctx);
1263 assert_eq!(result.stdout, "hello world\n");
1264 }
1265
1266 #[test]
1267 fn echo_flag_n() {
1268 let (fs, env, limits, np) = test_ctx();
1269 let ctx = CommandContext {
1270 fs: &*fs,
1271 cwd: "/",
1272 env: &env,
1273 variables: None,
1274 stdin: "",
1275 stdin_bytes: None,
1276 limits: &limits,
1277 network_policy: &np,
1278 exec: None,
1279 shell_opts: None,
1280 };
1281 let result = EchoCommand.execute(&["-n".into(), "hello".into()], &ctx);
1282 assert_eq!(result.stdout, "hello");
1283 }
1284
1285 #[test]
1286 fn echo_escape_newline() {
1287 let (fs, env, limits, np) = test_ctx();
1288 let ctx = CommandContext {
1289 fs: &*fs,
1290 cwd: "/",
1291 env: &env,
1292 variables: None,
1293 stdin: "",
1294 stdin_bytes: None,
1295 limits: &limits,
1296 network_policy: &np,
1297 exec: None,
1298 shell_opts: None,
1299 };
1300 let result = EchoCommand.execute(&["-e".into(), "hello\\nworld".into()], &ctx);
1301 assert_eq!(result.stdout, "hello\nworld\n");
1302 }
1303
1304 #[test]
1305 fn echo_escape_tab() {
1306 let (fs, env, limits, np) = test_ctx();
1307 let ctx = CommandContext {
1308 fs: &*fs,
1309 cwd: "/",
1310 env: &env,
1311 variables: None,
1312 stdin: "",
1313 stdin_bytes: None,
1314 limits: &limits,
1315 network_policy: &np,
1316 exec: None,
1317 shell_opts: None,
1318 };
1319 let result = EchoCommand.execute(&["-e".into(), "a\\tb".into()], &ctx);
1320 assert_eq!(result.stdout, "a\tb\n");
1321 }
1322
1323 #[test]
1324 fn echo_escape_stop_output() {
1325 let (fs, env, limits, np) = test_ctx();
1326 let ctx = CommandContext {
1327 fs: &*fs,
1328 cwd: "/",
1329 env: &env,
1330 variables: None,
1331 stdin: "",
1332 stdin_bytes: None,
1333 limits: &limits,
1334 network_policy: &np,
1335 exec: None,
1336 shell_opts: None,
1337 };
1338 let result = EchoCommand.execute(&["-e".into(), "hello\\cworld".into()], &ctx);
1339 assert_eq!(result.stdout, "hello");
1340 }
1341
1342 #[test]
1343 fn echo_non_flag_dash_arg() {
1344 let (fs, env, limits, np) = test_ctx();
1345 let ctx = CommandContext {
1346 fs: &*fs,
1347 cwd: "/",
1348 env: &env,
1349 variables: None,
1350 stdin: "",
1351 stdin_bytes: None,
1352 limits: &limits,
1353 network_policy: &np,
1354 exec: None,
1355 shell_opts: None,
1356 };
1357 let result = EchoCommand.execute(&["-z".into(), "hello".into()], &ctx);
1358 assert_eq!(result.stdout, "-z hello\n");
1359 }
1360
1361 #[test]
1362 fn echo_combined_flags() {
1363 let (fs, env, limits, np) = test_ctx();
1364 let ctx = CommandContext {
1365 fs: &*fs,
1366 cwd: "/",
1367 env: &env,
1368 variables: None,
1369 stdin: "",
1370 stdin_bytes: None,
1371 limits: &limits,
1372 network_policy: &np,
1373 exec: None,
1374 shell_opts: None,
1375 };
1376 let result = EchoCommand.execute(&["-ne".into(), "hello\\nworld".into()], &ctx);
1377 assert_eq!(result.stdout, "hello\nworld");
1378 }
1379
1380 #[test]
1381 fn true_succeeds() {
1382 let (fs, env, limits, np) = test_ctx();
1383 let ctx = CommandContext {
1384 fs: &*fs,
1385 cwd: "/",
1386 env: &env,
1387 variables: None,
1388 stdin: "",
1389 stdin_bytes: None,
1390 limits: &limits,
1391 network_policy: &np,
1392 exec: None,
1393 shell_opts: None,
1394 };
1395 assert_eq!(TrueCommand.execute(&[], &ctx).exit_code, 0);
1396 }
1397
1398 #[test]
1399 fn false_fails() {
1400 let (fs, env, limits, np) = test_ctx();
1401 let ctx = CommandContext {
1402 fs: &*fs,
1403 cwd: "/",
1404 env: &env,
1405 variables: None,
1406 stdin: "",
1407 stdin_bytes: None,
1408 limits: &limits,
1409 network_policy: &np,
1410 exec: None,
1411 shell_opts: None,
1412 };
1413 assert_eq!(FalseCommand.execute(&[], &ctx).exit_code, 1);
1414 }
1415}