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, Box<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}
394
395pub(crate) fn parser_options() -> brush_parser::ParserOptions {
398 brush_parser::ParserOptions {
399 sh_mode: false,
400 posix_mode: false,
401 enable_extended_globbing: true,
402 tilde_expansion: true,
403 }
404}
405
406pub fn parse(input: &str) -> Result<ast::Program, RustBashError> {
408 let tokens =
409 brush_parser::tokenize_str(input).map_err(|e| RustBashError::Parse(e.to_string()))?;
410
411 if tokens.is_empty() {
412 return Ok(ast::Program {
413 complete_commands: vec![],
414 });
415 }
416
417 let options = parser_options();
418 let source_info = brush_parser::SourceInfo {
419 source: input.to_string(),
420 };
421
422 brush_parser::parse_tokens(&tokens, &options, &source_info)
423 .map_err(|e| RustBashError::Parse(e.to_string()))
424}
425
426pub(crate) fn set_variable(
429 state: &mut InterpreterState,
430 name: &str,
431 value: String,
432) -> Result<(), RustBashError> {
433 if value.len() > state.limits.max_string_length {
434 return Err(RustBashError::LimitExceeded {
435 limit_name: "max_string_length",
436 limit_value: state.limits.max_string_length,
437 actual_value: value.len(),
438 });
439 }
440
441 let target = resolve_nameref(name, state)?;
443
444 if let Some(bracket_pos) = target.find('[')
447 && target.ends_with(']')
448 {
449 let arr_name = &target[..bracket_pos];
450 let index_raw = &target[bracket_pos + 1..target.len() - 1];
451 let word = brush_parser::ast::Word {
453 value: index_raw.to_string(),
454 loc: None,
455 };
456 let expanded_key = crate::interpreter::expansion::expand_word_to_string_mut(&word, state)?;
457
458 if let Some(var) = state.env.get(arr_name)
459 && var.readonly()
460 {
461 return Err(RustBashError::Execution(format!(
462 "{arr_name}: readonly variable"
463 )));
464 }
465
466 let is_assoc = state
468 .env
469 .get(arr_name)
470 .is_some_and(|v| matches!(v.value, VariableValue::AssociativeArray(_)));
471 let numeric_idx = if !is_assoc {
472 crate::interpreter::arithmetic::eval_arithmetic(&expanded_key, state).unwrap_or(0)
473 } else {
474 0
475 };
476
477 match state.env.get_mut(arr_name) {
478 Some(var) => match &mut var.value {
479 VariableValue::AssociativeArray(map) => {
480 map.insert(expanded_key, value);
481 }
482 VariableValue::IndexedArray(map) => {
483 let actual_idx = if numeric_idx < 0 {
484 let max_key = map.keys().next_back().copied().unwrap_or(0);
485 let resolved = max_key as i64 + 1 + numeric_idx;
486 if resolved < 0 {
487 0usize
488 } else {
489 resolved as usize
490 }
491 } else {
492 numeric_idx as usize
493 };
494 map.insert(actual_idx, value);
495 }
496 VariableValue::Scalar(s) => {
497 if numeric_idx == 0 || numeric_idx == -1 {
498 *s = value;
499 }
500 }
501 },
502 None => {
503 let idx = expanded_key.parse::<usize>().unwrap_or(0);
505 let mut map = std::collections::BTreeMap::new();
506 map.insert(idx, value);
507 state.env.insert(
508 arr_name.to_string(),
509 Variable {
510 value: VariableValue::IndexedArray(map),
511 attrs: VariableAttrs::empty(),
512 },
513 );
514 }
515 }
516 return Ok(());
517 }
518
519 if target == "SECONDS" {
521 if let Ok(offset) = value.parse::<u64>() {
522 state.shell_start_time = Instant::now() - std::time::Duration::from_secs(offset);
525 } else {
526 state.shell_start_time = Instant::now();
527 }
528 return Ok(());
529 }
530
531 if let Some(var) = state.env.get(&target)
532 && var.readonly()
533 {
534 return Err(RustBashError::Execution(format!(
535 "{target}: readonly variable"
536 )));
537 }
538
539 let attrs = state
541 .env
542 .get(&target)
543 .map(|v| v.attrs)
544 .unwrap_or(VariableAttrs::empty());
545
546 let value = if attrs.contains(VariableAttrs::INTEGER) {
548 let result = crate::interpreter::arithmetic::eval_arithmetic(&value, state)?;
549 result.to_string()
550 } else {
551 value
552 };
553
554 let value = if attrs.contains(VariableAttrs::LOWERCASE) {
556 value.to_lowercase()
557 } else if attrs.contains(VariableAttrs::UPPERCASE) {
558 value.to_uppercase()
559 } else {
560 value
561 };
562
563 match state.env.get_mut(&target) {
564 Some(var) => {
565 match &mut var.value {
566 VariableValue::IndexedArray(map) => {
567 map.insert(0, value);
568 }
569 VariableValue::AssociativeArray(map) => {
570 map.insert("0".to_string(), value);
571 }
572 VariableValue::Scalar(s) => *s = value,
573 }
574 if state.shell_opts.allexport {
576 var.attrs.insert(VariableAttrs::EXPORTED);
577 }
578 }
579 None => {
580 let attrs = if state.shell_opts.allexport {
581 VariableAttrs::EXPORTED
582 } else {
583 VariableAttrs::empty()
584 };
585 state.env.insert(
586 target,
587 Variable {
588 value: VariableValue::Scalar(value),
589 attrs,
590 },
591 );
592 }
593 }
594 Ok(())
595}
596
597pub(crate) fn set_array_element(
600 state: &mut InterpreterState,
601 name: &str,
602 index: usize,
603 value: String,
604) -> Result<(), RustBashError> {
605 let target = resolve_nameref(name, state)?;
606 if let Some(var) = state.env.get(&target)
607 && var.readonly()
608 {
609 return Err(RustBashError::Execution(format!(
610 "{target}: readonly variable"
611 )));
612 }
613
614 let attrs = state
616 .env
617 .get(&target)
618 .map(|v| v.attrs)
619 .unwrap_or(VariableAttrs::empty());
620 let value = if attrs.contains(VariableAttrs::INTEGER) {
621 crate::interpreter::arithmetic::eval_arithmetic(&value, state)?.to_string()
622 } else {
623 value
624 };
625 let value = if attrs.contains(VariableAttrs::LOWERCASE) {
626 value.to_lowercase()
627 } else if attrs.contains(VariableAttrs::UPPERCASE) {
628 value.to_uppercase()
629 } else {
630 value
631 };
632
633 let limit = state.limits.max_array_elements;
634 match state.env.get_mut(&target) {
635 Some(var) => match &mut var.value {
636 VariableValue::IndexedArray(map) => {
637 if !map.contains_key(&index) && map.len() >= limit {
638 return Err(RustBashError::LimitExceeded {
639 limit_name: "max_array_elements",
640 limit_value: limit,
641 actual_value: map.len() + 1,
642 });
643 }
644 map.insert(index, value);
645 }
646 VariableValue::Scalar(_) => {
647 let mut map = BTreeMap::new();
648 map.insert(index, value);
649 var.value = VariableValue::IndexedArray(map);
650 }
651 VariableValue::AssociativeArray(_) => {
652 return Err(RustBashError::Execution(format!(
653 "{target}: cannot use numeric index on associative array"
654 )));
655 }
656 },
657 None => {
658 let mut map = BTreeMap::new();
659 map.insert(index, value);
660 state.env.insert(
661 target,
662 Variable {
663 value: VariableValue::IndexedArray(map),
664 attrs: VariableAttrs::empty(),
665 },
666 );
667 }
668 }
669 Ok(())
670}
671
672pub(crate) fn set_assoc_element(
674 state: &mut InterpreterState,
675 name: &str,
676 key: String,
677 value: String,
678) -> Result<(), RustBashError> {
679 let target = resolve_nameref(name, state)?;
680 if let Some(var) = state.env.get(&target)
681 && var.readonly()
682 {
683 return Err(RustBashError::Execution(format!(
684 "{target}: readonly variable"
685 )));
686 }
687
688 let attrs = state
690 .env
691 .get(&target)
692 .map(|v| v.attrs)
693 .unwrap_or(VariableAttrs::empty());
694 let value = if attrs.contains(VariableAttrs::INTEGER) {
695 crate::interpreter::arithmetic::eval_arithmetic(&value, state)?.to_string()
696 } else {
697 value
698 };
699 let value = if attrs.contains(VariableAttrs::LOWERCASE) {
700 value.to_lowercase()
701 } else if attrs.contains(VariableAttrs::UPPERCASE) {
702 value.to_uppercase()
703 } else {
704 value
705 };
706
707 let limit = state.limits.max_array_elements;
708 match state.env.get_mut(&target) {
709 Some(var) => match &mut var.value {
710 VariableValue::AssociativeArray(map) => {
711 if !map.contains_key(&key) && map.len() >= limit {
712 return Err(RustBashError::LimitExceeded {
713 limit_name: "max_array_elements",
714 limit_value: limit,
715 actual_value: map.len() + 1,
716 });
717 }
718 map.insert(key, value);
719 }
720 _ => {
721 return Err(RustBashError::Execution(format!(
722 "{target}: not an associative array"
723 )));
724 }
725 },
726 None => {
727 return Err(RustBashError::Execution(format!(
728 "{target}: not an associative array"
729 )));
730 }
731 }
732 Ok(())
733}
734
735pub(crate) fn next_random(state: &mut InterpreterState) -> u16 {
737 let mut s = state.random_seed;
738 if s == 0 {
739 s = 12345;
740 }
741 s ^= s << 13;
742 s ^= s >> 17;
743 s ^= s << 5;
744 state.random_seed = s;
745 (s & 0x7FFF) as u16
746}
747
748pub(crate) fn resolve_nameref(
752 name: &str,
753 state: &InterpreterState,
754) -> Result<String, RustBashError> {
755 let mut current = name.to_string();
756 for _ in 0..10 {
757 match state.env.get(¤t) {
758 Some(var) if var.attrs.contains(VariableAttrs::NAMEREF) => {
759 current = var.value.as_scalar().to_string();
760 }
761 _ => return Ok(current),
762 }
763 }
764 Err(RustBashError::Execution(format!(
765 "{name}: circular name reference"
766 )))
767}
768
769pub(crate) fn resolve_nameref_or_self(name: &str, state: &InterpreterState) -> String {
772 resolve_nameref(name, state).unwrap_or_else(|_| name.to_string())
773}
774
775pub(crate) fn execute_trap(
777 trap_cmd: &str,
778 state: &mut InterpreterState,
779) -> Result<ExecResult, RustBashError> {
780 let was_in_trap = state.in_trap;
781 state.in_trap = true;
782 let program = parse(trap_cmd)?;
783 let result = walker::execute_program(&program, state);
784 state.in_trap = was_in_trap;
785 result
786}
787
788#[cfg(test)]
789mod tests {
790 use super::*;
791
792 #[test]
793 fn parse_empty_input() {
794 let program = parse("").unwrap();
795 assert!(program.complete_commands.is_empty());
796 }
797
798 #[test]
799 fn parse_simple_command() {
800 let program = parse("echo hello").unwrap();
801 assert_eq!(program.complete_commands.len(), 1);
802 }
803
804 #[test]
805 fn parse_sequential_commands() {
806 let program = parse("echo a; echo b").unwrap();
807 assert!(!program.complete_commands.is_empty());
808 }
809
810 #[test]
811 fn parse_pipeline() {
812 let program = parse("echo hello | cat").unwrap();
813 assert_eq!(program.complete_commands.len(), 1);
814 }
815
816 #[test]
817 fn parse_and_or() {
818 let program = parse("true && echo yes").unwrap();
819 assert_eq!(program.complete_commands.len(), 1);
820 }
821
822 #[test]
823 fn parse_error_on_unclosed_quote() {
824 let result = parse("echo 'unterminated");
825 assert!(result.is_err());
826 }
827
828 #[test]
829 fn expand_simple_text() {
830 let word = ast::Word {
831 value: "hello".to_string(),
832 loc: None,
833 };
834 let state = make_test_state();
835 assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello"]);
836 }
837
838 #[test]
839 fn expand_single_quoted_text() {
840 let word = ast::Word {
841 value: "'hello world'".to_string(),
842 loc: None,
843 };
844 let state = make_test_state();
845 assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello world"]);
846 }
847
848 #[test]
849 fn expand_double_quoted_text() {
850 let word = ast::Word {
851 value: "\"hello world\"".to_string(),
852 loc: None,
853 };
854 let state = make_test_state();
855 assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello world"]);
856 }
857
858 #[test]
859 fn expand_escaped_character() {
860 let word = ast::Word {
861 value: "hello\\ world".to_string(),
862 loc: None,
863 };
864 let state = make_test_state();
865 assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello world"]);
866 }
867
868 fn make_test_state() -> InterpreterState {
869 use crate::vfs::InMemoryFs;
870 InterpreterState {
871 fs: Arc::new(InMemoryFs::new()),
872 env: HashMap::new(),
873 cwd: "/".to_string(),
874 functions: HashMap::new(),
875 last_exit_code: 0,
876 commands: HashMap::new(),
877 shell_opts: ShellOpts::default(),
878 shopt_opts: ShoptOpts::default(),
879 limits: ExecutionLimits::default(),
880 counters: ExecutionCounters::default(),
881 network_policy: NetworkPolicy::default(),
882 should_exit: false,
883 loop_depth: 0,
884 control_flow: None,
885 positional_params: Vec::new(),
886 shell_name: "rust-bash".to_string(),
887 random_seed: 42,
888 local_scopes: Vec::new(),
889 in_function_depth: 0,
890 traps: HashMap::new(),
891 in_trap: false,
892 errexit_suppressed: 0,
893 stdin_offset: 0,
894 dir_stack: Vec::new(),
895 command_hash: HashMap::new(),
896 aliases: HashMap::new(),
897 current_lineno: 0,
898 shell_start_time: Instant::now(),
899 last_argument: String::new(),
900 call_stack: Vec::new(),
901 machtype: "x86_64-pc-linux-gnu".to_string(),
902 hosttype: "x86_64".to_string(),
903 persistent_fds: HashMap::new(),
904 next_auto_fd: 10,
905 proc_sub_counter: 0,
906 proc_sub_prealloc: HashMap::new(),
907 pipe_stdin_bytes: None,
908 }
909 }
910}