mod config_files; mod logger; mod test_bins; #[cfg(test)] mod tests; #[cfg(feature = "plugin")] use crate::config_files::NUSHELL_FOLDER; use crate::logger::{configure, logger}; use log::{info, Level}; use miette::Result; #[cfg(feature = "plugin")] use nu_cli::read_plugin_file; use nu_cli::{ evaluate_commands, evaluate_file, evaluate_repl, gather_parent_env_vars, get_init_cwd, report_error, report_error_new, }; use nu_command::{create_default_context, BufferedReader}; use nu_engine::{get_full_help, CallExt}; use nu_parser::{escape_for_script_arg, escape_quote_string, parse}; use nu_path::canonicalize_with; use nu_protocol::{ ast::{Call, Expr, Expression, PipelineElement}, engine::{Command, EngineState, Stack, StateWorkingSet}, Category, Example, IntoPipelineData, PipelineData, RawStream, ShellError, Signature, Spanned, SyntaxShape, Value, }; use nu_utils::stdout_write_all_and_flush; use std::{ io::BufReader, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, }; use std::{path::Path, str::FromStr}; // Inspired by fish's acquire_tty_or_exit #[cfg(unix)] fn take_control(interactive: bool) { use nix::{ errno::Errno, sys::signal::{self, SaFlags, SigAction, SigHandler, SigSet, Signal}, unistd::{self, Pid}, }; let shell_pgid = unistd::getpgrp(); match unistd::tcgetpgrp(nix::libc::STDIN_FILENO) { Ok(owner_pgid) if owner_pgid == shell_pgid => { // Common case, nothing to do return; } Ok(owner_pgid) if owner_pgid == unistd::getpid() => { // This can apparently happen with sudo: https://github.com/fish-shell/fish-shell/issues/7388 let _ = unistd::setpgid(owner_pgid, owner_pgid); return; } _ => (), } // Reset all signal handlers to default for sig in Signal::iterator() { unsafe { if let Ok(old_act) = signal::sigaction( sig, &SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty()), ) { // fish preserves ignored SIGHUP, presumably for nohup support, so let's do the same if sig == Signal::SIGHUP && old_act.handler() == SigHandler::SigIgn { let _ = signal::sigaction(sig, &old_act); } } } } let mut success = false; for _ in 0..4096 { match unistd::tcgetpgrp(nix::libc::STDIN_FILENO) { Ok(owner_pgid) if owner_pgid == shell_pgid => { success = true; break; } Ok(owner_pgid) if owner_pgid == Pid::from_raw(0) => { // Zero basically means something like "not owned" and we can just take it let _ = unistd::tcsetpgrp(nix::libc::STDIN_FILENO, shell_pgid); } Err(Errno::ENOTTY) => { if !interactive { // that's fine return; } eprintln!("ERROR: no TTY for interactive shell"); std::process::exit(1); } _ => { // fish also has other heuristics than "too many attempts" for the orphan check, but they're optional if signal::killpg(Pid::from_raw(-shell_pgid.as_raw()), Signal::SIGTTIN).is_err() { if !interactive { // that's fine return; } eprintln!("ERROR: failed to SIGTTIN ourselves"); std::process::exit(1); } } } } if !success && interactive { eprintln!("ERROR: failed take control of the terminal, we might be orphaned"); std::process::exit(1); } } #[cfg(unix)] fn acquire_terminal(interactive: bool) { use nix::sys::signal::{signal, SigHandler, Signal}; if !atty::is(atty::Stream::Stdin) { return; } take_control(interactive); unsafe { // SIGINT and SIGQUIT have special handling above signal(Signal::SIGTSTP, SigHandler::SigIgn).expect("signal ignore"); signal(Signal::SIGTTIN, SigHandler::SigIgn).expect("signal ignore"); signal(Signal::SIGTTOU, SigHandler::SigIgn).expect("signal ignore"); // signal::signal(Signal::SIGCHLD, SigHandler::SigIgn).expect("signal ignore"); // needed for std::command's waitpid usage } } #[cfg(not(unix))] fn acquire_terminal(_: bool) {} fn main() -> Result<()> { // miette::set_panic_hook(); let miette_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |x| { crossterm::terminal::disable_raw_mode().expect("unable to disable raw mode"); miette_hook(x); })); // Get initial current working directory. let init_cwd = get_init_cwd(); let mut engine_state = create_default_context(); // Custom additions let delta = { let mut working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state); working_set.add_decl(Box::new(nu_cli::NuHighlight)); working_set.add_decl(Box::new(nu_cli::Print)); working_set.render() }; if let Err(err) = engine_state.merge_delta(delta) { report_error_new(&engine_state, &err); } // TODO: make this conditional in the future // Ctrl-c protection section let ctrlc = Arc::new(AtomicBool::new(false)); let handler_ctrlc = ctrlc.clone(); let engine_state_ctrlc = ctrlc.clone(); ctrlc::set_handler(move || { handler_ctrlc.store(true, Ordering::SeqCst); }) .expect("Error setting Ctrl-C handler"); engine_state.ctrlc = Some(engine_state_ctrlc); // End ctrl-c protection section // SIGQUIT protection section (only works for POSIX system) #[cfg(not(windows))] { use signal_hook::consts::SIGQUIT; let sig_quit = Arc::new(AtomicBool::new(false)); signal_hook::flag::register(SIGQUIT, sig_quit.clone()).expect("Error setting SIGQUIT flag"); engine_state.set_sig_quit(sig_quit); } // End SIGQUIT protection section let mut args_to_nushell = vec![]; let mut script_name = String::new(); let mut args_to_script = vec![]; // Would be nice if we had a way to parse this. The first flags we see will be going to nushell // then it'll be the script name // then the args to the script let mut args = std::env::args(); let argv0 = args.next(); while let Some(arg) = args.next() { if !script_name.is_empty() { args_to_script.push(escape_for_script_arg(&arg)); } else if arg.starts_with('-') { // Cool, it's a flag let flag_value = match arg.as_ref() { "--commands" | "-c" | "--table-mode" | "-m" | "-e" | "--execute" => { args.next().map(|a| escape_quote_string(&a)) } "--config" | "--env-config" => args.next().map(|a| escape_quote_string(&a)), #[cfg(feature = "plugin")] "--plugin-config" => args.next().map(|a| escape_quote_string(&a)), "--log-level" | "--log-target" | "--testbin" | "--threads" | "-t" => args.next(), _ => None, }; args_to_nushell.push(arg); if let Some(flag_value) = flag_value { args_to_nushell.push(flag_value); } } else { // Our script file script_name = arg; } } args_to_nushell.insert(0, "nu".into()); if let Some(argv0) = argv0 { if argv0.starts_with('-') { args_to_nushell.push("--login".into()); } } let nushell_commandline_args = args_to_nushell.join(" "); let parsed_nu_cli_args = parse_commandline_args(&nushell_commandline_args, &mut engine_state); if let Ok(ref args) = parsed_nu_cli_args { set_config_path( &mut engine_state, &init_cwd, "config.nu", "config-path", &args.config_file, ); set_config_path( &mut engine_state, &init_cwd, "env.nu", "env-path", &args.env_file, ); } match parsed_nu_cli_args { Ok(binary_args) => { // keep this condition in sync with the branches below acquire_terminal( binary_args.commands.is_none() && (script_name.is_empty() || binary_args.interactive_shell.is_some()), ); if let Some(t) = binary_args.threads { // 0 means to let rayon decide how many threads to use let threads = t.as_i64().unwrap_or(0); rayon::ThreadPoolBuilder::new() .num_threads(threads as usize) .build_global() .expect("error setting number of threads"); } if binary_args.log_level.is_some() { let mut level = binary_args .log_level .map(|level| level.item) .unwrap_or_else(|| "info".to_string()); if Level::from_str(level.as_str()).is_err() { eprintln!("ERROR: log library did not recognize log level '{level}', using default 'info'"); level = "info".to_string(); } let target = binary_args .log_target .map(|target| target.item) .unwrap_or_else(|| "stderr".to_string()); logger(|builder| configure(level.as_str(), target.as_str(), builder))?; info!("start logging {}:{}:{}", file!(), line!(), column!()); } if let Some(testbin) = &binary_args.testbin { // Call out to the correct testbin match testbin.item.as_str() { "echo_env" => test_bins::echo_env(true), "echo_env_stderr" => test_bins::echo_env(false), "cococo" => test_bins::cococo(), "meow" => test_bins::meow(), "meowb" => test_bins::meowb(), "relay" => test_bins::relay(), "iecho" => test_bins::iecho(), "fail" => test_bins::fail(), "nonu" => test_bins::nonu(), "chop" => test_bins::chop(), "repeater" => test_bins::repeater(), "nu_repl" => test_bins::nu_repl(), _ => std::process::exit(1), } std::process::exit(0) } let input = if let Some(redirect_stdin) = &binary_args.redirect_stdin { let stdin = std::io::stdin(); let buf_reader = BufReader::new(stdin); PipelineData::ExternalStream { stdout: Some(RawStream::new( Box::new(BufferedReader::new(buf_reader)), Some(ctrlc), redirect_stdin.span, )), stderr: None, exit_code: None, span: redirect_stdin.span, metadata: None, trim_end_newline: false, } } else { PipelineData::empty() }; info!("redirect_stdin {}:{}:{}", file!(), line!(), column!()); // First, set up env vars as strings only gather_parent_env_vars(&mut engine_state, &init_cwd); let mut stack = nu_protocol::engine::Stack::new(); if let Some(commands) = &binary_args.commands { #[cfg(feature = "plugin")] read_plugin_file( &mut engine_state, &mut stack, binary_args.plugin_file, NUSHELL_FOLDER, ); // only want to load config and env if relative argument is provided. if binary_args.env_file.is_some() { config_files::read_config_file( &mut engine_state, &mut stack, binary_args.env_file, true, ); } else { config_files::read_default_env_file(&mut engine_state, &mut stack) } if binary_args.config_file.is_some() { config_files::read_config_file( &mut engine_state, &mut stack, binary_args.config_file, false, ); } let ret_val = evaluate_commands( commands, &mut engine_state, &mut stack, input, binary_args.table_mode, ); info!("-c command execution {}:{}:{}", file!(), line!(), column!()); match ret_val { Ok(Some(exit_code)) => std::process::exit(exit_code as i32), Ok(None) => Ok(()), Err(e) => Err(e), } } else if !script_name.is_empty() && binary_args.interactive_shell.is_none() { #[cfg(feature = "plugin")] read_plugin_file( &mut engine_state, &mut stack, binary_args.plugin_file, NUSHELL_FOLDER, ); // only want to load config and env if relative argument is provided. if binary_args.env_file.is_some() { config_files::read_config_file( &mut engine_state, &mut stack, binary_args.env_file, true, ); } else { config_files::read_default_env_file(&mut engine_state, &mut stack) } if binary_args.config_file.is_some() { config_files::read_config_file( &mut engine_state, &mut stack, binary_args.config_file, false, ); } let ret_val = evaluate_file( script_name, &args_to_script, &mut engine_state, &mut stack, input, ); let last_exit_code = stack.get_env_var(&engine_state, "LAST_EXIT_CODE"); if let Some(last_exit_code) = last_exit_code { let value = last_exit_code.as_integer(); if let Ok(value) = value { if value != 0 { std::process::exit(value as i32); } } } info!("eval_file execution {}:{}:{}", file!(), line!(), column!()); ret_val } else { setup_config( &mut engine_state, &mut stack, #[cfg(feature = "plugin")] binary_args.plugin_file, binary_args.config_file, binary_args.env_file, binary_args.login_shell.is_some(), ); let ret_val = evaluate_repl( &mut engine_state, &mut stack, config_files::NUSHELL_FOLDER, binary_args.execute, ); info!("repl eval {}:{}:{}", file!(), line!(), column!()); ret_val } } Err(_) => std::process::exit(1), } } fn setup_config( engine_state: &mut EngineState, stack: &mut Stack, #[cfg(feature = "plugin")] plugin_file: Option>, config_file: Option>, env_file: Option>, is_login_shell: bool, ) { #[cfg(feature = "plugin")] read_plugin_file(engine_state, stack, plugin_file, NUSHELL_FOLDER); info!("read_config_file {}:{}:{}", file!(), line!(), column!()); config_files::read_config_file(engine_state, stack, env_file, true); config_files::read_config_file(engine_state, stack, config_file, false); if is_login_shell { config_files::read_loginshell_file(engine_state, stack); } // Give a warning if we see `$config` for a few releases { let working_set = StateWorkingSet::new(engine_state); if working_set.find_variable(b"$config").is_some() { println!("warning: use `let-env config = ...` instead of `let config = ...`"); } } } fn parse_commandline_args( commandline_args: &str, engine_state: &mut EngineState, ) -> Result { let (block, delta) = { let mut working_set = StateWorkingSet::new(engine_state); working_set.add_decl(Box::new(Nu)); let (output, err) = parse( &mut working_set, None, commandline_args.as_bytes(), false, &[], ); if let Some(err) = err { report_error(&working_set, &err); std::process::exit(1); } working_set.hide_decl(b"nu"); (output, working_set.render()) }; engine_state.merge_delta(delta)?; let mut stack = Stack::new(); // We should have a successful parse now if let Some(pipeline) = block.pipelines.get(0) { if let Some(PipelineElement::Expression( _, Expression { expr: Expr::Call(call), .. }, )) = pipeline.elements.get(0) { let redirect_stdin = call.get_named_arg("stdin"); let login_shell = call.get_named_arg("login"); let interactive_shell = call.get_named_arg("interactive"); let commands: Option = call.get_flag_expr("commands"); let testbin: Option = call.get_flag_expr("testbin"); #[cfg(feature = "plugin")] let plugin_file: Option = call.get_flag_expr("plugin-config"); let config_file: Option = call.get_flag_expr("config"); let env_file: Option = call.get_flag_expr("env-config"); let log_level: Option = call.get_flag_expr("log-level"); let log_target: Option = call.get_flag_expr("log-target"); let execute: Option = call.get_flag_expr("execute"); let threads: Option = call.get_flag(engine_state, &mut stack, "threads")?; let table_mode: Option = call.get_flag(engine_state, &mut stack, "table-mode")?; fn extract_contents( expression: Option, ) -> Result>, ShellError> { if let Some(expr) = expression { let str = expr.as_string(); if let Some(str) = str { Ok(Some(Spanned { item: str, span: expr.span, })) } else { Err(ShellError::TypeMismatch("string".into(), expr.span)) } } else { Ok(None) } } let commands = extract_contents(commands)?; let testbin = extract_contents(testbin)?; #[cfg(feature = "plugin")] let plugin_file = extract_contents(plugin_file)?; let config_file = extract_contents(config_file)?; let env_file = extract_contents(env_file)?; let log_level = extract_contents(log_level)?; let log_target = extract_contents(log_target)?; let execute = extract_contents(execute)?; let help = call.has_flag("help"); if help { let full_help = get_full_help( &Nu.signature(), &Nu.examples(), engine_state, &mut stack, true, ); let _ = std::panic::catch_unwind(move || stdout_write_all_and_flush(full_help)); std::process::exit(1); } if call.has_flag("version") { let version = env!("CARGO_PKG_VERSION").to_string(); let _ = std::panic::catch_unwind(move || { stdout_write_all_and_flush(format!("{}\n", version)) }); std::process::exit(0); } return Ok(NushellCliArgs { redirect_stdin, login_shell, interactive_shell, commands, testbin, #[cfg(feature = "plugin")] plugin_file, config_file, env_file, log_level, log_target, execute, threads, table_mode, }); } } // Just give the help and exit if the above fails let full_help = get_full_help( &Nu.signature(), &Nu.examples(), engine_state, &mut stack, true, ); print!("{}", full_help); std::process::exit(1); } struct NushellCliArgs { redirect_stdin: Option>, login_shell: Option>, interactive_shell: Option>, commands: Option>, testbin: Option>, #[cfg(feature = "plugin")] plugin_file: Option>, config_file: Option>, env_file: Option>, log_level: Option>, log_target: Option>, execute: Option>, threads: Option, table_mode: Option, } #[derive(Clone)] struct Nu; impl Command for Nu { fn name(&self) -> &str { "nu" } fn signature(&self) -> Signature { let signature = Signature::build("nu") .usage("The nushell language and shell.") .switch( "stdin", "redirect standard input to a command (with `-c`) or a script file", None, ) .switch("login", "start as a login shell", Some('l')) .switch("interactive", "start as an interactive shell", Some('i')) .switch("version", "print the version", Some('v')) .named( "testbin", SyntaxShape::String, "run internal test binary", None, ) .named( "commands", SyntaxShape::String, "run the given commands and then exit", Some('c'), ) .named( "config", SyntaxShape::String, "start with an alternate config file", None, ) .named( "env-config", SyntaxShape::String, "start with an alternate environment config file", None, ) .named( "log-level", SyntaxShape::String, "log level for diagnostic logs (error, warn, info, debug, trace). Off by default", None, ) .named( "log-target", SyntaxShape::String, "set the target for the log to output. stdout, stderr(default), mixed or file", None, ) .named( "execute", SyntaxShape::String, "run the given commands and then enter an interactive shell", Some('e'), ) .named( "threads", SyntaxShape::Int, "threads to use for parallel commands", Some('t'), ) .named( "table-mode", SyntaxShape::String, "the table mode to use. rounded is default.", Some('m'), ) .optional( "script file", SyntaxShape::Filepath, "name of the optional script file to run", ) .rest( "script args", SyntaxShape::String, "parameters to the script file", ) .category(Category::System); #[cfg(feature = "plugin")] { signature.named( "plugin-config", SyntaxShape::String, "start with an alternate plugin signature file", None, ) } #[cfg(not(feature = "plugin"))] { signature } } fn usage(&self) -> &str { "The nushell language and shell." } fn run( &self, engine_state: &EngineState, stack: &mut Stack, call: &Call, _input: PipelineData, ) -> Result { Ok(Value::String { val: get_full_help(&Nu.signature(), &Nu.examples(), engine_state, stack, true), span: call.head, } .into_pipeline_data()) } fn examples(&self) -> Vec { vec![ Example { description: "Run a script", example: "nu myfile.nu", result: None, }, Example { description: "Run nushell interactively (as a shell or REPL)", example: "nu", result: None, }, ] } } fn set_config_path( engine_state: &mut EngineState, cwd: &Path, default_config_name: &str, key: &str, config_file: &Option>, ) { let config_path = match config_file { Some(s) => canonicalize_with(&s.item, cwd).ok(), None => nu_path::config_dir().map(|mut p| { p.push(config_files::NUSHELL_FOLDER); p.push(default_config_name); p }), }; if let Some(path) = config_path { engine_state.set_config_path(key, path); } }