use crate::error;
#[derive(Clone, Debug)]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum PromptPiece {
AsciiCharacter(u32),
Backslash,
BellCharacter,
CarriageReturn,
CurrentCommandNumber,
CurrentHistoryNumber,
CurrentUser,
CurrentWorkingDirectory {
tilde_replaced: bool,
basename: bool,
},
Date(PromptDateFormat),
DollarOrPound,
EndNonPrintingSequence,
EscapeCharacter,
EscapedSequence(String),
Hostname {
only_up_to_first_dot: bool,
},
Literal(String),
Newline,
NumberOfManagedJobs,
ShellBaseName,
ShellRelease,
ShellVersion,
StartNonPrintingSequence,
TerminalDeviceBaseName,
Time(PromptTimeFormat),
}
#[derive(Clone, Debug)]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum PromptDateFormat {
WeekdayMonthDate,
Custom(String),
}
#[derive(Clone, Debug)]
#[cfg_attr(
any(test, feature = "serde"),
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
)]
pub enum PromptTimeFormat {
TwelveHourAM,
TwelveHourHHMMSS,
TwentyFourHourHHMM,
TwentyFourHourHHMMSS,
}
peg::parser! {
grammar prompt_parser() for str {
pub(crate) rule prompt() -> Vec<PromptPiece> =
pieces:prompt_piece()*
rule prompt_piece() -> PromptPiece =
special_sequence() /
literal_sequence()
rule special_sequence() -> PromptPiece =
"\\a" { PromptPiece::BellCharacter } /
"\\A" { PromptPiece::Time(PromptTimeFormat::TwentyFourHourHHMM) } /
"\\d" { PromptPiece::Date(PromptDateFormat::WeekdayMonthDate) } /
"\\D{" f:date_format() "}" { PromptPiece::Date(PromptDateFormat::Custom(f)) } /
"\\e" { PromptPiece::EscapeCharacter } /
"\\h" { PromptPiece::Hostname { only_up_to_first_dot: true } } /
"\\H" { PromptPiece::Hostname { only_up_to_first_dot: false } } /
"\\j" { PromptPiece::NumberOfManagedJobs } /
"\\l" { PromptPiece::TerminalDeviceBaseName } /
"\\n" { PromptPiece::Newline } /
"\\r" { PromptPiece::CarriageReturn } /
"\\s" { PromptPiece::ShellBaseName } /
"\\t" { PromptPiece::Time(PromptTimeFormat::TwentyFourHourHHMMSS ) } /
"\\T" { PromptPiece::Time(PromptTimeFormat::TwelveHourHHMMSS ) } /
"\\@" { PromptPiece::Time(PromptTimeFormat::TwelveHourAM ) } /
"\\u" { PromptPiece::CurrentUser } /
"\\v" { PromptPiece::ShellVersion } /
"\\V" { PromptPiece::ShellRelease } /
"\\w" { PromptPiece::CurrentWorkingDirectory { tilde_replaced: true, basename: false, } } /
"\\W" { PromptPiece::CurrentWorkingDirectory { tilde_replaced: true, basename: true, } } /
"\\!" { PromptPiece::CurrentHistoryNumber } /
"\\#" { PromptPiece::CurrentCommandNumber } /
"\\$" { PromptPiece::DollarOrPound } /
"\\" n:octal_number() { PromptPiece::AsciiCharacter(n) } /
"\\\\" { PromptPiece::Backslash } /
"\\[" { PromptPiece::StartNonPrintingSequence } /
"\\]" { PromptPiece::EndNonPrintingSequence } /
s:$("\\" [_]) { PromptPiece::EscapedSequence(s.to_owned()) }
rule literal_sequence() -> PromptPiece =
s:$((!special_sequence() [c])+) { PromptPiece::Literal(s.to_owned()) }
rule date_format() -> String =
s:$([c if c != '}']*) { s.to_owned() }
rule octal_number() -> u32 =
s:$(['0'..='7']*<1,3>) {? u32::from_str_radix(s, 8).or(Err("invalid octal number")) }
}
}
pub fn parse(s: &str) -> Result<Vec<PromptPiece>, error::WordParseError> {
let result = prompt_parser::prompt(s).map_err(|e| error::WordParseError::Prompt(e.into()))?;
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
use pretty_assertions::assert_eq;
#[test]
fn basic_prompt() -> Result<()> {
assert_eq!(
parse(r"\u@\h:\w$ ")?,
&[
PromptPiece::CurrentUser,
PromptPiece::Literal("@".to_owned()),
PromptPiece::Hostname {
only_up_to_first_dot: true
},
PromptPiece::Literal(":".to_owned()),
PromptPiece::CurrentWorkingDirectory {
tilde_replaced: true,
basename: false
},
PromptPiece::Literal("$ ".to_owned()),
]
);
Ok(())
}
#[test]
fn brackets_and_vars() -> Result<()> {
assert_eq!(
parse(r"\[$foo\]\u > ")?,
&[
PromptPiece::StartNonPrintingSequence,
PromptPiece::Literal("$foo".to_owned()),
PromptPiece::EndNonPrintingSequence,
PromptPiece::CurrentUser,
PromptPiece::Literal(" > ".to_owned()),
]
);
Ok(())
}
}