1pub(crate) mod arithmetic;
4pub(crate) mod brace;
5pub(crate) mod builtins;
6mod expansion;
7pub(crate) mod pattern;
8mod walker;
9
10use crate::commands::VirtualCommand;
11use crate::error::RustBashError;
12use crate::network::NetworkPolicy;
13use crate::platform::Instant;
14use crate::vfs::VirtualFs;
15use bitflags::bitflags;
16use brush_parser::ast;
17use std::collections::{BTreeMap, HashMap};
18use std::sync::Arc;
19use std::time::Duration;
20
21pub use builtins::builtin_names;
22pub use expansion::expand_word;
23pub use walker::execute_program;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ControlFlow {
30 Break(usize),
31 Continue(usize),
32 Return(i32),
33}
34
35#[derive(Debug, Clone, Default, PartialEq, Eq)]
37pub struct ExecResult {
38 pub stdout: String,
39 pub stderr: String,
40 pub exit_code: i32,
41 pub stdout_bytes: Option<Vec<u8>>,
43}
44
45#[derive(Debug, Clone, PartialEq)]
49pub enum VariableValue {
50 Scalar(String),
51 IndexedArray(BTreeMap<usize, String>),
52 AssociativeArray(BTreeMap<String, String>),
53}
54
55impl VariableValue {
56 pub fn as_scalar(&self) -> &str {
59 match self {
60 VariableValue::Scalar(s) => s,
61 VariableValue::IndexedArray(map) => map.get(&0).map(|s| s.as_str()).unwrap_or(""),
62 VariableValue::AssociativeArray(map) => map.get("0").map(|s| s.as_str()).unwrap_or(""),
63 }
64 }
65
66 pub fn count(&self) -> usize {
68 match self {
69 VariableValue::Scalar(s) => usize::from(!s.is_empty()),
70 VariableValue::IndexedArray(map) => map.len(),
71 VariableValue::AssociativeArray(map) => map.len(),
72 }
73 }
74}
75
76bitflags! {
77 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
79 pub struct VariableAttrs: u8 {
80 const EXPORTED = 0b0000_0001;
81 const READONLY = 0b0000_0010;
82 const INTEGER = 0b0000_0100;
83 const LOWERCASE = 0b0000_1000;
84 const UPPERCASE = 0b0001_0000;
85 const NAMEREF = 0b0010_0000;
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct Variable {
92 pub value: VariableValue,
93 pub attrs: VariableAttrs,
94}
95
96#[derive(Debug, Clone)]
98pub(crate) enum PersistentFd {
99 OutputFile(String),
101 InputFile(String),
103 ReadWriteFile(String),
105 DevNull,
107 Closed,
109 DupStdFd(i32),
111}
112
113impl Variable {
114 pub fn exported(&self) -> bool {
116 self.attrs.contains(VariableAttrs::EXPORTED)
117 }
118
119 pub fn readonly(&self) -> bool {
121 self.attrs.contains(VariableAttrs::READONLY)
122 }
123}
124
125#[derive(Debug, Clone)]
127pub struct ExecutionLimits {
128 pub max_call_depth: usize,
129 pub max_command_count: usize,
130 pub max_loop_iterations: usize,
131 pub max_execution_time: Duration,
132 pub max_output_size: usize,
133 pub max_string_length: usize,
134 pub max_glob_results: usize,
135 pub max_substitution_depth: usize,
136 pub max_heredoc_size: usize,
137 pub max_brace_expansion: usize,
138 pub max_array_elements: usize,
139}
140
141impl Default for ExecutionLimits {
142 fn default() -> Self {
143 Self {
144 max_call_depth: 50,
145 max_command_count: 10_000,
146 max_loop_iterations: 10_000,
147 max_execution_time: Duration::from_secs(30),
148 max_output_size: 10 * 1024 * 1024,
149 max_string_length: 10 * 1024 * 1024,
150 max_glob_results: 100_000,
151 max_substitution_depth: 50,
152 max_heredoc_size: 10 * 1024 * 1024,
153 max_brace_expansion: 10_000,
154 max_array_elements: 100_000,
155 }
156 }
157}
158
159#[derive(Debug, Clone)]
161pub struct ExecutionCounters {
162 pub command_count: usize,
163 pub call_depth: usize,
164 pub output_size: usize,
165 pub start_time: Instant,
166 pub substitution_depth: usize,
167}
168
169impl Default for ExecutionCounters {
170 fn default() -> Self {
171 Self {
172 command_count: 0,
173 call_depth: 0,
174 output_size: 0,
175 start_time: Instant::now(),
176 substitution_depth: 0,
177 }
178 }
179}
180
181impl ExecutionCounters {
182 pub fn reset(&mut self) {
183 *self = Self::default();
184 }
185}
186
187#[derive(Debug, Clone, Default)]
189pub struct ShellOpts {
190 pub errexit: bool,
191 pub nounset: bool,
192 pub pipefail: bool,
193 pub xtrace: bool,
194 pub verbose: bool,
195 pub noexec: bool,
196 pub noclobber: bool,
197 pub allexport: bool,
198 pub noglob: bool,
199 pub posix: bool,
200 pub vi_mode: bool,
201 pub emacs_mode: bool,
202}
203
204#[derive(Debug, Clone)]
206pub struct ShoptOpts {
207 pub nullglob: bool,
208 pub globstar: bool,
209 pub dotglob: bool,
210 pub globskipdots: bool,
211 pub failglob: bool,
212 pub nocaseglob: bool,
213 pub nocasematch: bool,
214 pub lastpipe: bool,
215 pub expand_aliases: bool,
216 pub xpg_echo: bool,
217 pub extglob: bool,
218 pub progcomp: bool,
219 pub hostcomplete: bool,
220 pub complete_fullquote: bool,
221 pub sourcepath: bool,
222 pub promptvars: bool,
223 pub interactive_comments: bool,
224 pub cmdhist: bool,
225 pub lithist: bool,
226 pub autocd: bool,
227 pub cdspell: bool,
228 pub dirspell: bool,
229 pub direxpand: bool,
230 pub checkhash: bool,
231 pub checkjobs: bool,
232 pub checkwinsize: bool,
233 pub extquote: bool,
234 pub force_fignore: bool,
235 pub globasciiranges: bool,
236 pub gnu_errfmt: bool,
237 pub histappend: bool,
238 pub histreedit: bool,
239 pub histverify: bool,
240 pub huponexit: bool,
241 pub inherit_errexit: bool,
242 pub login_shell: bool,
243 pub mailwarn: bool,
244 pub no_empty_cmd_completion: bool,
245 pub progcomp_alias: bool,
246 pub shift_verbose: bool,
247 pub execfail: bool,
248 pub cdable_vars: bool,
249 pub localvar_inherit: bool,
250 pub localvar_unset: bool,
251 pub extdebug: bool,
252 pub patsub_replacement: bool,
253 pub assoc_expand_once: bool,
254 pub varredir_close: bool,
255}
256
257impl Default for ShoptOpts {
258 fn default() -> Self {
259 Self {
260 nullglob: false,
261 globstar: false,
262 dotglob: false,
263 globskipdots: true,
264 failglob: false,
265 nocaseglob: false,
266 nocasematch: false,
267 lastpipe: false,
268 expand_aliases: false,
269 xpg_echo: false,
270 extglob: true,
271 progcomp: true,
272 hostcomplete: true,
273 complete_fullquote: true,
274 sourcepath: true,
275 promptvars: true,
276 interactive_comments: true,
277 cmdhist: true,
278 lithist: false,
279 autocd: false,
280 cdspell: false,
281 dirspell: false,
282 direxpand: false,
283 checkhash: false,
284 checkjobs: false,
285 checkwinsize: true,
286 extquote: true,
287 force_fignore: true,
288 globasciiranges: true,
289 gnu_errfmt: false,
290 histappend: false,
291 histreedit: false,
292 histverify: false,
293 huponexit: false,
294 inherit_errexit: false,
295 login_shell: false,
296 mailwarn: false,
297 no_empty_cmd_completion: false,
298 progcomp_alias: false,
299 shift_verbose: false,
300 execfail: false,
301 cdable_vars: false,
302 localvar_inherit: false,
303 localvar_unset: false,
304 extdebug: false,
305 patsub_replacement: true,
306 assoc_expand_once: false,
307 varredir_close: false,
308 }
309 }
310}
311
312#[derive(Debug, Clone)]
314pub struct FunctionDef {
315 pub body: ast::FunctionBody,
316}
317
318#[derive(Debug, Clone)]
321pub struct CallFrame {
322 pub func_name: String,
323 pub source: String,
324 pub lineno: usize,
325}
326
327pub struct InterpreterState {
329 pub fs: Arc<dyn VirtualFs>,
330 pub env: HashMap<String, Variable>,
331 pub cwd: String,
332 pub functions: HashMap<String, FunctionDef>,
333 pub last_exit_code: i32,
334 pub commands: HashMap<String, Arc<dyn VirtualCommand>>,
335 pub shell_opts: ShellOpts,
336 pub shopt_opts: ShoptOpts,
337 pub limits: ExecutionLimits,
338 pub counters: ExecutionCounters,
339 pub network_policy: NetworkPolicy,
340 pub(crate) should_exit: bool,
341 pub(crate) loop_depth: usize,
342 pub(crate) control_flow: Option<ControlFlow>,
343 pub positional_params: Vec<String>,
344 pub shell_name: String,
345 pub(crate) random_seed: u32,
347 pub(crate) local_scopes: Vec<HashMap<String, Option<Variable>>>,
349 pub(crate) in_function_depth: usize,
351 pub(crate) traps: HashMap<String, String>,
353 pub(crate) in_trap: bool,
355 pub(crate) errexit_suppressed: usize,
358 pub(crate) stdin_offset: usize,
361 pub(crate) dir_stack: Vec<String>,
363 pub(crate) command_hash: HashMap<String, String>,
365 pub(crate) aliases: HashMap<String, String>,
367 pub(crate) current_lineno: usize,
369 pub(crate) shell_start_time: Instant,
371 pub(crate) last_argument: String,
373 pub(crate) call_stack: Vec<CallFrame>,
375 pub(crate) machtype: String,
377 pub(crate) hosttype: String,
379 pub(crate) persistent_fds: HashMap<i32, PersistentFd>,
381 pub(crate) next_auto_fd: i32,
383 pub(crate) proc_sub_counter: u64,
385 pub(crate) proc_sub_prealloc: HashMap<usize, String>,
390 pub(crate) pipe_stdin_bytes: Option<Vec<u8>>,
393 pub(crate) pending_cmdsub_stderr: String,
396}
397
398pub(crate) fn parser_options() -> brush_parser::ParserOptions {
401 brush_parser::ParserOptions {
402 sh_mode: false,
403 posix_mode: false,
404 enable_extended_globbing: true,
405 tilde_expansion: true,
406 }
407}
408
409pub fn parse(input: &str) -> Result<ast::Program, RustBashError> {
411 let tokens =
412 brush_parser::tokenize_str(input).map_err(|e| RustBashError::Parse(e.to_string()))?;
413
414 if tokens.is_empty() {
415 return Ok(ast::Program {
416 complete_commands: vec![],
417 });
418 }
419
420 let options = parser_options();
421 let source_info = brush_parser::SourceInfo {
422 source: input.to_string(),
423 };
424
425 brush_parser::parse_tokens(&tokens, &options, &source_info)
426 .map_err(|e| RustBashError::Parse(e.to_string()))
427}
428
429pub(crate) fn set_variable(
432 state: &mut InterpreterState,
433 name: &str,
434 value: String,
435) -> Result<(), RustBashError> {
436 if value.len() > state.limits.max_string_length {
437 return Err(RustBashError::LimitExceeded {
438 limit_name: "max_string_length",
439 limit_value: state.limits.max_string_length,
440 actual_value: value.len(),
441 });
442 }
443
444 let target = resolve_nameref(name, state)?;
446
447 if let Some(bracket_pos) = target.find('[')
450 && target.ends_with(']')
451 {
452 let arr_name = &target[..bracket_pos];
453 let index_raw = &target[bracket_pos + 1..target.len() - 1];
454 let word = brush_parser::ast::Word {
456 value: index_raw.to_string(),
457 loc: None,
458 };
459 let expanded_key = crate::interpreter::expansion::expand_word_to_string_mut(&word, state)?;
460
461 if let Some(var) = state.env.get(arr_name)
462 && var.readonly()
463 {
464 return Err(RustBashError::Execution(format!(
465 "{arr_name}: readonly variable"
466 )));
467 }
468
469 let is_assoc = state
471 .env
472 .get(arr_name)
473 .is_some_and(|v| matches!(v.value, VariableValue::AssociativeArray(_)));
474 let numeric_idx = if !is_assoc {
475 crate::interpreter::arithmetic::eval_arithmetic(&expanded_key, state).unwrap_or(0)
476 } else {
477 0
478 };
479
480 match state.env.get_mut(arr_name) {
481 Some(var) => match &mut var.value {
482 VariableValue::AssociativeArray(map) => {
483 map.insert(expanded_key, value);
484 }
485 VariableValue::IndexedArray(map) => {
486 let actual_idx = if numeric_idx < 0 {
487 let max_key = map.keys().next_back().copied().unwrap_or(0);
488 let resolved = max_key as i64 + 1 + numeric_idx;
489 if resolved < 0 {
490 0usize
491 } else {
492 resolved as usize
493 }
494 } else {
495 numeric_idx as usize
496 };
497 map.insert(actual_idx, value);
498 }
499 VariableValue::Scalar(s) => {
500 if numeric_idx == 0 || numeric_idx == -1 {
501 *s = value;
502 }
503 }
504 },
505 None => {
506 let idx = expanded_key.parse::<usize>().unwrap_or(0);
508 let mut map = std::collections::BTreeMap::new();
509 map.insert(idx, value);
510 state.env.insert(
511 arr_name.to_string(),
512 Variable {
513 value: VariableValue::IndexedArray(map),
514 attrs: VariableAttrs::empty(),
515 },
516 );
517 }
518 }
519 return Ok(());
520 }
521
522 if target == "SECONDS" {
524 if let Ok(offset) = value.parse::<u64>() {
525 state.shell_start_time = Instant::now() - std::time::Duration::from_secs(offset);
528 } else {
529 state.shell_start_time = Instant::now();
530 }
531 return Ok(());
532 }
533
534 if let Some(var) = state.env.get(&target)
535 && var.readonly()
536 {
537 return Err(RustBashError::Execution(format!(
538 "{target}: readonly variable"
539 )));
540 }
541
542 let attrs = state
544 .env
545 .get(&target)
546 .map(|v| v.attrs)
547 .unwrap_or(VariableAttrs::empty());
548
549 let value = if attrs.contains(VariableAttrs::INTEGER) {
551 let result = crate::interpreter::arithmetic::eval_arithmetic(&value, state)?;
552 result.to_string()
553 } else {
554 value
555 };
556
557 let value = if attrs.contains(VariableAttrs::LOWERCASE) {
559 value.to_lowercase()
560 } else if attrs.contains(VariableAttrs::UPPERCASE) {
561 value.to_uppercase()
562 } else {
563 value
564 };
565
566 match state.env.get_mut(&target) {
567 Some(var) => {
568 match &mut var.value {
569 VariableValue::IndexedArray(map) => {
570 map.insert(0, value);
571 }
572 VariableValue::AssociativeArray(map) => {
573 map.insert("0".to_string(), value);
574 }
575 VariableValue::Scalar(s) => *s = value,
576 }
577 if state.shell_opts.allexport {
579 var.attrs.insert(VariableAttrs::EXPORTED);
580 }
581 }
582 None => {
583 let attrs = if state.shell_opts.allexport {
584 VariableAttrs::EXPORTED
585 } else {
586 VariableAttrs::empty()
587 };
588 state.env.insert(
589 target,
590 Variable {
591 value: VariableValue::Scalar(value),
592 attrs,
593 },
594 );
595 }
596 }
597 Ok(())
598}
599
600pub(crate) fn set_array_element(
603 state: &mut InterpreterState,
604 name: &str,
605 index: usize,
606 value: String,
607) -> Result<(), RustBashError> {
608 let target = resolve_nameref(name, state)?;
609 if let Some(var) = state.env.get(&target)
610 && var.readonly()
611 {
612 return Err(RustBashError::Execution(format!(
613 "{target}: readonly variable"
614 )));
615 }
616
617 let attrs = state
619 .env
620 .get(&target)
621 .map(|v| v.attrs)
622 .unwrap_or(VariableAttrs::empty());
623 let value = if attrs.contains(VariableAttrs::INTEGER) {
624 crate::interpreter::arithmetic::eval_arithmetic(&value, state)?.to_string()
625 } else {
626 value
627 };
628 let value = if attrs.contains(VariableAttrs::LOWERCASE) {
629 value.to_lowercase()
630 } else if attrs.contains(VariableAttrs::UPPERCASE) {
631 value.to_uppercase()
632 } else {
633 value
634 };
635
636 let limit = state.limits.max_array_elements;
637 match state.env.get_mut(&target) {
638 Some(var) => match &mut var.value {
639 VariableValue::IndexedArray(map) => {
640 if !map.contains_key(&index) && map.len() >= limit {
641 return Err(RustBashError::LimitExceeded {
642 limit_name: "max_array_elements",
643 limit_value: limit,
644 actual_value: map.len() + 1,
645 });
646 }
647 map.insert(index, value);
648 }
649 VariableValue::Scalar(_) => {
650 let mut map = BTreeMap::new();
651 map.insert(index, value);
652 var.value = VariableValue::IndexedArray(map);
653 }
654 VariableValue::AssociativeArray(_) => {
655 return Err(RustBashError::Execution(format!(
656 "{target}: cannot use numeric index on associative array"
657 )));
658 }
659 },
660 None => {
661 let mut map = BTreeMap::new();
662 map.insert(index, value);
663 state.env.insert(
664 target,
665 Variable {
666 value: VariableValue::IndexedArray(map),
667 attrs: VariableAttrs::empty(),
668 },
669 );
670 }
671 }
672 Ok(())
673}
674
675pub(crate) fn set_assoc_element(
677 state: &mut InterpreterState,
678 name: &str,
679 key: String,
680 value: String,
681) -> Result<(), RustBashError> {
682 let target = resolve_nameref(name, state)?;
683 if let Some(var) = state.env.get(&target)
684 && var.readonly()
685 {
686 return Err(RustBashError::Execution(format!(
687 "{target}: readonly variable"
688 )));
689 }
690
691 let attrs = state
693 .env
694 .get(&target)
695 .map(|v| v.attrs)
696 .unwrap_or(VariableAttrs::empty());
697 let value = if attrs.contains(VariableAttrs::INTEGER) {
698 crate::interpreter::arithmetic::eval_arithmetic(&value, state)?.to_string()
699 } else {
700 value
701 };
702 let value = if attrs.contains(VariableAttrs::LOWERCASE) {
703 value.to_lowercase()
704 } else if attrs.contains(VariableAttrs::UPPERCASE) {
705 value.to_uppercase()
706 } else {
707 value
708 };
709
710 let limit = state.limits.max_array_elements;
711 match state.env.get_mut(&target) {
712 Some(var) => match &mut var.value {
713 VariableValue::AssociativeArray(map) => {
714 if !map.contains_key(&key) && map.len() >= limit {
715 return Err(RustBashError::LimitExceeded {
716 limit_name: "max_array_elements",
717 limit_value: limit,
718 actual_value: map.len() + 1,
719 });
720 }
721 map.insert(key, value);
722 }
723 _ => {
724 return Err(RustBashError::Execution(format!(
725 "{target}: not an associative array"
726 )));
727 }
728 },
729 None => {
730 return Err(RustBashError::Execution(format!(
731 "{target}: not an associative array"
732 )));
733 }
734 }
735 Ok(())
736}
737
738pub(crate) fn next_random(state: &mut InterpreterState) -> u16 {
740 let mut s = state.random_seed;
741 if s == 0 {
742 s = 12345;
743 }
744 s ^= s << 13;
745 s ^= s >> 17;
746 s ^= s << 5;
747 state.random_seed = s;
748 (s & 0x7FFF) as u16
749}
750
751pub(crate) fn resolve_nameref(
755 name: &str,
756 state: &InterpreterState,
757) -> Result<String, RustBashError> {
758 let mut current = name.to_string();
759 for _ in 0..10 {
760 match state.env.get(¤t) {
761 Some(var) if var.attrs.contains(VariableAttrs::NAMEREF) => {
762 current = var.value.as_scalar().to_string();
763 }
764 _ => return Ok(current),
765 }
766 }
767 Err(RustBashError::Execution(format!(
768 "{name}: circular name reference"
769 )))
770}
771
772pub(crate) fn resolve_nameref_or_self(name: &str, state: &InterpreterState) -> String {
775 resolve_nameref(name, state).unwrap_or_else(|_| name.to_string())
776}
777
778pub(crate) fn execute_trap(
780 trap_cmd: &str,
781 state: &mut InterpreterState,
782) -> Result<ExecResult, RustBashError> {
783 let was_in_trap = state.in_trap;
784 state.in_trap = true;
785 let program = parse(trap_cmd)?;
786 let result = walker::execute_program(&program, state);
787 state.in_trap = was_in_trap;
788 result
789}
790
791#[cfg(test)]
792mod tests {
793 use super::*;
794
795 #[test]
796 fn parse_empty_input() {
797 let program = parse("").unwrap();
798 assert!(program.complete_commands.is_empty());
799 }
800
801 #[test]
802 fn parse_simple_command() {
803 let program = parse("echo hello").unwrap();
804 assert_eq!(program.complete_commands.len(), 1);
805 }
806
807 #[test]
808 fn parse_sequential_commands() {
809 let program = parse("echo a; echo b").unwrap();
810 assert!(!program.complete_commands.is_empty());
811 }
812
813 #[test]
814 fn parse_pipeline() {
815 let program = parse("echo hello | cat").unwrap();
816 assert_eq!(program.complete_commands.len(), 1);
817 }
818
819 #[test]
820 fn parse_and_or() {
821 let program = parse("true && echo yes").unwrap();
822 assert_eq!(program.complete_commands.len(), 1);
823 }
824
825 #[test]
826 fn parse_error_on_unclosed_quote() {
827 let result = parse("echo 'unterminated");
828 assert!(result.is_err());
829 }
830
831 #[test]
832 fn expand_simple_text() {
833 let word = ast::Word {
834 value: "hello".to_string(),
835 loc: None,
836 };
837 let state = make_test_state();
838 assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello"]);
839 }
840
841 #[test]
842 fn expand_single_quoted_text() {
843 let word = ast::Word {
844 value: "'hello world'".to_string(),
845 loc: None,
846 };
847 let state = make_test_state();
848 assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello world"]);
849 }
850
851 #[test]
852 fn expand_double_quoted_text() {
853 let word = ast::Word {
854 value: "\"hello world\"".to_string(),
855 loc: None,
856 };
857 let state = make_test_state();
858 assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello world"]);
859 }
860
861 #[test]
862 fn expand_escaped_character() {
863 let word = ast::Word {
864 value: "hello\\ world".to_string(),
865 loc: None,
866 };
867 let state = make_test_state();
868 assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello world"]);
869 }
870
871 fn make_test_state() -> InterpreterState {
872 use crate::vfs::InMemoryFs;
873 InterpreterState {
874 fs: Arc::new(InMemoryFs::new()),
875 env: HashMap::new(),
876 cwd: "/".to_string(),
877 functions: HashMap::new(),
878 last_exit_code: 0,
879 commands: HashMap::new(),
880 shell_opts: ShellOpts::default(),
881 shopt_opts: ShoptOpts::default(),
882 limits: ExecutionLimits::default(),
883 counters: ExecutionCounters::default(),
884 network_policy: NetworkPolicy::default(),
885 should_exit: false,
886 loop_depth: 0,
887 control_flow: None,
888 positional_params: Vec::new(),
889 shell_name: "rust-bash".to_string(),
890 random_seed: 42,
891 local_scopes: Vec::new(),
892 in_function_depth: 0,
893 traps: HashMap::new(),
894 in_trap: false,
895 errexit_suppressed: 0,
896 stdin_offset: 0,
897 dir_stack: Vec::new(),
898 command_hash: HashMap::new(),
899 aliases: HashMap::new(),
900 current_lineno: 0,
901 shell_start_time: Instant::now(),
902 last_argument: String::new(),
903 call_stack: Vec::new(),
904 machtype: "x86_64-pc-linux-gnu".to_string(),
905 hosttype: "x86_64".to_string(),
906 persistent_fds: HashMap::new(),
907 next_auto_fd: 10,
908 proc_sub_counter: 0,
909 proc_sub_prealloc: HashMap::new(),
910 pipe_stdin_bytes: None,
911 pending_cmdsub_stderr: String::new(),
912 }
913 }
914}