xpanda 0.1.0

Unix shell-like parameter expansion/variable substitution
Documentation
/*!
This crate provides the ability to expand/substitute variables in strings similar to [`envsubst`]
and [`Bash parameter expansion`].

There is a single public struct (not counting errors and builders), [`Xpanda`], which in turn
contains a single method: `expand`. The expand method takes a string by reference and returns
a copy of it with all variables expanded/substituted according to some patterns.

[`envsubst`]: https://www.gnu.org/software/gettext/manual/html_node/envsubst-Invocation.html
[`Bash parameter expansion`]: https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html
[`Xpanda`]: struct.Xpanda.html
*/

#![deny(clippy::all)]
#![warn(clippy::pedantic, clippy::nursery)]
#![allow(unused)]

mod ast;
mod eval;
mod forward_peekable;
mod lexer;
mod parser;
mod str_read;
mod token;

use crate::eval::Evaluator;
use crate::lexer::Lexer;
use crate::parser::Parser;
use std::collections::HashMap;
use std::env;

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Error {
    pub message: String,
    pub line: usize,
    pub col: usize,
}

impl Error {
    #[must_use]
    pub const fn new(message: String, line: usize, col: usize) -> Self {
        Self { message, line, col }
    }
}

impl From<parser::Error> for Error {
    fn from(error: parser::Error) -> Self {
        Self {
            message: error.message,
            line: error.line,
            col: error.col,
        }
    }
}

impl From<eval::Error> for Error {
    fn from(error: eval::Error) -> Self {
        Self {
            message: error.message,
            line: error.line,
            col: error.col,
        }
    }
}

#[derive(Default)]
pub struct Builder {
    no_unset: bool,
    positional_vars: Vec<String>,
    named_vars: HashMap<String, String>,
}

impl Builder {
    /// With this flag set, missing variables without any default value will cause an error
    /// instead of omitting en empty string. Off by default.
    #[must_use]
    pub const fn no_unset(mut self, no_unset: bool) -> Self {
        self.no_unset = no_unset;
        self
    }

    /// Adds all environment variables as named variables.
    #[must_use]
    pub fn with_env_vars(mut self) -> Self {
        self.named_vars.extend(env::vars());
        self
    }

    /// Adds the given map values as named variables.
    #[must_use]
    pub fn with_named_vars(mut self, vars: HashMap<String, String>) -> Self {
        self.named_vars.extend(vars);
        self
    }

    /// Adds the given strings as positional variables.
    #[must_use]
    pub fn with_positional_vars(mut self, vars: Vec<String>) -> Self {
        self.positional_vars.extend(vars);
        self
    }

    /// Builds a new [`Xpanda`] instance.
    #[must_use]
    pub fn build(self) -> Xpanda {
        Xpanda::new(self)
    }
}

/// [`Xpanda`] substitutes the values of variables in strings similar to [`envsubst`] and
/// [`Bash parameter expansion`].
///
/// [`envsubst`]: https://www.gnu.org/software/gettext/manual/html_node/envsubst-Invocation.html
/// [`Bash parameter expansion`]: https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
#[derive(Default)]
pub struct Xpanda {
    evaluator: Evaluator,
}

impl Xpanda {
    fn new(builder: Builder) -> Self {
        Self {
            evaluator: Evaluator::new(
                builder.no_unset,
                builder.positional_vars,
                builder.named_vars,
            ),
        }
    }

    #[must_use]
    pub fn builder() -> Builder {
        Builder::default()
    }

