blob: 75af21453c12cff3cf35b368cfa13644d99ea60a [file] [log] [blame] [edit]
//! 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<str>`] 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<String> { 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<Option<Cow<'static, str>>, 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<str>`], 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<PathBuf> { Some(Path::new("/home/user").into()) }
///
/// fn get_env(name: &str) -> Result<Option<&'static str>, &'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<SI: ?Sized, CO, C, E, P, HD>(
input: &SI,
home_dir: HD,
context: C,
) -> Result<Cow<str>, LookupError<E>>
where
SI: AsRef<str>,
CO: AsRef<str>,
C: FnMut(&str) -> Result<Option<CO>, E>,
P: AsRef<Path>,
HD: FnOnce() -> Option<P>,
{
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<CO>`] instead of [`Result<Option<CO>, E>`]. Therefore, the function itself also
/// returns just [`Cow<str>`] instead of [`Result<Cow<str>, LookupError<E>>`]. Otherwise it is
/// identical to [`full_with_context()`].
///
/// # Examples
///
/// ```
/// use std::path::{PathBuf, Path};
/// use std::borrow::Cow;
///
/// fn home_dir() -> Option<PathBuf> { 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<SI: ?Sized, CO, C, P, HD>(
input: &SI,
home_dir: HD,
mut context: C,
) -> Cow<str>
where
SI: AsRef<str>,
CO: AsRef<str>,
C: FnMut(&str) -> Option<CO>,
P: AsRef<Path>,
HD: FnOnce() -> Option<P>,
{
match full_with_context(input, home_dir, move |s| Ok::<Option<CO>, ()>(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<SI: ?Sized>(input: &SI) -> Result<Cow<str>, LookupError<VarError>>
where
SI: AsRef<str>,
{
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<E> {
/// 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<E: fmt::Display> fmt::Display for LookupError<E> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"error looking key '{}' up: {}",
self.var_name, self.cause
)
}
}
impl<E: Error + 'static> Error for LookupError<E> {
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 `$<number>` 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<str>`]'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<Option<&'static str>, &'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<SI: ?Sized, CO, C, E>(
input: &SI,
mut context: C,
) -> Result<Cow<str>, LookupError<E>>
where
SI: AsRef<str>,
CO: AsRef<str>,
C: FnMut(&str) -> Result<Option<CO>, 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<CO>` instead of `FnMut(&str) -> Result<Option<CO>, E>`. This simplifies
/// the API when you know in advance that the context lookups may not fail.
///
/// Because of the above, instead of [`Result<Cow<str>, LookupError<E>>`] this function returns just
/// [`Cow<str>`].
///
/// 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<SI: ?Sized, CO, C>(input: &SI, mut context: C) -> Cow<str>
where
SI: AsRef<str>,
CO: AsRef<str>,
C: FnMut(&str) -> Option<CO>,
{
match env_with_context(input, move |s| Ok::<Option<CO>, ()>(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<SI: ?Sized>(input: &SI) -> Result<Cow<str>, LookupError<VarError>>
where
SI: AsRef<str>,
{
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<str>`], and `P` must be a type, a
/// reference to which can be converted to a `Path` via [`AsRef<Path>`]. For example, `P` may be
/// [`Path`], [`std::path::PathBuf`] or [`Cow<Path>`], 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<PathBuf> { 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<SI: ?Sized, P, HD>(input: &SI, home_dir: HD) -> Cow<str>
where
SI: AsRef<str>,
P: AsRef<Path>,
HD: FnOnce() -> Option<P>,
{
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<SI: ?Sized>(input: &SI) -> Cow<str>
where
SI: AsRef<str>,
{
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<PathBuf> {
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<PathBuf> {
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<Option<String>, ()> {
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<Option<String>, ()> {
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<Option<&'static str>, ()> {
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<PathBuf> {
Some(Path::new("$VAR").into())
}
fn env(s: &str) -> Result<Option<&'static str>, ()> {
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<PathBuf> {
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<PathBuf> {
Some(Path::new("C:\\users\\public").into())
}
assert_eq!(
tilde_with_context("~\\some\\dir", home_dir),
"C:\\users\\public\\some\\dir"
);
}
}