pub const MAX_LINE_LEN: usize = 65536;
const REJECTED_UNQUOTED_METACHARACTERS: &[char] = &['|', ';', '&', '<', '>', '`'];
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Token {
Word(String),
Flag { name: String, value: Option<String> },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum LexError {
UnterminatedQuote,
InvalidEscape(char),
NulByte,
TooLong { max: usize, actual: usize },
UnsupportedMetacharacter(char),
}
pub fn tokenize(line: &str) -> Result<Vec<Token>, LexError> {
if line.len() > MAX_LINE_LEN {
return Err(LexError::TooLong {
max: MAX_LINE_LEN,
actual: line.len(),
});
}
if line.contains('\0') {
return Err(LexError::NulByte);
}
if line.contains("$(") {
return Err(LexError::UnsupportedMetacharacter('$'));
}
let mut tokens = Vec::new();
let mut chars = line.chars().peekable();
while let Some(&ch) = chars.peek() {
if ch.is_whitespace() {
chars.next();
continue;
}
if REJECTED_UNQUOTED_METACHARACTERS.contains(&ch) {
return Err(LexError::UnsupportedMetacharacter(ch));
}
let word = read_word(&mut chars)?;
tokens.push(classify(word));
}
Ok(tokens)
}
fn classify(word: String) -> Token {
match word.strip_prefix("--") {
Some(rest) => match rest.split_once('=') {
Some((name, value)) => Token::Flag {
name: name.to_string(),
value: Some(value.to_string()),
},
None => Token::Flag {
name: rest.to_string(),
value: None,
},
},
None => Token::Word(word),
}
}
fn read_word(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Result<String, LexError> {
let mut word = String::new();
let mut started = false;
while let Some(&ch) = chars.peek() {
if ch.is_whitespace() {
break;
}
if REJECTED_UNQUOTED_METACHARACTERS.contains(&ch) && started {
return Err(LexError::UnsupportedMetacharacter(ch));
}
match ch {
'\'' => {
chars.next();
word.push_str(&read_single_quoted(chars)?);
started = true;
}
'"' => {
chars.next();
word.push_str(&read_double_quoted(chars)?);
started = true;
}
_ => {
word.push(ch);
chars.next();
started = true;
}
}
}
Ok(word)
}
fn read_single_quoted(
chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
) -> Result<String, LexError> {
let mut content = String::new();
loop {
match chars.next() {
None => return Err(LexError::UnterminatedQuote),
Some('\'') => return Ok(content),
Some(ch) => content.push(ch),
}
}
}
fn read_double_quoted(
chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
) -> Result<String, LexError> {
let mut content = String::new();
loop {
match chars.next() {
None => return Err(LexError::UnterminatedQuote),
Some('"') => return Ok(content),
Some('\\') => match chars.next() {
None => return Err(LexError::UnterminatedQuote),
Some('n') => content.push('\n'),
Some('t') => content.push('\t'),
Some('"') => content.push('"'),
Some('\\') => content.push('\\'),
Some(other) => return Err(LexError::InvalidEscape(other)),
},
Some(ch) => content.push(ch),
}
}
}