//! Provides functions for performing shell-like expansions in strings. //! //! In particular, the following expansions are supported: //! //! * tilde expansion, when `~` in the beginning of a string, like in `"~/some/path"`, //! is expanded into the home directory of the current user; //! * environment expansion, when `$A` or `${B}`, like in `"~/$A/${B}something"`, //! are expanded into their values in some environment. //! //! Environment expansion also supports default values with the familiar shell syntax, //! so for example `${UNSET_ENV:-42}` will use the specified default value, i.e. `42`, if //! the `UNSET_ENV` variable is not set in the environment. //! //! The source of external information for these expansions (home directory and environment //! variables) is called their *context*. The context is provided to these functions as a closure //! of the respective type. //! //! This crate provides both customizable functions, which require their context to be provided //! explicitly, and wrapper functions which use [`dirs::home_dir()`] and [`std::env::var()`] //! for obtaining home directory and environment variables, respectively. //! //! Also there is a "full" function which performs both tilde and environment //! expansion, but does it correctly, rather than just doing one after another: for example, //! if the string starts with a variable whose value starts with a `~`, then this tilde //! won't be expanded. //! //! All functions return [`Cow`] because it is possible for their input not to contain anything //! which triggers the expansion. In that case performing allocations can be avoided. //! //! Please note that by default unknown variables in environment expansion are left as they are //! and are not, for example, substituted with an empty string: //! //! ``` //! fn context(_: &str) -> Option { None } //! //! assert_eq!( //! shellexpand::env_with_context_no_errors("$A $B", context), //! "$A $B" //! ); //! ``` //! //! Environment expansion context allows for a very fine tweaking of how results should be handled, //! so it is up to the user to pass a context function which does the necessary thing. For example, //! [`env()`] and [`full()`] functions from this library pass all errors returned by [`std::env::var()`] //! through, therefore they will also return an error if some unknown environment //! variable is used, because [`std::env::var()`] returns an error in this case: //! //! ``` //! use std::env; //! //! // make sure that the variable indeed does not exist //! env::remove_var("MOST_LIKELY_NONEXISTING_VAR"); //! //! assert_eq!( //! shellexpand::env("$MOST_LIKELY_NONEXISTING_VAR"), //! Err(shellexpand::LookupError { //! var_name: "MOST_LIKELY_NONEXISTING_VAR".into(), //! cause: env::VarError::NotPresent //! }) //! ); //! ``` //! //! The author thinks that this approach is more useful than just substituting an empty string //! (like, for example, does Go with its [os.ExpandEnv](https://golang.org/pkg/os/#ExpandEnv) //! function), but if you do need `os.ExpandEnv`-like behavior, it is fairly easy to get one: //! //! ``` //! use std::env; //! use std::borrow::Cow; //! //! fn context(s: &str) -> Result>, env::VarError> { //! match env::var(s) { //! Ok(value) => Ok(Some(value.into())), //! Err(env::VarError::NotPresent) => Ok(Some("".into())), //! Err(e) => Err(e) //! } //! } //! //! // make sure that the variable indeed does not exist //! env::remove_var("MOST_LIKELY_NONEXISTING_VAR"); //! //! assert_eq!( //! shellexpand::env_with_context("a${MOST_LIKELY_NOEXISTING_VAR}b", context).unwrap(), //! "ab" //! ); //! ``` //! //! The above example also demonstrates the flexibility of context function signatures: the context //! function may return anything which can be `AsRef`ed into a string slice. use std::borrow::Cow; use std::env::VarError; use std::error::Error; use std::fmt; use std::path::Path; /// Performs both tilde and environment expansion using the provided contexts. /// /// `home_dir` and `context` are contexts for tilde expansion and environment expansion, /// respectively. See [`env_with_context()`] and [`tilde_with_context()`] for more details on /// them. /// /// Unfortunately, expanding both `~` and `$VAR`s at the same time is not that simple. First, /// this function has to track ownership of the data. Since all functions in this crate /// return [`Cow`], this function takes some precautions in order not to allocate more than /// necessary. In particular, if the input string contains neither tilde nor `$`-vars, this /// function will perform no allocations. /// /// Second, if the input string starts with a variable, and the value of this variable starts /// with tilde, the naive approach may result into expansion of this tilde. This function /// avoids this. /// /// # Examples /// /// ``` /// use std::path::{PathBuf, Path}; /// use std::borrow::Cow; /// /// fn home_dir() -> Option { Some(Path::new("/home/user").into()) } /// /// fn get_env(name: &str) -> Result, &'static str> { /// match name { /// "A" => Ok(Some("a value")), /// "B" => Ok(Some("b value")), /// "T" => Ok(Some("~")), /// "E" => Err("some error"), /// _ => Ok(None) /// } /// } /// /// // Performs both tilde and environment expansions /// assert_eq!( /// shellexpand::full_with_context("~/$A/$B", home_dir, get_env).unwrap(), /// "/home/user/a value/b value" /// ); /// /// // Errors from environment expansion are propagated to the result /// assert_eq!( /// shellexpand::full_with_context("~/$E/something", home_dir, get_env), /// Err(shellexpand::LookupError { /// var_name: "E".into(), /// cause: "some error" /// }) /// ); /// /// // Input without starting tilde and without variables does not cause allocations /// let s = shellexpand::full_with_context("some/path", home_dir, get_env); /// match s { /// Ok(Cow::Borrowed(s)) => assert_eq!(s, "some/path"), /// _ => unreachable!("the above variant is always valid") /// } /// /// // Input with a tilde inside a variable in the beginning of the string does not cause tilde /// // expansion /// assert_eq!( /// shellexpand::full_with_context("$T/$A/$B", home_dir, get_env).unwrap(), /// "~/a value/b value" /// ); /// ``` pub fn full_with_context( input: &SI, home_dir: HD, context: C, ) -> Result, LookupError> where SI: AsRef, CO: AsRef, C: FnMut(&str) -> Result, E>, P: AsRef, HD: FnOnce() -> Option

