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}