fix(config): Make print-config not panic without a config (#5001)

This commit is contained in:
Dom Slee 2023-04-14 10:29:21 +10:00 committed by GitHub
parent b44f22e375
commit ce7f984932
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 307 additions and 117 deletions

View File

@ -1,5 +1,6 @@
use crate::configs::Palette;
use crate::context::Context;
use crate::serde_utils::{ValueDeserializer, ValueRef};
use crate::utils;
use nu_ansi_term::Color;
@ -10,9 +11,9 @@ use serde::{
use std::borrow::Cow;
use std::clone::Clone;
use std::collections::HashMap;
use std::ffi::OsString;
use std::io::ErrorKind;
use std::env;
use toml::Value;
/// Root config of a module.
@ -120,25 +121,10 @@ pub struct StarshipConfig {
pub config: Option<toml::Table>,
}
pub fn get_config_path() -> Option<String> {
if let Ok(path) = env::var("STARSHIP_CONFIG") {
// Use $STARSHIP_CONFIG as the config path if available
log::debug!("STARSHIP_CONFIG is set: {}", &path);
Some(path)
} else {
// Default to using ~/.config/starship.toml
log::debug!("STARSHIP_CONFIG is not set");
let config_path = utils::home_dir()?.join(".config/starship.toml");
let config_path_str = config_path.to_str()?.to_owned();
log::debug!("Using default config path: {}", config_path_str);
Some(config_path_str)
}
}
impl StarshipConfig {
/// Initialize the Config struct
pub fn initialize() -> Self {
Self::config_from_file()
pub fn initialize(config_file_path: &Option<OsString>) -> Self {
Self::config_from_file(config_file_path)
.map(|config| Self {
config: Some(config),
})
@ -146,10 +132,30 @@ impl StarshipConfig {
}
/// Create a config from a starship configuration file
fn config_from_file() -> Option<toml::Table> {
let file_path = get_config_path()?;
fn config_from_file(config_file_path: &Option<OsString>) -> Option<toml::Table> {
let toml_content = Self::read_config_content_as_str(config_file_path)?;
let toml_content = match utils::read_file(file_path) {
match toml::from_str(&toml_content) {
Ok(parsed) => {
log::debug!("Config parsed: {:?}", &parsed);
Some(parsed)
}
Err(error) => {
log::error!("Unable to parse the config file: {}", error);
None
}
}
}
pub fn read_config_content_as_str(config_file_path: &Option<OsString>) -> Option<String> {
if config_file_path.is_none() {
log::debug!(
"Unable to determine `config_file_path`. Perhaps `utils::home_dir` is not defined on your platform?"
);
return None;
}
let config_file_path = config_file_path.as_ref().unwrap();
match utils::read_file(config_file_path) {
Ok(content) => {
log::trace!("Config file content: \"\n{}\"", &content);
Some(content)
@ -164,17 +170,6 @@ impl StarshipConfig {
log::log!(level, "Unable to read config file content: {}", &e);
None
}
}?;
match toml::from_str(&toml_content) {
Ok(parsed) => {
log::debug!("Config parsed: {:?}", &parsed);
Some(parsed)
}
Err(error) => {
log::error!("Unable to parse the config file: {}", error);
None
}
}
}
@ -921,4 +916,13 @@ mod tests {
// Test default behavior
assert!(get_palette(&palettes, None).is_none());
}
#[test]
fn read_config_no_config_file_path_provided() {
assert_eq!(
None,
StarshipConfig::read_config_content_as_str(&None),
"if the platform doesn't have utils::home_dir(), it should return None"
);
}
}

View File

@ -1,6 +1,3 @@
use std::env;
use std::ffi::OsString;
use std::io::ErrorKind;
use std::process;
use std::process::Stdio;
use std::str::FromStr;
@ -8,6 +5,7 @@ use std::str::FromStr;
use crate::config::ModuleConfig;
use crate::config::StarshipConfig;
use crate::configs::PROMPT_ORDER;
use crate::context::Context;
use crate::utils;
use std::fs::File;
use std::io::Write;
@ -18,15 +16,15 @@ const STD_EDITOR: &str = "vi";
#[cfg(windows)]
const STD_EDITOR: &str = "notepad.exe";
pub fn update_configuration(name: &str, value: &str) {
let mut doc = get_configuration_edit();
pub fn update_configuration(context: &Context, name: &str, value: &str) {
let mut doc = get_configuration_edit(context);
match handle_update_configuration(&mut doc, name, value) {
Err(e) => {
eprintln!("{e}");
process::exit(1);
}
_ => write_configuration(&doc),
_ => write_configuration(context, &doc),
}
}
@ -71,7 +69,7 @@ fn handle_update_configuration(doc: &mut Document, name: &str, value: &str) -> R
Ok(())
}
pub fn print_configuration(use_default: bool, paths: &[String]) {
pub fn print_configuration(context: &Context, use_default: bool, paths: &[String]) -> String {
let config = if use_default {
// Get default config
let default_config = crate::configs::FullConfig::default();
@ -79,7 +77,7 @@ pub fn print_configuration(use_default: bool, paths: &[String]) {
toml::value::Value::try_from(default_config).unwrap()
} else {
// Get config as toml::Value
let user_config = get_configuration();
let user_config = get_configuration(context);
// Convert into FullConfig and fill in default values
let user_config = crate::configs::FullConfig::load(&user_config);
// Convert back to Value because toml can't serialize FullConfig directly
@ -124,6 +122,7 @@ pub fn print_configuration(use_default: bool, paths: &[String]) {
let string_config = toml::to_string_pretty(&print_config).unwrap();
println!("{string_config}");
string_config
}
fn extract_toml_paths(mut config: toml::Value, paths: &[String]) -> toml::Value {
@ -176,15 +175,15 @@ fn extract_toml_paths(mut config: toml::Value, paths: &[String]) -> toml::Value
toml::Value::Table(subset)
}
pub fn toggle_configuration(name: &str, key: &str) {
let mut doc = get_configuration_edit();
pub fn toggle_configuration(context: &Context, name: &str, key: &str) {
let mut doc = get_configuration_edit(context);
match handle_toggle_configuration(&mut doc, name, key) {
Err(e) => {
eprintln!("{e}");
process::exit(1);
}
_ => write_configuration(&doc),
_ => write_configuration(context, &doc),
}
}
@ -217,32 +216,15 @@ fn handle_toggle_configuration(doc: &mut Document, name: &str, key: &str) -> Res
Ok(())
}
pub fn get_configuration() -> toml::Table {
let starship_config = StarshipConfig::initialize();
pub fn get_configuration(context: &Context) -> toml::Table {
let starship_config = StarshipConfig::initialize(&context.get_config_path_os());
starship_config
.config
.expect("Failed to load starship config")
starship_config.config.unwrap_or(toml::Table::new())
}
pub fn get_configuration_edit() -> Document {
let file_path = get_config_path();
let toml_content = match utils::read_file(file_path) {
Ok(content) => {
log::trace!("Config file content: \"\n{}\"", &content);
Some(content)
}
Err(e) => {
let level = if e.kind() == ErrorKind::NotFound {
log::Level::Debug
} else {
log::Level::Error
};
log::log!(level, "Unable to read config file content: {}", &e);
None
}
};
pub fn get_configuration_edit(context: &Context) -> Document {
let config_file_path = context.get_config_path_os();
let toml_content = StarshipConfig::read_config_content_as_str(&config_file_path);
toml_content
.unwrap_or_default()
@ -250,8 +232,11 @@ pub fn get_configuration_edit() -> Document {
.expect("Failed to load starship config")
}
pub fn write_configuration(doc: &Document) {
let config_path = get_config_path();
pub fn write_configuration(context: &Context, doc: &Document) {
let config_path = context.get_config_path_os().unwrap_or_else(|| {
eprintln!("config path required to write configuration");
process::exit(1);
});
let config_str = doc.to_string();
@ -260,10 +245,16 @@ pub fn write_configuration(doc: &Document) {
.expect("Error writing starship config");
}
pub fn edit_configuration(editor_override: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
pub fn edit_configuration(
context: &Context,
editor_override: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
// Argument currently only used for testing, but could be used to specify
// an editor override on the command line.
let config_path = get_config_path();
let config_path = context.get_config_path_os().unwrap_or_else(|| {
eprintln!("config path required to edit configuration");
process::exit(1);
});
let editor_cmd = shell_words::split(&get_editor(editor_override))?;
let mut command = match utils::create_command(&editor_cmd[0]) {
@ -313,19 +304,18 @@ fn get_editor_internal(visual: Option<String>, editor: Option<String>) -> String
STD_EDITOR.into()
}
fn get_config_path() -> OsString {
if let Some(config_path) = env::var_os("STARSHIP_CONFIG") {
return config_path;
}
utils::home_dir()
.expect("couldn't find home directory")
.join(".config")
.join("starship.toml")
.into()
}
#[cfg(test)]
mod tests {
use std::{fs::create_dir, io};
use tempfile::TempDir;
use toml_edit::Item;
use crate::{
context::{Shell, Target},
context_env::Env,
};
use super::*;
// This is every possible permutation, 3² = 9.
@ -379,13 +369,13 @@ mod tests {
#[test]
fn no_panic_when_editor_unparsable() {
let outcome = edit_configuration(Some("\"vim"));
let outcome = edit_configuration(&Default::default(), Some("\"vim"));
assert!(outcome.is_err());
}
#[test]
fn no_panic_when_editor_not_found() {
let outcome = edit_configuration(Some("this_editor_does_not_exist"));
let outcome = edit_configuration(&Default::default(), Some("this_editor_does_not_exist"));
assert!(outcome.is_err());
}
@ -581,4 +571,115 @@ mod tests {
.as_bool()
.unwrap())
}
#[test]
fn write_and_get_configuration_test() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let context = setup_config(&dir, true, StarshipConfigEnvScenario::NotSpecified)?;
let mut doc = get_configuration_edit(&context);
doc["directory"]["format"] = Item::Value("myformat".into());
write_configuration(&context, &doc);
let doc_reloaded = get_configuration_edit(&context);
assert_eq!(
"myformat",
doc_reloaded["directory"]["format"].as_str().unwrap()
);
dir.close()
}
const PRINT_CONFIG_DEFAULT: &str = "[custom]";
const PRINT_CONFIG_HOME: &str = "[custom.home]";
const PRINT_CONFIG_ENV: &str = "[custom.STARSHIP_CONFIG]";
#[test]
fn print_configuration_scenarios() -> io::Result<()> {
run_print_configuration_test(
"~/.config/starship.toml, no STARSHIP_CONFIG uses home",
true,
StarshipConfigEnvScenario::NotSpecified,
PRINT_CONFIG_HOME,
)?;
run_print_configuration_test(
"no ~/.config/starship.toml, no STARSHIP_CONFIG uses default",
false,
StarshipConfigEnvScenario::NotSpecified,
PRINT_CONFIG_DEFAULT,
)?;
run_print_configuration_test(
"~/.config/starship.toml, STARSHIP_CONFIG nonexisting file uses default",
true,
StarshipConfigEnvScenario::NonExistingFile,
PRINT_CONFIG_DEFAULT,
)?;
run_print_configuration_test(
"~/.config/starship.toml, STARSHIP_CONFIG existing file uses STARSHIP_CONFIG file",
true,
StarshipConfigEnvScenario::ExistingFile,
PRINT_CONFIG_ENV,
)?;
Ok(())
}
enum StarshipConfigEnvScenario {
NotSpecified,
NonExistingFile,
ExistingFile,
}
fn run_print_configuration_test(
message: &str,
home_file_exists: bool,
starship_config_env_scenario: StarshipConfigEnvScenario,
expected_first_line: &str,
) -> io::Result<()> {
let dir = tempfile::tempdir()?;
let context = setup_config(&dir, home_file_exists, starship_config_env_scenario)?;
let config = print_configuration(&context, false, &["custom".to_string()]);
let first_line = config.split('\n').next().unwrap();
assert_eq!(expected_first_line, first_line, "{message}");
dir.close()
}
fn setup_config(
dir: &TempDir,
home_file_exists: bool,
starship_config_env_scenario: StarshipConfigEnvScenario,
) -> io::Result<Context> {
let config_path = dir.path().to_path_buf().join(".config");
create_dir(&config_path)?;
let home_starship_toml = config_path.join("starship.toml");
let env_toml = dir.path().join("env.toml");
if home_file_exists {
let mut home_file = File::create(home_starship_toml)?;
home_file.write_all(PRINT_CONFIG_HOME.as_bytes())?;
}
let env_starship_config = match starship_config_env_scenario {
StarshipConfigEnvScenario::NotSpecified => None,
StarshipConfigEnvScenario::NonExistingFile => Some(env_toml),
StarshipConfigEnvScenario::ExistingFile => {
let mut env_toml_file = File::create(&env_toml)?;
env_toml_file.write_all(PRINT_CONFIG_ENV.as_bytes())?;
Some(env_toml)
}
};
let mut env = Env::default();
if let Some(v) = env_starship_config {
env.insert("STARSHIP_CONFIG", v.to_string_lossy().to_string());
}
env.insert(
"HOME",
dir.path().to_path_buf().to_string_lossy().to_string(),
);
Ok(Context::new_with_shell_and_path(
Default::default(),
Shell::Unknown,
Target::Main,
Default::default(),
Default::default(),
env,
))
}
}

View File

@ -1,10 +1,11 @@
use crate::config::{ModuleConfig, StarshipConfig};
use crate::configs::StarshipRootConfig;
use crate::context_env::Env;
use crate::module::Module;
use crate::utils::{create_command, exec_timeout, read_file, CommandOutput, PathExt};
use crate::modules;
use crate::utils::{self, home_dir};
use crate::utils;
use clap::Parser;
use gix::{
sec::{self as git_sec, trust::DefaultForLevel},
@ -60,8 +61,7 @@ pub struct Context<'a> {
pub width: usize,
/// A HashMap of environment variable mocks
#[cfg(test)]
pub env: HashMap<&'a str, String>,
pub env: Env<'a>,
/// A HashMap of command mocks
#[cfg(test)]
@ -107,7 +107,14 @@ impl<'a> Context<'a> {
.or_else(|| env::var("PWD").map(PathBuf::from).ok())
.unwrap_or_else(|| path.clone());
Context::new_with_shell_and_path(arguments, shell, target, path, logical_path)
Context::new_with_shell_and_path(
arguments,
shell,
target,
path,
logical_path,
Default::default(),
)
}
/// Create a new instance of Context for the provided directory
@ -117,8 +124,9 @@ impl<'a> Context<'a> {
target: Target,
path: PathBuf,
logical_path: PathBuf,
env: Env<'a>,
) -> Context<'a> {
let config = StarshipConfig::initialize();
let config = StarshipConfig::initialize(&get_config_path_os(&env));
// If the vector is zero-length, we should pretend that we didn't get a
// pipestatus at all (since this is the input `--pipestatus=""`)
@ -162,11 +170,10 @@ impl<'a> Context<'a> {
shell,
target,
width,
env,
#[cfg(test)]
root_dir: tempfile::TempDir::new().unwrap(),
#[cfg(test)]
env: HashMap::new(),
#[cfg(test)]
cmd: HashMap::new(),
#[cfg(feature = "battery")]
battery_info_provider: &crate::modules::BatteryInfoProviderImpl,
@ -177,37 +184,19 @@ impl<'a> Context<'a> {
// Tries to retrieve home directory from a table in testing mode or else retrieves it from the os
pub fn get_home(&self) -> Option<PathBuf> {
if cfg!(test) {
return self.get_env("HOME").map(PathBuf::from).or_else(home_dir);
}
home_dir()
home_dir(&self.env)
}
// Retrieves a environment variable from the os or from a table if in testing mode
#[cfg(test)]
pub fn get_env<K: AsRef<str>>(&self, key: K) -> Option<String> {
self.env
.get(key.as_ref())
.map(std::string::ToString::to_string)
}
#[cfg(not(test))]
#[inline]
pub fn get_env<K: AsRef<str>>(&self, key: K) -> Option<String> {
env::var(key.as_ref()).ok()
self.env.get_env(key)
}
// Retrieves a environment variable from the os or from a table if in testing mode (os version)
#[cfg(test)]
pub fn get_env_os<K: AsRef<str>>(&self, key: K) -> Option<OsString> {
self.env.get(key.as_ref()).map(OsString::from)
}
#[cfg(not(test))]
#[inline]
pub fn get_env_os<K: AsRef<str>>(&self, key: K) -> Option<OsString> {
env::var_os(key.as_ref())
self.env.get_env_os(key)
}
/// Convert a `~` in a path to the home directory
@ -401,6 +390,32 @@ impl<'a> Context<'a> {
read_file(self.current_dir.join(file_name)).ok()
}
pub fn get_config_path_os(&self) -> Option<OsString> {
get_config_path_os(&self.env)
}
}
impl Default for Context<'_> {
fn default() -> Self {
Context::new(Default::default(), Target::Main)
}
}
fn home_dir(env: &Env) -> Option<PathBuf> {
if cfg!(test) {
if let Some(home) = env.get_env("HOME") {
return Some(PathBuf::from(home));
}
}
utils::home_dir()
}
fn get_config_path_os(env: &Env) -> Option<OsString> {
if let Some(config_path) = env.get_env_os("STARSHIP_CONFIG") {
return Some(config_path);
}
Some(home_dir(env)?.join(".config").join("starship.toml").into())
}
#[derive(Debug)]
@ -907,6 +922,7 @@ mod tests {
Target::Main,
test_path.clone(),
test_path.clone(),
Default::default(),
);
assert_ne!(context.current_dir, context.logical_dir);
@ -931,6 +947,7 @@ mod tests {
Target::Main,
test_path.clone(),
test_path.clone(),
Default::default(),
);
let expected_current_dir = &test_path;
@ -952,6 +969,7 @@ mod tests {
Target::Main,
test_path.clone(),
test_path.clone(),
Default::default(),
);
let expected_current_dir = home_dir()
@ -973,6 +991,7 @@ mod tests {
Target::Main,
test_path.clone(),
test_path,
Default::default(),
);
let expected_path = Path::new(r"C:\");

48
src/context_env.rs Normal file
View File

@ -0,0 +1,48 @@
#[cfg(test)]
use std::collections::HashMap;
#[cfg(not(test))]
use std::env;
use std::ffi::OsString;
#[derive(Default)]
pub struct Env<'a> {
/// A HashMap of environment variable mocks
#[cfg(test)]
pub env: HashMap<&'a str, String>,
#[cfg(not(test))]
_marker: std::marker::PhantomData<&'a ()>,
}
impl<'a> Env<'a> {
// Retrieves a environment variable from the os or from a table if in testing mode
#[cfg(test)]
pub fn get_env<K: AsRef<str>>(&self, key: K) -> Option<String> {
self.env
.get(key.as_ref())
.map(std::string::ToString::to_string)
}
#[cfg(not(test))]
#[inline]
pub fn get_env<K: AsRef<str>>(&self, key: K) -> Option<String> {
env::var(key.as_ref()).ok()
}
// Retrieves a environment variable from the os or from a table if in testing mode (os version)
#[cfg(test)]
pub fn get_env_os<K: AsRef<str>>(&self, key: K) -> Option<OsString> {
self.env.get(key.as_ref()).map(OsString::from)
}
#[cfg(not(test))]
#[inline]
pub fn get_env_os<K: AsRef<str>>(&self, key: K) -> Option<OsString> {
env::var_os(key.as_ref())
}
#[cfg(test)]
pub fn insert(&mut self, k: &'a str, v: String) -> Option<String> {
self.env.insert(k, v)
}
}

View File

@ -11,6 +11,7 @@ pub mod config;
pub mod configs;
pub mod configure;
pub mod context;
pub mod context_env;
pub mod formatter;
pub mod init;
pub mod logger;

View File

@ -10,7 +10,7 @@ use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell as CompletionShell};
use rand::distributions::Alphanumeric;
use rand::Rng;
use starship::context::{Properties, Target};
use starship::context::{Context, Properties, Target};
use starship::module::ALL_MODULES;
use starship::*;
@ -208,17 +208,22 @@ fn main() {
}
Commands::Preset { name, list, output } => print::preset_command(name, output, list),
Commands::Config { name, value } => {
let context = Context::default();
if let Some(name) = name {
if let Some(value) = value {
configure::update_configuration(&name, &value)
configure::update_configuration(&context, &name, &value)
}
} else if let Err(reason) = configure::edit_configuration(None) {
} else if let Err(reason) = configure::edit_configuration(&context, None) {
eprintln!("Could not edit configuration: {reason}");
std::process::exit(1);
}
}
Commands::PrintConfig { default, name } => configure::print_configuration(default, &name),
Commands::Toggle { name, value } => configure::toggle_configuration(&name, &value),
Commands::PrintConfig { default, name } => {
configure::print_configuration(&Context::default(), default, &name);
}
Commands::Toggle { name, value } => {
configure::toggle_configuration(&Context::default(), &name, &value)
}
Commands::BugReport => bug_report::create(),
Commands::Time => {
match SystemTime::now()

View File

@ -386,6 +386,7 @@ fn git_status_wsl(context: &Context, conf: &GitStatusConfig) -> Option<String> {
use crate::utils::create_command;
use nix::sys::utsname::uname;
use std::env;
use std::ffi::OsString;
use std::io::ErrorKind;
let starship_exe = conf.windows_starship?;
@ -454,7 +455,9 @@ fn git_status_wsl(context: &Context, conf: &GitStatusConfig) -> Option<String> {
.map(|mut c| {
c.env(
"STARSHIP_CONFIG",
crate::config::get_config_path().unwrap_or_else(|| "/dev/null".to_string()),
context
.get_config_path_os()
.unwrap_or_else(|| OsString::from("/dev/null")),
)
.env("WSLENV", wslenv)
.args(["module", "git_status", "--path", winpath]);

View File

@ -735,6 +735,7 @@ version = "12"
Target::Main,
dir.path().into(),
dir.path().into(),
Default::default(),
);
assert_eq!(
@ -756,6 +757,7 @@ version = "12"
Target::Main,
dir.path().into(),
dir.path().into(),
Default::default(),
);
assert_eq!(
@ -777,6 +779,7 @@ version = "12"
Target::Main,
dir.path().into(),
dir.path().into(),
Default::default(),
);
assert_eq!(
@ -800,6 +803,7 @@ version = "12"
Target::Main,
child_dir_path.clone(),
child_dir_path,
Default::default(),
);
assert_eq!(
@ -820,6 +824,7 @@ version = "12"
Target::Main,
dir.path().into(),
dir.path().into(),
Default::default(),
);
assert_eq!(find_rust_toolchain_file(&context), None);
@ -838,6 +843,7 @@ version = "12"
Target::Main,
dir.path().into(),
dir.path().into(),
Default::default(),
);
assert_eq!(
@ -859,6 +865,7 @@ version = "12"
Target::Main,
dir.path().into(),
dir.path().into(),
Default::default(),
);
assert_eq!(
@ -882,6 +889,7 @@ version = "12"
Target::Main,
child_dir_path.clone(),
child_dir_path,
Default::default(),
);
assert_eq!(

View File

@ -43,6 +43,7 @@ pub fn default_context() -> Context<'static> {
Target::Main,
PathBuf::new(),
PathBuf::new(),
Default::default(),
);
context.config = StarshipConfig { config: None };
context