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