use std::{
borrow::Cow,
fmt::{Debug, Display, Write},
sync::Arc,
};
use super::{
parser::{ParseError, apml_lst},
pattern::BashPattern,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ApmlLst<'a>(pub Vec<Token<'a>>);
impl Display for ApmlLst<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for token in &self.0 {
Display::fmt(token, f)?;
}
Ok(())
}
}
impl<'a> ApmlLst<'a> {
pub fn parse(src: &'a str) -> Result<Self, ParseError> {
let (out, tree) = apml_lst(src)?;
if !out.is_empty() {
return Err(ParseError::UnexpectedSource {
pos: nom::Offset::offset(src, out) + 1,
});
}
Ok(tree)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Token<'a> {
Spacy(char),
Newline,
Comment(Cow<'a, str>),
Variable(VariableDefinition<'a>),
}
impl Token<'_> {
pub fn is_empty(&self) -> bool {
matches!(&self, Token::Newline | Token::Spacy(_))
}
}
impl Display for Token<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Token::Spacy(ch) => f.write_char(*ch),
Token::Newline => f.write_char('\n'),
Token::Comment(text) => f.write_fmt(format_args!("#{text}")),
Token::Variable(def) => Display::fmt(def, f),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct VariableDefinition<'a> {
pub name: Cow<'a, str>,
pub op: VariableOp,
pub value: VariableValue<'a>,
}
impl Display for VariableDefinition<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.name)?;
Display::fmt(&self.op, f)?;
Display::fmt(&self.value, f)?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum VariableOp {
Assignment,
Append,
}
impl Display for VariableOp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VariableOp::Assignment => f.write_char('='),
VariableOp::Append => f.write_str("+="),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum VariableValue<'a> {
String(Arc<Text<'a>>),
Array(Vec<ArrayToken<'a>>),
}
impl Display for VariableValue<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VariableValue::String(text) => Display::fmt(text, f),
VariableValue::Array(tokens) => {
f.write_char('(')?;
for token in tokens {
Display::fmt(token, f)?;
}
f.write_char(')')?;
Ok(())
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Text<'a>(pub Vec<TextUnit<'a>>);
impl Display for Text<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for unit in &self.0 {
Display::fmt(unit, f)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TextUnit<'a> {
Unquoted(Vec<Word<'a>>),
SingleQuote(Cow<'a, str>),
DoubleQuote(Vec<Word<'a>>),
}
impl Display for TextUnit<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TextUnit::Unquoted(words) => {
for word in words {
Display::fmt(word, f)?;
}
Ok(())
}
TextUnit::SingleQuote(text) => {
f.write_fmt(format_args!("'{text}'"))
}
TextUnit::DoubleQuote(words) => {
f.write_char('"')?;
for word in words {
Display::fmt(word, f)?;
}
f.write_char('"')?;
Ok(())
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Word<'a> {
Literal(Vec<LiteralPart<'a>>),
UnbracedVariable(Cow<'a, str>),
BracedVariable(BracedExpansion<'a>),
Subcommand(Vec<ArrayToken<'a>>),
}
impl Display for Word<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Word::Literal(parts) => {
for part in parts {
Display::fmt(part, f)?;
}
Ok(())
}
Word::UnbracedVariable(name) => {
f.write_fmt(format_args!("${name}"))
}
Word::BracedVariable(exp) => {
f.write_fmt(format_args!("${{{exp}}}"))
}
Word::Subcommand(tokens) => {
f.write_str("$(")?;
for token in tokens {
Display::fmt(token, f)?;
}
f.write_str(")")?;
Ok(())
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum LiteralPart<'a> {
String(Cow<'a, str>),
Escaped(char),
LineContinuation,
}
impl Display for LiteralPart<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LiteralPart::String(text) => f.write_str(text),
LiteralPart::Escaped(ch) => f.write_fmt(format_args!("\\{ch}")),
LiteralPart::LineContinuation => f.write_str("\\\n"),
}
}
}
impl LiteralPart<'_> {
pub fn should_escape(ch: char) -> bool {
matches!(ch, '$' | '"' | '\\')
}
pub fn escape<S: AsRef<str>>(text: S) -> Vec<Self> {
let mut result = Vec::new();
let mut buffer = String::new();
for ch in text.as_ref().chars() {
if Self::should_escape(ch) {
if !buffer.is_empty() {
result.push(LiteralPart::String(Cow::Owned(buffer)));
buffer = String::new();
}
result.push(LiteralPart::Escaped(ch));
} else {
buffer.push(ch);
}
}
if !buffer.is_empty() {
result.push(LiteralPart::String(Cow::Owned(buffer)));
}
result
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct BracedExpansion<'a> {
pub name: Cow<'a, str>,
pub modifier: Option<ExpansionModifier<'a>>,
}
impl Display for BracedExpansion<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.modifier {
Some(ExpansionModifier::Length) => {
f.write_fmt(format_args!("#{}", self.name))
}
None => f.write_str(&self.name),
Some(modifier) => {
f.write_fmt(format_args!("{}{}", self.name, modifier))
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ExpansionModifier<'a> {
Substring {
offset: Cow<'a, str>,
length: Option<Cow<'a, str>>,
},
StripShortestPrefix(Arc<BashPattern<'a>>),
StripLongestPrefix(Arc<BashPattern<'a>>),
StripShortestSuffix(Arc<BashPattern<'a>>),
StripLongestSuffix(Arc<BashPattern<'a>>),
ReplaceOnce {
pattern: Arc<BashPattern<'a>>,
string: Option<Arc<Text<'a>>>,
},
ReplaceAll {
pattern: Arc<BashPattern<'a>>,
string: Option<Arc<Text<'a>>>,
},
ReplacePrefix {
pattern: Arc<BashPattern<'a>>,
string: Option<Arc<Text<'a>>>,
},
ReplaceSuffix {
pattern: Arc<BashPattern<'a>>,
string: Option<Arc<Text<'a>>>,
},
UpperOnce(Arc<BashPattern<'a>>),
UpperAll(Arc<BashPattern<'a>>),
LowerOnce(Arc<BashPattern<'a>>),
LowerAll(Arc<BashPattern<'a>>),
ErrorOnUnset(Arc<Text<'a>>),
Length,
WhenUnset(Arc<Text<'a>>),
WhenSet(Arc<Text<'a>>),
ArrayElements,
SingleWordElements,
}
impl Display for ExpansionModifier<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExpansionModifier::Substring { offset, length } => match length {
None => f.write_fmt(format_args!(":{offset}")),
Some(length) => f.write_fmt(format_args!(":{offset}:{length}")),
},
ExpansionModifier::StripShortestPrefix(pattern) => {
f.write_fmt(format_args!("#{pattern}"))
}
ExpansionModifier::StripLongestPrefix(pattern) => {
f.write_fmt(format_args!("##{pattern}"))
}
ExpansionModifier::StripShortestSuffix(pattern) => {
f.write_fmt(format_args!("%{pattern}"))
}
ExpansionModifier::StripLongestSuffix(pattern) => {
f.write_fmt(format_args!("%%{pattern}"))
}
ExpansionModifier::ReplaceOnce { pattern, string } => {
match string {
Some(string) => {
f.write_fmt(format_args!("/{pattern}/{string}"))
}
None => f.write_fmt(format_args!("/{pattern}")),
}
}
ExpansionModifier::ReplaceAll { pattern, string } => match string {
Some(string) => {
f.write_fmt(format_args!("//{pattern}/{string}"))
}
None => f.write_fmt(format_args!("//{pattern}")),
},
ExpansionModifier::ReplacePrefix { pattern, string } => {
match string {
Some(string) => {
f.write_fmt(format_args!("/#{pattern}/{string}"))
}
None => f.write_fmt(format_args!("/#{pattern}")),
}
}
ExpansionModifier::ReplaceSuffix { pattern, string } => {
match string {
Some(string) => {
f.write_fmt(format_args!("/%{pattern}/{string}"))
}
None => f.write_fmt(format_args!("/%{pattern}")),
}
}
ExpansionModifier::UpperOnce(pattern) => {
f.write_fmt(format_args!("^{pattern}"))
}
ExpansionModifier::UpperAll(pattern) => {
f.write_fmt(format_args!("^^{pattern}"))
}
ExpansionModifier::LowerOnce(pattern) => {
f.write_fmt(format_args!(",{pattern}"))
}
ExpansionModifier::LowerAll(pattern) => {
f.write_fmt(format_args!(",,{pattern}"))
}
ExpansionModifier::ErrorOnUnset(text) => {
f.write_fmt(format_args!(":?{text}"))
}
ExpansionModifier::Length => f.write_char('#'),
ExpansionModifier::WhenUnset(text) => {
f.write_fmt(format_args!(":-{text}"))
}
ExpansionModifier::WhenSet(text) => {
f.write_fmt(format_args!(":+{text}"))
}
ExpansionModifier::ArrayElements => f.write_str("[@]"),
ExpansionModifier::SingleWordElements => f.write_str("[*]"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ArrayToken<'a> {
Spacy(char),
Newline,
Comment(Cow<'a, str>),
Element(Arc<Text<'a>>),
}
impl Display for ArrayToken<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ArrayToken::Spacy(ch) => f.write_char(*ch),
ArrayToken::Newline => f.write_char('\n'),
ArrayToken::Comment(text) => {
f.write_char('#')?;
f.write_str(text)?;
Ok(())
}
ArrayToken::Element(text) => Display::fmt(text, f),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_apml_parse() {
let tree = ApmlLst::parse(r##"# Test APML"##).unwrap();
dbg!(&tree);
let tree = ApmlLst::parse(r##"aaa"##).unwrap_err();
dbg!(&tree);
}
#[test]
fn test_token() {
assert!(Token::Newline.is_empty());
assert!(Token::Spacy(' ').is_empty());
assert!(Token::Spacy('\t').is_empty());
assert!(!Token::Comment(Cow::Borrowed("Test")).is_empty());
assert!(
!Token::Variable(VariableDefinition {
name: Cow::Borrowed("Test"),
op: VariableOp::Assignment,
value: VariableValue::String(Arc::new(Text(vec![])))
})
.is_empty()
);
}
#[test]
fn test_literal_part_escape() {
assert!(LiteralPart::should_escape('$'));
assert!(LiteralPart::should_escape('"'));
assert!(LiteralPart::should_escape('\\'));
assert!(!LiteralPart::should_escape('a'));
assert!(!LiteralPart::should_escape(' '));
assert_eq!(
LiteralPart::escape("asdf"),
vec![LiteralPart::String("asdf".into())]
);
assert_eq!(
LiteralPart::escape("asd$$f\ng\\"),
vec![
LiteralPart::String("asd".into()),
LiteralPart::Escaped('$'),
LiteralPart::Escaped('$'),
LiteralPart::String("f\ng".into()),
LiteralPart::Escaped('\\'),
]
);
}
}