mod command;
mod command_context;
mod config_files;
mod experimental_options;
mod ide;
mod logger;
mod run;
mod signals;
#[cfg(unix)]
mod terminal;
mod test_bins;
use crate::{
command::parse_cli_args_from_env,
config_files::set_config_path,
logger::{configure, logger},
};
use log::{Level, trace};
use miette::Result;
use nu_cli::gather_parent_env_vars;
use nu_engine::{convert_env_values, exit::cleanup_exit};
use nu_lsp::LanguageServer;
use nu_path::absolute_with;
use nu_protocol::{
ByteStream, Config, IntoValue, PipelineData, ShellError, Span, Spanned, Type, Value,
engine::{EngineState, Stack},
record, report_shell_error,
};
use nu_std::load_standard_library;
use nu_utils::perf;
use run::{run_commands, run_file, run_repl};
use signals::ctrlc_protection;
use std::{
borrow::Cow,
path::{PathBuf, absolute},
str::FromStr,
sync::Arc,
};
fn current_exe_directory() -> PathBuf {
let mut path = std::env::current_exe().expect("current_exe() should succeed");
path.pop();
path
}
fn current_dir_from_environment() -> PathBuf {
let cwd = std::env::current_dir();
let pwd = std::env::var("PWD");
match (cwd, pwd) {
(Ok(cwd), Ok(pwd)) => {
if matches!(same_file::is_same_file(&cwd, &pwd), Ok(true)) {
pwd.into()
} else {
cwd
}
}
(Ok(cwd), _) => cwd,
(_, Ok(pwd)) => pwd.into(),
_ => {
if let Some(home) = nu_path::home_dir() {
home.into_std_path_buf()
} else {
current_exe_directory()
}
}
}
}
fn main() -> Result<()> {
let entire_start_time = nu_utils::time::Instant::now();
let mut start_time = nu_utils::time::Instant::now();
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);
}));
let engine_state = EngineState::new();
let parsed = parse_cli_args_from_env().unwrap_or_else(|err| {
report_shell_error(None, &engine_state, &err.into());
std::process::exit(1)
});
let parsed_nu_cli_args = parsed.nu;
let script_name = parsed.script_name;
let args_to_script = parsed.args_to_script;
experimental_options::load(&engine_state, &parsed_nu_cli_args, !script_name.is_empty());
let mut engine_state = command_context::add_command_context(engine_state);
let cargo_features = env!("NU_FEATURES").split(",").map(Cow::Borrowed).collect();
nu_cmd_lang::VERSION_NU_FEATURES
.set(cargo_features)
.expect("unable to set VERSION_NU_FEATURES");
let init_cwd = current_dir_from_environment();
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_shell_error(None, &engine_state, &err);
}
#[cfg(feature = "mcp")]
let handle_ctrlc = !parsed_nu_cli_args.mcp;
#[cfg(not(feature = "mcp"))]
let handle_ctrlc = true;
if handle_ctrlc {
ctrlc_protection(&mut engine_state);
}
#[cfg(all(feature = "rustls-tls", feature = "network"))]
nu_command::tls::CRYPTO_PROVIDER.default();
let nushell_config_path: PathBuf = nu_path::nu_config_dir().map(Into::into).unwrap_or_default();
if let Ok(xdg_config_home) = std::env::var("XDG_CONFIG_HOME")
&& !xdg_config_home.is_empty()
{
if nushell_config_path
!= absolute_with(&xdg_config_home, &init_cwd)
.unwrap_or(PathBuf::from(&xdg_config_home))
.join("nushell")
{
report_shell_error(
None,
&engine_state,
&ShellError::InvalidXdgConfig {
xdg: xdg_config_home,
default: nushell_config_path.display().to_string(),
},
);
} else if let Some(old_config) = dirs::config_dir()
.and_then(|p| absolute(p).ok())
.map(|p| p.join("nushell"))
{
let xdg_config_empty = nushell_config_path
.read_dir()
.map_or(true, |mut dir| dir.next().is_none());
let old_config_empty = old_config
.read_dir()
.map_or(true, |mut dir| dir.next().is_none());
if !old_config_empty && xdg_config_empty {
eprintln!(
"WARNING: XDG_CONFIG_HOME has been set but {} is empty.\n",
nushell_config_path.display(),
);
eprintln!(
"Nushell will not move your configuration files from {}",
old_config.display()
);
}
}
}
let default_nushell_completions_path = if let Some(mut path) = nu_path::data_dir() {
path.push("nushell");
path.push("completions");
path.into()
} else {
std::path::PathBuf::new()
};
let mut default_nu_lib_dirs_path = nushell_config_path.clone();
default_nu_lib_dirs_path.push("scripts");
let include_paths = &parsed_nu_cli_args.include_path;
let mut default_nu_plugin_dirs_path = nushell_config_path;
default_nu_plugin_dirs_path.push("plugins");
engine_state.add_env_var("NU_PLUGIN_DIRS".to_string(), Value::test_list(vec![]));
let mut working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
let var_id = working_set.add_variable(
b"$NU_PLUGIN_DIRS".into(),
Span::unknown(),
Type::List(Box::new(Type::String)),
false,
);
working_set.set_variable_const_val(
var_id,
Value::test_list(vec![
Value::test_string(default_nu_plugin_dirs_path.to_string_lossy()),
Value::test_string(current_exe_directory().to_string_lossy()),
]),
);
engine_state.merge_delta(working_set.render())?;
#[cfg(feature = "sqlite")]
let db = nu_command::open_connection_in_memory_custom()?;
#[cfg(feature = "sqlite")]
db.last_insert_rowid();
engine_state.is_interactive = parsed_nu_cli_args.interactive_shell.is_some()
|| (parsed_nu_cli_args.testbin.is_none()
&& parsed_nu_cli_args.commands.is_none()
&& script_name.is_empty()
&& !parsed_nu_cli_args.lsp);
engine_state.is_login = parsed_nu_cli_args.login_shell.is_some();
engine_state.history_enabled = parsed_nu_cli_args.no_history.is_none();
engine_state.is_lsp = parsed_nu_cli_args.lsp;
let use_color = engine_state
.get_config()
.use_ansi_coloring
.get(&engine_state);
let level_opt = parsed_nu_cli_args
.log_level
.as_ref()
.map(|level| level.item.clone());
let target_opt = parsed_nu_cli_args
.log_target
.as_ref()
.map(|target| target.item.clone());
let file_opt = parsed_nu_cli_args.log_file.as_ref().map(|f| f.item.clone());
if file_opt.is_some() && target_opt.as_deref() != Some("file") {
eprintln!("ERROR: --log-file requires --log-target file");
std::process::exit(1);
}
if target_opt.as_deref() == Some("file") && file_opt.is_none() {
eprintln!("ERROR: --log-target file requires --log-file");
std::process::exit(1);
}
if target_opt.as_deref() == Some("file") && file_opt.is_some() && level_opt.is_none() {
eprintln!("ERROR: --log-target file with --log-file requires --log-level");
std::process::exit(1);
}
if let Some(level) = level_opt {
let level = if Level::from_str(&level).is_ok() {
level
} else {
eprintln!(
"ERROR: log library did not recognize log level '{level}', using default 'info'"
);
"info".to_string()
};
let target = target_opt.unwrap_or_else(|| "stderr".to_string());
let make_filters = |filters: &Option<Vec<Spanned<String>>>| {
filters.as_ref().map(|filters| {
filters
.iter()
.map(|filter| filter.item.clone())
.collect::<Vec<String>>()
})
};
let filters = logger::Filters {
include: make_filters(&parsed_nu_cli_args.log_include),
exclude: make_filters(&parsed_nu_cli_args.log_exclude),
};
logger(|builder| configure(&level, &target, file_opt.as_deref(), filters, builder))?;
perf!("start logging", start_time, use_color);
}
start_time = nu_utils::time::Instant::now();
set_config_path(
&mut engine_state,
init_cwd.as_ref(),
"config.nu",
"config-path",
parsed_nu_cli_args.config_file.as_ref(),
);
set_config_path(
&mut engine_state,
init_cwd.as_ref(),
"env.nu",
"env-path",
parsed_nu_cli_args.env_file.as_ref(),
);
perf!("set_config_path", start_time, use_color);
#[cfg(unix)]
{
start_time = nu_utils::time::Instant::now();
terminal::acquire(engine_state.is_interactive);
perf!("acquire_terminal", start_time, use_color);
}
start_time = nu_utils::time::Instant::now();
engine_state.add_env_var(
"config".into(),
Config::default().into_value(Span::unknown()),
);
perf!("$env.config setup", start_time, use_color);
engine_state.add_env_var(
"ENV_CONVERSIONS".to_string(),
Value::test_record(record! {}),
);
start_time = nu_utils::time::Instant::now();
gather_parent_env_vars(&mut engine_state, init_cwd.as_ref());
perf!("gather env vars", start_time, use_color);
let mut stack = Stack::new();
start_time = nu_utils::time::Instant::now();
let config = engine_state.get_config();
let use_color = config.use_ansi_coloring.get(&engine_state);
if let Err(e) = convert_env_values(&mut engine_state, &mut stack) {
report_shell_error(None, &engine_state, &e);
}
perf!("Convert path to list", start_time, use_color);
start_time = nu_utils::time::Instant::now();
{
fn parse_path_list(value: &str, separators: &[char]) -> Vec<String> {
value
.split(|c| separators.contains(&c))
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
let mut user_lib_dirs: Vec<String> =
if let Some(val) = engine_state.get_env_var("NU_LIB_DIRS") {
match val {
Value::List { vals, .. } => vals
.iter()
.filter_map(|v| v.as_str().ok())
.map(|s| s.to_string())
.collect(),
Value::String { val, .. } => {
parse_path_list(val, if cfg!(windows) { &[';'] } else { &[':'] })
}
_ => vec![],
}
} else {
vec![]
};
if let Some(spanned) = include_paths {
let paths = parse_path_list(&spanned.item, &[':', ';', '\x1e']);
user_lib_dirs.extend(paths);
}
let default_paths = vec![
default_nu_lib_dirs_path.to_string_lossy().to_string(),
default_nushell_completions_path
.to_string_lossy()
.to_string(),
];
let all_lib_dirs: Vec<String> = user_lib_dirs.into_iter().chain(default_paths).collect();
let all_lib_dir_values: Vec<Value> = all_lib_dirs
.iter()
.map(|s| Value::string(s.clone(), Span::unknown()))
.collect();
engine_state.add_env_var(
"NU_LIB_DIRS".to_string(),
Value::list(all_lib_dir_values.clone(), Span::unknown()),
);
let mut working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
let var_id = working_set.add_variable(
b"$NU_LIB_DIRS".into(),
Span::unknown(),
Type::List(Box::new(Type::String)),
false, );
working_set
.set_variable_const_val(var_id, Value::list(all_lib_dir_values, Span::unknown()));
engine_state.merge_delta(working_set.render())?;
}
perf!("$env.NU_LIB_DIRS/$NU_LIB_DIRS setup", start_time, use_color);
engine_state.add_env_var(
"NU_VERSION".to_string(),
Value::string(env!("CARGO_PKG_VERSION"), Span::unknown()),
);
if parsed_nu_cli_args.no_std_lib.is_none() {
load_standard_library(&mut engine_state)?;
}
if let Some(ide_goto_def) = parsed_nu_cli_args.ide_goto_def {
ide::goto_def(&mut engine_state, &script_name, &ide_goto_def);
return Ok(());
} else if let Some(ide_hover) = parsed_nu_cli_args.ide_hover {
ide::hover(&mut engine_state, &script_name, &ide_hover);
return Ok(());
} else if let Some(ide_complete) = parsed_nu_cli_args.ide_complete {
let cwd = std::env::current_dir().expect("Could not get current working directory.");
engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
ide::complete(Arc::new(engine_state), &script_name, &ide_complete);
return Ok(());
} else if let Some(max_errors) = parsed_nu_cli_args.ide_check {
ide::check(&mut engine_state, &script_name, &max_errors);
return Ok(());
} else if parsed_nu_cli_args.ide_ast.is_some() {
ide::ast(&mut engine_state, &script_name);
return Ok(());
}
start_time = nu_utils::time::Instant::now();
if let Some(testbin) = &parsed_nu_cli_args.testbin {
let dispatcher = test_bins::new_testbin_dispatcher();
let test_bin = testbin.item.as_str();
match dispatcher.get(test_bin) {
Some(test_bin) => test_bin.run(),
None => {
if ["-h", "--help"].contains(&test_bin) {
test_bins::show_help(&dispatcher);
} else {
eprintln!("ERROR: Unknown testbin '{test_bin}'");
std::process::exit(1);
}
}
}
std::process::exit(0)
} else {
std::env::set_current_dir(current_exe_directory())
.expect("set_current_dir() should succeed");
}
perf!("run test_bins", start_time, use_color);
start_time = nu_utils::time::Instant::now();
let input = if let Some(redirect_stdin) = &parsed_nu_cli_args.redirect_stdin {
trace!("redirecting stdin");
PipelineData::byte_stream(ByteStream::stdin(redirect_stdin.span)?, None)
} else {
trace!("not redirecting stdin");
PipelineData::empty()
};
perf!("redirect stdin", start_time, use_color);
start_time = nu_utils::time::Instant::now();
engine_state.generate_nu_constant();
perf!("create_nu_constant", start_time, use_color);
#[cfg(feature = "plugin")]
if let Some(plugins) = &parsed_nu_cli_args.plugins {
use nu_plugin_engine::{GetPlugin, PluginDeclaration};
use nu_protocol::{ErrSpan, PluginIdentity, RegisteredPlugin, engine::StateWorkingSet};
start_time = nu_utils::time::Instant::now();
let mut working_set = StateWorkingSet::new(&engine_state);
for plugin_filename in plugins {
let filename = absolute_with(&plugin_filename.item, &init_cwd)
.map_err(|err| {
nu_protocol::shell_error::io::IoError::new_internal_with_path(
err,
"Could not resolve plugin path",
PathBuf::from(&plugin_filename.item),
)
})
.map_err(ShellError::from)?;
let identity = PluginIdentity::new(&filename, None)
.err_span(plugin_filename.span)
.map_err(ShellError::from)?;
let plugin = nu_plugin_engine::add_plugin_to_working_set(&mut working_set, &identity)?;
let interface = plugin.clone().get_plugin(None)?;
plugin.set_metadata(Some(interface.get_metadata()?));
for signature in interface.get_signature()? {
let decl = PluginDeclaration::new(plugin.clone(), signature);
working_set.add_decl(Box::new(decl));
}
}
engine_state.merge_delta(working_set.render())?;
perf!("load plugins specified in --plugins", start_time, use_color)
}
start_time = nu_utils::time::Instant::now();
#[cfg(feature = "mcp")]
if parsed_nu_cli_args.mcp {
perf!("mcp starting", start_time, use_color);
engine_state.is_mcp = true;
let mcp_transport_kind = parsed_nu_cli_args
.mcp_transport
.as_ref()
.map(|value| value.item.as_str());
let is_stdio_transport = !matches!(mcp_transport_kind, Some("http"));
if parsed_nu_cli_args.no_config_file.is_none() {
let mut stack = if is_stdio_transport {
nu_protocol::engine::Stack::new().collect_value()
} else {
nu_protocol::engine::Stack::new()
};
config_files::setup_config(
&mut engine_state,
&mut stack,
#[cfg(feature = "plugin")]
parsed_nu_cli_args.plugin_file,
parsed_nu_cli_args.config_file,
parsed_nu_cli_args.env_file,
parsed_nu_cli_args.login_shell.is_some(),
);
}
let transport = match mcp_transport_kind {
Some("http") => {
let port = parsed_nu_cli_args.mcp_port.unwrap_or(8080);
nu_mcp::McpTransport::Http { port }
}
_ => nu_mcp::McpTransport::Stdio,
};
nu_mcp::initialize_mcp_server(engine_state, transport)?;
return Ok(());
}
if parsed_nu_cli_args.lsp {
perf!("lsp starting", start_time, use_color);
if parsed_nu_cli_args.no_config_file.is_none() {
let mut stack = nu_protocol::engine::Stack::new();
config_files::setup_config(
&mut engine_state,
&mut stack,
#[cfg(feature = "plugin")]
parsed_nu_cli_args.plugin_file,
parsed_nu_cli_args.config_file,
parsed_nu_cli_args.env_file,
false,
);
}
LanguageServer::initialize_stdio_connection(engine_state)?.serve_requests()?
} else if let Some(commands) = parsed_nu_cli_args.commands.clone() {
run_commands(
&mut engine_state,
stack,
parsed_nu_cli_args,
use_color,
&commands,
input,
entire_start_time,
);
cleanup_exit(0, &engine_state, 0);
} else if !script_name.is_empty() {
run_file(
&mut engine_state,
stack,
parsed_nu_cli_args,
use_color,
script_name,
args_to_script,
input,
);
cleanup_exit(0, &engine_state, 0);
} else {
engine_state.add_env_var("PROMPT_INDICATOR".to_string(), Value::test_string("> "));
engine_state.add_env_var(
"PROMPT_INDICATOR_VI_NORMAL".to_string(),
Value::test_string("> "),
);
engine_state.add_env_var(
"PROMPT_INDICATOR_VI_INSERT".to_string(),
Value::test_string(": "),
);
engine_state.add_env_var(
"PROMPT_MULTILINE_INDICATOR".to_string(),
Value::test_string("::: "),
);
engine_state.add_env_var(
"TRANSIENT_PROMPT_MULTILINE_INDICATOR".to_string(),
Value::test_string(""),
);
engine_state.add_env_var(
"TRANSIENT_PROMPT_COMMAND_RIGHT".to_string(),
Value::test_string(""),
);
let mut shlvl = engine_state
.get_env_var("SHLVL")
.map(|x| x.as_str().unwrap_or("0").parse::<i64>().unwrap_or(0))
.unwrap_or(0);
shlvl += 1;
engine_state.add_env_var("SHLVL".to_string(), Value::int(shlvl, Span::unknown()));
run_repl(
&mut engine_state,
stack,
parsed_nu_cli_args,
entire_start_time,
)?;
cleanup_exit(0, &engine_state, 0);
}
Ok(())
}