shellexpand-2.1.0/.github/workflows/main.yml000064400000000000000000000010221377032102500172460ustar0000000000000000name: CI on: push: branches: [master] pull_request: branches: [master] jobs: test: runs-on: ubuntu-latest strategy: matrix: rust: [stable, beta, nightly] steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust }} override: true - uses: actions-rs/cargo@v1 with: command: build - uses: actions-rs/cargo@v1 with: command: test shellexpand-2.1.0/.gitignore000064400000000000000000000000521363633435100142030ustar0000000000000000target Cargo.lock .*.sw? .idea *.iml shellexpand-2.1.0/Cargo.toml.orig000064400000000000000000000005701377032200600151000ustar0000000000000000[package] name = "shellexpand" version = "2.1.0" authors = ["Vladimir Matveev "] license = "MIT/Apache-2.0" description = "Shell-like expansions in strings" repository = "https://github.com/netvl/shellexpand" documentation = "http://docs.rs/shellexpand/" readme = "Readme.md" keywords = ["strings", "shell", "variables"] [dependencies] dirs-next = "2.0" shellexpand-2.1.0/Cargo.toml0000644000000016141377032237000114450ustar00# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] name = "shellexpand" version = "2.1.0" authors = ["Vladimir Matveev "] description = "Shell-like expansions in strings" documentation = "http://docs.rs/shellexpand/" readme = "Readme.md" keywords = ["strings", "shell", "variables"] license = "MIT/Apache-2.0" repository = "https://github.com/netvl/shellexpand" [dependencies.dirs-next] version = "2.0" shellexpand-2.1.0/LICENSE-APACHE000064400000000000000000000254501363633435100141500ustar0000000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. shellexpand-2.1.0/LICENSE-MIT000064400000000000000000000021201363633435100136450ustar0000000000000000The MIT License (MIT) Copyright (c) 2016 Vladimir Matveev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. shellexpand-2.1.0/Readme.md000064400000000000000000000062241377032175400137430ustar0000000000000000shellexpand, a library for shell-like expansion in strings ========================================================== [![Build Status][actions]](https://github.com/netvl/xml-rs/actions?query=workflow%3ACI) [![crates.io][crates]](https://crates.io/crates/shellexpand) [![docs][docs]](https://docs.rs/shellexpand) [actions]: https://img.shields.io/github/workflow/status/netvl/shellexpand/CI/master?style=flat-square [crates]: https://img.shields.io/crates/v/shellexpand.svg?style=flat-square [docs]: https://img.shields.io/badge/docs-latest%20release-6495ed.svg?style=flat-square [Documentation](https://docs.rs/shellexpand/) shellexpand is a single dependency library which allows one to perform shell-like expansions in strings, that is, to expand variables like `$A` or `${B}` into their values inside some context and to expand `~` in the beginning of a string into the home directory (again, inside some context). This crate provides generic functions which accept arbitrary contexts as well as default, system-based functions which perform expansions using the system-wide context (represented by functions from `std::env` module and [dirs](https://crates.io/crates/dirs) crate). --- **Note: because I no longer have capacity to support it, I'm now looking for a new maintainer for this library. Until I'm able to find one, it is unlikely to receive new updates in any reasonably timely manner.** --- ## Usage Just add a dependency in your `Cargo.toml`: ```toml [dependencies] shellexpand = "2.1" ``` See the crate documentation (a link is present in the beginning of this readme) for more information and examples. ## Changelog ### Version 2.1.0 * Switched to `dirs-next` instead of the obsolete `dirs` as the underlying dependency used to resolve the home directory * Switched to GitHub Actions instead of Travis CI for building the project. ### Version 2.0.0 * Added support for default values in variable expansion (i.e. `${ANSWER:-42}`) * Breaking changes (minimum Rust version is now 1.30.0): + Using `dyn` for trait objects to fix deprecation warning + Switched to using `source()` instead of `cause()` in the `Error` implementation, and therefore added a `'static` bound for the generic error parameter `E` ### Version 1.1.1 * Bump `dirs` dependency to 2.0. ### Version 1.1.0 * Changed use of deprecated `std::env::home_dir` to the [dirs](https://crates.io/crates/dirs)::home_dir function ### Version 1.0.0 * Fixed typos and minor incompletenesses in the documentation * Changed `home_dir` argument type for tilde expansion functions to `FnOnce` instead `FnMut` * Changed `LookupError::name` field name to `var_name` ### Version 0.1.0 * Initial release ## License This program is licensed under either of * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. shellexpand-2.1.0/src/lib.rs000064400000000000000000001064021377032026600141230ustar0000000000000000//! Provides functions for performing shell-like expansions in strings. //! //! In particular, the following expansions are supported: //! //! * tilde expansion, when `~` in the beginning of a string, like in `"~/some/path"`, //! is expanded into the home directory of the current user; //! * environment expansion, when `$A` or `${B}`, like in `"~/$A/${B}something"`, //! are expanded into their values in some environment. //! //! Environment expansion also supports default values with the familiar shell syntax, //! so for example `${UNSET_ENV:-42}` will use the specified default value, i.e. `42`, if //! the `UNSET_ENV` variable is not set in the environment. //! //! The source of external information for these expansions (home directory and environment //! variables) is called their *context*. The context is provided to these functions as a closure //! of the respective type. //! //! This crate provides both customizable functions, which require their context to be provided //! explicitly, and wrapper functions which use `dirs::home_dir()` and `std::env::var()` //! for obtaining home directory and environment variables, respectively. //! //! Also there is a "full" function which performs both tilde and environment //! expansion, but does it correctly, rather than just doing one after another: for example, //! if the string starts with a variable whose value starts with a `~`, then this tilde //! won't be expanded. //! //! All functions return `Cow` because it is possible for their input not to contain anything //! which triggers the expansion. In that case performing allocations can be avoided. //! //! Please note that by default unknown variables in environment expansion are left as they are //! and are not, for example, substituted with an empty string: //! //! ``` //! fn context(_: &str) -> Option { None } //! //! assert_eq!( //! shellexpand::env_with_context_no_errors("$A $B", context), //! "$A $B" //! ); //! ``` //! //! Environment expansion context allows for a very fine tweaking of how results should be handled, //! so it is up to the user to pass a context function which does the necessary thing. For example, //! `env()` and `full()` functions from this library pass all errors returned by `std::env::var()` //! through, therefore they will also return an error if some unknown environment //! variable is used, because `std::env::var()` returns an error in this case: //! //! ``` //! use std::env; //! //! // make sure that the variable indeed does not exist //! env::remove_var("MOST_LIKELY_NONEXISTING_VAR"); //! //! assert_eq!( //! shellexpand::env("$MOST_LIKELY_NONEXISTING_VAR"), //! Err(shellexpand::LookupError { //! var_name: "MOST_LIKELY_NONEXISTING_VAR".into(), //! cause: env::VarError::NotPresent //! }) //! ); //! ``` //! //! The author thinks that this approach is more useful than just substituting an empty string //! (like, for example, does Go with its [os.ExpandEnv](https://golang.org/pkg/os/#ExpandEnv) //! function), but if you do need `os.ExpandEnv`-like behavior, it is fairly easy to get one: //! //! ``` //! use std::env; //! use std::borrow::Cow; //! //! fn context(s: &str) -> Result>, env::VarError> { //! match env::var(s) { //! Ok(value) => Ok(Some(value.into())), //! Err(env::VarError::NotPresent) => Ok(Some("".into())), //! Err(e) => Err(e) //! } //! } //! //! // make sure that the variable indeed does not exist //! env::remove_var("MOST_LIKELY_NONEXISTING_VAR"); //! //! assert_eq!( //! shellexpand::env_with_context("a${MOST_LIKELY_NOEXISTING_VAR}b", context).unwrap(), //! "ab" //! ); //! ``` //! //! The above example also demonstrates the flexibility of context function signatures: the context //! function may return anything which can be `AsRef`ed into a string slice. extern crate dirs_next as dirs; use std::borrow::Cow; use std::env::VarError; use std::error::Error; use std::fmt; use std::path::Path; /// Performs both tilde and environment expansion using the provided contexts. /// /// `home_dir` and `context` are contexts for tilde expansion and environment expansion, /// respectively. See `env_with_context()` and `tilde_with_context()` for more details on /// them. /// /// Unfortunately, expanding both `~` and `$VAR`s at the same time is not that simple. First, /// this function has to track ownership of the data. Since all functions in this crate /// return `Cow`, this function takes some precautions in order not to allocate more than /// necessary. In particular, if the input string contains neither tilde nor `$`-vars, this /// function will perform no allocations. /// /// Second, if the input string starts with a variable, and the value of this variable starts /// with tilde, the naive approach may result into expansion of this tilde. This function /// avoids this. /// /// # Examples /// /// ``` /// use std::path::{PathBuf, Path}; /// use std::borrow::Cow; /// /// fn home_dir() -> Option { Some(Path::new("/home/user").into()) } /// /// fn get_env(name: &str) -> Result, &'static str> { /// match name { /// "A" => Ok(Some("a value")), /// "B" => Ok(Some("b value")), /// "T" => Ok(Some("~")), /// "E" => Err("some error"), /// _ => Ok(None) /// } /// } /// /// // Performs both tilde and environment expansions /// assert_eq!( /// shellexpand::full_with_context("~/$A/$B", home_dir, get_env).unwrap(), /// "/home/user/a value/b value" /// ); /// /// // Errors from environment expansion are propagated to the result /// assert_eq!( /// shellexpand::full_with_context("~/$E/something", home_dir, get_env), /// Err(shellexpand::LookupError { /// var_name: "E".into(), /// cause: "some error" /// }) /// ); /// /// // Input without starting tilde and without variables does not cause allocations /// let s = shellexpand::full_with_context("some/path", home_dir, get_env); /// match s { /// Ok(Cow::Borrowed(s)) => assert_eq!(s, "some/path"), /// _ => unreachable!("the above variant is always valid") /// } /// /// // Input with a tilde inside a variable in the beginning of the string does not cause tilde /// // expansion /// assert_eq!( /// shellexpand::full_with_context("$T/$A/$B", home_dir, get_env).unwrap(), /// "~/a value/b value" /// ); /// ``` pub fn full_with_context( input: &SI, home_dir: HD, context: C, ) -> Result, LookupError> where SI: AsRef, CO: AsRef, C: FnMut(&str) -> Result, E>, P: AsRef, HD: FnOnce() -> Option

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

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

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