1use crate::args::CommandLineArgs;
4use crate::args::InputBackendType;
5use crate::brushctl::ShellBuilderBrushBuiltinExt as _;
6use crate::bundled;
7use crate::config;
8use crate::error_formatter;
9use crate::events;
10use crate::productinfo;
11use brush_builtins::ShellBuilderExt as _;
12#[cfg(feature = "experimental-builtins")]
13use brush_experimental_builtins::ShellBuilderExt as _;
14use clap::CommandFactory;
15use std::sync::LazyLock;
16use std::{path::Path, sync::Arc};
17use tokio::sync::Mutex;
18
19#[allow(unused_imports, reason = "only used in some configs")]
20use std::io::IsTerminal;
21
22static TRACE_EVENT_CONFIG: LazyLock<Arc<tokio::sync::Mutex<Option<events::TraceEventConfig>>>> =
23 LazyLock::new(|| Arc::new(tokio::sync::Mutex::new(None)));
24
25type BrushShellExtensions = brush_core::extensions::ShellExtensionsImpl<error_formatter::Formatter>;
26type BrushShell = brush_core::Shell<BrushShellExtensions>;
27
28impl CommandLineArgs {
31 fn try_parse_from(itr: impl IntoIterator<Item = String>) -> Result<Self, clap::Error> {
36 let mut args: Vec<String> = itr.into_iter().collect();
37
38 if let Some(dd_idx) = args.iter().position(|a| a == "--") {
51 if let Some(flag_idx) = dd_idx
52 .checked_sub(1)
53 .filter(|&i| Self::has_pending_c_flag(&args[i]))
54 {
55 args.remove(dd_idx);
57
58 if args.get(dd_idx).map(String::as_str) == Some("--") {
64 let value = args.remove(dd_idx);
65 args[flag_idx].push_str(&value);
66 }
67 }
68 }
69
70 let (mut this, script_args) = brush_core::builtins::try_parse_known::<Self>(args)?;
71
72 if let Some(args) = script_args {
75 this.script_args.extend(args);
76 }
77
78 Ok(this)
79 }
80
81 fn has_pending_c_flag(arg: &str) -> bool {
91 let Some(flags) = arg.strip_prefix('-') else {
93 return false;
94 };
95 let Some(preceding) = flags.strip_suffix('c') else {
96 return false;
97 };
98 if preceding.starts_with('-') {
100 return false;
101 }
102
103 let cmd = Self::command();
108 preceding.chars().all(|ch| {
109 cmd.get_arguments().any(|a| {
110 a.get_short() == Some(ch)
111 && !matches!(
112 a.get_action(),
113 clap::ArgAction::Set | clap::ArgAction::Append
114 )
115 })
116 })
117 }
118}
119
120pub fn run() {
122 bundled::install_default_providers();
129
130 if let Some(code) = bundled::maybe_dispatch() {
136 std::process::exit(code);
137 }
138
139 install_panic_handlers();
143
144 let mut args: Vec<_> = std::env::args().collect();
148
149 for arg in &mut args {
151 if arg.starts_with("+O") {
152 arg.insert_str(0, "--");
153 }
154 }
155
156 let parsed_args = match CommandLineArgs::try_parse_from(args.iter().cloned()) {
157 Ok(parsed_args) => parsed_args,
158 Err(e) => {
159 let _ = e.print();
160
161 let exit_code = match e.kind() {
164 clap::error::ErrorKind::DisplayVersion => 0,
165 clap::error::ErrorKind::DisplayHelp => 0,
166 _ => 2,
167 };
168
169 std::process::exit(exit_code);
170 }
171 };
172
173 #[cfg(any(unix, windows))]
177 let mut builder = tokio::runtime::Builder::new_multi_thread();
178 #[cfg(not(any(unix, windows)))]
179 let mut builder = tokio::runtime::Builder::new_current_thread();
180
181 let Ok(runtime) = builder.enable_all().build() else {
182 tracing::error!("error: failed to create Tokio runtime");
183 std::process::exit(1);
184 };
185
186 let result = runtime.block_on(run_async(args, parsed_args));
187
188 let exit_code = match result {
189 Ok(code) => code,
190 Err(err) => {
191 tracing::error!("error: {err:#}");
192 1
193 }
194 };
195
196 std::process::exit(i32::from(exit_code));
197}
198
199fn install_panic_handlers() {
201 human_panic::setup_panic!(
206 human_panic::Metadata::new(productinfo::PRODUCT_NAME, productinfo::PRODUCT_VERSION)
207 .homepage(env!("CARGO_PKG_HOMEPAGE"))
208 .support("please post a GitHub issue at https://github.com/reubeno/brush/issues/new")
209 );
210
211 if std::io::stdout().is_terminal() {
218 let original_panic_handler = std::panic::take_hook();
219 std::panic::set_hook(Box::new(move |panic_info| {
220 let _ = try_reset_terminal_to_defaults();
222
223 original_panic_handler(panic_info);
225 }));
226 }
227}
228
229#[cfg(feature = "experimental")]
230pub(crate) const DEFAULT_ENABLE_HIGHLIGHTING: bool = true;
231#[cfg(not(feature = "experimental"))]
232pub(crate) const DEFAULT_ENABLE_HIGHLIGHTING: bool = false;
233
234#[doc(hidden)]
241async fn run_async(
242 cli_args: Vec<String>,
243 args: CommandLineArgs,
244) -> Result<u8, brush_interactive::ShellError> {
245 let mut event_config = TRACE_EVENT_CONFIG.lock().await;
247 *event_config = Some(events::TraceEventConfig::init(
248 &args.enabled_debug_events,
249 &args.disabled_events,
250 ));
251 drop(event_config);
252
253 let file_config = config::load_config(args.no_config, args.config_file.as_deref())
255 .into_config_or_log()
256 .map_err(|e| brush_interactive::ShellError::IoError(std::io::Error::other(e)))?;
257
258 let shell: BrushShell = instantiate_shell(&args, cli_args).await?;
262 let shell = Arc::new(Mutex::new(shell));
263
264 let default_backend = get_default_input_backend_type(&args);
267 let selected_backend = args.input_backend.unwrap_or(default_backend);
268
269 #[allow(unused_variables, reason = "not used when no backend features enabled")]
271 let ui_options = file_config.to_ui_options(&args);
272
273 let result = match selected_backend {
274 #[cfg(all(feature = "reedline", any(unix, windows)))]
275 InputBackendType::Reedline => {
276 let mut input_backend =
277 brush_interactive::ReedlineInputBackend::new(&ui_options, &shell)?;
278 run_in_shell(&shell, args.clone(), &mut input_backend, &ui_options).await
279 }
280 #[cfg(any(not(feature = "reedline"), not(any(unix, windows))))]
281 InputBackendType::Reedline => Err(brush_interactive::ShellError::InputBackendNotSupported),
282
283 #[cfg(feature = "basic")]
284 InputBackendType::Basic => {
285 let mut input_backend = brush_interactive::BasicInputBackend;
286 run_in_shell(&shell, args.clone(), &mut input_backend, &ui_options).await
287 }
288 #[cfg(not(feature = "basic"))]
289 InputBackendType::Basic => Err(brush_interactive::ShellError::InputBackendNotSupported),
290
291 #[cfg(feature = "minimal")]
292 InputBackendType::Minimal => {
293 let mut input_backend = brush_interactive::MinimalInputBackend;
294 run_in_shell(&shell, args.clone(), &mut input_backend, &ui_options).await
295 }
296 #[cfg(not(feature = "minimal"))]
297 InputBackendType::Minimal => Err(brush_interactive::ShellError::InputBackendNotSupported),
298 };
299
300 let exit_code = match result {
302 Ok(code) => code,
303 Err(brush_interactive::ShellError::ShellError(e)) => {
304 let shell = shell.lock().await;
305 let mut stderr = shell.stderr();
306 let _ = shell.display_error(&mut stderr, &e);
307 drop(shell);
308 1
309 }
310 Err(err) => {
311 tracing::error!("error: {err:#}");
312 1
313 }
314 };
315
316 Ok(exit_code)
317}
318
319const fn will_run_interactively(args: &CommandLineArgs) -> bool {
321 if args.command.is_some() {
322 false
323 } else if args.read_commands_from_stdin {
324 true
325 } else {
326 args.script_args.is_empty()
327 }
328}
329
330async fn run_in_shell(
340 shell_ref: &brush_interactive::ShellRef<impl brush_core::ShellExtensions>,
341 args: CommandLineArgs,
342 input_backend: &mut impl brush_interactive::InputBackend,
343 ui_options: &brush_interactive::UIOptions,
344) -> Result<u8, brush_interactive::ShellError> {
345 initialize_shell(shell_ref, &args).await?;
347
348 if let Some(command) = args.command {
350 shell_ref.lock().await.run_dash_c_command(command).await?;
351
352 } else if args.read_commands_from_stdin {
356 let interactive_options = ui_options.into();
357 brush_interactive::InteractiveShell::new(shell_ref, input_backend, &interactive_options)?
358 .run_interactively()
359 .await?;
360
361 } else if !args.script_args.is_empty() {
363 shell_ref
365 .lock()
366 .await
367 .run_script(
368 Path::new(&args.script_args[0]),
369 args.script_args.iter().skip(1),
370 )
371 .await?;
372
373 } else {
376 let interactive_options = ui_options.into();
377 brush_interactive::InteractiveShell::new(shell_ref, input_backend, &interactive_options)?
378 .run_interactively()
379 .await?;
380 }
381
382 let result = shell_ref.lock().await.last_exit_status();
384
385 Ok(result)
386}
387
388async fn initialize_shell(
395 shell_ref: &brush_interactive::ShellRef<impl brush_core::ShellExtensions>,
396 args: &CommandLineArgs,
397) -> Result<(), brush_interactive::ShellError> {
398 let profile = if args.no_profile {
400 brush_core::ProfileLoadBehavior::Skip
401 } else {
402 brush_core::ProfileLoadBehavior::LoadDefault
403 };
404
405 let rc = if args.no_rc {
407 brush_core::RcLoadBehavior::Skip
408 } else if let Some(rc_file) = &args.rc_file {
409 brush_core::RcLoadBehavior::LoadCustom(rc_file.clone())
410 } else {
411 brush_core::RcLoadBehavior::LoadDefault
412 };
413
414 shell_ref.lock().await.load_config(&profile, &rc).await?;
415
416 Ok(())
417}
418
419async fn instantiate_shell(
426 args: &CommandLineArgs,
427 cli_args: Vec<String>,
428) -> Result<BrushShell, brush_interactive::ShellError> {
429 #[cfg(feature = "experimental-load")]
430 let mut shell = if let Some(load_file) = &args.load_file {
431 instantiate_shell_from_file(load_file.as_path())?
432 } else {
433 instantiate_shell_from_args(args, cli_args).await?
434 };
435
436 #[cfg(not(feature = "experimental-load"))]
437 let mut shell = instantiate_shell_from_args(args, cli_args).await?;
438
439 bundled::register_shims(&mut shell);
443
444 Ok(shell)
445}
446
447#[cfg(feature = "experimental-load")]
448fn instantiate_shell_from_file(
449 file_path: &Path,
450) -> Result<BrushShell, brush_interactive::ShellError> {
451 let mut shell: BrushShell = serde_json::from_reader(std::fs::File::open(file_path)?)
452 .map_err(|e| brush_interactive::ShellError::IoError(std::io::Error::other(e)))?;
453
454 let builtin_set = if shell.options().sh_mode {
457 brush_builtins::BuiltinSet::ShMode
458 } else {
459 brush_builtins::BuiltinSet::BashMode
460 };
461
462 let builtins = brush_builtins::default_builtins(builtin_set);
463
464 for (builtin_name, builtin) in builtins {
465 shell.register_builtin(&builtin_name, builtin);
466 }
467
468 #[cfg(feature = "experimental-builtins")]
470 for (builtin_name, builtin) in brush_experimental_builtins::experimental_builtins() {
471 shell.register_builtin(&builtin_name, builtin);
472 }
473
474 Ok(shell)
475}
476
477async fn instantiate_shell_from_args(
484 args: &CommandLineArgs,
485 cli_args: Vec<String>,
486) -> Result<BrushShell, brush_interactive::ShellError> {
487 let login = args.login || cli_args.first().is_some_and(|argv0| argv0.starts_with('-'));
489
490 let shell_name = if args.command.is_some() && !args.script_args.is_empty() {
492 Some(args.script_args[0].clone())
493 } else if !cli_args.is_empty() {
494 Some(cli_args[0].clone())
495 } else if args.sh_mode {
496 Some(String::from("sh"))
498 } else {
499 None
500 };
501
502 let shell_args = if args.command.is_some() {
504 Some(args.script_args.iter().skip(1).cloned().collect())
505 } else if args.read_commands_from_stdin {
506 Some(args.script_args.clone())
507 } else {
508 None
509 };
510
511 let read_commands_from_stdin = (args.read_commands_from_stdin && args.command.is_none())
514 || (args.script_args.is_empty() && args.command.is_none());
515
516 let builtin_set = if args.sh_mode {
517 brush_builtins::BuiltinSet::ShMode
518 } else {
519 brush_builtins::BuiltinSet::BashMode
520 };
521
522 let fds = args
524 .inherited_fds
525 .iter()
526 .filter_map(|&fd| brush_core::sys::fd::try_get_file_for_open_fd(fd).map(|file| (fd, file)))
527 .collect();
528
529 #[cfg(feature = "experimental-parser")]
531 let parser_impl = if args.experimental_parser {
532 brush_core::parser::ParserImpl::Winnow
533 } else {
534 brush_core::parser::ParserImpl::Peg
535 };
536
537 #[cfg(not(feature = "experimental-parser"))]
538 let parser_impl = brush_core::parser::ParserImpl::Peg;
539
540 let shell = brush_core::Shell::builder_with_extensions::<BrushShellExtensions>()
544 .disable_options(args.disabled_options.clone())
545 .disable_shopt_options(args.disabled_shopt_options.clone())
546 .disallow_overwriting_regular_files_via_output_redirection(
547 args.disallow_overwriting_regular_files_via_output_redirection,
548 )
549 .enable_options(args.enabled_options.clone())
550 .enable_shopt_options(args.enabled_shopt_options.clone())
551 .do_not_execute_commands(args.do_not_execute_commands)
552 .exit_after_one_command(args.exit_after_one_command)
553 .login(login)
554 .interactive(args.is_interactive())
555 .command_string_mode(args.command.is_some())
556 .no_editing(args.no_editing)
557 .profile(brush_core::ProfileLoadBehavior::Skip)
558 .rc(brush_core::RcLoadBehavior::Skip)
559 .do_not_inherit_env(args.do_not_inherit_env)
560 .fds(fds)
561 .maybe_shell_args(shell_args)
562 .posix(args.posix || args.sh_mode)
563 .print_commands_and_arguments(args.print_commands_and_arguments)
564 .read_commands_from_stdin(read_commands_from_stdin)
565 .maybe_shell_name(shell_name)
566 .shell_product_display_str(productinfo::get_product_display_str())
567 .sh_mode(args.sh_mode)
568 .treat_unset_variables_as_error(args.treat_unset_variables_as_error)
569 .exit_on_nonzero_command_exit(args.exit_on_nonzero_command_exit)
570 .disable_pathname_expansion(args.disable_pathname_expansion)
571 .verbose(args.verbose)
572 .parser(parser_impl)
573 .error_formatter(new_error_behavior(args))
574 .shell_version(env!("CARGO_PKG_VERSION").to_string());
575
576 let shell = shell.default_builtins(builtin_set).brush_builtins();
578
579 #[cfg(feature = "experimental-builtins")]
581 let shell = shell.experimental_builtins();
582
583 let mut shell = shell.build().await?;
585
586 if let Some(xtrace_file_path) = &args.xtrace_file_path {
588 enable_xtrace_to_file(&mut shell, xtrace_file_path)?;
589 }
590
591 Ok(shell)
592}
593
594fn enable_xtrace_to_file(
595 shell: &mut brush_core::Shell<impl brush_core::ShellExtensions>,
596 file_path: &Path,
597) -> Result<(), brush_interactive::ShellError> {
598 let file = std::fs::OpenOptions::new()
599 .create(true)
600 .write(true)
601 .truncate(true)
602 .open(file_path)
603 .map_err(|e| {
604 brush_interactive::ShellError::FailedToCreateXtraceFile(file_path.to_path_buf(), e)
605 })?;
606
607 let file = brush_core::openfiles::OpenFile::from(file);
608 let file_fd = shell.open_files_mut().add(file)?;
609
610 shell.options_mut().print_commands_and_arguments = true;
611 shell.set_env_global(
612 "BASH_XTRACEFD",
613 brush_core::ShellVariable::new(file_fd.to_string()),
614 )?;
615
616 Ok(())
617}
618
619const fn new_error_behavior(args: &CommandLineArgs) -> error_formatter::Formatter {
620 error_formatter::Formatter {
621 use_color: !args.disable_color,
622 }
623}
624
625fn get_default_input_backend_type(args: &CommandLineArgs) -> InputBackendType {
626 #[cfg(any(unix, windows))]
627 {
628 if std::io::stdin().is_terminal() && will_run_interactively(args) {
632 InputBackendType::Reedline
633 } else {
634 InputBackendType::Minimal
635 }
636 }
637 #[cfg(not(any(unix, windows)))]
638 {
639 let _args = args;
640 InputBackendType::Minimal
641 }
642}
643
644pub(crate) fn get_event_config() -> Arc<tokio::sync::Mutex<Option<events::TraceEventConfig>>> {
645 TRACE_EVENT_CONFIG.clone()
646}
647
648fn try_reset_terminal_to_defaults() -> Result<(), std::io::Error> {
649 #[cfg(any(unix, windows))]
650 {
651 let exec_result = crossterm::execute!(
653 std::io::stdout(),
654 crossterm::terminal::LeaveAlternateScreen,
655 crossterm::terminal::EnableLineWrap,
656 crossterm::style::ResetColor,
657 crossterm::event::DisableMouseCapture,
658 crossterm::event::DisableBracketedPaste,
659 crossterm::cursor::Show,
660 crossterm::cursor::MoveToNextLine(1),
661 );
662
663 let raw_result = crossterm::terminal::disable_raw_mode();
664
665 exec_result?;
666 raw_result?;
667 }
668
669 Ok(())
670}
671
672#[cfg(test)]
673#[allow(clippy::panic_in_result_fn)]
674mod tests {
675 use super::*;
676 use anyhow::Result;
677 use pretty_assertions::{assert_eq, assert_matches};
678
679 fn args(strs: &[&str]) -> Vec<String> {
680 strs.iter().map(|s| s.to_string()).collect()
681 }
682
683 #[test]
684 fn parse_empty_args() -> Result<()> {
685 let parsed_args = CommandLineArgs::try_parse_from(args(&["brush"]))?;
686 assert_matches!(parsed_args.script_args.as_slice(), []);
687 Ok(())
688 }
689
690 #[test]
691 fn parse_script_and_args() -> Result<()> {
692 let parsed_args = CommandLineArgs::try_parse_from(args(&[
693 "brush",
694 "some-script",
695 "-x",
696 "1",
697 "--option",
698 ]))?;
699 assert_eq!(
700 parsed_args.script_args,
701 ["some-script", "-x", "1", "--option"]
702 );
703 Ok(())
704 }
705
706 #[test]
707 fn parse_script_and_args_with_double_dash_in_script_args() -> Result<()> {
708 let parsed_args = CommandLineArgs::try_parse_from(args(&["brush", "some-script", "--"]))?;
709 assert_eq!(parsed_args.script_args, ["some-script", "--"]);
710 Ok(())
711 }
712
713 #[test]
714 fn parse_unknown_args() {
715 let result = CommandLineArgs::try_parse_from(args(&["brush", "--unknown-option"]));
716 assert!(result.is_err());
717 }
718
719 #[test]
720 fn parse_c_with_double_dash_separator() -> Result<()> {
721 let parsed_args =
722 CommandLineArgs::try_parse_from(args(&["brush", "-c", "--", "echo hello", "arg0"]))?;
723 assert_eq!(parsed_args.command, Some("echo hello".to_string()));
724 assert_eq!(parsed_args.script_args, ["arg0"]);
725 Ok(())
726 }
727
728 #[test]
729 fn parse_c_with_double_dash_no_command() {
730 assert!(CommandLineArgs::try_parse_from(args(&["brush", "-c", "--"])).is_err());
731 }
732
733 #[test]
734 fn parse_c_with_double_dash_command_is_double_dash() -> Result<()> {
735 let parsed_args =
736 CommandLineArgs::try_parse_from(args(&["brush", "-c", "--", "--", "echo", "hi"]))?;
737 assert_eq!(parsed_args.command, Some("--".to_string()));
738 assert_eq!(parsed_args.script_args, ["echo", "hi"]);
739 Ok(())
740 }
741
742 #[test]
743 fn parse_ec_with_double_dash_separator() -> Result<()> {
744 let parsed_args =
745 CommandLineArgs::try_parse_from(args(&["brush", "-ec", "--", "echo hello", "arg0"]))?;
746 assert_eq!(parsed_args.command, Some("echo hello".to_string()));
747 assert!(parsed_args.exit_on_nonzero_command_exit);
748 assert_eq!(parsed_args.script_args, ["arg0"]);
749 Ok(())
750 }
751
752 #[test]
753 fn parse_c_with_value_before_double_dash_unchanged() -> Result<()> {
754 let parsed_args =
755 CommandLineArgs::try_parse_from(args(&["brush", "-c", "echo hi", "--", "arg0"]))?;
756 assert_eq!(parsed_args.command, Some("echo hi".to_string()));
757 assert_eq!(parsed_args.script_args, ["--", "arg0"]);
758 Ok(())
759 }
760
761 #[test]
762 fn parse_o_with_double_dash_is_not_transformed() {
763 let result = CommandLineArgs::try_parse_from(args(&["brush", "-o", "--"]));
766 assert!(result.is_err());
771 }
772
773 #[test]
774 fn parse_oc_not_treated_as_pending_c() -> Result<()> {
775 let parsed_args = CommandLineArgs::try_parse_from(args(&["brush", "-oc", "--", "echo"]))?;
778 assert!(parsed_args.command.is_none());
780 assert_eq!(parsed_args.script_args, ["--", "echo"]);
781 Ok(())
782 }
783
784 #[test]
785 fn parse_bool_flag_before_double_dash_not_transformed() -> Result<()> {
786 let parsed_args =
789 CommandLineArgs::try_parse_from(args(&["brush", "-e", "--", "-c", "echo"]))?;
790 assert!(parsed_args.command.is_none());
791 assert!(parsed_args.exit_on_nonzero_command_exit);
792 assert_eq!(parsed_args.script_args, ["--", "-c", "echo"]);
793 Ok(())
794 }
795
796 #[test]
797 fn parse_c_with_double_dash_and_later_double_dash() -> Result<()> {
798 let parsed_args =
801 CommandLineArgs::try_parse_from(args(&["brush", "-c", "--", "echo", "--", "more"]))?;
802 assert_eq!(parsed_args.command, Some("echo".to_string()));
803 assert_eq!(parsed_args.script_args, ["--", "more"]);
804 Ok(())
805 }
806
807 #[test]
808 fn has_pending_c_flag_edge_cases() {
809 assert!(CommandLineArgs::has_pending_c_flag("-c"));
811 assert!(CommandLineArgs::has_pending_c_flag("-ec"));
812 assert!(!CommandLineArgs::has_pending_c_flag("-C")); assert!(!CommandLineArgs::has_pending_c_flag("-oc")); assert!(!CommandLineArgs::has_pending_c_flag("--c")); assert!(!CommandLineArgs::has_pending_c_flag("-")); assert!(!CommandLineArgs::has_pending_c_flag("c")); assert!(!CommandLineArgs::has_pending_c_flag("")); }
819}