, { env_with_context(input, context).map(|r| match r { // variable expansion did not modify the original string, so we can apply tilde expansion // directly Cow::Borrowed(s) => tilde_with_context(s, home_dir), Cow::Owned(s) => { // if the original string does not start with a tilde but the processed one does, // then the tilde is contained in one of variables and should not be expanded if !input.as_ref().starts_with('~') && s.starts_with('~') { // return as is s.into() } else if let Cow::Owned(s) = tilde_with_context(&s, home_dir) { s.into() } else { s.into() } } }) } /// Same as [`full_with_context()`], but forbids the variable lookup function to return errors. /// /// This function also performs full shell-like expansion, but it uses /// [`env_with_context_no_errors()`] for environment expansion whose context lookup function returns /// just [`Option`] instead of [`Result, E>`]. Therefore, the function itself also /// returns just [`Cow`] instead of [`Result, LookupError>`]. Otherwise it is /// identical to [`full_with_context()`]. /// /// # Examples /// /// ``` /// use std::path::{PathBuf, Path}; /// use std::borrow::Cow; /// /// fn home_dir() -> Option { Some(Path::new("/home/user").into()) } /// /// fn get_env(name: &str) -> Option<&'static str> { /// match name { /// "A" => Some("a value"), /// "B" => Some("b value"), /// "T" => Some("~"), /// _ => None /// } /// } /// /// // Performs both tilde and environment expansions /// assert_eq!( /// shellexpand::full_with_context_no_errors("~/$A/$B", home_dir, get_env), /// "/home/user/a value/b value" /// ); /// /// // Input without starting tilde and without variables does not cause allocations /// let s = shellexpand::full_with_context_no_errors("some/path", home_dir, get_env); /// match s { /// Cow::Borrowed(s) => assert_eq!(s, "some/path"), /// _ => unreachable!("the above variant is always valid") /// } /// /// // Input with a tilde inside a variable in the beginning of the string does not cause tilde /// // expansion /// assert_eq!( /// shellexpand::full_with_context_no_errors("$T/$A/$B", home_dir, get_env), /// "~/a value/b value" /// ); /// ``` #[inline] pub fn full_with_context_no_errors( input: &SI, home_dir: HD, mut context: C, ) -> Cow where SI: AsRef, CO: AsRef, C: FnMut(&str) -> Option, P: AsRef, HD: FnOnce() -> Option

, { match full_with_context(input, home_dir, move |s| Ok::, ()>(context(s))) { Ok(result) => result, Err(_) => unreachable!(), } } /// Performs both tilde and environment expansions in the default system context. /// /// This function delegates to [`full_with_context()`], using the default system sources for both /// home directory and environment, namely [`dirs::home_dir()`] and [`std::env::var()`]. /// /// Note that variable lookup of unknown variables will fail with an error instead of, for example, /// replacing the unknown variable with an empty string. The author thinks that this behavior is /// more useful than the other ones. If you need to change it, use [`full_with_context()`] or /// [`full_with_context_no_errors()`] with an appropriate context function instead. /// /// This function behaves exactly like [`full_with_context()`] in regard to tilde-containing /// variables in the beginning of the input string. /// /// # Examples /// /// ``` /// use std::env; /// /// env::set_var("A", "a value"); /// env::set_var("B", "b value"); /// /// let home_dir = dirs::home_dir() /// .map(|p| p.display().to_string()) /// .unwrap_or_else(|| "~".to_owned()); /// /// // Performs both tilde and environment expansions using the system contexts /// assert_eq!( /// shellexpand::full("~/$A/${B}s").unwrap(), /// format!("{}/a value/b values", home_dir) /// ); /// /// // Unknown variables cause expansion errors /// assert_eq!( /// shellexpand::full("~/$UNKNOWN/$B"), /// Err(shellexpand::LookupError { /// var_name: "UNKNOWN".into(), /// cause: env::VarError::NotPresent /// }) /// ); /// ``` #[inline] pub fn full(input: &SI) -> Result, LookupError> where SI: AsRef, { full_with_context(input, dirs::home_dir, |s| std::env::var(s).map(Some)) } /// Represents a variable lookup error. /// /// This error is returned by [`env_with_context()`] function (and, therefore, also by [`env()`], /// [`full_with_context()`] and [`full()`]) when the provided context function returns an error. The /// original error is provided in the `cause` field, while `name` contains the name of a variable /// whose expansion caused the error. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LookupError { /// The name of the problematic variable inside the input string. pub var_name: String, /// The original error returned by the context function. pub cause: E, } impl fmt::Display for LookupError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "error looking key '{}' up: {}", self.var_name, self.cause ) } } impl Error for LookupError { fn source(&self) -> Option<&(dyn Error + 'static)> { Some(&self.cause) } } macro_rules! try_lookup { ($name:expr, $e:expr) => { match $e { Ok(s) => s, Err(e) => { return Err(LookupError { var_name: $name.into(), cause: e, }) } } }; } fn is_valid_var_name_char(c: char) -> bool { c.is_alphanumeric() || c == '_' } /// Performs the environment expansion using the provided context. /// /// This function walks through the input string `input` and attempts to construct a new string by /// replacing all shell-like variable sequences with the corresponding values obtained via the /// `context` function. The latter may return an error; in this case the error will be returned /// immediately, along with the name of the offending variable. Also the context function may /// return [`Ok(None)`], indicating that the given variable is not available; in this case the /// variable sequence is left as it is in the output string. /// /// The syntax of variables resembles the one of bash-like shells: all of `$VAR`, `${VAR}`, /// `$NAME_WITH_UNDERSCORES` are valid variable references, and the form with braces may be used to /// separate the reference from the surrounding alphanumeric text: `before${VAR}after`. Note, /// however, that for simplicity names like `$123` or `$1AB` are also valid, as opposed to shells /// where `$` has special meaning of positional arguments. Also note that "alphanumericity" /// of variable names is checked with [`std::primitive::char::is_alphanumeric()`], therefore lots of characters which /// are considered alphanumeric by the Unicode standard are also valid names for variables. When /// unsure, use braces to separate variables from the surrounding text. /// /// This function has four generic type parameters: `SI` represents the input string, `CO` is the /// output of context lookups, `C` is the context closure and `E` is the type of errors returned by /// the context function. `SI` and `CO` must be types, a references to which can be converted to /// a string slice. For example, it is fine for the context function to return [`&str`]'s, [`String`]'s or /// [`Cow`]'s, which gives the user a lot of flexibility. /// /// If the context function returns an error, it will be wrapped into [`LookupError`] and returned /// immediately. [`LookupError`], besides the original error, also contains a string with the name of /// the variable whose expansion caused the error. [`LookupError`] implements [`Error`], [`Clone`] and /// [`Eq`] traits for further convenience and interoperability. /// /// If you need to expand system environment variables, you can use [`env()`] or [`full()`] functions. /// If your context does not have errors, you may use [`env_with_context_no_errors()`] instead of /// this function because it provides a simpler API. /// /// # Examples /// /// ``` /// fn context(s: &str) -> Result, &'static str> { /// match s { /// "A" => Ok(Some("a value")), /// "B" => Ok(Some("b value")), /// "E" => Err("something went wrong"), /// _ => Ok(None) /// } /// } /// /// // Regular variables are expanded /// assert_eq!( /// shellexpand::env_with_context("begin/$A/${B}s/end", context).unwrap(), /// "begin/a value/b values/end" /// ); /// /// // Expand to a default value if the variable is not defined /// assert_eq!( /// shellexpand::env_with_context("begin/${UNSET_ENV:-42}/end", context).unwrap(), /// "begin/42/end" /// ); /// /// // Unknown variables are left as is /// assert_eq!( /// shellexpand::env_with_context("begin/$UNKNOWN/end", context).unwrap(), /// "begin/$UNKNOWN/end" /// ); /// /// // Errors are propagated /// assert_eq!( /// shellexpand::env_with_context("begin${E}end", context), /// Err(shellexpand::LookupError { /// var_name: "E".into(), /// cause: "something went wrong" /// }) /// ); /// ``` pub fn env_with_context( input: &SI, mut context: C, ) -> Result, LookupError> where SI: AsRef, CO: AsRef, C: FnMut(&str) -> Result, E>, { let input_str = input.as_ref(); if let Some(idx) = input_str.find('$') { let mut result = String::with_capacity(input_str.len()); let mut input_str = input_str; let mut next_dollar_idx = idx; loop { result.push_str(&input_str[..next_dollar_idx]); input_str = &input_str[next_dollar_idx..]; if input_str.is_empty() { break; } fn find_dollar(s: &str) -> usize { s.find('$').unwrap_or(s.len()) } let next_char = input_str[1..].chars().next(); if next_char == Some('{') { match input_str.find('}') { Some(closing_brace_idx) => { let mut default_value = None; // Search for the default split let var_name_end_idx = match input_str[..closing_brace_idx].find(":-") { // Only match if there's a variable name, ie. this is not valid ${:-value} Some(default_split_idx) if default_split_idx != 2 => { default_value = Some(&input_str[default_split_idx + 2..closing_brace_idx]); default_split_idx } _ => closing_brace_idx, }; let var_name = &input_str[2..var_name_end_idx]; match context(var_name) { // if we have the variable set to some value Ok(Some(var_value)) => { result.push_str(var_value.as_ref()); input_str = &input_str[closing_brace_idx + 1..]; next_dollar_idx = find_dollar(input_str); } // if the variable is set and empty or unset not_found_or_empty => { let value = match (not_found_or_empty, default_value) { // return an error if we don't have a default and the variable is unset (Err(err), None) => { return Err(LookupError { var_name: var_name.into(), cause: err, }); } // use the default value if set (_, Some(default)) => default, // leave the variable as it is if the environment is empty (_, None) => &input_str[..closing_brace_idx + 1], }; result.push_str(value); input_str = &input_str[closing_brace_idx + 1..]; next_dollar_idx = find_dollar(input_str); } } } // unbalanced braces None => { result.push_str(&input_str[..2]); input_str = &input_str[2..]; next_dollar_idx = find_dollar(input_str); } } } else if next_char.map(is_valid_var_name_char) == Some(true) { let end_idx = 2 + input_str[2..] .find(|c: char| !is_valid_var_name_char(c)) .unwrap_or(input_str.len() - 2); let var_name = &input_str[1..end_idx]; match try_lookup!(var_name, context(var_name)) { Some(var_value) => { result.push_str(var_value.as_ref()); input_str = &input_str[end_idx..]; next_dollar_idx = find_dollar(input_str); } None => { result.push_str(&input_str[..end_idx]); input_str = &input_str[end_idx..]; next_dollar_idx = find_dollar(input_str); } } } else { result.push('$'); input_str = if next_char == Some('$') { &input_str[2..] // skip the next dollar for escaping } else { &input_str[1..] }; next_dollar_idx = find_dollar(input_str); }; } Ok(result.into()) } else { Ok(input_str.into()) } } /// Same as [`env_with_context()`], but forbids the variable lookup function to return errors. /// /// This function also performs environment expansion, but it requires context function of type /// `FnMut(&str) -> Option` instead of `FnMut(&str) -> Result, E>`. This simplifies /// the API when you know in advance that the context lookups may not fail. /// /// Because of the above, instead of [`Result, LookupError>`] this function returns just /// [`Cow`]. /// /// Note that if the context function returns [`None`], the behavior remains the same as that of /// [`env_with_context()`]: the variable reference will remain in the output string unexpanded. /// /// # Examples /// /// ``` /// fn context(s: &str) -> Option<&'static str> { /// match s { /// "A" => Some("a value"), /// "B" => Some("b value"), /// _ => None /// } /// } /// /// // Known variables are expanded /// assert_eq!( /// shellexpand::env_with_context_no_errors("begin/$A/${B}s/end", context), /// "begin/a value/b values/end" /// ); /// /// // Unknown variables are left as is /// assert_eq!( /// shellexpand::env_with_context_no_errors("begin/$U/end", context), /// "begin/$U/end" /// ); /// ``` #[inline] pub fn env_with_context_no_errors(input: &SI, mut context: C) -> Cow where SI: AsRef, CO: AsRef, C: FnMut(&str) -> Option, { match env_with_context(input, move |s| Ok::, ()>(context(s))) { Ok(value) => value, Err(_) => unreachable!(), } } /// Performs the environment expansion using the default system context. /// /// This function delegates to [`env_with_context()`], using the default system source for /// environment variables, namely the [`std::env::var()`] function. /// /// Note that variable lookup of unknown variables will fail with an error instead of, for example, /// replacing the offending variables with an empty string. The author thinks that such behavior is /// more useful than the other ones. If you need something else, use [`env_with_context()`] or /// [`env_with_context_no_errors()`] with an appropriate context function. /// /// # Examples /// /// ``` /// use std::env; /// /// // make sure that some environment variables are set /// env::set_var("X", "x value"); /// env::set_var("Y", "y value"); /// /// // Known variables are expanded /// assert_eq!( /// shellexpand::env("begin/$X/${Y}s/end").unwrap(), /// "begin/x value/y values/end" /// ); /// /// // Unknown variables result in an error /// assert_eq!( /// shellexpand::env("begin/$Z/end"), /// Err(shellexpand::LookupError { /// var_name: "Z".into(), /// cause: env::VarError::NotPresent /// }) /// ); /// ``` #[inline] pub fn env(input: &SI) -> Result, LookupError> where SI: AsRef, { env_with_context(input, |s| std::env::var(s).map(Some)) } /// Performs the tilde expansion using the provided context. /// /// This function expands tilde (`~`) character in the beginning of the input string into contents /// of the path returned by `home_dir` function. If the input string does not contain a tilde, or /// if it is not followed either by a slash (`/`) or by the end of string, then it is also left as /// is. This means, in particular, that expansions like `~anotheruser/directory` are not supported. /// The context function may also return a `None`, in that case even if the tilde is present in the /// input in the correct place, it won't be replaced (there is nothing to replace it with, after /// all). /// /// This function has three generic type parameters: `SI` represents the input string, `P` is the /// output of a context lookup, and `HD` is the context closure. `SI` must be a type, a reference /// to which can be converted to a string slice via [`AsRef`], and `P` must be a type, a /// reference to which can be converted to a `Path` via [`AsRef`]. For example, `P` may be /// [`Path`], [`std::path::PathBuf`] or [`Cow`], which gives a lot of flexibility. /// /// If you need to expand the tilde into the actual user home directory, you can use [`tilde()`] or /// [`full()`] functions. /// /// # Examples /// /// ``` /// use std::path::{PathBuf, Path}; /// /// fn home_dir() -> Option { Some(Path::new("/home/user").into()) } /// /// assert_eq!( /// shellexpand::tilde_with_context("~/some/dir", home_dir), /// "/home/user/some/dir" /// ); /// ``` pub fn tilde_with_context(input: &SI, home_dir: HD) -> Cow where SI: AsRef, P: AsRef, HD: FnOnce() -> Option

, { let input_str = input.as_ref(); if let Some(input_after_tilde) = input_str.strip_prefix('~') { if input_after_tilde.is_empty() || input_after_tilde.starts_with('/') || (cfg!(windows) && input_after_tilde.starts_with('\\')) { if let Some(hd) = home_dir() { let result = format!("{}{}", hd.as_ref().display(), input_after_tilde); result.into() } else { // home dir is not available input_str.into() } } else { // we cannot handle `~otheruser/` paths yet input_str.into() } } else { // input doesn't start with tilde input_str.into() } } /// Performs the tilde expansion using the default system context. /// /// This function delegates to [`tilde_with_context()`], using the default system source of home /// directory path, namely [`dirs::home_dir()`] function. /// /// # Examples /// /// ``` /// let hds = dirs::home_dir() /// .map(|p| p.display().to_string()) /// .unwrap_or_else(|| "~".to_owned()); /// /// assert_eq!( /// shellexpand::tilde("~/some/dir"), /// format!("{}/some/dir", hds) /// ); /// ``` #[inline] pub fn tilde(input: &SI) -> Cow where SI: AsRef, { tilde_with_context(input, dirs::home_dir) } #[cfg(test)] mod tilde_tests { use std::path::{Path, PathBuf}; use super::{tilde, tilde_with_context}; #[test] fn test_with_tilde_no_hd() { fn hd() -> Option { None } assert_eq!(tilde_with_context("whatever", hd), "whatever"); assert_eq!(tilde_with_context("whatever/~", hd), "whatever/~"); assert_eq!(tilde_with_context("~/whatever", hd), "~/whatever"); assert_eq!(tilde_with_context("~", hd), "~"); assert_eq!(tilde_with_context("~something", hd), "~something"); } #[test] fn test_with_tilde() { fn hd() -> Option { Some(Path::new("/home/dir").into()) } assert_eq!(tilde_with_context("whatever/path", hd), "whatever/path"); assert_eq!(tilde_with_context("whatever/~/path", hd), "whatever/~/path"); assert_eq!(tilde_with_context("~", hd), "/home/dir"); assert_eq!(tilde_with_context("~/path", hd), "/home/dir/path"); assert_eq!(tilde_with_context("~whatever/path", hd), "~whatever/path"); } #[test] fn test_global_tilde() { match dirs::home_dir() { Some(hd) => assert_eq!(tilde("~/something"), format!("{}/something", hd.display())), None => assert_eq!(tilde("~/something"), "~/something"), } } } #[cfg(test)] mod env_test { use std; use super::{env, env_with_context, LookupError}; macro_rules! table { ($env:expr, unwrap, $($source:expr => $target:expr),+) => { $( assert_eq!(env_with_context($source, $env).unwrap(), $target); )+ }; ($env:expr, error, $($source:expr => $name:expr),+) => { $( assert_eq!(env_with_context($source, $env), Err(LookupError { var_name: $name.into(), cause: () })); )+ } } #[test] fn test_empty_env() { fn e(_: &str) -> Result, ()> { Ok(None) } table! { e, unwrap, "whatever/path" => "whatever/path", "$VAR/whatever/path" => "$VAR/whatever/path", "whatever/$VAR/path" => "whatever/$VAR/path", "whatever/path/$VAR" => "whatever/path/$VAR", "${VAR}/whatever/path" => "${VAR}/whatever/path", "whatever/${VAR}path" => "whatever/${VAR}path", "whatever/path/${VAR}" => "whatever/path/${VAR}", "${}/whatever/path" => "${}/whatever/path", "whatever/${}path" => "whatever/${}path", "whatever/path/${}" => "whatever/path/${}", "$/whatever/path" => "$/whatever/path", "whatever/$path" => "whatever/$path", "whatever/path/$" => "whatever/path/$", "$$/whatever/path" => "$/whatever/path", "whatever/$$path" => "whatever/$path", "whatever/path/$$" => "whatever/path/$", "$A$B$C" => "$A$B$C", "$A_B_C" => "$A_B_C" }; } #[test] fn test_error_env() { fn e(_: &str) -> Result, ()> { Err(()) } table! { e, unwrap, "whatever/path" => "whatever/path", // check that escaped $ does nothing "whatever/$/path" => "whatever/$/path", "whatever/path$" => "whatever/path$", "whatever/$$path" => "whatever/$path" }; table! { e, error, "$VAR/something" => "VAR", "${VAR}/something" => "VAR", "whatever/${VAR}/something" => "VAR", "whatever/${VAR}" => "VAR", "whatever/$VAR/something" => "VAR", "whatever/$VARsomething" => "VARsomething", "whatever/$VAR" => "VAR", "whatever/$VAR_VAR_VAR" => "VAR_VAR_VAR" }; } #[test] fn test_regular_env() { fn e(s: &str) -> Result, ()> { match s { "VAR" => Ok(Some("value")), "a_b" => Ok(Some("X_Y")), "EMPTY" => Ok(Some("")), "ERR" => Err(()), _ => Ok(None), } } table! { e, unwrap, // no variables "whatever/path" => "whatever/path", // empty string "" => "", // existing variable without braces in various positions "$VAR/whatever/path" => "value/whatever/path", "whatever/$VAR/path" => "whatever/value/path", "whatever/path/$VAR" => "whatever/path/value", "whatever/$VARpath" => "whatever/$VARpath", "$VAR$VAR/whatever" => "valuevalue/whatever", "/whatever$VAR$VAR" => "/whatevervaluevalue", "$VAR $VAR" => "value value", "$a_b" => "X_Y", "$a_b$VAR" => "X_Yvalue", // existing variable with braces in various positions "${VAR}/whatever/path" => "value/whatever/path", "whatever/${VAR}/path" => "whatever/value/path", "whatever/path/${VAR}" => "whatever/path/value", "whatever/${VAR}path" => "whatever/valuepath", "${VAR}${VAR}/whatever" => "valuevalue/whatever", "/whatever${VAR}${VAR}" => "/whatevervaluevalue", "${VAR} ${VAR}" => "value value", "${VAR}$VAR" => "valuevalue", // default values "/answer/${UNKNOWN:-42}" => "/answer/42", "/answer/${:-42}" => "/answer/${:-42}", "/whatever/${UNKNOWN:-other}$VAR" => "/whatever/othervalue", "/whatever/${UNKNOWN:-other}/$VAR" => "/whatever/other/value", ":-/whatever/${UNKNOWN:-other}/$VAR :-" => ":-/whatever/other/value :-", "/whatever/${VAR:-other}" => "/whatever/value", "/whatever/${VAR:-other}$VAR" => "/whatever/valuevalue", "/whatever/${VAR} :-" => "/whatever/value :-", "/whatever/${:-}" => "/whatever/${:-}", "/whatever/${UNKNOWN:-}" => "/whatever/", // empty variable in various positions "${EMPTY}/whatever/path" => "/whatever/path", "whatever/${EMPTY}/path" => "whatever//path", "whatever/path/${EMPTY}" => "whatever/path/" }; table! { e, error, "$ERR" => "ERR", "${ERR}" => "ERR" }; } #[test] fn test_global_env() { match std::env::var("PATH") { Ok(value) => assert_eq!(env("x/$PATH/x").unwrap(), format!("x/{}/x", value)), Err(e) => assert_eq!( env("x/$PATH/x"), Err(LookupError { var_name: "PATH".into(), cause: e }) ), } match std::env::var("SOMETHING_DEFINITELY_NONEXISTING") { Ok(value) => assert_eq!( env("x/$SOMETHING_DEFINITELY_NONEXISTING/x").unwrap(), format!("x/{}/x", value) ), Err(e) => assert_eq!( env("x/$SOMETHING_DEFINITELY_NONEXISTING/x"), Err(LookupError { var_name: "SOMETHING_DEFINITELY_NONEXISTING".into(), cause: e }) ), } } } #[cfg(test)] mod full_tests { use std::path::{Path, PathBuf}; use super::{full_with_context, tilde_with_context}; #[test] fn test_quirks() { fn hd() -> Option { Some(Path::new("$VAR").into()) } fn env(s: &str) -> Result, ()> { match s { "VAR" => Ok(Some("value")), "SVAR" => Ok(Some("/value")), "TILDE" => Ok(Some("~")), _ => Ok(None), } } // any variable-like sequence in ~ expansion should not trigger variable expansion assert_eq!( full_with_context("~/something/$VAR", hd, env).unwrap(), "$VAR/something/value" ); // variable just after tilde should be substituted first and trigger regular tilde // expansion assert_eq!(full_with_context("~$VAR", hd, env).unwrap(), "~value"); assert_eq!(full_with_context("~$SVAR", hd, env).unwrap(), "$VAR/value"); // variable expanded into a tilde in the beginning should not trigger tilde expansion assert_eq!( full_with_context("$TILDE/whatever", hd, env).unwrap(), "~/whatever" ); assert_eq!( full_with_context("${TILDE}whatever", hd, env).unwrap(), "~whatever" ); assert_eq!(full_with_context("$TILDE", hd, env).unwrap(), "~"); } #[test] fn test_tilde_expansion() { fn home_dir() -> Option { Some(Path::new("/home/user").into()) } assert_eq!( tilde_with_context("~/some/dir", home_dir), "/home/user/some/dir" ); } #[cfg(target_family = "windows")] #[test] fn test_tilde_expansion_windows() { fn home_dir() -> Option { Some(Path::new("C:\\users\\public").into()) } assert_eq!( tilde_with_context("~\\some\\dir", home_dir), "C:\\users\\public\\some\\dir" ); } }