1use crate::args::{CommandLineArgs, InputBackend};
4use crate::brushctl;
5use crate::error_formatter;
6use crate::events;
7use crate::productinfo;
8use crate::shell_factory;
9use brush_interactive::InteractiveShell;
10use std::sync::LazyLock;
11use std::{path::Path, sync::Arc};
12use tokio::sync::Mutex;
13
14#[allow(unused_imports, reason = "only used in some configs")]
15use std::io::IsTerminal;
16
17static TRACE_EVENT_CONFIG: LazyLock<Arc<tokio::sync::Mutex<Option<events::TraceEventConfig>>>> =
18 LazyLock::new(|| Arc::new(tokio::sync::Mutex::new(None)));
19
20impl CommandLineArgs {
23 fn try_parse_from(itr: impl IntoIterator<Item = String>) -> Result<Self, clap::Error> {
28 let (mut this, script_args) = brush_core::builtins::try_parse_known::<Self>(itr)?;
29
30 if let Some(args) = script_args {
32 this.script_args.extend(args);
33 }
34
35 Ok(this)
36 }
37}
38
39pub fn run() {
41 install_panic_handlers();
45
46 let mut args: Vec<_> = std::env::args().collect();
50
51 for arg in &mut args {
53 if arg.starts_with("+O") {
54 arg.insert_str(0, "--");
55 }
56 }
57
58 let parsed_args = match CommandLineArgs::try_parse_from(args.iter().cloned()) {
59 Ok(parsed_args) => parsed_args,
60 Err(e) => {
61 let _ = e.print();
62
63 let exit_code = match e.kind() {
66 clap::error::ErrorKind::DisplayVersion => 0,
67 clap::error::ErrorKind::DisplayHelp => 0,
68 _ => 1,
69 };
70
71 std::process::exit(exit_code);
72 }
73 };
74
75 #[cfg(any(unix, windows))]
79 let mut builder = tokio::runtime::Builder::new_multi_thread();
80 #[cfg(not(any(unix, windows)))]
81 let mut builder = tokio::runtime::Builder::new_current_thread();
82
83 let result = builder
84 .enable_all()
85 .build()
86 .unwrap()
87 .block_on(run_async(args, parsed_args));
88
89 let exit_code = match result {
90 Ok(code) => code,
91 Err(err) => {
92 tracing::error!("error: {err:#}");
93 1
94 }
95 };
96
97 std::process::exit(i32::from(exit_code));
98}
99
100fn install_panic_handlers() {
102 human_panic::setup_panic!(
107 human_panic::Metadata::new(productinfo::PRODUCT_NAME, productinfo::PRODUCT_VERSION)
108 .homepage(env!("CARGO_PKG_HOMEPAGE"))
109 .support("please post a GitHub issue at https://github.com/reubeno/brush/issues/new")
110 );
111
112 if std::io::stdout().is_terminal() {
119 let original_panic_handler = std::panic::take_hook();
120 std::panic::set_hook(Box::new(move |panic_info| {
121 let _ = try_reset_terminal_to_defaults();
123
124 original_panic_handler(panic_info);
126 }));
127 }
128}
129
130#[doc(hidden)]
137async fn run_async(
138 cli_args: Vec<String>,
139 args: CommandLineArgs,
140) -> Result<u8, brush_interactive::ShellError> {
141 let default_backend = get_default_input_backend();
142 let selected_backend = args.input_backend.unwrap_or(default_backend);
143
144 match selected_backend {
145 InputBackend::Reedline => {
146 run_impl(cli_args, args, shell_factory::ReedlineShellFactory).await
147 }
148 InputBackend::Basic => run_impl(cli_args, args, shell_factory::BasicShellFactory).await,
149 InputBackend::Minimal => run_impl(cli_args, args, shell_factory::MinimalShellFactory).await,
150 }
151}
152
153#[doc(hidden)]
160async fn run_impl(
161 cli_args: Vec<String>,
162 args: CommandLineArgs,
163 factory: impl shell_factory::ShellFactory + Send + 'static,
164) -> Result<u8, brush_interactive::ShellError> {
165 let mut event_config = TRACE_EVENT_CONFIG.try_lock().unwrap();
167 *event_config = Some(events::TraceEventConfig::init(
168 &args.enabled_debug_events,
169 &args.disabled_events,
170 ));
171 drop(event_config);
172
173 let mut shell = instantiate_shell(&args, cli_args, factory).await?;
175
176 let result = run_in_shell(&mut shell, args).await;
178
179 let exit_code = match result {
181 Ok(code) => code,
182 Err(brush_interactive::ShellError::ShellError(e)) => {
183 let core_shell = shell.shell();
184 let mut stderr = core_shell.as_ref().stderr();
185 let _ = core_shell.as_ref().display_error(&mut stderr, &e).await;
186 1
187 }
188 Err(err) => {
189 tracing::error!("error: {err:#}");
190 1
191 }
192 };
193
194 Ok(exit_code)
195}
196
197async fn run_in_shell(
198 shell: &mut impl brush_interactive::InteractiveShell,
199 args: CommandLineArgs,
200) -> Result<u8, brush_interactive::ShellError> {
201 if let Some(command) = args.command {
203 if !args.script_args.is_empty() {
205 shell.shell_mut().as_mut().shell_name = Some(args.script_args[0].clone());
206 }
207 shell.shell_mut().as_mut().positional_parameters =
208 args.script_args.iter().skip(1).cloned().collect();
209
210 let params = shell.shell().as_ref().default_exec_params();
212 shell
213 .shell_mut()
214 .as_mut()
215 .run_string(command, ¶ms)
216 .await?;
217
218 } else if args.read_commands_from_stdin {
222 if !args.script_args.is_empty() {
223 shell
224 .shell_mut()
225 .as_mut()
226 .positional_parameters
227 .clone_from(&args.script_args);
228 }
229
230 shell.run_interactively().await?;
231
232 } else if !args.script_args.is_empty() {
234 shell
236 .shell_mut()
237 .as_mut()
238 .run_script(
239 Path::new(&args.script_args[0]),
240 args.script_args.iter().skip(1),
241 )
242 .await?;
243
244 } else {
247 shell.run_interactively().await?;
248 }
249
250 let result = shell.shell().as_ref().last_result();
252
253 Ok(result)
254}
255
256async fn instantiate_shell(
257 args: &CommandLineArgs,
258 cli_args: Vec<String>,
259 factory: impl shell_factory::ShellFactory + Send + 'static,
260) -> Result<impl brush_interactive::InteractiveShell + 'static, brush_interactive::ShellError> {
261 let argv0 = if args.sh_mode {
262 Some(String::from("sh"))
264 } else if !cli_args.is_empty() {
265 Some(cli_args[0].clone())
266 } else {
267 None
268 };
269
270 let read_commands_from_stdin = (args.read_commands_from_stdin && args.command.is_none())
273 || (args.script_args.is_empty() && args.command.is_none());
274
275 let interactive = args.is_interactive();
276
277 let builtins = brush_builtins::default_builtins(if args.sh_mode {
278 brush_builtins::BuiltinSet::ShMode
279 } else {
280 brush_builtins::BuiltinSet::BashMode
281 });
282
283 let fds = args
284 .inherited_fds
285 .iter()
286 .filter_map(|&fd| brush_core::sys::fd::try_get_file_for_open_fd(fd).map(|file| (fd, file)))
287 .collect();
288
289 let options = brush_interactive::Options {
291 shell: brush_core::CreateOptions {
292 disabled_options: args.disabled_options.clone(),
293 disabled_shopt_options: args.disabled_shopt_options.clone(),
294 disallow_overwriting_regular_files_via_output_redirection: args
295 .disallow_overwriting_regular_files_via_output_redirection,
296 enabled_options: args.enabled_options.clone(),
297 enabled_shopt_options: args.enabled_shopt_options.clone(),
298 do_not_execute_commands: args.do_not_execute_commands,
299 exit_after_one_command: args.exit_after_one_command,
300 login: args.login || argv0.as_ref().is_some_and(|a0| a0.starts_with('-')),
301 interactive,
302 no_editing: args.no_editing,
303 no_profile: args.no_profile,
304 no_rc: args.no_rc,
305 rc_file: args.rc_file.clone(),
306 do_not_inherit_env: args.do_not_inherit_env,
307 fds: Some(fds),
308 posix: args.posix || args.sh_mode,
309 print_commands_and_arguments: args.print_commands_and_arguments,
310 read_commands_from_stdin,
311 shell_name: argv0,
312 shell_product_display_str: Some(productinfo::get_product_display_str()),
313 sh_mode: args.sh_mode,
314 verbose: args.verbose,
315 max_function_call_depth: None,
316 key_bindings: None,
317 error_formatter: Some(new_error_formatter(args)),
318 shell_version: Some(env!("CARGO_PKG_VERSION").to_string()),
319 builtins,
320 },
321 disable_bracketed_paste: args.disable_bracketed_paste,
322 disable_color: args.disable_color,
323 disable_highlighting: !args.enable_highlighting,
324 };
325
326 let mut shell = factory.create(options).await?;
328
329 brushctl::register(shell.shell_mut().as_mut());
331
332 Ok(shell)
333}
334
335fn new_error_formatter(
336 args: &CommandLineArgs,
337) -> Arc<Mutex<dyn brush_core::error::ErrorFormatter>> {
338 let formatter = error_formatter::Formatter {
339 use_color: !args.disable_color,
340 };
341
342 Arc::new(Mutex::new(formatter))
343}
344
345fn get_default_input_backend() -> InputBackend {
346 #[cfg(any(unix, windows))]
347 {
348 if std::io::stdin().is_terminal() {
352 InputBackend::Reedline
353 } else {
354 InputBackend::Minimal
355 }
356 }
357 #[cfg(not(any(unix, windows)))]
358 {
359 InputBackend::Minimal
360 }
361}
362
363pub(crate) fn get_event_config() -> Arc<tokio::sync::Mutex<Option<events::TraceEventConfig>>> {
364 TRACE_EVENT_CONFIG.clone()
365}
366
367fn try_reset_terminal_to_defaults() -> Result<(), std::io::Error> {
368 #[cfg(any(unix, windows))]
369 {
370 let exec_result = crossterm::execute!(
372 std::io::stdout(),
373 crossterm::terminal::LeaveAlternateScreen,
374 crossterm::terminal::EnableLineWrap,
375 crossterm::style::ResetColor,
376 crossterm::event::DisableMouseCapture,
377 crossterm::event::DisableBracketedPaste,
378 crossterm::cursor::Show,
379 crossterm::cursor::MoveToNextLine(1),
380 );
381
382 let raw_result = crossterm::terminal::disable_raw_mode();
383
384 exec_result?;
385 raw_result?;
386 }
387
388 Ok(())
389}
390
391#[cfg(test)]
392#[allow(clippy::panic_in_result_fn)]
393mod tests {
394 use super::*;
395 use anyhow::Result;
396 use pretty_assertions::{assert_eq, assert_matches};
397
398 #[test]
399 fn parse_empty_args() -> Result<()> {
400 let args = vec!["brush"];
401 let args = args.into_iter().map(|s| s.to_string()).collect::<Vec<_>>();
402
403 let parsed_args = CommandLineArgs::try_parse_from(args)?;
404 assert_matches!(parsed_args.script_args.as_slice(), []);
405
406 Ok(())
407 }
408
409 #[test]
410 fn parse_script_and_args() -> Result<()> {
411 let args = vec!["brush", "some-script", "-x", "1", "--option"];
412 let args = args.into_iter().map(|s| s.to_string()).collect::<Vec<_>>();
413
414 let parsed_args = CommandLineArgs::try_parse_from(args)?;
415 assert_eq!(
416 parsed_args.script_args,
417 ["some-script", "-x", "1", "--option"]
418 );
419
420 Ok(())
421 }
422
423 #[test]
424 fn parse_script_and_args_with_double_dash_in_script_args() -> Result<()> {
425 let args = vec!["brush", "some-script", "--"];
426 let args = args.into_iter().map(|s| s.to_string()).collect::<Vec<_>>();
427
428 let parsed_args = CommandLineArgs::try_parse_from(args)?;
429 assert_eq!(parsed_args.script_args, ["some-script", "--"]);
430
431 Ok(())
432 }
433
434 #[test]
435 fn parse_unknown_args() {
436 let args = vec!["brush", "--unknown-option"];
437 let args = args.into_iter().map(|s| s.to_string()).collect::<Vec<_>>();
438
439 let result = CommandLineArgs::try_parse_from(args);
440 if let Ok(parsed_args) = &result {
441 assert_matches!(parsed_args.script_args.as_slice(), []);
442 assert!(result.is_err());
443 }
444 }
445}