#![allow(missing_docs)]
pub const SIGIL_ALL_POSITIONAL: &str = "*";
pub const SIGIL_ALL_ARGS: &str = "**";
pub const SIGIL_ARG_COUNT: &str = "#";
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
pub struct Text {
pub segments: Vec<TextSegment>,
}
impl Text {
#[must_use]
pub const fn new() -> Self {
Self {
segments: Vec::new(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.segments
.iter()
.all(|s| matches!(s, TextSegment::Literal(literal) if literal.is_empty()))
}
#[must_use]
pub fn literal_str(&self) -> Option<&str> {
match self.segments.as_slice() {
[] => Some(""),
[TextSegment::Literal(s)] => Some(s),
_ => None,
}
}
pub fn iter(&self) -> std::slice::Iter<'_, TextSegment> {
self.segments.iter()
}
}
impl<'a> IntoIterator for &'a Text {
type Item = &'a TextSegment;
type IntoIter = std::slice::Iter<'a, TextSegment>;
fn into_iter(self) -> Self::IntoIter {
self.segments.iter()
}
}
impl From<&str> for Text {
fn from(s: &str) -> Self {
Self {
segments: vec![TextSegment::Literal(s.to_owned())],
}
}
}
impl From<String> for Text {
fn from(s: String) -> Self {
Self {
segments: vec![TextSegment::Literal(s)],
}
}
}
impl From<MacroRef> for Text {
fn from(m: MacroRef) -> Self {
Self {
segments: vec![TextSegment::Macro(Box::new(m))],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum TextSegment {
Literal(String),
Macro(Box<MacroRef>),
}
impl TextSegment {
pub fn macro_ref(m: MacroRef) -> Self {
Self::Macro(Box::new(m))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
pub struct MacroRef {
pub kind: MacroKind,
pub name: String,
pub args: Vec<Text>,
pub conditional: ConditionalMacro,
pub with_value: Option<Text>,
}
impl MacroRef {
#[must_use]
pub fn positional_index(&self) -> Option<u32> {
if self.name.len() > 1 && self.name.starts_with('0') {
return None;
}
self.name.parse::<u32>().ok()
}
#[must_use]
pub fn is_all_positional(&self) -> bool {
self.name == SIGIL_ALL_POSITIONAL
}
#[must_use]
pub fn is_all_args(&self) -> bool {
self.name == SIGIL_ALL_ARGS
}
#[must_use]
pub fn is_arg_count(&self) -> bool {
self.name == SIGIL_ARG_COUNT
}
#[must_use]
pub fn flag_ref(&self) -> Option<(&str, bool)> {
let rest = self.name.strip_prefix('-')?;
match rest.strip_suffix('*') {
Some("") | None if rest.is_empty() => None,
Some(name) if !name.is_empty() => Some((name, true)),
Some(_) => None,
None => Some((rest, false)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum MacroKind {
Plain,
Braced,
Parametric,
Shell,
Expr,
Lua,
Builtin(BuiltinMacro),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ConditionalMacro {
None,
IfDefined,
IfNotDefined,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum BuiltinMacro {
Expand,
Expr,
Shrink,
Quote,
Gsub,
Sub,
Len,
Upper,
Lower,
Reverse,
Basename,
Dirname,
Suffix,
Exists,
Load,
Echo,
Warn,
Error,
Dnl,
Trace,
Dump,
Other(Box<str>),
}
#[cfg(test)]
mod tests {
use super::*;
fn macro_named(name: &str) -> MacroRef {
MacroRef {
kind: MacroKind::Braced,
name: name.into(),
args: Vec::new(),
conditional: ConditionalMacro::None,
with_value: None,
}
}
#[test]
fn positional_index_basic() {
assert_eq!(macro_named("1").positional_index(), Some(1));
assert_eq!(macro_named("9").positional_index(), Some(9));
assert_eq!(macro_named("0").positional_index(), Some(0));
}
#[test]
fn positional_index_rejects_leading_zero_compound() {
assert_eq!(macro_named("01").positional_index(), None);
assert_eq!(macro_named("007").positional_index(), None);
}
#[test]
fn positional_index_rejects_non_numeric() {
assert_eq!(macro_named("foo").positional_index(), None);
assert_eq!(macro_named("*").positional_index(), None);
assert_eq!(macro_named("-f").positional_index(), None);
}
#[test]
fn all_positional_args_count_classifiers() {
assert!(macro_named("*").is_all_positional());
assert!(!macro_named("1").is_all_positional());
assert!(!macro_named("**").is_all_positional());
assert!(macro_named("**").is_all_args());
assert!(!macro_named("*").is_all_args());
assert!(macro_named("#").is_arg_count());
assert!(!macro_named("1").is_arg_count());
}
#[test]
fn flag_ref_named() {
assert_eq!(macro_named("-f").flag_ref(), Some(("f", false)));
assert_eq!(macro_named("-foo").flag_ref(), Some(("foo", false)));
assert_eq!(macro_named("-f*").flag_ref(), Some(("f", true)));
assert_eq!(macro_named("-foo*").flag_ref(), Some(("foo", true)));
}
#[test]
fn flag_ref_rejects_degenerate() {
assert_eq!(macro_named("-").flag_ref(), None);
assert_eq!(macro_named("-*").flag_ref(), None);
assert_eq!(macro_named("foo").flag_ref(), None);
assert_eq!(macro_named("*").flag_ref(), None);
}
#[test]
fn text_from_str() {
let t = Text::from("hello");
assert_eq!(t.literal_str(), Some("hello"));
assert!(!t.is_empty());
}
#[test]
fn text_is_empty_with_macro_is_false() {
let t: Text = macro_named("foo").into();
assert!(!t.is_empty());
assert_eq!(t.literal_str(), None);
}
#[test]
fn text_literal_str_empty() {
let t = Text::new();
assert_eq!(t.literal_str(), Some(""));
assert!(t.is_empty());
}
#[test]
fn text_iter_yields_segments() {
let t = Text::from("abc");
assert_eq!(t.iter().count(), 1);
let count: usize = (&t).into_iter().count();
assert_eq!(count, 1);
}
}