brush_parser/
prompt.rs

1//! Parser for shell prompt syntax (e.g., `PS1`).
2
3use crate::error;
4
5/// A piece of a prompt string.
6#[derive(Clone)]
7#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
8pub enum PromptPiece {
9    /// An ASCII character.
10    AsciiCharacter(u32),
11    /// A backslash character.
12    Backslash,
13    /// The bell character.
14    BellCharacter,
15    /// A carriage return character.
16    CarriageReturn,
17    /// The current command number.
18    CurrentCommandNumber,
19    /// The current history number.
20    CurrentHistoryNumber,
21    /// The name of the current user.
22    CurrentUser,
23    /// Path to the current working directory.
24    CurrentWorkingDirectory {
25        /// Whether or not to apply tilde-replacement before expanding.
26        tilde_replaced: bool,
27        /// Whether or not to only expand to the basename of the directory.
28        basename: bool,
29    },
30    /// The current date, using the given format.
31    Date(PromptDateFormat),
32    /// The dollar or pound character.
33    DollarOrPound,
34    /// Special marker indicating the end of a non-printing sequence of characters.
35    EndNonPrintingSequence,
36    /// The escape character.
37    EscapeCharacter,
38    /// An escaped sequence not otherwise recognized.
39    EscapedSequence(String),
40    /// The hostname of the system.
41    Hostname {
42        /// Whether or not to include only up to the first dot of the name.
43        only_up_to_first_dot: bool,
44    },
45    /// A literal string.
46    Literal(String),
47    /// A newline character.
48    Newline,
49    /// The number of actively managed jobs.
50    NumberOfManagedJobs,
51    /// The base name of the shell.
52    ShellBaseName,
53    /// The release of the shell.
54    ShellRelease,
55    /// The version of the shell.
56    ShellVersion,
57    /// Special marker indicating the start of a non-printing sequence of characters.
58    StartNonPrintingSequence,
59    /// The base name of the terminal device.
60    TerminalDeviceBaseName,
61    /// The current time, using the given format.
62    Time(PromptTimeFormat),
63}
64
65/// Format for a date in a prompt.
66#[derive(Clone)]
67#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
68pub enum PromptDateFormat {
69    /// A format including weekday, month, and date.
70    WeekdayMonthDate,
71    /// A customer string format.
72    Custom(String),
73}
74
75/// Format for a time in a prompt.
76#[derive(Clone)]
77#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
78pub enum PromptTimeFormat {
79    /// A twelve-hour time format with AM/PM.
80    TwelveHourAM,
81    /// A twelve-hour time format (HHMMSS).
82    TwelveHourHHMMSS,
83    /// A twenty-four-hour time format (HHMM).
84    TwentyFourHourHHMM,
85    /// A twenty-four-hour time format (HHMMSS).
86    TwentyFourHourHHMMSS,
87}
88
89peg::parser! {
90    grammar prompt_parser() for str {
91        pub(crate) rule prompt() -> Vec<PromptPiece> =
92            pieces:prompt_piece()*
93
94        rule prompt_piece() -> PromptPiece =
95            special_sequence() /
96            literal_sequence()
97
98        //
99        // Reference: https://www.gnu.org/software/bash/manual/bash.html#Controlling-the-Prompt
100        //
101        rule special_sequence() -> PromptPiece =
102            "\\a" { PromptPiece::BellCharacter } /
103            "\\A" { PromptPiece::Time(PromptTimeFormat::TwentyFourHourHHMM) } /
104            "\\d" { PromptPiece::Date(PromptDateFormat::WeekdayMonthDate) } /
105            "\\D{" f:date_format() "}" { PromptPiece::Date(PromptDateFormat::Custom(f)) } /
106            "\\e" { PromptPiece::EscapeCharacter } /
107            "\\h" { PromptPiece::Hostname { only_up_to_first_dot: true } } /
108            "\\H" { PromptPiece::Hostname { only_up_to_first_dot: false } } /
109            "\\j" { PromptPiece::NumberOfManagedJobs } /
110            "\\l" { PromptPiece::TerminalDeviceBaseName } /
111            "\\n" { PromptPiece::Newline } /
112            "\\r" { PromptPiece::CarriageReturn } /
113            "\\s" { PromptPiece::ShellBaseName } /
114            "\\t" { PromptPiece::Time(PromptTimeFormat::TwentyFourHourHHMMSS ) } /
115            "\\T" { PromptPiece::Time(PromptTimeFormat::TwelveHourHHMMSS ) } /
116            "\\@" { PromptPiece::Time(PromptTimeFormat::TwelveHourAM ) } /
117            "\\u" { PromptPiece::CurrentUser } /
118            "\\v" { PromptPiece::ShellVersion } /
119            "\\V" { PromptPiece::ShellRelease } /
120            "\\w" { PromptPiece::CurrentWorkingDirectory { tilde_replaced: true, basename: false, } } /
121            "\\W" { PromptPiece::CurrentWorkingDirectory { tilde_replaced: true, basename: true, } } /
122            "\\!" { PromptPiece::CurrentHistoryNumber } /
123            "\\#" { PromptPiece::CurrentCommandNumber } /
124            "\\$" { PromptPiece::DollarOrPound } /
125            "\\" n:octal_number() { PromptPiece::AsciiCharacter(n) } /
126            "\\\\" { PromptPiece::Backslash } /
127            "\\[" { PromptPiece::StartNonPrintingSequence } /
128            "\\]" { PromptPiece::EndNonPrintingSequence } /
129            s:$("\\" [_]) { PromptPiece::EscapedSequence(s.to_owned()) }
130
131        rule literal_sequence() -> PromptPiece =
132            s:$((!special_sequence() [c])+) { PromptPiece::Literal(s.to_owned()) }
133
134        rule date_format() -> String =
135            s:$([c if c != '}']*) { s.to_owned() }
136
137        rule octal_number() -> u32 =
138            s:$(['0'..='7']*<1,3>) {? u32::from_str_radix(s, 8).or(Err("invalid octal number")) }
139    }
140}
141
142/// Parses a shell prompt string.
143///
144/// # Arguments
145///
146/// * `s` - The prompt string to parse.
147pub fn parse(s: &str) -> Result<Vec<PromptPiece>, error::WordParseError> {
148    let result = prompt_parser::prompt(s).map_err(|e| error::WordParseError::Prompt(e.into()))?;
149    Ok(result)
150}