diff --git a/Cargo.lock b/Cargo.lock index 7c337190..d16bc71d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -265,6 +265,7 @@ dependencies = [ "brush-parser", "cached", "cfg-if", + "chrono", "clap", "command-fds", "criterion", @@ -475,8 +476,10 @@ checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -1579,9 +1582,9 @@ checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "os_info" -version = "3.9.0" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ca711d8b83edbb00b44d504503cd247c9c0bd8b0fa2694f2a1a3d8165379ce" +checksum = "eb6651f4be5e39563c4fe5cc8326349eb99a25d805a3493f791d5bfd0269e430" dependencies = [ "log", "serde", diff --git a/brush-core/Cargo.toml b/brush-core/Cargo.toml index 6b2427ca..eca6cb5f 100644 --- a/brush-core/Cargo.toml +++ b/brush-core/Cargo.toml @@ -22,6 +22,7 @@ async-trait = "0.1.83" brush-parser = { version = "^0.2.11", path = "../brush-parser" } cached = "0.54.0" cfg-if = "1.0.0" +chrono = "0.4.39" clap = { version = "4.5.21", features = ["derive", "wrap_help"] } fancy-regex = "0.14.0" futures = "0.3.31" diff --git a/brush-core/benches/shell.rs b/brush-core/benches/shell.rs index 82e0e8b9..bbd8d008 100644 --- a/brush-core/benches/shell.rs +++ b/brush-core/benches/shell.rs @@ -76,6 +76,13 @@ mod unix { c.bench_function("expand_one_string", |b| { b.iter(|| black_box(expand_one_string())); }); + c.bench_function("for_loop", |b| { + b.to_async(tokio()).iter(|| { + black_box(run_one_command( + "for ((i = 0; i < 100000; i++)); do :; done", + )) + }); + }); } } diff --git a/brush-core/src/prompt.rs b/brush-core/src/prompt.rs index 5337d81b..ba5053aa 100644 --- a/brush-core/src/prompt.rs +++ b/brush-core/src/prompt.rs @@ -52,7 +52,9 @@ pub(crate) fn format_prompt_piece( tilde_replaced, basename, } => format_current_working_directory(shell, tilde_replaced, basename), - brush_parser::prompt::PromptPiece::Date(_) => return error::unimp("prompt: date"), + brush_parser::prompt::PromptPiece::Date(format) => { + format_date(&chrono::Local::now(), &format) + } brush_parser::prompt::PromptPiece::DollarOrPound => { if users::is_root() { "#".to_owned() @@ -77,9 +79,7 @@ pub(crate) fn format_prompt_piece( hn } brush_parser::prompt::PromptPiece::Newline => "\n".to_owned(), - brush_parser::prompt::PromptPiece::NumberOfManagedJobs => { - return error::unimp("prompt: number of managed jobs") - } + brush_parser::prompt::PromptPiece::NumberOfManagedJobs => shell.jobs.jobs.len().to_string(), brush_parser::prompt::PromptPiece::ShellBaseName => { if let Some(shell_name) = &shell.shell_name { Path::new(shell_name) @@ -100,7 +100,9 @@ pub(crate) fn format_prompt_piece( brush_parser::prompt::PromptPiece::TerminalDeviceBaseName => { return error::unimp("prompt: terminal device base name") } - brush_parser::prompt::PromptPiece::Time(_) => return error::unimp("prompt: time"), + brush_parser::prompt::PromptPiece::Time(time_fmt) => { + format_time(&chrono::Local::now(), &time_fmt) + } }; Ok(formatted) @@ -125,3 +127,101 @@ fn format_current_working_directory(shell: &Shell, tilde_replaced: bool, basenam working_dir_str } + +fn format_time( + datetime: &chrono::DateTime, + format: &brush_parser::prompt::PromptTimeFormat, +) -> String +where + Tz::Offset: std::fmt::Display, +{ + let formatted = match format { + brush_parser::prompt::PromptTimeFormat::TwelveHourAM => datetime.format("%I:%M %p"), + brush_parser::prompt::PromptTimeFormat::TwelveHourHHMMSS => datetime.format("%I:%M:%S"), + brush_parser::prompt::PromptTimeFormat::TwentyFourHourHHMMSS => datetime.format("%H:%M:%S"), + }; + + formatted.to_string() +} + +fn format_date( + datetime: &chrono::DateTime, + format: &brush_parser::prompt::PromptDateFormat, +) -> String +where + Tz::Offset: std::fmt::Display, +{ + match format { + brush_parser::prompt::PromptDateFormat::WeekdayMonthDate => { + datetime.format("%a %b %d").to_string() + } + brush_parser::prompt::PromptDateFormat::Custom(fmt) => { + let fmt_items = chrono::format::StrftimeItems::new(fmt); + datetime.format_with_items(fmt_items).to_string() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_time() { + // Create a well-known test date/time. + let dt = chrono::DateTime::parse_from_rfc3339("2024-12-25T13:34:56.789Z").unwrap(); + + assert_eq!( + format_time(&dt, &brush_parser::prompt::PromptTimeFormat::TwelveHourAM), + "01:34 PM" + ); + + assert_eq!( + format_time( + &dt, + &brush_parser::prompt::PromptTimeFormat::TwentyFourHourHHMMSS + ), + "13:34:56" + ); + + assert_eq!( + format_time( + &dt, + &brush_parser::prompt::PromptTimeFormat::TwelveHourHHMMSS + ), + "01:34:56" + ); + } + + #[test] + fn test_format_date() { + // Create a well-known test date/time. + let dt = chrono::DateTime::parse_from_rfc3339("2024-12-25T12:34:56.789Z").unwrap(); + + assert_eq!( + format_date( + &dt, + &brush_parser::prompt::PromptDateFormat::WeekdayMonthDate + ), + "Wed Dec 25" + ); + + assert_eq!( + format_date( + &dt, + &brush_parser::prompt::PromptDateFormat::Custom(String::from("%Y-%m-%d")) + ), + "2024-12-25" + ); + + assert_eq!( + format_date( + &dt, + &brush_parser::prompt::PromptDateFormat::Custom(String::from( + "%Y-%m-%d %H:%M:%S.%f" + )) + ), + "2024-12-25 12:34:56.789000000" + ); + } +} diff --git a/brush-parser/src/prompt.rs b/brush-parser/src/prompt.rs index ba273fee..9190b17d 100644 --- a/brush-parser/src/prompt.rs +++ b/brush-parser/src/prompt.rs @@ -123,7 +123,7 @@ peg::parser! { s:$((!special_sequence() [c])+) { PromptPiece::Literal(s.to_owned()) } rule date_format() -> String = - s:$(!"}" [c]+) { s.to_owned() } + s:$([c if c != '}']*) { s.to_owned() } rule octal_number() -> u32 = s:$(['0'..='9']*<3,3>) {? u32::from_str_radix(s, 8).or(Err("invalid octal number")) } diff --git a/brush-shell/tests/cases/prompt.yaml b/brush-shell/tests/cases/prompt.yaml index 1bca3d71..7053ff3a 100644 --- a/brush-shell/tests/cases/prompt.yaml +++ b/brush-shell/tests/cases/prompt.yaml @@ -54,3 +54,39 @@ cases: prompt='\V' [[ "${prompt@P}" == ^\d+\.\d+\.\d+$ ]] && echo "Release is correct" + + - name: "Simple date" + stdin: | + prompt='\d' + [[ "${prompt@P}" == $(date +'%a %b %d') ]] && echo '\d date matches' + + - name: "Date format with plain string" + stdin: | + prompt='\D{something}' + echo "Date format with plain string: '${prompt@P}'" + + - name: "Date format with year" + stdin: | + prompt='\d{%Y}' + echo "Date format with year: '${prompt@P}'" + + - name: "Time format: @" + stdin: | + prompt='\@' + expanded=${prompt@P} + roundtripped=$(date --date="${expanded}" +'%I:%M %p') + [[ ${expanded} == ${roundtripped} ]] && echo "Time matches" + + - name: "Time format: T" + stdin: | + prompt='\T' + expanded=${prompt@P} + roundtripped=$(date --date="${expanded}" +'%I:%M:%S') + [[ ${expanded} == ${roundtripped} ]] && echo "Time matches" + + - name: "Time format: t" + stdin: | + prompt='\t' + expanded=${prompt@P} + roundtripped=$(date --date="${expanded}" +'%H:%M:%S') + [[ ${expanded} == ${roundtripped} ]] && echo "Time matches"