use log::info;
#[cfg(feature = "plugin")]
use nu_cli::read_plugin_file;
use nu_cli::{
StartupFileKind, StartupLoadContext, eval_config_contents_with_kind, eval_source,
report_startup_file_not_found,
};
use nu_config::ConfigFileKind;
use nu_protocol::{
Config, PipelineData, Spanned,
engine::{EngineState, Stack},
report_shell_error,
};
use std::{
fs,
fs::File,
io::{Result, Write},
panic::{AssertUnwindSafe, catch_unwind},
path::Path,
sync::Arc,
};
const LOGINSHELL_FILE: &str = "login.nu";
fn is_dangling_symlink(path: &Path) -> bool {
path.is_symlink() && !path.exists()
}
fn warn_dangling_symlink(kind: &str, path: &Path) {
let target = fs::read_link(path)
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "<unknown>".to_string());
eprintln!(
"Warning: {kind} is a broken symlink ({} -> {target}); using built-in defaults.",
path.display()
);
}
pub(crate) fn read_config_file(
engine_state: &mut EngineState,
stack: &mut Stack,
config_kind: ConfigFileKind,
create_scaffold: bool,
strict_mode: bool,
cli_override: Option<&Spanned<String>>,
) {
info!("read_config_file() {config_kind:?}");
eval_default_config(engine_state, stack, config_kind);
info!("read_config_file() loading default {config_kind:?}");
let resolved = match config_kind {
ConfigFileKind::Config => &engine_state.config_dirs.config_file,
ConfigFileKind::Env => &engine_state.config_dirs.env_file,
};
let is_override = resolved.is_override();
let config_path = resolved.to_path_buf();
let startup_kind = match config_kind {
ConfigFileKind::Config => StartupFileKind::Config,
ConfigFileKind::Env => StartupFileKind::Env,
};
if is_override {
if config_path.exists() {
eval_config_contents_with_kind(
config_path,
engine_state,
stack,
strict_mode,
startup_kind,
);
} else {
let (display_path, span) = match cli_override {
Some(s) => (s.item.clone(), Some(s.span)),
None => (config_path.display().to_string(), None),
};
let startup = StartupLoadContext::new(startup_kind, config_path.clone());
report_startup_file_not_found(engine_state, &display_path, span, Some(&startup));
if strict_mode {
std::process::exit(1);
}
}
return;
}
let mut config_dir = engine_state.config_dirs.config_home.clone();
if !config_dir.exists() {
if is_dangling_symlink(&config_dir) {
eprintln!(
"Warning: config directory is a broken symlink ({}); using built-in defaults.",
config_dir.display()
);
return;
}
if let Err(err) = std::fs::create_dir_all(&config_dir) {
eprintln!("Failed to create config directory: {err}");
return;
}
}
let config_path = if config_path.as_os_str().is_empty() {
config_dir.push(config_kind.path());
config_dir
} else {
config_path
};
if is_dangling_symlink(&config_path) {
warn_dangling_symlink(config_kind.path(), &config_path);
return;
}
if !config_path.exists() {
let scaffold_config_file = config_kind.scaffold();
if !create_scaffold {
return;
}
let Ok(mut output) = File::create(&config_path) else {
return eprintln!("Unable to create {scaffold_config_file}");
};
if write!(output, "{scaffold_config_file}").is_err() {
return eprintln!(
"Unable to write to {}, sourcing default file instead",
config_path.to_string_lossy(),
);
}
let config_name = config_kind.name();
if engine_state.is_mcp {
eprintln!(
"{} file created at: {}",
config_name,
config_path.to_string_lossy()
);
} else {
println!(
"{} file created at: {}",
config_name,
config_path.to_string_lossy()
);
}
}
eval_config_contents_with_kind(config_path, engine_state, stack, strict_mode, startup_kind);
}
pub(crate) fn read_loginshell_file(
engine_state: &mut EngineState,
stack: &mut Stack,
strict_mode: bool,
) {
info!(
"read_loginshell_file() {}:{}:{}",
file!(),
line!(),
column!()
);
let mut config_path = engine_state.config_dirs.config_home.clone();
config_path.push(LOGINSHELL_FILE);
info!("loginshell_file: {}", config_path.display());
if is_dangling_symlink(&config_path) {
warn_dangling_symlink(LOGINSHELL_FILE, &config_path);
return;
}
if config_path.exists() {
eval_config_contents_with_kind(
config_path,
engine_state,
stack,
strict_mode,
StartupFileKind::Login,
);
}
}
pub(crate) fn read_default_env_file(engine_state: &mut EngineState, stack: &mut Stack) {
let config_file = ConfigFileKind::Env.default();
eval_source(
engine_state,
stack,
config_file.as_bytes(),
"default_env.nu",
PipelineData::empty(),
false,
);
info!(
"read_default_env_file() env_file_contents: {config_file} {}:{}:{}",
file!(),
line!(),
column!()
);
if let Err(e) = engine_state.merge_env(stack) {
report_shell_error(None, engine_state, &e);
}
}
fn read_and_sort_directory(path: &Path) -> Result<Vec<String>> {
let mut entries = Vec::new();
for entry in fs::read_dir(path)? {
let entry = entry?;
let file_name = entry.file_name();
let file_name_str = file_name.into_string().unwrap_or_default();
entries.push(file_name_str);
}
entries.sort();
Ok(entries)
}
pub(crate) fn read_vendor_autoload_files(engine_state: &mut EngineState, stack: &mut Stack) {
info!(
"read_vendor_autoload_files() {}:{}:{}",
file!(),
line!(),
column!()
);
let vendor_dirs = engine_state.config_dirs.vendor_autoload_dirs.clone();
let user_dirs = engine_state.config_dirs.user_autoload_dirs.clone();
vendor_dirs
.iter()
.chain(user_dirs.iter())
.for_each(|autoload_dir| {
info!("read_vendor_autoload_files: {}", autoload_dir.display());
if autoload_dir.exists() {
let entries = read_and_sort_directory(autoload_dir);
if let Ok(entries) = entries {
for entry in entries {
if !entry.ends_with(".nu") {
continue;
}
let path = autoload_dir.join(entry);
info!("AutoLoading: {path:?}");
eval_config_contents_with_kind(
path,
engine_state,
stack,
false,
StartupFileKind::Autoload,
);
}
}
}
});
}
fn eval_default_config(
engine_state: &mut EngineState,
stack: &mut Stack,
config_kind: ConfigFileKind,
) {
info!("eval_default_config() {config_kind:?}");
eval_source(
engine_state,
stack,
config_kind.default().as_bytes(),
config_kind.default_path(),
PipelineData::empty(),
false,
);
if let Err(e) = engine_state.merge_env(stack) {
report_shell_error(Some(stack), engine_state, &e);
}
}
pub(crate) fn setup_config(
engine_state: &mut EngineState,
stack: &mut Stack,
is_login_shell: bool,
) {
info!("setup_config() login: {is_login_shell}");
let create_scaffold = !engine_state.config_dirs.config_home.exists();
let result = catch_unwind(AssertUnwindSafe(|| {
#[cfg(feature = "plugin")]
read_plugin_file(engine_state, None);
read_config_file(
engine_state,
stack,
ConfigFileKind::Env,
create_scaffold,
false,
None,
);
read_config_file(
engine_state,
stack,
ConfigFileKind::Config,
create_scaffold,
false,
None,
);
if is_login_shell {
read_loginshell_file(engine_state, stack, false);
}
read_vendor_autoload_files(engine_state, stack);
}));
if result.is_err() {
eprintln!(
"A panic occurred while reading configuration files, using default configuration."
);
engine_state.config = Arc::new(Config::default())
}
}