shellexpand/
lib.rs

1//! Provides functions for performing shell-like expansions in strings.
2//!
3//! In particular, the following expansions are supported:
4//!
5//! * tilde expansion, when `~` in the beginning of a string, like in `"~/some/path"`,
6//!   is expanded into the home directory of the current user;
7//! * environment expansion, when `$A` or `${B}`, like in `"~/$A/${B}something"`,
8//!   are expanded into their values in some environment.
9//!
10//! Environment expansion also supports default values with the familiar shell syntax,
11//! so for example `${UNSET_ENV:-42}` will use the specified default value, i.e. `42`, if
12//! the `UNSET_ENV` variable is not set in the environment.
13//!
14//! The source of external information for these expansions (home directory and environment
15//! variables) is called their *context*. The context is provided to these functions as a closure
16//! of the respective type.
17//!
18//! This crate provides both customizable functions, which require their context to be provided
19//! explicitly, and wrapper functions which use [`dirs::home_dir()`] and [`std::env::var()`]
20//! for obtaining home directory and environment variables, respectively.
21//!
22//! Also there is a "full" function which performs both tilde and environment
23//! expansion, but does it correctly, rather than just doing one after another: for example,
24//! if the string starts with a variable whose value starts with a `~`, then this tilde
25//! won't be expanded.
26//!
27//! All functions return [`Cow<str>`] because it is possible for their input not to contain anything
28//! which triggers the expansion. In that case performing allocations can be avoided.
29//!
30//! Please note that by default unknown variables in environment expansion are left as they are
31//! and are not, for example, substituted with an empty string:
32//!
33//! ```
34//! fn context(_: &str) -> Option<String> { None }
35//!
36//! assert_eq!(
37//!     shellexpand::env_with_context_no_errors("$A $B", context),
38//!     "$A $B"
39//! );
40//! ```
41//!
42//! Environment expansion context allows for a very fine tweaking of how results should be handled,
43//! so it is up to the user to pass a context function which does the necessary thing. For example,
44//! [`env()`] and [`full()`] functions from this library pass all errors returned by [`std::env::var()`]
45//! through, therefore they will also return an error if some unknown environment
46//! variable is used, because [`std::env::var()`] returns an error in this case:
47//!
48//! ```
49//! use std::env;
50//!
51//! // make sure that the variable indeed does not exist
52//! env::remove_var("MOST_LIKELY_NONEXISTING_VAR");
53//!
54//! assert_eq!(
55//!     shellexpand::env("$MOST_LIKELY_NONEXISTING_VAR"),
56//!     Err(shellexpand::LookupError {
57//!         var_name: "MOST_LIKELY_NONEXISTING_VAR".into(),
58//!         cause: env::VarError::NotPresent
59//!     })
60//! );
61//! ```
62//!
63//! The author thinks that this approach is more useful than just substituting an empty string
64//! (like, for example, does Go with its [os.ExpandEnv](https://golang.org/pkg/os/#ExpandEnv)
65//! function), but if you do need `os.ExpandEnv`-like behavior, it is fairly easy to get one:
66//!
67//! ```
68//! use std::env;
69//! use std::borrow::Cow;
70//!
71//! fn context(s: &str) -> Result<Option<Cow<'static, str>>, env::VarError> {
72//!     match env::var(s) {
73//!         Ok(value) => Ok(Some(value.into())),
74//!         Err(env::VarError::NotPresent) => Ok(Some("".into())),
75//!         Err(e) => Err(e)
76//!     }
77//! }
78//!
79//! // make sure that the variable indeed does not exist
80//! env::remove_var("MOST_LIKELY_NONEXISTING_VAR");
81//!
82//! assert_eq!(
83//!     shellexpand::env_with_context("a${MOST_LIKELY_NOEXISTING_VAR}b", context).unwrap(),
84//!     "ab"
85//! );
86//! ```
87//!
88//! The above example also demonstrates the flexibility of context function signatures: the context
89//! function may return anything which can be `AsRef`ed into a string slice.
90
91use std::borrow::Cow;
92use std::env::VarError;
93use std::error::Error;
94use std::fmt;
95use std::path::Path;
96
97/// Performs both tilde and environment expansion using the provided contexts.
98///
99/// `home_dir` and `context` are contexts for tilde expansion and environment expansion,
100/// respectively. See [`env_with_context()`] and [`tilde_with_context()`] for more details on
101/// them.
102///
103/// Unfortunately, expanding both `~` and `$VAR`s at the same time is not that simple. First,
104/// this function has to track ownership of the data. Since all functions in this crate
105/// return [`Cow<str>`], this function takes some precautions in order not to allocate more than
106/// necessary. In particular, if the input string contains neither tilde nor `$`-vars, this
107/// function will perform no allocations.
108///
109/// Second, if the input string starts with a variable, and the value of this variable starts
110/// with tilde, the naive approach may result into expansion of this tilde. This function
111/// avoids this.
112///
113/// # Examples
114///
115/// ```
116/// use std::path::{PathBuf, Path};
117/// use std::borrow::Cow;
118///
119/// fn home_dir() -> Option<PathBuf> { Some(Path::new("/home/user").into()) }
120///
121/// fn get_env(name: &str) -> Result<Option<&'static str>, &'static str> {
122///     match name {
123///         "A" => Ok(Some("a value")),
124///         "B" => Ok(Some("b value")),
125///         "T" => Ok(Some("~")),
126///         "E" => Err("some error"),
127///         _ => Ok(None)
128///     }
129/// }
130///
131/// // Performs both tilde and environment expansions
132/// assert_eq!(
133///     shellexpand::full_with_context("~/$A/$B", home_dir, get_env).unwrap(),
134///     "/home/user/a value/b value"
135/// );
136///
137/// // Errors from environment expansion are propagated to the result
138/// assert_eq!(
139///     shellexpand::full_with_context("~/$E/something", home_dir, get_env),
140///     Err(shellexpand::LookupError {
141///         var_name: "E".into(),
142///         cause: "some error"
143///     })
144/// );
145///
146/// // Input without starting tilde and without variables does not cause allocations
147/// let s = shellexpand::full_with_context("some/path", home_dir, get_env);
148/// match s {
149///     Ok(Cow::Borrowed(s)) => assert_eq!(s, "some/path"),
150///     _ => unreachable!("the above variant is always valid")
151/// }
152///
153/// // Input with a tilde inside a variable in the beginning of the string does not cause tilde
154/// // expansion
155/// assert_eq!(
156///     shellexpand::full_with_context("$T/$A/$B", home_dir, get_env).unwrap(),
157///     "~/a value/b value"
158/// );
159/// ```
160pub fn full_with_context<SI: ?Sized, CO, C, E, P, HD>(
161    input: &SI,
162    home_dir: HD,
163    context: C,
164) -> Result<Cow<str>, LookupError<E>>
165where
166    SI: AsRef<str>,
167    CO: AsRef<str>,
168    C: FnMut(&str) -> Result<Option<CO>, E>,
169    P: AsRef<Path>,
170    HD: FnOnce() -> Option<P>,
171{
172    env_with_context(input, context).map(|r| match r {
173        // variable expansion did not modify the original string, so we can apply tilde expansion
174        // directly
175        Cow::Borrowed(s) => tilde_with_context(s, home_dir),
176        Cow::Owned(s) => {
177            // if the original string does not start with a tilde but the processed one does,
178            // then the tilde is contained in one of variables and should not be expanded
179            if !input.as_ref().starts_with('~') && s.starts_with('~') {
180                // return as is
181                s.into()
182            } else if let Cow::Owned(s) = tilde_with_context(&s, home_dir) {
183                s.into()
184            } else {
185                s.into()
186            }
187        }
188    })
189}
190
191/// Same as [`full_with_context()`], but forbids the variable lookup function to return errors.
192///
193/// This function also performs full shell-like expansion, but it uses
194/// [`env_with_context_no_errors()`] for environment expansion whose context lookup function returns
195/// just [`Option<CO>`] instead of [`Result<Option<CO>, E>`]. Therefore, the function itself also
196/// returns just [`Cow<str>`] instead of [`Result<Cow<str>, LookupError<E>>`]. Otherwise it is
197/// identical to [`full_with_context()`].
198///
199/// # Examples
200///
201/// ```
202/// use std::path::{PathBuf, Path};
203/// use std::borrow::Cow;
204///
205/// fn home_dir() -> Option<PathBuf> { Some(Path::new("/home/user").into()) }
206///
207/// fn get_env(name: &str) -> Option<&'static str> {
208///     match name {
209///         "A" => Some("a value"),
210///         "B" => Some("b value"),
211///         "T" => Some("~"),
212///         _ => None
213///     }
214/// }
215///
216/// // Performs both tilde and environment expansions
217/// assert_eq!(
218///     shellexpand::full_with_context_no_errors("~/$A/$B", home_dir, get_env),
219///     "/home/user/a value/b value"
220/// );
221///
222/// // Input without starting tilde and without variables does not cause allocations
223/// let s = shellexpand::full_with_context_no_errors("some/path", home_dir, get_env);
224/// match s {
225///     Cow::Borrowed(s) => assert_eq!(s, "some/path"),
226///     _ => unreachable!("the above variant is always valid")
227/// }
228///
229/// // Input with a tilde inside a variable in the beginning of the string does not cause tilde
230/// // expansion
231/// assert_eq!(
232///     shellexpand::full_with_context_no_errors("$T/$A/$B", home_dir, get_env),
233///     "~/a value/b value"
234/// );
235/// ```
236#[inline]
237pub fn full_with_context_no_errors<SI: ?Sized, CO, C, P, HD>(
238    input: &SI,
239    home_dir: HD,
240    mut context: C,
241) -> Cow<str>
242where
243    SI: AsRef<str>,
244    CO: AsRef<str>,
245    C: FnMut(&str) -> Option<CO>,
246    P: AsRef<Path>,
247    HD: FnOnce() -> Option<P>,
248{
249    match full_with_context(input, home_dir, move |s| Ok::<Option<CO>, ()>(context(s))) {
250        Ok(result) => result,
251        Err(_) => unreachable!(),
252    }
253}
254
255/// Performs both tilde and environment expansions in the default system context.
256///
257/// This function delegates to [`full_with_context()`], using the default system sources for both
258/// home directory and environment, namely [`dirs::home_dir()`] and [`std::env::var()`].
259///
260/// Note that variable lookup of unknown variables will fail with an error instead of, for example,
261/// replacing the unknown variable with an empty string. The author thinks that this behavior is
262/// more useful than the other ones. If you need to change it, use [`full_with_context()`] or
263/// [`full_with_context_no_errors()`] with an appropriate context function instead.
264///
265/// This function behaves exactly like [`full_with_context()`] in regard to tilde-containing
266/// variables in the beginning of the input string.
267///
268/// # Examples
269///
270/// ```
271/// use std::env;
272///
273/// env::set_var("A", "a value");
274/// env::set_var("B", "b value");
275///
276/// let home_dir = dirs::home_dir()
277///     .map(|p| p.display().to_string())
278///     .unwrap_or_else(|| "~".to_owned());
279///
280/// // Performs both tilde and environment expansions using the system contexts
281/// assert_eq!(
282///     shellexpand::full("~/$A/${B}s").unwrap(),
283///     format!("{}/a value/b values", home_dir)
284/// );
285///
286/// // Unknown variables cause expansion errors
287/// assert_eq!(
288///     shellexpand::full("~/$UNKNOWN/$B"),
289///     Err(shellexpand::LookupError {
290///         var_name: "UNKNOWN".into(),
291///         cause: env::VarError::NotPresent
292///     })
293/// );
294/// ```
295#[inline]
296pub fn full<SI: ?Sized>(input: &SI) -> Result<Cow<str>, LookupError<VarError>>
297where
298    SI: AsRef<str>,
299{
300    full_with_context(input, dirs::home_dir, |s| std::env::var(s).map(Some))
301}
302
303/// Represents a variable lookup error.
304///
305/// This error is returned by [`env_with_context()`] function (and, therefore, also by [`env()`],
306/// [`full_with_context()`] and [`full()`]) when the provided context function returns an error. The
307/// original error is provided in the `cause` field, while `name` contains the name of a variable
308/// whose expansion caused the error.
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct LookupError<E> {
311    /// The name of the problematic variable inside the input string.
312    pub var_name: String,
313    /// The original error returned by the context function.
314    pub cause: E,
315}
316
317impl<E: fmt::Display> fmt::Display for LookupError<E> {
318    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
319        write!(
320            f,
321            "error looking key '{}' up: {}",
322            self.var_name, self.cause
323        )
324    }
325}
326
327impl<E: Error + 'static> Error for LookupError<E> {
328    fn source(&self) -> Option<&(dyn Error + 'static)> {
329        Some(&self.cause)
330    }
331}
332
333macro_rules! try_lookup {
334    ($name:expr, $e:expr) => {
335        match $e {
336            Ok(s) => s,
337            Err(e) => {
338                return Err(LookupError {
339                    var_name: $name.into(),
340                    cause: e,
341                })
342            }
343        }
344    };
345}
346
347fn is_valid_var_name_char(c: char) -> bool {
348    c.is_alphanumeric() || c == '_'
349}
350
351/// Performs the environment expansion using the provided context.
352///
353/// This function walks through the input string `input` and attempts to construct a new string by
354/// replacing all shell-like variable sequences with the corresponding values obtained via the
355/// `context` function. The latter may return an error; in this case the error will be returned
356/// immediately, along with the name of the offending variable. Also the context function may
357/// return [`Ok(None)`], indicating that the given variable is not available; in this case the
358/// variable sequence is left as it is in the output string.
359///
360/// The syntax of variables resembles the one of bash-like shells: all of `$VAR`, `${VAR}`,
361/// `$NAME_WITH_UNDERSCORES` are valid variable references, and the form with braces may be used to
362/// separate the reference from the surrounding alphanumeric text: `before${VAR}after`. Note,
363/// however, that for simplicity names like `$123` or `$1AB` are also valid, as opposed to shells
364/// where `$<number>` has special meaning of positional arguments. Also note that "alphanumericity"
365/// of variable names is checked with [`std::primitive::char::is_alphanumeric()`], therefore lots of characters which
366/// are considered alphanumeric by the Unicode standard are also valid names for variables. When
367/// unsure, use braces to separate variables from the surrounding text.
368///
369/// This function has four generic type parameters: `SI` represents the input string, `CO` is the
370/// output of context lookups, `C` is the context closure and `E` is the type of errors returned by
371/// the context function. `SI` and `CO` must be types, a references to which can be converted to
372/// a string slice. For example, it is fine for the context function to return [`&str`]'s, [`String`]'s or
373/// [`Cow<str>`]'s, which gives the user a lot of flexibility.
374///
375/// If the context function returns an error, it will be wrapped into [`LookupError`] and returned
376/// immediately. [`LookupError`], besides the original error, also contains a string with the name of
377/// the variable whose expansion caused the error. [`LookupError`] implements [`Error`], [`Clone`] and
378/// [`Eq`] traits for further convenience and interoperability.
379///
380/// If you need to expand system environment variables, you can use [`env()`] or [`full()`] functions.
381/// If your context does not have errors, you may use [`env_with_context_no_errors()`] instead of
382/// this function because it provides a simpler API.
383///
384/// # Examples
385///
386/// ```
387/// fn context(s: &str) -> Result<Option<&'static str>, &'static str> {
388///     match s {
389///         "A" => Ok(Some("a value")),
390///         "B" => Ok(Some("b value")),
391///         "E" => Err("something went wrong"),
392///         _ => Ok(None)
393///     }
394/// }
395///
396/// // Regular variables are expanded
397/// assert_eq!(
398///     shellexpand::env_with_context("begin/$A/${B}s/end", context).unwrap(),
399///     "begin/a value/b values/end"
400/// );
401///
402/// // Expand to a default value if the variable is not defined
403/// assert_eq!(
404///     shellexpand::env_with_context("begin/${UNSET_ENV:-42}/end", context).unwrap(),
405///     "begin/42/end"
406/// );
407///
408/// // Unknown variables are left as is
409/// assert_eq!(
410///     shellexpand::env_with_context("begin/$UNKNOWN/end", context).unwrap(),
411///     "begin/$UNKNOWN/end"
412/// );
413///
414/// // Errors are propagated
415/// assert_eq!(
416///     shellexpand::env_with_context("begin${E}end", context),
417///     Err(shellexpand::LookupError {
418///         var_name: "E".into(),
419///         cause: "something went wrong"
420///     })
421/// );
422/// ```
423pub fn env_with_context<SI: ?Sized, CO, C, E>(
424    input: &SI,
425    mut context: C,
426) -> Result<Cow<str>, LookupError<E>>
427where
428    SI: AsRef<str>,
429    CO: AsRef<str>,
430    C: FnMut(&str) -> Result<Option<CO>, E>,
431{
432    let input_str = input.as_ref();
433    if let Some(idx) = input_str.find('$') {
434        let mut result = String::with_capacity(input_str.len());
435
436        let mut input_str = input_str;
437        let mut next_dollar_idx = idx;
438        loop {
439            result.push_str(&input_str[..next_dollar_idx]);
440
441            input_str = &input_str[next_dollar_idx..];
442            if input_str.is_empty() {
443                break;
444            }
445
446            fn find_dollar(s: &str) -> usize {
447                s.find('$').unwrap_or(s.len())
448            }
449
450            let next_char = input_str[1..].chars().next();
451            if next_char == Some('{') {
452                match input_str.find('}') {
453                    Some(closing_brace_idx) => {
454                        let mut default_value = None;
455
456                        // Search for the default split
457                        let var_name_end_idx = match input_str[..closing_brace_idx].find(":-") {
458                            // Only match if there's a variable name, ie. this is not valid ${:-value}
459                            Some(default_split_idx) if default_split_idx != 2 => {
460                                default_value =
461                                    Some(&input_str[default_split_idx + 2..closing_brace_idx]);
462                                default_split_idx
463                            }
464                            _ => closing_brace_idx,
465                        };
466
467                        let var_name = &input_str[2..var_name_end_idx];
468                        match context(var_name) {
469                            // if we have the variable set to some value
470                            Ok(Some(var_value)) => {
471                                result.push_str(var_value.as_ref());
472                                input_str = &input_str[closing_brace_idx + 1..];
473                                next_dollar_idx = find_dollar(input_str);
474                            }
475
476                            // if the variable is set and empty or unset
477                            not_found_or_empty => {
478                                let value = match (not_found_or_empty, default_value) {
479                                    // return an error if we don't have a default and the variable is unset
480                                    (Err(err), None) => {
481                                        return Err(LookupError {
482                                            var_name: var_name.into(),
483                                            cause: err,
484                                        });
485                                    }
486                                    // use the default value if set
487                                    (_, Some(default)) => default,
488                                    // leave the variable as it is if the environment is empty
489                                    (_, None) => &input_str[..closing_brace_idx + 1],
490                                };
491
492                                result.push_str(value);
493                                input_str = &input_str[closing_brace_idx + 1..];
494                                next_dollar_idx = find_dollar(input_str);
495                            }
496                        }
497                    }
498                    // unbalanced braces
499                    None => {
500                        result.push_str(&input_str[..2]);
501                        input_str = &input_str[2..];
502                        next_dollar_idx = find_dollar(input_str);
503                    }
504                }
505            } else if next_char.map(is_valid_var_name_char) == Some(true) {
506                let end_idx = 2 + input_str[2..]
507                    .find(|c: char| !is_valid_var_name_char(c))
508                    .unwrap_or(input_str.len() - 2);
509
510                let var_name = &input_str[1..end_idx];
511                match try_lookup!(var_name, context(var_name)) {
512                    Some(var_value) => {
513                        result.push_str(var_value.as_ref());
514                        input_str = &input_str[end_idx..];
515                        next_dollar_idx = find_dollar(input_str);
516                    }
517                    None => {
518                        result.push_str(&input_str[..end_idx]);
519                        input_str = &input_str[end_idx..];
520                        next_dollar_idx = find_dollar(input_str);
521                    }
522                }
523            } else {
524                result.push('$');
525                input_str = if next_char == Some('$') {
526                    &input_str[2..] // skip the next dollar for escaping
527                } else {
528                    &input_str[1..]
529                };
530                next_dollar_idx = find_dollar(input_str);
531            };
532        }
533        Ok(result.into())
534    } else {
535        Ok(input_str.into())
536    }
537}
538
539/// Same as [`env_with_context()`], but forbids the variable lookup function to return errors.
540///
541/// This function also performs environment expansion, but it requires context function of type
542/// `FnMut(&str) -> Option<CO>` instead of `FnMut(&str) -> Result<Option<CO>, E>`. This simplifies
543/// the API when you know in advance that the context lookups may not fail.
544///
545/// Because of the above, instead of [`Result<Cow<str>, LookupError<E>>`] this function returns just
546/// [`Cow<str>`].
547///
548/// Note that if the context function returns [`None`], the behavior remains the same as that of
549/// [`env_with_context()`]: the variable reference will remain in the output string unexpanded.
550///
551/// # Examples
552///
553/// ```
554/// fn context(s: &str) -> Option<&'static str> {
555///     match s {
556///         "A" => Some("a value"),
557///         "B" => Some("b value"),
558///         _ => None
559///     }
560/// }
561///
562/// // Known variables are expanded
563/// assert_eq!(
564///     shellexpand::env_with_context_no_errors("begin/$A/${B}s/end", context),
565///     "begin/a value/b values/end"
566/// );
567///
568/// // Unknown variables are left as is
569/// assert_eq!(
570///     shellexpand::env_with_context_no_errors("begin/$U/end", context),
571///     "begin/$U/end"
572/// );
573/// ```
574#[inline]
575pub fn env_with_context_no_errors<SI: ?Sized, CO, C>(input: &SI, mut context: C) -> Cow<str>
576where
577    SI: AsRef<str>,
578    CO: AsRef<str>,
579    C: FnMut(&str) -> Option<CO>,
580{
581    match env_with_context(input, move |s| Ok::<Option<CO>, ()>(context(s))) {
582        Ok(value) => value,
583        Err(_) => unreachable!(),
584    }
585}
586
587/// Performs the environment expansion using the default system context.
588///
589/// This function delegates to [`env_with_context()`], using the default system source for
590/// environment variables, namely the [`std::env::var()`] function.
591///
592/// Note that variable lookup of unknown variables will fail with an error instead of, for example,
593/// replacing the offending variables with an empty string. The author thinks that such behavior is
594/// more useful than the other ones. If you need something else, use [`env_with_context()`] or
595/// [`env_with_context_no_errors()`] with an appropriate context function.
596///
597/// # Examples
598///
599/// ```
600/// use std::env;
601///
602/// // make sure that some environment variables are set
603/// env::set_var("X", "x value");
604/// env::set_var("Y", "y value");
605///
606/// // Known variables are expanded
607/// assert_eq!(
608///     shellexpand::env("begin/$X/${Y}s/end").unwrap(),
609///     "begin/x value/y values/end"
610/// );
611///
612/// // Unknown variables result in an error
613/// assert_eq!(
614///     shellexpand::env("begin/$Z/end"),
615///     Err(shellexpand::LookupError {
616///         var_name: "Z".into(),
617///         cause: env::VarError::NotPresent
618///     })
619/// );
620/// ```
621#[inline]
622pub fn env<SI: ?Sized>(input: &SI) -> Result<Cow<str>, LookupError<VarError>>
623where
624    SI: AsRef<str>,
625{
626    env_with_context(input, |s| std::env::var(s).map(Some))
627}
628
629/// Performs the tilde expansion using the provided context.
630///
631/// This function expands tilde (`~`) character in the beginning of the input string into contents
632/// of the path returned by `home_dir` function. If the input string does not contain a tilde, or
633/// if it is not followed either by a slash (`/`) or by the end of string, then it is also left as
634/// is. This means, in particular, that expansions like `~anotheruser/directory` are not supported.
635/// The context function may also return a `None`, in that case even if the tilde is present in the
636/// input in the correct place, it won't be replaced (there is nothing to replace it with, after
637/// all).
638///
639/// This function has three generic type parameters: `SI` represents the input string, `P` is the
640/// output of a context lookup, and `HD` is the context closure. `SI` must be a type, a reference
641/// to which can be converted to a string slice via [`AsRef<str>`], and `P` must be a type, a
642/// reference to which can be converted to a `Path` via [`AsRef<Path>`]. For example, `P` may be
643/// [`Path`], [`std::path::PathBuf`] or [`Cow<Path>`], which gives a lot of flexibility.
644///
645/// If you need to expand the tilde into the actual user home directory, you can use [`tilde()`] or
646/// [`full()`] functions.
647///
648/// # Examples
649///
650/// ```
651/// use std::path::{PathBuf, Path};
652///
653/// fn home_dir() -> Option<PathBuf> { Some(Path::new("/home/user").into()) }
654///
655/// assert_eq!(
656///    shellexpand::tilde_with_context("~/some/dir", home_dir),
657///    "/home/user/some/dir"
658/// );
659/// ```
660pub fn tilde_with_context<SI: ?Sized, P, HD>(input: &SI, home_dir: HD) -> Cow<str>
661where
662    SI: AsRef<str>,
663    P: AsRef<Path>,
664    HD: FnOnce() -> Option<P>,
665{
666    let input_str = input.as_ref();
667    if let Some(input_after_tilde) = input_str.strip_prefix('~') {
668        if input_after_tilde.is_empty()
669            || input_after_tilde.starts_with('/')
670            || (cfg!(windows) && input_after_tilde.starts_with('\\'))
671        {
672            if let Some(hd) = home_dir() {
673                let result = format!("{}{}", hd.as_ref().display(), input_after_tilde);
674                result.into()
675            } else {
676                // home dir is not available
677                input_str.into()
678            }
679        } else {
680            // we cannot handle `~otheruser/` paths yet
681            input_str.into()
682        }
683    } else {
684        // input doesn't start with tilde
685        input_str.into()
686    }
687}
688
689/// Performs the tilde expansion using the default system context.
690///
691/// This function delegates to [`tilde_with_context()`], using the default system source of home
692/// directory path, namely [`dirs::home_dir()`] function.
693///
694/// # Examples
695///
696/// ```
697/// let hds = dirs::home_dir()
698///     .map(|p| p.display().to_string())
699///     .unwrap_or_else(|| "~".to_owned());
700///
701/// assert_eq!(
702///     shellexpand::tilde("~/some/dir"),
703///     format!("{}/some/dir", hds)
704/// );
705/// ```
706#[inline]
707pub fn tilde<SI: ?Sized>(input: &SI) -> Cow<str>
708where
709    SI: AsRef<str>,
710{
711    tilde_with_context(input, dirs::home_dir)
712}
713
714#[cfg(test)]
715mod tilde_tests {
716    use std::path::{Path, PathBuf};
717
718    use super::{tilde, tilde_with_context};
719
720    #[test]
721    fn test_with_tilde_no_hd() {
722        fn hd() -> Option<PathBuf> {
723            None
724        }
725
726        assert_eq!(tilde_with_context("whatever", hd), "whatever");
727        assert_eq!(tilde_with_context("whatever/~", hd), "whatever/~");
728        assert_eq!(tilde_with_context("~/whatever", hd), "~/whatever");
729        assert_eq!(tilde_with_context("~", hd), "~");
730        assert_eq!(tilde_with_context("~something", hd), "~something");
731    }
732
733    #[test]
734    fn test_with_tilde() {
735        fn hd() -> Option<PathBuf> {
736            Some(Path::new("/home/dir").into())
737        }
738
739        assert_eq!(tilde_with_context("whatever/path", hd), "whatever/path");
740        assert_eq!(tilde_with_context("whatever/~/path", hd), "whatever/~/path");
741        assert_eq!(tilde_with_context("~", hd), "/home/dir");
742        assert_eq!(tilde_with_context("~/path", hd), "/home/dir/path");
743        assert_eq!(tilde_with_context("~whatever/path", hd), "~whatever/path");
744    }
745
746    #[test]
747    fn test_global_tilde() {
748        match dirs::home_dir() {
749            Some(hd) => assert_eq!(tilde("~/something"), format!("{}/something", hd.display())),
750            None => assert_eq!(tilde("~/something"), "~/something"),
751        }
752    }
753}
754
755#[cfg(test)]
756mod env_test {
757    use std;
758
759    use super::{env, env_with_context, LookupError};
760
761    macro_rules! table {
762        ($env:expr, unwrap, $($source:expr => $target:expr),+) => {
763            $(
764                assert_eq!(env_with_context($source, $env).unwrap(), $target);
765            )+
766        };
767        ($env:expr, error, $($source:expr => $name:expr),+) => {
768            $(
769                assert_eq!(env_with_context($source, $env), Err(LookupError {
770                    var_name: $name.into(),
771                    cause: ()
772                }));
773            )+
774        }
775    }
776
777    #[test]
778    fn test_empty_env() {
779        fn e(_: &str) -> Result<Option<String>, ()> {
780            Ok(None)
781        }
782
783        table! { e, unwrap,
784            "whatever/path"        => "whatever/path",
785            "$VAR/whatever/path"   => "$VAR/whatever/path",
786            "whatever/$VAR/path"   => "whatever/$VAR/path",
787            "whatever/path/$VAR"   => "whatever/path/$VAR",
788            "${VAR}/whatever/path" => "${VAR}/whatever/path",
789            "whatever/${VAR}path"  => "whatever/${VAR}path",
790            "whatever/path/${VAR}" => "whatever/path/${VAR}",
791            "${}/whatever/path"    => "${}/whatever/path",
792            "whatever/${}path"     => "whatever/${}path",
793            "whatever/path/${}"    => "whatever/path/${}",
794            "$/whatever/path"      => "$/whatever/path",
795            "whatever/$path"       => "whatever/$path",
796            "whatever/path/$"      => "whatever/path/$",
797            "$$/whatever/path"     => "$/whatever/path",
798            "whatever/$$path"      => "whatever/$path",
799            "whatever/path/$$"     => "whatever/path/$",
800            "$A$B$C"               => "$A$B$C",
801            "$A_B_C"               => "$A_B_C"
802        };
803    }
804
805    #[test]
806    fn test_error_env() {
807        fn e(_: &str) -> Result<Option<String>, ()> {
808            Err(())
809        }
810
811        table! { e, unwrap,
812            "whatever/path" => "whatever/path",
813            // check that escaped $ does nothing
814            "whatever/$/path" => "whatever/$/path",
815            "whatever/path$" => "whatever/path$",
816            "whatever/$$path" => "whatever/$path"
817        };
818
819        table! { e, error,
820            "$VAR/something" => "VAR",
821            "${VAR}/something" => "VAR",
822            "whatever/${VAR}/something" => "VAR",
823            "whatever/${VAR}" => "VAR",
824            "whatever/$VAR/something" => "VAR",
825            "whatever/$VARsomething" => "VARsomething",
826            "whatever/$VAR" => "VAR",
827            "whatever/$VAR_VAR_VAR" => "VAR_VAR_VAR"
828        };
829    }
830
831    #[test]
832    fn test_regular_env() {
833        fn e(s: &str) -> Result<Option<&'static str>, ()> {
834            match s {
835                "VAR" => Ok(Some("value")),
836                "a_b" => Ok(Some("X_Y")),
837                "EMPTY" => Ok(Some("")),
838                "ERR" => Err(()),
839                _ => Ok(None),
840            }
841        }
842
843        table! { e, unwrap,
844            // no variables
845            "whatever/path" => "whatever/path",
846
847            // empty string
848            "" => "",
849
850            // existing variable without braces in various positions
851            "$VAR/whatever/path" => "value/whatever/path",
852            "whatever/$VAR/path" => "whatever/value/path",
853            "whatever/path/$VAR" => "whatever/path/value",
854            "whatever/$VARpath" => "whatever/$VARpath",
855            "$VAR$VAR/whatever" => "valuevalue/whatever",
856            "/whatever$VAR$VAR" => "/whatevervaluevalue",
857            "$VAR $VAR" => "value value",
858            "$a_b" => "X_Y",
859            "$a_b$VAR" => "X_Yvalue",
860
861            // existing variable with braces in various positions
862            "${VAR}/whatever/path" => "value/whatever/path",
863            "whatever/${VAR}/path" => "whatever/value/path",
864            "whatever/path/${VAR}" => "whatever/path/value",
865            "whatever/${VAR}path" => "whatever/valuepath",
866            "${VAR}${VAR}/whatever" => "valuevalue/whatever",
867            "/whatever${VAR}${VAR}" => "/whatevervaluevalue",
868            "${VAR} ${VAR}" => "value value",
869            "${VAR}$VAR" => "valuevalue",
870
871            // default values
872            "/answer/${UNKNOWN:-42}" => "/answer/42",
873            "/answer/${:-42}" => "/answer/${:-42}",
874            "/whatever/${UNKNOWN:-other}$VAR" => "/whatever/othervalue",
875            "/whatever/${UNKNOWN:-other}/$VAR" => "/whatever/other/value",
876            ":-/whatever/${UNKNOWN:-other}/$VAR :-" => ":-/whatever/other/value :-",
877            "/whatever/${VAR:-other}" => "/whatever/value",
878            "/whatever/${VAR:-other}$VAR" => "/whatever/valuevalue",
879            "/whatever/${VAR} :-" => "/whatever/value :-",
880            "/whatever/${:-}" => "/whatever/${:-}",
881            "/whatever/${UNKNOWN:-}" => "/whatever/",
882
883            // empty variable in various positions
884            "${EMPTY}/whatever/path" => "/whatever/path",
885            "whatever/${EMPTY}/path" => "whatever//path",
886            "whatever/path/${EMPTY}" => "whatever/path/"
887        };
888
889        table! { e, error,
890            "$ERR" => "ERR",
891            "${ERR}" => "ERR"
892        };
893    }
894
895    #[test]
896    fn test_global_env() {
897        match std::env::var("PATH") {
898            Ok(value) => assert_eq!(env("x/$PATH/x").unwrap(), format!("x/{}/x", value)),
899            Err(e) => assert_eq!(
900                env("x/$PATH/x"),
901                Err(LookupError {
902                    var_name: "PATH".into(),
903                    cause: e
904                })
905            ),
906        }
907        match std::env::var("SOMETHING_DEFINITELY_NONEXISTING") {
908            Ok(value) => assert_eq!(
909                env("x/$SOMETHING_DEFINITELY_NONEXISTING/x").unwrap(),
910                format!("x/{}/x", value)
911            ),
912            Err(e) => assert_eq!(
913                env("x/$SOMETHING_DEFINITELY_NONEXISTING/x"),
914                Err(LookupError {
915                    var_name: "SOMETHING_DEFINITELY_NONEXISTING".into(),
916                    cause: e
917                })
918            ),
919        }
920    }
921}
922
923#[cfg(test)]
924mod full_tests {
925    use std::path::{Path, PathBuf};
926
927    use super::{full_with_context, tilde_with_context};
928
929    #[test]
930    fn test_quirks() {
931        fn hd() -> Option<PathBuf> {
932            Some(Path::new("$VAR").into())
933        }
934        fn env(s: &str) -> Result<Option<&'static str>, ()> {
935            match s {
936                "VAR" => Ok(Some("value")),
937                "SVAR" => Ok(Some("/value")),
938                "TILDE" => Ok(Some("~")),
939                _ => Ok(None),
940            }
941        }
942
943        // any variable-like sequence in ~ expansion should not trigger variable expansion
944        assert_eq!(
945            full_with_context("~/something/$VAR", hd, env).unwrap(),
946            "$VAR/something/value"
947        );
948
949        // variable just after tilde should be substituted first and trigger regular tilde
950        // expansion
951        assert_eq!(full_with_context("~$VAR", hd, env).unwrap(), "~value");
952        assert_eq!(full_with_context("~$SVAR", hd, env).unwrap(), "$VAR/value");
953
954        // variable expanded into a tilde in the beginning should not trigger tilde expansion
955        assert_eq!(
956            full_with_context("$TILDE/whatever", hd, env).unwrap(),
957            "~/whatever"
958        );
959        assert_eq!(
960            full_with_context("${TILDE}whatever", hd, env).unwrap(),
961            "~whatever"
962        );
963        assert_eq!(full_with_context("$TILDE", hd, env).unwrap(), "~");
964    }
965
966    #[test]
967    fn test_tilde_expansion() {
968        fn home_dir() -> Option<PathBuf> {
969            Some(Path::new("/home/user").into())
970        }
971
972        assert_eq!(
973            tilde_with_context("~/some/dir", home_dir),
974            "/home/user/some/dir"
975        );
976    }
977
978    #[cfg(target_family = "windows")]
979    #[test]
980    fn test_tilde_expansion_windows() {
981        fn home_dir() -> Option<PathBuf> {
982            Some(Path::new("C:\\users\\public").into())
983        }
984
985        assert_eq!(
986            tilde_with_context("~\\some\\dir", home_dir),
987            "C:\\users\\public\\some\\dir"
988        );
989    }
990}