#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
use std::fmt::{self, Display, Formatter};
use std::str::FromStr;
use chumsky::{primitive::end, Parser};
use crate::expand::GlobGroup;
pub use crate::expand::ExpandGlobIter;
mod expand;
mod parse;
#[derive(Debug, Clone)]
pub struct Literal(Box<str>);
impl Literal {
#[inline]
pub fn from_text(text: impl Into<String>) -> Self {
Literal(text.into().into_boxed_str())
}
#[inline]
pub fn text(&self) -> &str {
&self.0
}
#[inline]
pub fn into_expr(self) -> GlobExpr {
GlobExpr {
kind: GlobExprKind::Literal(self),
}
}
pub fn equivalent_expr(&self) -> String {
let mut buffer = String::new();
self.write_equivalent_expr(&mut buffer).unwrap();
buffer
}
pub fn escape(text: &str) -> String {
Literal::from_text(text).equivalent_expr()
}
fn write_equivalent_expr(&self, out: &mut impl fmt::Write) -> fmt::Result {
for c in self.0.chars() {
if parse::SpecialChar::from_char(c).is_some() {
out.write_char('\\')?;
}
out.write_char(c)?;
}
Ok(())
}
}
impl Display for Literal {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.write_equivalent_expr(f)
}
}
impl FromStr for Literal {
type Err = LiteralParseError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
parse::literal()
.then_ignore(end())
.labelled("literal")
.parse(text)
.map_err(GlobParseError::from_causes)
.map_err(LiteralParseError)
}
}
#[derive(thiserror::Error, Debug)]
#[error(transparent)]
pub struct LiteralParseError(GlobParseError);
#[derive(Debug, Clone)]
pub struct GlobExpr {
kind: GlobExprKind,
}
impl GlobExpr {
#[inline]
pub fn expand(&self) -> ExpandGlobIter<'_> {
ExpandGlobIter::new(self)
}
pub fn equivalent_expr(&self) -> String {
let mut buffer = String::new();
self.write_equivalent_expr(&mut buffer);
buffer
}
fn write_equivalent_expr(&self, buffer: &mut String) {
match self.kind {
GlobExprKind::Literal(ref text) => text.write_equivalent_expr(buffer).unwrap(),
GlobExprKind::Group(ref group) => group.write_equivalent_expr(buffer),
}
}
}
impl FromStr for GlobExpr {
type Err = GlobParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse::expr()
.then_ignore(end())
.labelled("glob expression")
.parse(s)
.map_err(GlobParseError::from_causes)
}
}
#[derive(thiserror::Error, Debug)]
#[error("{first_error}")]
pub struct GlobParseError {
first_error: parse::ErrorImpl,
}
impl GlobParseError {
pub(crate) fn from_causes(mut causes: Vec<parse::ErrorImpl>) -> Self {
assert!(!causes.is_empty(), "No causes");
GlobParseError {
first_error: causes.swap_remove(0),
}
}
}
#[derive(Debug, Clone)]
enum GlobExprKind {
Literal(Literal),
Group(Box<GlobGroup>),
}
#[cfg(test)]
mod test {
use super::*;
use itertools::Itertools;
use pretty_assertions::assert_eq;
struct GlobTest {
expression: &'static str,
expected_expansions: &'static [&'static str],
}
impl GlobTest {
pub fn parse(&self) -> GlobExpr {
self.expression.parse().expect("Parse failure")
}
}
macro_rules! test_data {
($($expression:literal => [$($expansion:literal),*]),*) => {
&[$(GlobTest {
expression: $expression,
expected_expansions: &[$($expansion),*]
}),*]
};
}
const TEST_DATA: &[GlobTest] = test_data! {
"foo-{bar,baz}-beat" => ["foo-bar-beat", "foo-baz-beat"],
"foo-{bar,beat{nest,foop}}-baz" => [
"foo-bar-baz",
"foo-beatnest-baz",
"foo-beatfoop-baz"
],
"foo{,-\\{baz{teach,wo\\,\\}}}\\\\\\{" => [
"foo\\{",
"foo-{bazteach\\{",
"foo-{bazwo,}\\{"
],
"feet\\{\\\\\\}" => ["feet{\\}"]
};
#[test]
fn test_expansion() {
for test in TEST_DATA {
let expanded = test.parse().expand().join("\n");
let expected_expansions = test
.expected_expansions
.iter()
.cloned()
.map(String::from)
.join("\n");
assert_eq!(expanded, expected_expansions);
}
}
#[test]
fn test_roundtrip() {
for test in TEST_DATA {
assert_eq!(test.parse().equivalent_expr(), test.expression);
}
}
}