shellexpand/
funcs.rs

1//! Implementation - **instantiated twice**
2//!
3//! **IMPORTANT NOTE TO IMPLEMENTORS**
4//!
5//! This module is included twice - there are two `mod` statements.
6//! The `use super::wtraits::*` line imports *different types* each type.
7//!
8//! This allows the same code to do double duty: it works with `str`, and also with `Path`.
9//! Working with `Path` is quite awkward and complicated - see `path.rs` for the type definitions.
10//!
11//! The `wtraits` module has all the type names and traits we use,
12//! along with documentation of their semantics.
13//!
14//! But we also allow the use of inherent methods
15//! (if they do the right things with both the string and path types).
16
17use std::env::VarError;
18use std::error::Error;
19
20use super::wtraits::*;
21
22#[cfg(test)]
23mod test;
24
25/// Performs both tilde and environment expansion using the provided contexts.
26///
27/// `home_dir` and `context` are contexts for tilde expansion and environment expansion,
28/// respectively. See [`env_with_context()`] and [`tilde_with_context()`] for more details on
29/// them.
30///
31/// Unfortunately, expanding both `~` and `$VAR`s at the same time is not that simple. First,
32/// this function has to track ownership of the data. Since all functions in this crate
33/// return [`Cow<str>`], this function takes some precautions in order not to allocate more than
34/// necessary. In particular, if the input string contains neither tilde nor `$`-vars, this
35/// function will perform no allocations.
36///
37/// Second, if the input string starts with a variable, and the value of this variable starts
38/// with tilde, the naive approach may result into expansion of this tilde. This function
39/// avoids this.
40///
41/// # Examples
42///
43/// ```
44/// use std::path::{PathBuf, Path};
45/// use std::borrow::Cow;
46///
47/// fn home_dir() -> Option<String> { Some("/home/user".into()) }
48///
49/// fn get_env(name: &str) -> Result<Option<&'static str>, &'static str> {
50///     match name {
51///         "A" => Ok(Some("a value")),
52///         "B" => Ok(Some("b value")),
53///         "T" => Ok(Some("~")),
54///         "E" => Err("some error"),
55///         _ => Ok(None)
56///     }
57/// }
58///
59/// // Performs both tilde and environment expansions
60/// assert_eq!(
61///     shellexpand::full_with_context("~/$A/$B", home_dir, get_env).unwrap(),
62///     "/home/user/a value/b value"
63/// );
64///
65/// // Errors from environment expansion are propagated to the result
66/// assert_eq!(
67///     shellexpand::full_with_context("~/$E/something", home_dir, get_env),
68///     Err(shellexpand::LookupError {
69///         var_name: "E".into(),
70///         cause: "some error"
71///     })
72/// );
73///
74/// // Input without starting tilde and without variables does not cause allocations
75/// let s = shellexpand::full_with_context("some/path", home_dir, get_env);
76/// match s {
77///     Ok(Cow::Borrowed(s)) => assert_eq!(s, "some/path"),
78///     _ => unreachable!("the above variant is always valid")
79/// }
80///
81/// // Input with a tilde inside a variable in the beginning of the string does not cause tilde
82/// // expansion
83/// assert_eq!(
84///     shellexpand::full_with_context("$T/$A/$B", home_dir, get_env).unwrap(),
85///     "~/a value/b value"
86/// );
87/// ```
88pub fn full_with_context<SI: ?Sized, CO, C, E, P, HD>(
89    input: &SI,
90    home_dir: HD,
91    context: C,
92) -> Result<Cow<Xstr>, LookupError<E>>
93where
94    SI: AsRef<Xstr>,
95    CO: AsRef<Xstr>,
96    C: FnMut(&str) -> Result<Option<CO>, E>,
97    P: AsRef<Xstr>,
98    HD: FnOnce() -> Option<P>,
99{
100    env_with_context(input, context).map(|r| match r {
101        // variable expansion did not modify the original string, so we can apply tilde expansion
102        // directly
103        Cow::Borrowed(s) => tilde_with_context(s, home_dir),
104        Cow::Owned(s) => {
105            // if the original string does not start with a tilde but the processed one does,
106            // then the tilde is contained in one of variables and should not be expanded
107            // (We must convert the input to WInput here because it might be `AsRef<Path>`.
108            // and `Path`'s `starts_with` checks only whole components;
109            // and `OsStr` doesn't let us test prefixes at all.)
110            if !input.into_winput().starts_with('~') && s.starts_with("~") {
111                // return as is
112                s.into()
113            } else if let Cow::Owned(s) = tilde_with_context(&s, home_dir) {
114                s.into()
115            } else {
116                s.into()
117            }
118        }
119    })
120}
121
122/// Same as [`full_with_context()`], but forbids the variable lookup function to return errors.
123///
124/// This function also performs full shell-like expansion, but it uses
125/// [`env_with_context_no_errors()`] for environment expansion whose context lookup function returns
126/// just [`Option<CO>`] instead of [`Result<Option<CO>, E>`]. Therefore, the function itself also
127/// returns just [`Cow<str>`] instead of [`Result<Cow<str>, LookupError<E>>`]. Otherwise it is
128/// identical to [`full_with_context()`].
129///
130/// # Examples
131///
132/// ```
133/// use std::path::{PathBuf, Path};
134/// use std::borrow::Cow;
135///
136/// fn home_dir() -> Option<String> { Some("/home/user".into()) }
137///
138/// fn get_env(name: &str) -> Option<&'static str> {
139///     match name {
140///         "A" => Some("a value"),
141///         "B" => Some("b value"),
142///         "T" => Some("~"),
143///         _ => None
144///     }
145/// }
146///
147/// // Performs both tilde and environment expansions
148/// assert_eq!(
149///     shellexpand::full_with_context_no_errors("~/$A/$B", home_dir, get_env),
150///     "/home/user/a value/b value"
151/// );
152///
153/// // Input without starting tilde and without variables does not cause allocations
154/// let s = shellexpand::full_with_context_no_errors("some/path", home_dir, get_env);
155/// match s {
156///     Cow::Borrowed(s) => assert_eq!(s, "some/path"),
157///     _ => unreachable!("the above variant is always valid")
158/// }
159///
160/// // Input with a tilde inside a variable in the beginning of the string does not cause tilde
161/// // expansion
162/// assert_eq!(
163///     shellexpand::full_with_context_no_errors("$T/$A/$B", home_dir, get_env),
164///     "~/a value/b value"
165/// );
166/// ```
167#[inline]
168pub fn full_with_context_no_errors<SI: ?Sized, CO, C, P, HD>(
169    input: &SI,
170    home_dir: HD,
171    mut context: C,
172) -> Cow<Xstr>
173where
174    SI: AsRef<Xstr>,
175    CO: AsRef<Xstr>,
176    C: FnMut(&str) -> Option<CO>,
177    P: AsRef<Xstr>,
178    HD: FnOnce() -> Option<P>,
179{
180    match full_with_context(input, home_dir, move |s| Ok::<Option<CO>, ()>(context(s))) {
181        Ok(result) => result,
182        Err(_) => unreachable!(),
183    }
184}
185
186/// Performs both tilde and environment expansions in the default system context.
187///
188/// This function delegates to [`full_with_context()`], using the default system sources for both
189/// home directory and environment, namely [`dirs::home_dir()`] and [`std::env::var()`].
190///
191/// Note that variable lookup of unknown variables will fail with an error instead of, for example,
192/// replacing the unknown variable with an empty string. The author thinks that this behavior is
193/// more useful than the other ones. If you need to change it, use [`full_with_context()`] or
194/// [`full_with_context_no_errors()`] with an appropriate context function instead.
195///
196/// This function behaves exactly like [`full_with_context()`] in regard to tilde-containing
197/// variables in the beginning of the input string.
198///
199/// # Examples
200///
201/// ```
202/// use std::env;
203///
204/// env::set_var("A", "a value");
205/// env::set_var("B", "b value");
206///
207/// let home_dir = dirs::home_dir()
208///     .map(|p| p.display().to_string())
209///     .unwrap_or_else(|| "~".to_owned());
210///
211/// // Performs both tilde and environment expansions using the system contexts
212/// assert_eq!(
213///     shellexpand::full("~/$A/${B}s").unwrap(),
214///     format!("{}/a value/b values", home_dir)
215/// );
216///
217/// // Unknown variables cause expansion errors
218/// assert_eq!(
219///     shellexpand::full("~/$UNKNOWN/$B"),
220///     Err(shellexpand::LookupError {
221///         var_name: "UNKNOWN".into(),
222///         cause: env::VarError::NotPresent
223///     })
224/// );
225/// ```
226#[cfg(feature = "tilde")]
227#[inline]
228pub fn full<SI: ?Sized>(input: &SI) -> Result<Cow<Xstr>, LookupError<VarError>>
229where
230    SI: AsRef<Xstr>,
231{
232    full_with_context(input, home_dir, |s| std::env::var(s).map(Some))
233}
234
235#[cfg(feature = "tilde")]
236fn home_dir() -> Option<XString> {
237    let hd = dirs::home_dir()?;
238
239    // If the home directory is not valid Unicode, we might not be able to substitute it.
240    // We don't have an error reporting channel suitable for this (very unusual) situation.
241    // If it happens, we just return `None`, causing `~`-substitution to not occur.
242    //
243    // In `shellexpand` 2.x, we use `Path::display()`, instead, which is lossy - so we would
244    // use a wrong pathname.  That is definitely worse.
245    hd.try_into_xstring()
246}
247
248/// Represents a variable lookup error.
249///
250/// This error is returned by [`env_with_context()`] function (and, therefore, also by [`env()`],
251/// [`full_with_context()`] and [`full()`]) when the provided context function returns an error. The
252/// original error is provided in the `cause` field, while `name` contains the name of a variable
253/// whose expansion caused the error.
254#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct LookupError<E> {
256    /// The name of the problematic variable inside the input string.
257    pub var_name: OString,
258    /// The original error returned by the context function.
259    pub cause: E,
260}
261
262impl<E: fmt::Display> fmt::Display for LookupError<E> {
263    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
264        write!(f, "error looking key '")?;
265        self.var_name.display_possibly_lossy(f)?;
266        write!(f, "' up: {}", self.cause)?;
267        Ok(())
268    }
269}
270
271impl<E: Error + 'static> Error for LookupError<E> {
272    fn source(&self) -> Option<&(dyn Error + 'static)> {
273        Some(&self.cause)
274    }
275}
276
277macro_rules! try_lookup {
278    ($name:expr, $e:expr) => {
279        match $e {
280            Ok(s) => s,
281            Err(e) => {
282                return Err(LookupError {
283                    var_name: $name.to_ostring(),
284                    cause: e,
285                })
286            }
287        }
288    };
289}
290
291fn is_valid_var_name_char(c: char) -> bool {
292    c.is_alphanumeric() || c == '_'
293}
294
295/// Performs the environment expansion using the provided context.
296///
297/// This function walks through the input string `input` and attempts to construct a new string by
298/// replacing all shell-like variable sequences with the corresponding values obtained via the
299/// `context` function. The latter may return an error; in this case the error will be returned
300/// immediately, along with the name of the offending variable. Also the context function may
301/// return `Ok(None)`, indicating that the given variable is not available; in this case the
302/// variable sequence is left as it is in the output string.
303///
304/// The syntax of variables resembles the one of bash-like shells: all of `$VAR`, `${VAR}`,
305/// `$NAME_WITH_UNDERSCORES` are valid variable references, and the form with braces may be used to
306/// separate the reference from the surrounding alphanumeric text: `before${VAR}after`. Note,
307/// however, that for simplicity names like `$123` or `$1AB` are also valid, as opposed to shells
308/// where `$<number>` has special meaning of positional arguments. Also note that "alphanumericity"
309/// of variable names is checked with [`std::primitive::char::is_alphanumeric()`], therefore lots of characters which
310/// are considered alphanumeric by the Unicode standard are also valid names for variables. When
311/// unsure, use braces to separate variables from the surrounding text.
312///
313/// This function has four generic type parameters: `SI` represents the input string, `CO` is the
314/// output of context lookups, `C` is the context closure and `E` is the type of errors returned by
315/// the context function. `SI` and `CO` must be types, a references to which can be converted to
316/// a string slice. For example, it is fine for the context function to return [`&str`]'s, [`String`]'s or
317/// [`Cow<str>`]'s, which gives the user a lot of flexibility.
318///
319/// If the context function returns an error, it will be wrapped into [`LookupError`] and returned
320/// immediately. [`LookupError`], besides the original error, also contains a string with the name of
321/// the variable whose expansion caused the error. [`LookupError`] implements [`Error`], [`Clone`] and
322/// [`Eq`] traits for further convenience and interoperability.
323///
324/// If you need to expand system environment variables, you can use [`env()`] or [`full()`] functions.
325/// If your context does not have errors, you may use [`env_with_context_no_errors()`] instead of
326/// this function because it provides a simpler API.
327///
328/// # Examples
329///
330/// ```
331/// fn context(s: &str) -> Result<Option<&'static str>, &'static str> {
332///     match s {
333///         "A" => Ok(Some("a value")),
334///         "B" => Ok(Some("b value")),
335///         "E" => Err("something went wrong"),
336///         _ => Ok(None)
337///     }
338/// }
339///
340/// // Regular variables are expanded
341/// assert_eq!(
342///     shellexpand::env_with_context("begin/$A/${B}s/end", context).unwrap(),
343///     "begin/a value/b values/end"
344/// );
345///
346/// // Expand to a default value if the variable is not defined
347/// assert_eq!(
348///     shellexpand::env_with_context("begin/${UNSET_ENV:-42}/end", context).unwrap(),
349///     "begin/42/end"
350/// );
351///
352/// // Unknown variables are left as is
353/// assert_eq!(
354///     shellexpand::env_with_context("begin/$UNKNOWN/end", context).unwrap(),
355///     "begin/$UNKNOWN/end"
356/// );
357///
358/// // Errors are propagated
359/// assert_eq!(
360///     shellexpand::env_with_context("begin${E}end", context),
361///     Err(shellexpand::LookupError {
362///         var_name: "E".into(),
363///         cause: "something went wrong"
364///     })
365/// );
366/// ```
367pub fn env_with_context<SI: ?Sized, CO, C, E>(
368    input: &SI,
369    mut context: C,
370) -> Result<Cow<Xstr>, LookupError<E>>
371where
372    SI: AsRef<Xstr>,
373    CO: AsRef<Xstr>,
374    C: FnMut(&str) -> Result<Option<CO>, E>,
375{
376    let input_str = input.into_winput();
377    if let Some(idx) = input_str.find('$') {
378        let mut result = OString::with_capacity(input_str.len());
379
380        let mut input_str = input_str.as_wstr();
381        let mut next_dollar_idx = idx;
382        loop {
383            result.push_wstr(&input_str[..next_dollar_idx]);
384
385            input_str = &input_str[next_dollar_idx..];
386            if input_str.is_empty() {
387                break;
388            }
389
390            fn find_dollar(s: &Wstr) -> usize {
391                s.find('$').unwrap_or(s.len())
392            }
393            let mut lookup = |var_name: &Wstr| {
394                let var_name = match var_name.as_str() {
395                    Some(var_name) => var_name,
396                    // No non-UTF-8 variables can exist
397                    None => return Ok(None),
398                };
399                context(var_name)
400            };
401
402            let mut next_chars = input_str[1..].chars_approx();
403            let next_char = next_chars.next();
404            if next_char == Some('{') {
405                match input_str.find('}') {
406                    Some(closing_brace_idx) => {
407                        let mut default_value = None;
408
409                        // Search for the default split
410                        let var_name_end_idx = match input_str[..closing_brace_idx].find(":-") {
411                            // Only match if there's a variable name, ie. this is not valid ${:-value}
412                            Some(default_split_idx) if default_split_idx != 2 => {
413                                default_value =
414                                    Some(&input_str[default_split_idx + 2..closing_brace_idx]);
415                                default_split_idx
416                            }
417                            _ => closing_brace_idx,
418                        };
419
420                        let var_name = &input_str[2..var_name_end_idx];
421                        match lookup(var_name) {
422                            // if we have the variable set to some value
423                            Ok(Some(var_value)) => {
424                                result.push_xstr(var_value.as_ref());
425                                input_str = &input_str[closing_brace_idx + 1..];
426                                next_dollar_idx = find_dollar(input_str);
427                            }
428
429                            // if the variable is set and empty or unset
430                            not_found_or_empty => {
431                                let value = match (not_found_or_empty, default_value) {
432                                    // return an error if we don't have a default and the variable is unset
433                                    (Err(err), None) => {
434                                        return Err(LookupError {
435                                            var_name: var_name.to_ostring(),
436                                            cause: err,
437                                        });
438                                    }
439                                    // use the default value if set
440                                    (_, Some(default)) => default,
441                                    // leave the variable as it is if the environment is empty
442                                    (_, None) => &input_str[..closing_brace_idx + 1],
443                                };
444
445                                result.push_wstr(value);
446                                input_str = &input_str[closing_brace_idx + 1..];
447                                next_dollar_idx = find_dollar(input_str);
448                            }
449                        }
450                    }
451                    // unbalanced braces
452                    None => {
453                        result.push_wstr(&input_str[..2]);
454                        input_str = &input_str[2..];
455                        next_dollar_idx = find_dollar(input_str);
456                    }
457                }
458            } else if next_char.map(is_valid_var_name_char) == Some(true) {
459                let mut end_idx;
460                loop {
461                    // Subtract the bytes length of the remainder from the length, and that's where we are
462                    end_idx = input_str.len() - next_chars.wstr_len();
463                    match next_chars.next() {
464                        Some(c) if is_valid_var_name_char(c) => {},
465                        _ => break,
466                    }
467                }
468
469                let var_name = &input_str[1..end_idx];
470                match try_lookup!(var_name, lookup(var_name)) {
471                    Some(var_value) => {
472                        result.push_xstr(var_value.as_ref());
473                        input_str = &input_str[end_idx..];
474                        next_dollar_idx = find_dollar(input_str);
475                    }
476                    None => {
477                        result.push_wstr(&input_str[..end_idx]);
478                        input_str = &input_str[end_idx..];
479                        next_dollar_idx = find_dollar(input_str);
480                    }
481                }
482            } else {
483                result.push_str("$");
484                input_str = if next_char == Some('$') {
485                    &input_str[2..] // skip the next dollar for escaping
486                } else {
487                    &input_str[1..]
488                };
489                next_dollar_idx = find_dollar(input_str);
490            };
491        }
492        Ok(result.into_ocow())
493    } else {
494        Ok(input.into_ocow())
495    }
496}
497
498/// Same as [`env_with_context()`], but forbids the variable lookup function to return errors.
499///
500/// This function also performs environment expansion, but it requires context function of type
501/// `FnMut(&str) -> Option<CO>` instead of `FnMut(&str) -> Result<Option<CO>, E>`. This simplifies
502/// the API when you know in advance that the context lookups may not fail.
503///
504/// Because of the above, instead of [`Result<Cow<str>, LookupError<E>>`] this function returns just
505/// [`Cow<str>`].
506///
507/// Note that if the context function returns [`None`], the behavior remains the same as that of
508/// [`env_with_context()`]: the variable reference will remain in the output string unexpanded.
509///
510/// # Examples
511///
512/// ```
513/// fn context(s: &str) -> Option<&'static str> {
514///     match s {
515///         "A" => Some("a value"),
516///         "B" => Some("b value"),
517///         _ => None
518///     }
519/// }
520///
521/// // Known variables are expanded
522/// assert_eq!(
523///     shellexpand::env_with_context_no_errors("begin/$A/${B}s/end", context),
524///     "begin/a value/b values/end"
525/// );
526///
527/// // Unknown variables are left as is
528/// assert_eq!(
529///     shellexpand::env_with_context_no_errors("begin/$U/end", context),
530///     "begin/$U/end"
531/// );
532/// ```
533#[inline]
534pub fn env_with_context_no_errors<SI: ?Sized, CO, C>(input: &SI, mut context: C) -> Cow<Xstr>
535where
536    SI: AsRef<Xstr>,
537    CO: AsRef<Xstr>,
538    C: FnMut(&str) -> Option<CO>,
539{
540    match env_with_context(input, move |s| Ok::<Option<CO>, ()>(context(s))) {
541        Ok(value) => value,
542        Err(_) => unreachable!(),
543    }
544}
545
546/// Performs the environment expansion using the default system context.
547///
548/// This function delegates to [`env_with_context()`], using the default system source for
549/// environment variables, namely the [`std::env::var()`] function.
550///
551/// Note that variable lookup of unknown variables will fail with an error instead of, for example,
552/// replacing the offending variables with an empty string. The author thinks that such behavior is
553/// more useful than the other ones. If you need something else, use [`env_with_context()`] or
554/// [`env_with_context_no_errors()`] with an appropriate context function.
555///
556/// # Examples
557///
558/// ```
559/// use std::env;
560///
561/// // make sure that some environment variables are set
562/// env::set_var("X", "x value");
563/// env::set_var("Y", "y value");
564///
565/// // Known variables are expanded
566/// assert_eq!(
567///     shellexpand::env("begin/$X/${Y}s/end").unwrap(),
568///     "begin/x value/y values/end"
569/// );
570///
571/// // Unknown variables result in an error
572/// assert_eq!(
573///     shellexpand::env("begin/$Z/end"),
574///     Err(shellexpand::LookupError {
575///         var_name: "Z".into(),
576///         cause: env::VarError::NotPresent
577///     })
578/// );
579/// ```
580#[inline]
581pub fn env<SI: ?Sized>(input: &SI) -> Result<Cow<Xstr>, LookupError<VarError>>
582where
583    SI: AsRef<Xstr>,
584{
585    env_with_context(input, |s| std::env::var(s).map(Some))
586}
587
588/// Performs the tilde expansion using the provided context.
589///
590/// This function expands tilde (`~`) character in the beginning of the input string into contents
591/// of the path returned by `home_dir` function. If the input string does not contain a tilde, or
592/// if it is not followed either by a slash (`/`) or by the end of string, then it is also left as
593/// is. This means, in particular, that expansions like `~anotheruser/directory` are not supported.
594/// The context function may also return a `None`, in that case even if the tilde is present in the
595/// input in the correct place, it won't be replaced (there is nothing to replace it with, after
596/// all).
597///
598/// This function has three generic type parameters: `SI` represents the input string, `P` is the
599/// output of a context lookup, and `HD` is the context closure. `SI` must be a type, a reference
600/// to which can be converted to a string slice via [`AsRef<str>`], and `P` must be a type, a
601/// reference to which can be converted to a `str` via [`AsRef<str>`].
602/// Home directories which are available only as a `Path` are not supported here,
603/// because they cannot be represented in the output string.
604/// If you wish to support home directories which are not valid Unicode,
605/// use the [`path`](crate::path) module.
606///
607/// If you need to expand the tilde into the actual user home directory, you can use [`tilde()`] or
608/// [`full()`] functions.
609///
610/// # Examples
611///
612/// ```
613/// use std::path::{PathBuf, Path};
614///
615/// fn home_dir() -> Option<String> { Some("/home/user".into()) }
616///
617/// assert_eq!(
618///    shellexpand::tilde_with_context("~/some/dir", home_dir),
619///    "/home/user/some/dir"
620/// );
621/// ```
622pub fn tilde_with_context<SI: ?Sized, P, HD>(input: &SI, home_dir: HD) -> Cow<Xstr>
623where
624    SI: AsRef<Xstr>,
625    P: AsRef<Xstr>,
626    HD: FnOnce() -> Option<P>,
627{
628    let input_str = input.into_winput();
629    if let Some(input_after_tilde) = input_str.strip_prefix('~') {
630        if input_after_tilde.is_empty()
631            || input_after_tilde.starts_with('/')
632            || (cfg!(windows) && input_after_tilde.starts_with('\\'))
633        {
634            if let Some(hd) = home_dir() {
635                let hd = hd.into_winput();
636                let mut result = OString::with_capacity(hd.len() + input_after_tilde.len());
637                result.push_wstr(hd.as_wstr());
638                result.push_wstr(input_after_tilde);
639                result.into_ocow()
640            } else {
641                // home dir is not available
642                input.into_ocow()
643            }
644        } else {
645            // we cannot handle `~otheruser/` paths yet
646            input.into_ocow()
647        }
648    } else {
649        // input doesn't start with tilde
650        input.into_ocow()
651    }
652}
653
654/// Performs the tilde expansion using the default system context.
655///
656/// This function delegates to [`tilde_with_context()`], using the default system source of home
657/// directory path, namely [`dirs::home_dir()`] function.
658///
659/// # Examples
660///
661/// ```
662/// let hds = dirs::home_dir()
663///     .map(|p| p.display().to_string())
664///     .unwrap_or_else(|| "~".to_owned());
665///
666/// assert_eq!(
667///     shellexpand::tilde("~/some/dir"),
668///     format!("{}/some/dir", hds)
669/// );
670/// ```
671#[cfg(feature = "tilde")]
672#[inline]
673pub fn tilde<SI: ?Sized>(input: &SI) -> Cow<Xstr>
674where
675    SI: AsRef<Xstr>,
676{
677    tilde_with_context(input, home_dir)
678}