const MAX_TEXTS: usize = 64;
const OVERFLOW_WORD: &str = "…";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Segment {
pub words: Vec<String>,
pub redirects: Vec<String>,
}
impl Segment {
fn overflow() -> Self {
Self {
words: vec![OVERFLOW_WORD.to_owned()],
redirects: Vec::new(),
}
}
}
pub fn segments(command: &str) -> Vec<Segment> {
let mut out = Vec::new();
let mut pending = vec![command.to_owned()];
let mut lexed = 0usize;
while let Some(text) = pending.pop() {
lexed += 1;
if lexed > MAX_TEXTS {
out.push(Segment::overflow());
break;
}
let (segs, inner) = lex(&text);
out.extend(segs);
pending.extend(inner);
}
out
}
#[derive(Default)]
struct Build {
segs: Vec<Segment>,
seg: Segment,
word: String,
redirect: bool,
}
impl Build {
fn word_end(&mut self) {
if self.word.is_empty() {
return;
}
let word = std::mem::take(&mut self.word);
if std::mem::take(&mut self.redirect) {
self.seg.redirects.push(word);
} else {
self.seg.words.push(word);
}
}
fn seg_end(&mut self) {
self.word_end();
let seg = std::mem::take(&mut self.seg);
if !seg.words.is_empty() || !seg.redirects.is_empty() {
self.segs.push(seg);
}
}
fn finish(mut self) -> Vec<Segment> {
self.seg_end();
self.segs
}
}
fn lex(text: &str) -> (Vec<Segment>, Vec<String>) {
let mut b = Build::default();
let mut inner: Vec<String> = Vec::new();
let mut chars = text.chars().peekable();
let mut quote: Option<char> = None;
while let Some(c) = chars.next() {
if let Some(q) = quote {
if c == q {
quote = None;
} else if q == '"' && c == '$' && chars.peek() == Some(&'(') {
chars.next();
inner.push(balanced(&mut chars));
} else {
b.word.push(c);
}
continue;
}
match c {
'\'' | '"' => quote = Some(c),
'\\' => {
if let Some(next) = chars.next() {
b.word.push(next);
}
}
'`' => inner.push(until(&mut chars, '`')),
'$' if chars.peek() == Some(&'(') => {
chars.next();
inner.push(balanced(&mut chars));
}
'>' => {
b.word_end();
if chars.peek() == Some(&'>') {
chars.next();
}
b.redirect = true;
}
'<' | ';' | '&' | '|' | '\n' => b.seg_end(),
c if c.is_whitespace() => b.word_end(),
c => b.word.push(c),
}
}
(b.finish(), inner)
}
fn until(chars: &mut std::iter::Peekable<std::str::Chars<'_>>, end: char) -> String {
let mut out = String::new();
for c in chars.by_ref() {
if c == end {
break;
}
out.push(c);
}
out
}
fn balanced(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> String {
let mut out = String::new();
let mut depth = 1usize;
for c in chars.by_ref() {
match c {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
out.push(c);
}
out
}
#[cfg(test)]
mod tests;