    /// Expands the given text by substituting the values of the variables inside it.
    ///
    /// Variables can appear in any of the following forms:
    ///
    /// <table>
    ///   <thead>
    ///     <tr>
    ///       <th>Pattern</th>
    ///       <th>Description</th>
    ///     </tr>
    ///   </thead>
    ///   <tbody>
    ///     <tr>
    ///       <td>$VAR</td>
    ///       <td>substituted with the corresponding value for 'VAR' if set, otherwise "".</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR}</td>
    ///       <td>substituted with the corresponding value for 'VAR' if set, otherwise "".</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR-default}</td>
    ///       <td>substituted with the corresponding value for 'VAR' if set, otherwise "default".</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR:-default}</td>
    ///       <td>
    ///         substituted with the corresponding value for 'VAR' if set and non-empty, otherwise
    ///         "default".
    ///       </td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR+alternative}</td>
    ///       <td>
    ///         substituted with "alternative" if the corresponding value for 'VAR' is set, otherwise "".
    ///       </td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR:+alternative}</td>
    ///       <td>
    ///         substituted with "alternative" if the corresponding value for 'VAR' is set and non-empty,
    ///         otherwise "".
    ///       </td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR?}</td>
    ///       <td>
    ///         substituted with the corresponding value for 'VAR' if set, otherwise yields an error.
    ///       </td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR?error}</td>
    ///       <td>
    ///         substituted with the corresponding value for 'VAR' if set, otherwise yields an error with
    ///         the given message (in this case "error").
    ///       </td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR?error}</td>
    ///       <td>
    ///         substituted with the corresponding value for 'VAR' if set and non-empty, otherwise yields
    ///         an error with the given message (in this case "error").
    ///       </td>
    ///     </tr>
    ///     <tr>
    ///       <td>${#VAR}</td>
    ///       <td>
    ///         substituted with the length of the corresponding value for 'VAR' if set, otherwise "0".
    ///       </td>
    ///     </tr>
    ///   </tbody>
    /// </table>
    ///
    /// `VAR` above is a named variable. Named variables can be provided using the builder:
    ///
    /// ```rust
    /// use std::collections::HashMap;
    /// use xpanda::Xpanda;
    ///
    /// let named_vars = HashMap::new();
    /// let xpanda = Xpanda::builder()
    ///     .with_named_vars(named_vars)
    ///     .build();
    /// ```
    ///
    /// Positional variables are also supported and can be provided in the same way:
    ///
    /// ```rust
    /// use xpanda::Xpanda;
    ///
    /// let xpanda = Xpanda::builder()
    ///     .with_positional_vars(Vec::new())
    ///     .build();
    /// ```
    ///
    /// Positional variables can be referenced using their index (starting at 1), for example, `$1`
    /// references the first positional variable, `$2` the second and so on. `$0` is a space concatenated
    /// string of all positional variables.
    ///
    /// Here are some examples and their output:
    ///
    /// <table>
    ///   <thead>
    ///     <tr>
    ///       <th>Pattern</th>
    ///       <th>VAR unset</th>
    ///       <th>VAR=""</th>
    ///       <th>VAR="example"</th>
    ///     </tr>
    ///   </thead>
    ///   <tbody>
    ///     <tr>
    ///       <td>$VAR</td>
    ///       <td></td>
    ///       <td></td>
    ///       <td>"example"</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR}</td>
    ///       <td></td>
    ///       <td></td>
    ///       <td>"example"</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR-default}</td>
    ///       <td>"default"</td>
    ///       <td></td>
    ///       <td>"example"</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR:-default}</td>
    ///       <td>"default"</td>
    ///       <td>"default"</td>
    ///       <td>"example"</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR+alternative}</td>
    ///       <td></td>
    ///       <td>"alternative"</td>
    ///       <td>"alternative"</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR:+alternative}</td>
    ///       <td></td>
    ///       <td></td>
    ///       <td>"alternative"</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR?message}</td>
    ///       <td>error: "message"</td>
    ///       <td></td>
    ///       <td>"example"</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR:?message}</td>
    ///       <td>error: "message"</td>
    ///       <td>error: "message"</td>
    ///       <td>"example"</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${#VAR}</td>
    ///       <td>"0"</td>
    ///       <td>"0"</td>
    ///       <td>"7"</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${!VAR}</td>
    ///       <td></td>
    ///       <td></td>
    ///       <td>$example</td>
    ///     </tr>
    ///   </tbody>
    /// </table>
    ///
    /// Special rules take precedence when [`Builder::no_unset`] is `true`:
    ///
    /// <table>
    ///   <thead>
    ///     <tr>
    ///       <th>Pattern</th>
    ///       <th>VAR unset</th>
    ///     </tr>
    ///   </thead>
    ///   <tbody>
    ///     <tr>
    ///       <td>$VAR</td>
    ///       <td>error</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${VAR}</td>
    ///       <td>error</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${#VAR}</td>
    ///       <td>error</td>
    ///     </tr>
    ///     <tr>
    ///       <td>${!VAR}</td>
    ///       <td>error</td>
    ///     </tr>
    ///   </tbody>
    /// </table>
    ///
    /// Default/Alternative values can also be variables:
    ///
    /// <table>
    ///   <thead>
    ///     <tr>
    ///       <th>Pattern</th>
    ///       <th>VAR unset</th>
    ///       <th>VAR=""</th>
    ///       <th>VAR="example"</th>
    ///     </tr>
    ///   </thead>
    ///   <tbody>
    ///     <tr>
    ///       <td>`${VAR:-$DEF}`</td>
    ///       <td>`$DEF`</td>
    ///       <td></td>
    ///       <td>"example"</td>
    ///     </tr>
    ///     <tr>
    ///       <td>`${VAR+${ALT:-alternative}}`</td>
    ///       <td></td>
    ///       <td>`${ALT:-alternative}`</td>
    ///       <td>${ALT:-alternative}</td>
    ///     </tr>
    ///   </tbody>
    /// </table>
    ///
    /// The `$` character is assumed to be the start of a variable. If the variable does not match
    /// any of the forms listed above, an error is returned. Variables can be escaped by prefixing them
    /// by an additional '$', for example: `$$VAR` which yields `$VAR` and `${VAR-$$text}` which yields
    /// `$text` if `VAR` is unset.
    ///
    /// # Errors
    ///
    /// Returns [`Err`] if the given string is badly formatted and cannot be parsed.
    ///
    /// # Examples
    ///
    /// ```
    /// use xpanda::Xpanda;
    ///
    /// let xpanda = Xpanda::default();
    /// assert_eq!(xpanda.expand("${1:-default}"), Ok(String::from("default")));
    /// ```
    pub fn expand(&self, input: &str) -> Result<String, Error> {
        let lexer = Lexer::new(input);
        let mut parser = Parser::new(lexer);
        let ast = parser.parse()?;
        let result = self.evaluator.eval(&ast)?;

        Ok(result)
    }
}