diff --git a/config/config.rush b/config/config.rush index 576c449..f0954fe 100644 --- a/config/config.rush +++ b/config/config.rush @@ -1,4 +1,4 @@ truncation-factor: false -multi-line-prompt: true +multiline-prompt: true history-limit: false show-errors: true diff --git a/src/errors.rs b/src/errors.rs index 13f0a0e..4e07eb4 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -165,49 +165,25 @@ pub enum DispatchError { #[derive(Debug)] pub enum BuiltinError { /// OVERVIEW - /// This error occurs when a builtin command is called with an incorrect number of arguments. + /// This error occurs when a builtin command is provided with invalid argument(s). /// - /// CAUSE - /// - The number of arguments supplied does not match the number of arguments expected. - /// - /// SOLUTION - /// - Check the builtin's documentation and adjust arguments accordingly. - /// - /// TECHNICAL DETAILS - /// When executing a builtin command, it checks the number of arguments provided by the user, - /// and if it does not match the expected count, this error is returned in order to prevent the - /// builtin from executing with malformed input. - WrongArgCount(usize, usize), - - /// OVERVIEW - /// This error occurs when a builtin command receives an argument it does not recognize. - /// - /// CAUSE - /// - The builtin command is provided with an argument that it is unable to process. - /// - /// SOLUTION - /// - Check the builtin's documentation and adjust arguments accordingly. - /// - /// TECHNICAL DETAILS - /// When executing a builtin command, if it encounters an argument it does not recognize, this - /// error is returned in order to prevent the builtin from executing with malformed input. - InvalidArg(String), - - /// OVERVIEW - /// This error occurs when a builtin command receives an invalid value for a valid argument. + /// COMMON CAUSES + /// - The builtin received a different number of arguments than it expected. + /// - An argument was misspelled or malformed. + /// - An argument which should have been escaped or enclosed in quotes, but was not. /// - /// CAUSE - /// - The builtin command receives a value that it cannot use for its associated argument. + /// RARE CAUSES + /// - A bug in the parsing logic prevented a valid argument from being parsed correctly. /// /// SOLUTION /// - Check the builtin's documentation and adjust arguments accordingly. + /// - File an issue on the Rush repository if an internal bug is suspected. /// /// TECHNICAL DETAILS - /// When executing a builtin command, if it encounters a valid argument, it will typically try - /// to parse the provided value for the argument. If the value is not expected by the parsing - /// logic, this error is returned in order to prevent the builtin from executing with malformed - /// input. - InvalidValue(String), + /// When executing a builtin command, it will parse the provided arguments into values it can + /// use to perform an operation. If there is some error in parsing these arguments, it is unable + /// to run without proper input, so this error is returned. + CouldNotParseArgs, /// OVERVIEW /// This error occurs when a builtin is unable to interact with the terminal. @@ -599,22 +575,7 @@ impl Display for BuiltinError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { use BuiltinError::*; match self { - WrongArgCount(expected, actual) => { - write!( - f, - "Expected {} {}, found {}", - expected, - match expected { - 1 => "argument", - _ => "arguments", - }, - actual - ) - } - InvalidArg(argument) => { - write!(f, "Argument '{}' is invalid", argument) - } - InvalidValue(value) => write!(f, "Argument value '{}' is invalid", value), + CouldNotParseArgs => write!(f, "Unable to parse the provided arguments"), TerminalOperationFailed => write!(f, "Terminal operation failed"), } } @@ -777,3 +738,13 @@ macro_rules! file_err { )) }}; } + +/// Shortcut for printing a `clap::Error` and returning a `BuiltinError::CouldNotParseArgs` +macro_rules! clap_handle { + ($expr:expr) => { + $expr.map_err(|e| { + eprintln!("{}", e.render().ansi()); + builtin_err!(CouldNotParseArgs) + })? + }; +} diff --git a/src/eval/dispatcher.rs b/src/eval/dispatcher.rs index d579173..0c7f48e 100644 --- a/src/eval/dispatcher.rs +++ b/src/eval/dispatcher.rs @@ -76,7 +76,7 @@ impl Dispatcher { pub fn eval(&self, shell: &mut ShellState, line: &str) -> Result<()> { let args = tokenize(line); let command_name = args.get(0).unwrap().as_str(); - let command_args: Vec<&str> = args.iter().skip(1).map(|a| a.as_str()).collect(); + let command_args: Vec<&str> = args.iter().map(|a| a.as_str()).collect(); self.dispatch(shell, command_name, command_args)?; Ok(()) diff --git a/src/exec/builtins/args.rs b/src/exec/builtins/args.rs index 2bda6ea..803bf6f 100644 --- a/src/exec/builtins/args.rs +++ b/src/exec/builtins/args.rs @@ -1,9 +1,186 @@ -use clap::Parser; +use std::{path::PathBuf, str::FromStr}; + +use clap::{Args, Parser, Subcommand}; + +use crate::state::EnvVariable; + +const TRUE_ARGS: [&str; 9] = [ + "true", "t", "enable", "enabled", "yes", "y", "on", "some", "1", +]; +const FALSE_ARGS: [&str; 9] = [ + "false", "f", "disable", "disabled", "no", "n", "off", "none", "0", +]; + +#[derive(Parser, Debug)] +pub struct TestArgs {} + +#[derive(Parser, Debug)] +pub struct ExitArgs {} + +#[derive(Parser, Debug)] +pub struct WorkingDirectoryArgs {} + +#[derive(Parser, Debug)] +pub struct ChangeDirectoryArgs { + pub path: PathBuf, +} + +#[derive(Parser, Debug)] +pub struct ListDirectoryArgs { + #[arg(short = 'a', long = "all")] + pub show_hidden: bool, + pub path: Option, +} + +#[derive(Parser, Debug)] +pub struct PreviousDirectoryArgs {} + +#[derive(Parser, Debug)] +pub struct NextDirectoryArgs {} + +#[derive(Parser, Debug)] +pub struct ClearTerminalArgs {} + +#[derive(Parser, Debug)] +pub struct MakeFileArgs { + pub path: PathBuf, +} + +#[derive(Parser, Debug)] +pub struct MakeDirectoryArgs { + pub path: PathBuf, +} + +#[derive(Parser, Debug)] +pub struct DeleteFileArgs { + pub path: PathBuf, +} + +#[derive(Parser, Debug)] +pub struct ReadFileArgs { + pub path: PathBuf, +} + +#[derive(Parser, Debug)] +pub struct RunExecutableArgs { + pub path: PathBuf, + pub arguments: Vec, +} + +#[derive(Parser, Debug)] +pub struct ConfigureArgs { + #[arg(long = "truncation-factor")] + pub truncation_factor: Option, + #[arg(long = "history-limit")] + pub history_limit: Option, + #[arg(long = "multiline-prompt")] + pub multiline_prompt: Option, + #[arg(long = "show-errors")] + pub show_errors: Option, +} + +#[derive(Debug, Clone)] +pub enum FancyBool { + True, + False, +} + +impl FromStr for FancyBool { + type Err = String; + fn from_str(s: &str) -> Result { + if TRUE_ARGS.contains(&s) { + Ok(FancyBool::True) + } else if FALSE_ARGS.contains(&s) { + Ok(FancyBool::False) + } else { + Err("invalid boolean value".to_owned()) + } + } +} + +impl From for bool { + fn from(b: FancyBool) -> Self { + match b { + FancyBool::True => true, + FancyBool::False => false, + } + } +} + +#[derive(Debug, Clone)] +pub enum MaybeUsize { + Some(usize), + None, +} + +impl FromStr for MaybeUsize { + type Err = String; + fn from_str(s: &str) -> Result { + if let Ok(n) = s.parse::() { + Ok(MaybeUsize::Some(n)) + } else if FALSE_ARGS.contains(&s) { + Ok(MaybeUsize::None) + } else { + Err("invalid integer or boolean value".to_owned()) + } + } +} + +impl From for Option { + fn from(n: MaybeUsize) -> Self { + match n { + MaybeUsize::Some(n) => Some(n), + MaybeUsize::None => None, + } + } +} + +#[derive(Parser, Debug)] +pub struct EnvironmentVariableArgs { + pub variable: EnvVariable, +} #[derive(Parser, Debug)] -#[command(no_binary_name = true)] -pub struct ListDirectoryArguments { - #[clap(short, long, default_value_t = false)] - pub all: bool, - pub path: Option, +pub struct EditPathArgs { + #[clap(subcommand)] + pub subcommand: EditPathSubcommand, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum EditPathSubcommand { + #[clap( + about = "Add the provided path to the end of the PATH variable so it is scanned last when resolving executables" + )] + Append(AppendPathCommand), + #[clap( + about = "Add the provided path to the beginning of the PATH variable so it is scanned first when resolving executables" + )] + Prepend(PrependPathCommand), + #[clap( + about = "Insert the provided path before the path at the specified index in the PATH variable" + )] + Insert(InsertPathCommand), + #[clap(about = "Delete the path at the specified index in the PATH variable")] + Delete(DeletePathCommand), +} + +#[derive(Args, Debug, Clone)] +pub struct AppendPathCommand { + pub path: PathBuf, +} + +#[derive(Args, Debug, Clone)] +pub struct PrependPathCommand { + pub path: PathBuf, +} + +#[derive(Args, Debug, Clone)] +pub struct InsertPathCommand { + pub index: usize, + pub path: PathBuf, +} + +#[derive(Args, Debug, Clone)] +pub struct DeletePathCommand { + pub index: usize, } diff --git a/src/exec/builtins/functions.rs b/src/exec/builtins/functions.rs index 7202c53..a006914 100644 --- a/src/exec/builtins/functions.rs +++ b/src/exec/builtins/functions.rs @@ -9,7 +9,6 @@ An executable will only have access to its arguments and environment variables, */ use std::io::{stderr, BufRead, BufReader}; -use std::path::PathBuf; use clap::Parser; use crossterm::cursor::MoveTo; @@ -17,46 +16,53 @@ use crossterm::execute; use crossterm::style::Stylize; use crossterm::terminal::{self, Clear, ClearType}; -use super::args::ListDirectoryArguments; +use super::args::{ + ChangeDirectoryArgs, ClearTerminalArgs, ConfigureArgs, DeleteFileArgs, EditPathArgs, + EditPathSubcommand, EnvironmentVariableArgs, ExitArgs, ListDirectoryArgs, MakeDirectoryArgs, + MakeFileArgs, NextDirectoryArgs, PreviousDirectoryArgs, ReadFileArgs, RunExecutableArgs, + WorkingDirectoryArgs, +}; use crate::errors::{Handle, Result}; +use crate::exec::builtins::args::{ + AppendPathCommand, DeletePathCommand, InsertPathCommand, PrependPathCommand, TestArgs, +}; use crate::exec::{Executable, Runnable}; -use crate::state::{Path, ShellState}; +use crate::state::{EnvVariable, Path, ShellState}; pub fn test(_shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - check_args(&args, 0, "test")?; + clap_handle!(TestArgs::try_parse_from(args)); println!("{}", "Test command!".yellow()); Ok(()) } pub fn exit(_shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - check_args(&args, 0, "exit")?; + clap_handle!(ExitArgs::try_parse_from(args)); std::process::exit(0); } pub fn working_directory(shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - check_args(&args, 0, "working-directory")?; + clap_handle!(WorkingDirectoryArgs::try_parse_from(args)); println!("{}", shell.environment.CWD); Ok(()) } pub fn change_directory(shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - check_args(&args, 1, "change-directory ")?; + let arguments = clap_handle!(ChangeDirectoryArgs::try_parse_from(args)); let history_limit = shell.config.history_limit; shell .environment - .set_CWD(args[0], history_limit) - .replace_err(|| file_err!(UnknownPath: args[0]))?; + .set_CWD(&arguments.path, history_limit) + .replace_err(|| file_err!(UnknownPath: arguments.path))?; Ok(()) } pub fn list_directory(shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - let arguments = ListDirectoryArguments::parse_from(&args); - let show_hidden = arguments.all; - let path_to_read = match arguments.path { - Some(path) => PathBuf::from(path), - None => shell.environment.CWD.path().to_path_buf(), - }; + let arguments = clap_handle!(ListDirectoryArgs::try_parse_from(&args)); + let show_hidden = arguments.show_hidden; + let path_to_read = arguments + .path + .unwrap_or(shell.environment.CWD.path().to_path_buf()); let read_dir_result = fs_err::read_dir(&path_to_read).replace_err(|| file_err!(UnknownPath: path_to_read))?; @@ -101,7 +107,7 @@ pub fn list_directory(shell: &mut ShellState, args: Vec<&str>) -> Result<()> { } pub fn previous_directory(shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - check_args(&args, 0, "go-back")?; + clap_handle!(PreviousDirectoryArgs::try_parse_from(args)); shell .environment .previous_directory() @@ -109,7 +115,7 @@ pub fn previous_directory(shell: &mut ShellState, args: Vec<&str>) -> Result<()> } pub fn next_directory(shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - check_args(&args, 0, "go-forward")?; + clap_handle!(NextDirectoryArgs::try_parse_from(args)); shell .environment .next_directory() @@ -117,7 +123,7 @@ pub fn next_directory(shell: &mut ShellState, args: Vec<&str>) -> Result<()> { } pub fn clear_terminal(_shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - check_args(&args, 0, "clear-terminal")?; + clap_handle!(ClearTerminalArgs::try_parse_from(args)); let y_size = terminal::size() .replace_err_with_msg( || builtin_err!(TerminalOperationFailed), @@ -138,24 +144,27 @@ pub fn clear_terminal(_shell: &mut ShellState, args: Vec<&str>) -> Result<()> { // TODO: Add prompt to confirm file overwrite pub fn make_file(_shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - check_args(&args, 1, "usage: make-file ")?; - fs_err::File::create(args[0]).replace_err(|| file_err!(CouldNotCreateFile: args[0]))?; + let arguments = clap_handle!(MakeFileArgs::try_parse_from(args)); + fs_err::File::create(&arguments.path) + .replace_err(|| file_err!(CouldNotCreateFile: arguments.path))?; Ok(()) } pub fn make_directory(_shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - check_args(&args, 1, "make-directory ")?; - fs_err::create_dir(args[0]).replace_err(|| file_err!(CouldNotCreateDirectory: args[0])) + let arguments = clap_handle!(MakeDirectoryArgs::try_parse_from(args)); + fs_err::create_dir(&arguments.path) + .replace_err(|| file_err!(CouldNotCreateDirectory: arguments.path)) } pub fn delete_file(_shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - check_args(&args, 1, "delete-file ")?; - fs_err::remove_file(args[0]).replace_err(|| file_err!(CouldNotDeleteFile: args[0])) + let arguments = clap_handle!(DeleteFileArgs::try_parse_from(args)); + fs_err::remove_file(&arguments.path) + .replace_err(|| file_err!(CouldNotDeleteFile: arguments.path)) } pub fn read_file(_shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - check_args(&args, 1, "read-file ")?; - let file_name = args[0].to_owned(); + let arguments = clap_handle!(ReadFileArgs::try_parse_from(args)); + let file_name = arguments.path; let file = fs_err::File::open(&file_name).replace_err(|| file_err!(CouldNotOpenFile: file_name))?; @@ -168,111 +177,79 @@ pub fn read_file(_shell: &mut ShellState, args: Vec<&str>) -> Result<()> { Ok(()) } -pub fn run_executable(shell: &mut ShellState, mut args: Vec<&str>) -> Result<()> { - let executable_name = args[0].to_owned(); - let executable_path = Path::try_from_str(&executable_name, &shell.environment.HOME) +pub fn run_executable(shell: &mut ShellState, args: Vec<&str>) -> Result<()> { + let arguments = clap_handle!(RunExecutableArgs::try_parse_from(&args)); + let executable_name = arguments.path; + let executable_path = Path::try_from_path(&executable_name, Some(&shell.environment.HOME)) .replace_err_with_msg( || file_err!(UnknownPath: executable_name), - &format!("Could not find executable '{}'", executable_name), + &format!("Could not find executable '{}'", executable_name.display()), )?; - // * Executable name is removed before running the executable because the std::process::Command - // * process builder automatically adds the executable name as the first argument - args.remove(0); + // TODO: Fix the usage of args and arg parsing here Executable::new(executable_path).run(shell, args) } pub fn configure(shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - check_args(&args, 2, "configure ")?; - let key = args[0]; - let value = args[1]; - - match key { - "truncation" => { - if value == "false" { - shell.config.truncation_factor = None; - return Ok(()); - } + let arguments = clap_handle!(ConfigureArgs::try_parse_from(args)); - shell.config.truncation_factor = Some(value.parse::().replace_err_with_msg( - || builtin_err!(InvalidValue: value), - &format!("Invalid truncation length: '{}'", value), - )?); - } - "multi-line-prompt" => { - shell.config.multi_line_prompt = value.parse::().replace_err_with_msg( - || builtin_err!(InvalidValue: value), - &format!("Invalid value for multi-line-prompt: '{}'", value), - )?; - } - "history-limit" => { - if value == "false" { - shell.config.history_limit = None; - return Ok(()); - } + if let Some(truncation_factor) = arguments.truncation_factor { + shell.config.truncation_factor = truncation_factor.into(); + } - shell.config.history_limit = Some(value.parse::().replace_err_with_msg( - || builtin_err!(InvalidValue: value), - &format!("Invalid history limit: '{}'", value), - )?); - } - "show-errors" => { - shell.config.show_errors = value.parse::().replace_err_with_msg( - || builtin_err!(InvalidValue: value), - &format!("Invalid value for show-errors: '{}'", value), - )?; - } - _ => { - return Err(builtin_err!(InvalidArg: value) - .set_context(&format!("Invalid configuration key: '{}'", key))); - } + if let Some(history_limit) = arguments.history_limit { + shell.config.history_limit = history_limit.into(); + } + + if let Some(multiline_prompt) = arguments.multiline_prompt { + shell.config.multiline_prompt = multiline_prompt.into(); + } + + if let Some(show_errors) = arguments.show_errors { + shell.config.show_errors = show_errors.into(); } Ok(()) } pub fn environment_variable(shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - check_args(&args, 1, "environment-variable ")?; - match args[0].to_uppercase().as_str() { - "PATH" => { + let arguments = clap_handle!(EnvironmentVariableArgs::try_parse_from(args)); + use EnvVariable::*; + match arguments.variable { + USER => println!("{}", shell.environment.USER), + HOME => println!("{}", shell.environment.HOME.display()), + CWD => println!("{}", shell.environment.CWD), + PATH => { for (i, path) in shell.environment.PATH.iter().enumerate() { println!("[{i}]: {path}"); } } - "USER" => println!("{}", shell.environment.USER), - "HOME" => println!("{}", shell.environment.HOME.display()), - "CWD" | "WORKING-DIRECTORY" => println!("{}", shell.environment.CWD), - _ => { - return Err(builtin_err!(InvalidArg: args[0])); - } } Ok(()) } pub fn edit_path(shell: &mut ShellState, args: Vec<&str>) -> Result<()> { - check_args(&args, 2, "edit-path ")?; - let action = args[0]; - let path = Path::try_from_str(args[1], &shell.environment.HOME) - .replace_err(|| file_err!(UnknownPath: args[1]))?; - - match action { - "append" => shell.environment.PATH.push_front(path), - "prepend" => shell.environment.PATH.push_back(path), - _ => { - return Err(builtin_err!(InvalidArg: action)); + // TODO: Needs to update the real PATH + let arguments = clap_handle!(EditPathArgs::try_parse_from(args)); + use EditPathSubcommand::*; + match arguments.subcommand { + Append(AppendPathCommand { path }) => shell + .environment + .PATH + .push_front(Path::try_from_path(&path, Some(&shell.environment.HOME))?), + Prepend(PrependPathCommand { path }) => shell + .environment + .PATH + .push_front(Path::try_from_path(&path, Some(&shell.environment.HOME))?), + Insert(InsertPathCommand { index, path }) => shell.environment.PATH.insert( + index, + Path::try_from_path(&path, Some(&shell.environment.HOME))?, + ), + Delete(DeletePathCommand { index }) => { + shell.environment.PATH.remove(index); } } Ok(()) } - -// Convenience function for exiting a builtin on invalid argument count -fn check_args(args: &Vec<&str>, expected_args: usize, usage: &str) -> Result<()> { - if args.len() == expected_args { - Ok(()) - } else { - Err(builtin_err!(WrongArgCount: expected_args, args.len()) - .set_context(&format!("Usage: {}", usage))) - } -} diff --git a/src/exec/executable.rs b/src/exec/executable.rs index 6e65fd3..e1127f5 100644 --- a/src/exec/executable.rs +++ b/src/exec/executable.rs @@ -23,8 +23,10 @@ impl Runnable for Executable { // * Executables do not have access to the shell state, but the context argument is required by the Runnable trait fn run(&self, _shell: &mut ShellState, arguments: Vec<&str>) -> Result<()> { // Create the Process, pass the provided arguments to it, and execute it + // * Executable name has to be removed because `std::process::Command` + // * automatically adds the executable name as the first argument let mut process = Process::new(self.path.path()) - .args(arguments) + .args(&arguments[1..]) .spawn() .replace_err(|| executable_err!(PathNoLongerExists: self.path))?; diff --git a/src/state/config.rs b/src/state/config.rs index 6610dfa..8bd85db 100644 --- a/src/state/config.rs +++ b/src/state/config.rs @@ -11,10 +11,10 @@ use crate::errors::{Handle, Result}; pub struct Configuration { /// The truncation length for the prompt pub truncation_factor: Option, - /// Whether to show the prompt tick on a new line - pub multi_line_prompt: bool, /// How many directories to store in the back/forward history pub history_limit: Option, + /// Whether to show the prompt tick on a new line + pub multiline_prompt: bool, /// Whether or not to print out full error messages and status codes when a command fails pub show_errors: bool, /// Paths to recursively search for plugins @@ -25,8 +25,8 @@ impl Default for Configuration { fn default() -> Self { Self { truncation_factor: None, - multi_line_prompt: false, history_limit: None, + multiline_prompt: false, show_errors: true, plugin_paths: vec![], } @@ -72,12 +72,6 @@ impl Configuration { ); } } - "multi-line-prompt" => { - config.multi_line_prompt = value.parse::().replace_err_with_msg( - || file_err!(CouldNotReadFile: filename), - &read_error_msg, - )?; - } "history-limit" => { if let Ok(limit) = value.parse::() { config.history_limit = Some(limit); @@ -85,6 +79,12 @@ impl Configuration { config.history_limit = None; } } + "multiline-prompt" => { + config.multiline_prompt = value.parse::().replace_err_with_msg( + || file_err!(CouldNotReadFile: filename), + &read_error_msg, + )?; + } "show-errors" => { config.show_errors = value.parse::().replace_err_with_msg( || file_err!(CouldNotReadFile: filename), diff --git a/src/state/environment.rs b/src/state/environment.rs index 5d19394..96c3152 100644 --- a/src/state/environment.rs +++ b/src/state/environment.rs @@ -4,17 +4,19 @@ use std::fmt::{Display, Formatter}; use std::path::{Path as StdPath, PathBuf}; use bitflags::bitflags; +use clap::ValueEnum; use super::path::Path; use crate::errors::{Handle, Result}; /// Identifier enum for safely accessing environment variables -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)] pub enum EnvVariable { - User, - Home, - Cwd, - Path, + USER, + HOME, + CWD, + PATH, } impl Display for EnvVariable { @@ -23,10 +25,10 @@ impl Display for EnvVariable { f, "{}", match self { - Self::User => "USER", - Self::Home => "HOME", - Self::Cwd => "CWD", - Self::Path => "PATH", + Self::USER => "USER", + Self::HOME => "HOME", + Self::CWD => "CWD", + Self::PATH => "PATH", } ) } @@ -36,10 +38,10 @@ impl EnvVariable { /// Does the same thing as `.to_string()`, but uses legacy environment variable names fn to_legacy_string(self) -> String { match self { - Self::User => "USER".to_string(), - Self::Home => "HOME".to_string(), - Self::Cwd => "PWD".to_string(), - Self::Path => "PATH".to_string(), + Self::USER => "USER".to_owned(), + Self::HOME => "HOME".to_owned(), + Self::CWD => "PWD".to_owned(), + Self::PATH => "PATH".to_owned(), } } } @@ -74,10 +76,10 @@ pub struct Environment { #[allow(non_snake_case)] impl Environment { pub fn new() -> Result { - let USER = get_parent_env_var(EnvVariable::User)?; - let HOME = PathBuf::from(get_parent_env_var(EnvVariable::Home)?); - let CWD = Path::try_from_str(get_parent_env_var(EnvVariable::Cwd)?.as_str(), &HOME)?; - let PATH = convert_path(get_parent_env_var(EnvVariable::Path)?.as_str(), &HOME)?; + let USER = get_parent_env_var(EnvVariable::USER)?; + let HOME = PathBuf::from(get_parent_env_var(EnvVariable::HOME)?); + let CWD = Path::try_from_str(get_parent_env_var(EnvVariable::CWD)?.as_str(), Some(&HOME))?; + let PATH = convert_path_var(get_parent_env_var(EnvVariable::PATH)?.as_str())?; Ok(Self { USER, @@ -103,16 +105,16 @@ impl Environment { if vars.contains(EnvVariables::CWD) { env::set_current_dir(self.CWD.path()) - .replace_err(|| state_err!(CouldNotUpdateEnv: EnvVariable::Cwd))?; + .replace_err(|| state_err!(CouldNotUpdateEnv: EnvVariable::CWD))?; } Ok(()) } /// Sets the current working directory and stores the previous working directory - pub fn set_CWD(&mut self, new_directory: &str, history_limit: Option) -> Result<()> { + pub fn set_CWD(&mut self, new_directory: &StdPath, history_limit: Option) -> Result<()> { let starting_directory = self.CWD.clone(); - let new_directory = Path::try_from_str(new_directory, &self.HOME)?; + let new_directory = Path::try_from_path(new_directory, Some(&self.HOME))?; // Add the old directory to the history, avoiding duplicates if new_directory != starting_directory { @@ -163,12 +165,12 @@ fn get_parent_env_var(variable: EnvVariable) -> Result { } /// Converts the PATH environment variable from a string to a collection of `Path`s -fn convert_path(path: &str, home: &StdPath) -> Result> { +fn convert_path_var(path: &str) -> Result> { let mut paths = VecDeque::new(); - let path_strings = path.split(':').collect::>(); + for path_string in path_strings { - if let Ok(path) = Path::try_from_str(path_string, home) { + if let Ok(path) = Path::try_from_str(path_string, None) { paths.push_back(path); } } diff --git a/src/state/mod.rs b/src/state/mod.rs index 7507562..b5139f3 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -1,7 +1,7 @@ mod config; -pub mod environment; -pub mod path; -pub mod shell; +mod environment; +mod path; +mod shell; pub use environment::{EnvVariable, EnvVariables, Environment}; pub use path::Path; diff --git a/src/state/path.rs b/src/state/path.rs index 64fb3ba..6a213c0 100644 --- a/src/state/path.rs +++ b/src/state/path.rs @@ -28,10 +28,14 @@ impl From for PathBuf { impl Path { /// Attempts to construct a new `Path` from a string by resolving it to an absolute path - pub fn try_from_str(path: &str, home_directory: &StdPath) -> Result { + pub fn try_from_str(path: &str, home_directory: Option<&StdPath>) -> Result { // The home directory shorthand must be expanded before resolving the path, // because PathBuf is not user-aware and only uses absolute and relative paths - let expanded_path = expand_home(path, home_directory)?; + let expanded_path = match home_directory { + Some(home_directory) => expand_home(path, home_directory)?, + None => PathBuf::from(path), + }; + // Canonicalizing a path will resolve any relative or absolute paths let absolute_path = canonicalize(&expanded_path) .replace_err(|| file_err!(CouldNotCanonicalize: expanded_path))?; @@ -45,6 +49,15 @@ impl Path { } } + /// Attempts to construct a new `Path` from a `std::path::Path` by resolving it to an absolute path + pub fn try_from_path(path: &StdPath, home_directory: Option<&StdPath>) -> Result { + let path_string = path + .to_str() + .replace_err(|| file_err!(FailedToConvertPathToString: path))?; + + Self::try_from_str(path_string, home_directory) + } + /// Attempts to locate an executable file in the PATH // ? Should this be a method of `Environment` instead? pub fn try_resolve_executable(name: &str, path: &VecDeque) -> Result { diff --git a/src/state/shell.rs b/src/state/shell.rs index 0ab7020..41442ef 100644 --- a/src/state/shell.rs +++ b/src/state/shell.rs @@ -33,7 +33,7 @@ impl ShellState { let home = &self.environment.HOME; let truncation = self.config.truncation_factor; let cwd = self.environment.CWD.collapse(home, truncation); - let prompt_delimiter = match self.config.multi_line_prompt { + let prompt_delimiter = match self.config.multiline_prompt { true => "\n", false => " ", };