diff --git a/Cargo.lock b/Cargo.lock index 1ee73ef..2fc05aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,10 +100,10 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.89" +name = "arrayvec" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "ascii_tree" @@ -189,6 +189,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -197,7 +203,9 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -282,11 +290,20 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctrlc" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +dependencies = [ + "nix 0.29.0", + "windows-sys 0.59.0", +] + [[package]] name = "deno_task_shell" version = "0.17.0" dependencies = [ - "anyhow", "dirs", "futures", "glob", @@ -295,7 +312,7 @@ dependencies = [ "os_pipe", "parking_lot", "path-dedot", - "pest 2.7.12 (git+https://github.com/pest-parser/pest.git?branch=master)", + "pest", "pest_ascii_tree", "pest_derive", "pretty_assertions", @@ -344,6 +361,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dtparse" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb403c0926d35af2cc54d961bc2696a10d40725c08360ef69db04a4c201fd7" +dependencies = [ + "chrono", + "lazy_static", + "num-traits", + "rust_decimal", +] + [[package]] name = "dunce" version = "1.0.5" @@ -401,6 +430,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "futures" version = "0.3.30" @@ -634,6 +675,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags", "libc", + "redox_syscall", ] [[package]] @@ -748,7 +790,19 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.1.1", + "libc", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases 0.2.1", "libc", ] @@ -877,19 +931,9 @@ dependencies = [ [[package]] name = "pest" -version = "2.7.12" +version = "2.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c73c26c01b8c87956cea613c907c9d6ecffd8d18a2a5908e5de0adfaa185cea" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest" -version = "2.7.12" -source = "git+https://github.com/pest-parser/pest.git?branch=master#65e5b2b754a4d9bb9b231ab61ef0af437b556fbe" +checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" dependencies = [ "memchr", "miette", @@ -902,11 +946,12 @@ dependencies = [ [[package]] name = "pest_ascii_tree" version = "0.1.0" -source = "git+https://github.com/prsabahrami/pest_ascii_tree.git?branch=master#65ef51a888730598336b0628c3e42bc5ec2f358b" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152b393638a9816cd3dedb5aea2fb60ab0b641315aa133cea219c8797e768fc" dependencies = [ "ascii_tree", "escape_string", - "pest 2.7.12 (git+https://github.com/pest-parser/pest.git?branch=master)", + "pest", "pest_derive", ] @@ -916,7 +961,7 @@ version = "2.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "664d22978e2815783adbdd2c588b455b1bd625299ce36b2a99881ac9627e6d8d" dependencies = [ - "pest 2.7.12 (registry+https://github.com/rust-lang/crates.io-index)", + "pest", "pest_generator", ] @@ -926,7 +971,7 @@ version = "2.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2d5487022d5d33f4c30d91c22afa240ce2a644e87fe08caad974d4eab6badbe" dependencies = [ - "pest 2.7.12 (registry+https://github.com/rust-lang/crates.io-index)", + "pest", "pest_meta", "proc-macro2", "quote", @@ -940,7 +985,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0091754bbd0ea592c4deb3a122ce8ecbb0753b738aa82bc055fcc2eccc8d8174" dependencies = [ "once_cell", - "pest 2.7.12 (registry+https://github.com/rust-lang/crates.io-index)", + "pest", "sha2", ] @@ -1053,6 +1098,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "num-traits", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1086,7 +1141,7 @@ dependencies = [ "libc", "log", "memchr", - "nix", + "nix 0.28.0", "radix_trie", "rustyline-derive", "unicode-segmentation", @@ -1165,17 +1220,24 @@ dependencies = [ name = "shell" version = "0.1.0" dependencies = [ - "anyhow", + "chrono", "clap", + "ctrlc", "deno_task_shell", "dirs", + "dtparse", + "filetime", "futures", + "miette", + "parse_datetime", "rustyline", "tokio", "uu_date", "uu_ls", + "uu_touch", "uu_uname", "which", + "windows-sys 0.59.0", ] [[package]] @@ -1289,9 +1351,10 @@ dependencies = [ name = "tests" version = "0.1.0" dependencies = [ - "anyhow", "deno_task_shell", + "dirs", "futures", + "miette", "pretty_assertions", "shell", "tempfile", @@ -1311,18 +1374,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -1446,6 +1509,20 @@ dependencies = [ "uutils_term_grid", ] +[[package]] +name = "uu_touch" +version = "0.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07698cd67da84b138369eeed24bee77c860b8b22021581a56edd31c4697dd61d" +dependencies = [ + "chrono", + "clap", + "filetime", + "parse_datetime", + "uucore", + "windows-sys 0.48.0", +] + [[package]] name = "uu_uname" version = "0.0.27" @@ -1468,7 +1545,7 @@ dependencies = [ "glob", "itertools", "libc", - "nix", + "nix 0.28.0", "number_prefix", "once_cell", "os_display", diff --git a/README.md b/README.md index 216f3fd..a86c4d9 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ cargo r # To run a script cargo r -- ./scripts/hello_world.sh + +# To run a script and continue in interactive mode +cargo r -- ./scripts/hello_world.sh --interact ``` ## License diff --git a/crates/deno_task_shell/Cargo.toml b/crates/deno_task_shell/Cargo.toml index 8a3f909..458a4ac 100644 --- a/crates/deno_task_shell/Cargo.toml +++ b/crates/deno_task_shell/Cargo.toml @@ -15,7 +15,6 @@ shell = ["futures", "glob", "os_pipe", "path-dedot", "tokio", "tokio-util"] serialization = ["serde"] [dependencies] -anyhow = "1.0.87" futures = { version = "0.3.30", optional = true } glob = { version = "0.3.1", optional = true } path-dedot = { version = "3.1.1", optional = true } @@ -24,11 +23,11 @@ tokio-util = { version = "0.7.12", optional = true } os_pipe = { version = "1.2.1", optional = true } serde = { version = "1", features = ["derive"], optional = true } thiserror = "1.0.63" -pest = { git = "https://github.com/pest-parser/pest.git", branch = "master", features = ["miette-error"] } +pest = { version="2.7.13", features = ["miette-error"] } pest_derive = "2.7.12" dirs = "5.0.1" -pest_ascii_tree = { git = "https://github.com/prsabahrami/pest_ascii_tree.git", branch = "master" } -miette = "7.2.0" +pest_ascii_tree = "0.1.0" +miette = { version = "7.2.0", features = ["fancy"] } lazy_static = "1.4.0" [dev-dependencies] diff --git a/crates/deno_task_shell/src/grammar.pest b/crates/deno_task_shell/src/grammar.pest index 1481416..68657bf 100644 --- a/crates/deno_task_shell/src/grammar.pest +++ b/crates/deno_task_shell/src/grammar.pest @@ -291,13 +291,13 @@ if_clause = !{ } else_part = !{ - Elif ~ conditional_expression ~ Then ~ complete_command ~ linebreak ~ else_part? | + Elif ~ conditional_expression ~ linebreak ~ Then ~ complete_command ~ linebreak ~ else_part? | Else ~ linebreak ~ complete_command } conditional_expression = !{ - ("[[" ~ (unary_conditional_expression | binary_conditional_expression | UNQUOTED_PENDING_WORD) ~ "]]") | - ("[" ~ (unary_conditional_expression | binary_conditional_expression | UNQUOTED_PENDING_WORD) ~ "]") | + ("[[" ~ (unary_conditional_expression | binary_conditional_expression | UNQUOTED_PENDING_WORD) ~ "]]" ~ ";"?) | + ("[" ~ (unary_conditional_expression | binary_conditional_expression | UNQUOTED_PENDING_WORD) ~ "]" ~ ";"?) | ("test" ~ (unary_conditional_expression | binary_conditional_expression | UNQUOTED_PENDING_WORD)) } diff --git a/crates/deno_task_shell/src/parser.rs b/crates/deno_task_shell/src/parser.rs index cff0f5f..2e4fb0d 100644 --- a/crates/deno_task_shell/src/parser.rs +++ b/crates/deno_task_shell/src/parser.rs @@ -1579,7 +1579,7 @@ fn parse_quoted_word(pair: Pair) -> Result { parts.push(WordPart::Command(command)); } Rule::VARIABLE => { - parts.push(WordPart::Variable(part.as_str()[1..].to_string())) + parts.push(WordPart::Variable(part.as_str().to_string())) } Rule::QUOTED_CHAR => { if let Some(WordPart::Text(ref mut s)) = parts.last_mut() { diff --git a/crates/deno_task_shell/src/shell/commands/args.rs b/crates/deno_task_shell/src/shell/commands/args.rs index d308500..fff34c5 100644 --- a/crates/deno_task_shell/src/shell/commands/args.rs +++ b/crates/deno_task_shell/src/shell/commands/args.rs @@ -1,7 +1,7 @@ // Copyright 2018-2024 the Deno authors. MIT license. -use anyhow::bail; -use anyhow::Result; +use miette::bail; +use miette::Result; #[derive(Debug, PartialEq, Eq)] pub enum ArgKind<'a> { diff --git a/crates/deno_task_shell/src/shell/commands/cat.rs b/crates/deno_task_shell/src/shell/commands/cat.rs index a82e6a5..d66d344 100644 --- a/crates/deno_task_shell/src/shell/commands/cat.rs +++ b/crates/deno_task_shell/src/shell/commands/cat.rs @@ -1,7 +1,8 @@ // Copyright 2018-2024 the Deno authors. MIT license. -use anyhow::Result; use futures::future::LocalBoxFuture; +use miette::IntoDiagnostic; +use miette::Result; use std::fs::File; use std::io::IsTerminal; use std::io::Read; @@ -52,7 +53,7 @@ fn execute_cat(mut context: ShellCommandContext) -> Result { return Ok(ExecuteResult::for_cancellation()); } - let size = file.read(&mut buf)?; + let size = file.read(&mut buf).into_diagnostic()?; if size == 0 { break; } else { diff --git a/crates/deno_task_shell/src/shell/commands/cd.rs b/crates/deno_task_shell/src/shell/commands/cd.rs index b4e1c3a..c2b6595 100644 --- a/crates/deno_task_shell/src/shell/commands/cd.rs +++ b/crates/deno_task_shell/src/shell/commands/cd.rs @@ -3,9 +3,9 @@ use std::path::Path; use std::path::PathBuf; -use anyhow::bail; -use anyhow::Result; use futures::future::LocalBoxFuture; +use miette::bail; +use miette::Result; use path_dedot::ParseDot; use crate::shell::fs_util; @@ -47,7 +47,7 @@ fn execute_cd(cwd: &Path, args: Vec) -> Result { let path = parse_args(args.clone())?; let new_dir = if path == "~" { dirs::home_dir() - .ok_or_else(|| anyhow::anyhow!("Home directory not found"))? + .ok_or_else(|| miette::miette!("Home directory not found"))? } else { cwd.join(&path) }; diff --git a/crates/deno_task_shell/src/shell/commands/cp_mv.rs b/crates/deno_task_shell/src/shell/commands/cp_mv.rs index 5647cfb..83fff6a 100644 --- a/crates/deno_task_shell/src/shell/commands/cp_mv.rs +++ b/crates/deno_task_shell/src/shell/commands/cp_mv.rs @@ -3,12 +3,13 @@ use std::path::Path; use std::path::PathBuf; -use anyhow::bail; -use anyhow::Context; -use anyhow::Result; use futures::future::BoxFuture; use futures::future::LocalBoxFuture; use futures::FutureExt; +use miette::bail; +use miette::Context; +use miette::IntoDiagnostic; +use miette::Result; use crate::shell::types::ExecuteResult; use crate::shell::types::ShellPipeWriter; @@ -87,7 +88,9 @@ async fn do_copy_operation( bail!("source was a directory; maybe specify -r") } } else { - tokio::fs::copy(&from.path, &to.path).await?; + tokio::fs::copy(&from.path, &to.path) + .await + .into_diagnostic()?; } Ok(()) } @@ -100,13 +103,15 @@ fn copy_dir_recursively( async move { tokio::fs::create_dir_all(&to) .await - .with_context(|| format!("Creating {}", to.display()))?; + .into_diagnostic() + .context(miette::miette!("Creating {}", to.display()))?; let mut read_dir = tokio::fs::read_dir(&from) .await - .with_context(|| format!("Reading {}", from.display()))?; + .into_diagnostic() + .context(miette::miette!("Reading {}", from.display()))?; - while let Some(entry) = read_dir.next_entry().await? { - let file_type = entry.file_type().await?; + while let Some(entry) = read_dir.next_entry().await.into_diagnostic()? { + let file_type = entry.file_type().await.into_diagnostic()?; let new_from = from.join(entry.file_name()); let new_to = to.join(entry.file_name()); @@ -117,9 +122,12 @@ fn copy_dir_recursively( format!("Dir {} to {}", new_from.display(), new_to.display()) })?; } else if file_type.is_file() { - tokio::fs::copy(&new_from, &new_to).await.with_context(|| { - format!("Copying {} to {}", new_from.display(), new_to.display()) - })?; + tokio::fs::copy(&new_from, &new_to) + .await + .into_diagnostic() + .with_context(|| { + format!("Copying {} to {}", new_from.display(), new_to.display()) + })?; } } diff --git a/crates/deno_task_shell/src/shell/commands/exit.rs b/crates/deno_task_shell/src/shell/commands/exit.rs index 5222c61..44ab7fb 100644 --- a/crates/deno_task_shell/src/shell/commands/exit.rs +++ b/crates/deno_task_shell/src/shell/commands/exit.rs @@ -1,8 +1,8 @@ // Copyright 2018-2024 the Deno authors. MIT license. -use anyhow::bail; -use anyhow::Result; use futures::future::LocalBoxFuture; +use miette::bail; +use miette::Result; use crate::shell::types::ExecuteResult; diff --git a/crates/deno_task_shell/src/shell/commands/head.rs b/crates/deno_task_shell/src/shell/commands/head.rs index bef3a46..c0a9344 100644 --- a/crates/deno_task_shell/src/shell/commands/head.rs +++ b/crates/deno_task_shell/src/shell/commands/head.rs @@ -3,9 +3,10 @@ use std::fs::File; use std::io::Read; -use anyhow::bail; -use anyhow::Result; use futures::future::LocalBoxFuture; +use miette::bail; +use miette::IntoDiagnostic; +use miette::Result; use tokio_util::sync::CancellationToken; use crate::ExecuteResult; @@ -96,7 +97,7 @@ fn execute_head(mut context: ShellCommandContext) -> Result { &mut context.stdout, flags.lines, context.state.token(), - |buf| file.read(buf).map_err(Into::into), + |buf| file.read(buf).into_diagnostic(), 512, ), Err(err) => { @@ -131,7 +132,7 @@ fn parse_args(args: Vec) -> Result { } ArgKind::ShortFlag('n') => match iterator.next() { Some(ArgKind::Arg(arg)) => { - lines = Some(arg.parse::()?); + lines = Some(arg.parse::().into_diagnostic()?); } _ => bail!("expected a value following -n"), }, @@ -139,7 +140,7 @@ fn parse_args(args: Vec) -> Result { if flag == "lines" || flag == "lines=" { bail!("expected a value for --lines"); } else if let Some(arg) = flag.strip_prefix("lines=") { - lines = Some(arg.parse::()?); + lines = Some(arg.parse::().into_diagnostic()?); } else { arg.bail_unsupported()? } diff --git a/crates/deno_task_shell/src/shell/commands/mkdir.rs b/crates/deno_task_shell/src/shell/commands/mkdir.rs index 25636ca..5d8d90f 100644 --- a/crates/deno_task_shell/src/shell/commands/mkdir.rs +++ b/crates/deno_task_shell/src/shell/commands/mkdir.rs @@ -1,9 +1,9 @@ // Copyright 2018-2024 the Deno authors. MIT license. -use anyhow::bail; -use anyhow::Result; use futures::future::LocalBoxFuture; use futures::FutureExt; +use miette::bail; +use miette::Result; use std::path::Path; use crate::shell::types::ExecuteResult; diff --git a/crates/deno_task_shell/src/shell/commands/pwd.rs b/crates/deno_task_shell/src/shell/commands/pwd.rs index 9da211c..bb6e933 100644 --- a/crates/deno_task_shell/src/shell/commands/pwd.rs +++ b/crates/deno_task_shell/src/shell/commands/pwd.rs @@ -1,8 +1,8 @@ // Copyright 2018-2024 the Deno authors. MIT license. -use anyhow::Context; -use anyhow::Result; use futures::future::LocalBoxFuture; +use miette::Context; +use miette::Result; use std::path::Path; use crate::shell::fs_util; diff --git a/crates/deno_task_shell/src/shell/commands/rm.rs b/crates/deno_task_shell/src/shell/commands/rm.rs index 72c1410..77cf9c3 100644 --- a/crates/deno_task_shell/src/shell/commands/rm.rs +++ b/crates/deno_task_shell/src/shell/commands/rm.rs @@ -1,9 +1,9 @@ // Copyright 2018-2024 the Deno authors. MIT license. -use anyhow::bail; -use anyhow::Result; use futures::future::LocalBoxFuture; use futures::FutureExt; +use miette::bail; +use miette::Result; use std::io::ErrorKind; use std::path::Path; diff --git a/crates/deno_task_shell/src/shell/commands/sleep.rs b/crates/deno_task_shell/src/shell/commands/sleep.rs index f1e68e6..b86d92d 100644 --- a/crates/deno_task_shell/src/shell/commands/sleep.rs +++ b/crates/deno_task_shell/src/shell/commands/sleep.rs @@ -2,10 +2,11 @@ use std::time::Duration; -use anyhow::bail; -use anyhow::Result; use futures::future::LocalBoxFuture; use futures::FutureExt; +use miette::bail; +use miette::IntoDiagnostic; +use miette::Result; use crate::shell::types::ExecuteResult; use crate::shell::types::ShellPipeWriter; @@ -54,19 +55,19 @@ async fn execute_sleep(args: Vec) -> Result<()> { fn parse_arg(arg: &str) -> Result { if let Some(t) = arg.strip_suffix('s') { - return Ok(t.parse()?); + return t.parse().into_diagnostic(); } if let Some(t) = arg.strip_suffix('m') { - return Ok(t.parse::()? * 60.); + return Ok(t.parse::().into_diagnostic()? * 60.); } if let Some(t) = arg.strip_suffix('h') { - return Ok(t.parse::()? * 60. * 60.); + return Ok(t.parse::().into_diagnostic()? * 60. * 60.); } if let Some(t) = arg.strip_suffix('d') { - return Ok(t.parse::()? * 60. * 60. * 24.); + return Ok(t.parse::().into_diagnostic()? * 60. * 60. * 24.); } - Ok(arg.parse()?) + arg.parse().into_diagnostic() } fn parse_args(args: Vec) -> Result { diff --git a/crates/deno_task_shell/src/shell/commands/unset.rs b/crates/deno_task_shell/src/shell/commands/unset.rs index 9a60c3b..bb9ec01 100644 --- a/crates/deno_task_shell/src/shell/commands/unset.rs +++ b/crates/deno_task_shell/src/shell/commands/unset.rs @@ -1,8 +1,8 @@ // Copyright 2018-2024 the Deno authors. MIT license. -use anyhow::bail; -use anyhow::Result; use futures::future::LocalBoxFuture; +use miette::bail; +use miette::Result; use crate::shell::types::ExecuteResult; use crate::EnvChange; diff --git a/crates/deno_task_shell/src/shell/commands/xargs.rs b/crates/deno_task_shell/src/shell/commands/xargs.rs index 7ff01e1..11e3609 100644 --- a/crates/deno_task_shell/src/shell/commands/xargs.rs +++ b/crates/deno_task_shell/src/shell/commands/xargs.rs @@ -1,9 +1,10 @@ // Copyright 2018-2024 the Deno authors. MIT license. -use anyhow::bail; -use anyhow::Result; use futures::future::LocalBoxFuture; use futures::FutureExt; +use miette::bail; +use miette::IntoDiagnostic; +use miette::Result; use crate::shell::types::ExecuteResult; use crate::shell::types::ShellPipeReader; @@ -51,7 +52,7 @@ fn xargs_collect_args( let flags = parse_args(cli_args)?; let mut buf = Vec::new(); stdin.pipe_to(&mut buf)?; - let text = String::from_utf8(buf)?; + let text = String::from_utf8(buf).into_diagnostic()?; let mut args = flags.initial_args; if args.is_empty() { diff --git a/crates/deno_task_shell/src/shell/execute.rs b/crates/deno_task_shell/src/shell/execute.rs index 4c8076f..52ce617 100644 --- a/crates/deno_task_shell/src/shell/execute.rs +++ b/crates/deno_task_shell/src/shell/execute.rs @@ -4,11 +4,10 @@ use std::collections::HashMap; use std::path::Path; use std::rc::Rc; -use anyhow::Context; -use anyhow::Error; use futures::future; use futures::future::LocalBoxFuture; use futures::FutureExt; +use miette::Error; use thiserror::Error; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; @@ -556,9 +555,9 @@ async fn evaluate_arithmetic_part( _ => { let var = state .get_var(name) - .ok_or_else(|| anyhow::anyhow!("Undefined variable: {}", name))?; + .ok_or_else(|| miette::miette!("Undefined variable: {}", name))?; let parsed_var = var.parse::().map_err(|e| { - anyhow::anyhow!("Failed to parse variable '{}': {}", name, e) + miette::miette!("Failed to parse variable '{}': {}", name, e) })?; match op { AssignmentOp::MultiplyAssign => val.checked_mul(&parsed_var), @@ -626,11 +625,11 @@ async fn evaluate_arithmetic_part( .get_var(name) .and_then(|s| s.parse::().ok()) .ok_or_else(|| { - anyhow::anyhow!("Undefined or non-integer variable: {}", name) + miette::miette!("Undefined or non-integer variable: {}", name) }), ArithmeticPart::Number(num_str) => num_str .parse::() - .map_err(|e| anyhow::anyhow!(e.to_string())), + .map_err(|e| miette::miette!(e.to_string())), } } @@ -1073,7 +1072,7 @@ pub enum EvaluateWordTextError { #[error("glob: no matches found '{}'", pattern)] NoFilesMatched { pattern: String }, #[error("Failed to get home directory")] - FailedToGetHomeDirectory(anyhow::Error), + FailedToGetHomeDirectory(miette::Error), } impl EvaluateWordTextError { @@ -1083,8 +1082,8 @@ impl EvaluateWordTextError { } } -impl From for EvaluateWordTextError { - fn from(err: anyhow::Error) -> Self { +impl From for EvaluateWordTextError { + fn from(err: miette::Error) -> Self { Self::FailedToGetHomeDirectory(err) } } @@ -1255,7 +1254,7 @@ fn evaluate_word_parts( WordPart::Tilde(tilde_prefix) => { if tilde_prefix.only_tilde() { let home_str = dirs::home_dir() - .context("Failed to get home directory")? + .ok_or_else(|| miette::miette!("Failed to get home directory"))? .display() .to_string(); current_text.push(TextPart::Text(home_str)); diff --git a/crates/deno_task_shell/src/shell/fs_util.rs b/crates/deno_task_shell/src/shell/fs_util.rs index 043b81f..a13aa77 100644 --- a/crates/deno_task_shell/src/shell/fs_util.rs +++ b/crates/deno_task_shell/src/shell/fs_util.rs @@ -3,11 +3,12 @@ use std::path::Path; use std::path::PathBuf; -use anyhow::Result; +use miette::IntoDiagnostic; +use miette::Result; /// Similar to `std::fs::canonicalize()` but strips UNC prefixes on Windows. pub fn canonicalize_path(path: &Path) -> Result { - let path = path.canonicalize()?; + let path = path.canonicalize().into_diagnostic()?; #[cfg(windows)] return Ok(strip_unc_prefix(path)); #[cfg(not(windows))] diff --git a/crates/deno_task_shell/src/shell/types.rs b/crates/deno_task_shell/src/shell/types.rs index 7dbed41..1ca905b 100644 --- a/crates/deno_task_shell/src/shell/types.rs +++ b/crates/deno_task_shell/src/shell/types.rs @@ -12,9 +12,11 @@ use std::path::PathBuf; use std::rc::Rc; use std::str::FromStr; -use anyhow::Error; -use anyhow::Result; use futures::future::LocalBoxFuture; +use miette::miette; +use miette::Error; +use miette::IntoDiagnostic; +use miette::Result; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; @@ -392,15 +394,19 @@ impl ShellPipeReader { loop { let mut buffer = [0; 512]; // todo: what is an appropriate buffer size? let size = match &mut self { - ShellPipeReader::OsPipe(pipe) => pipe.read(&mut buffer)?, - ShellPipeReader::StdFile(file) => file.read(&mut buffer)?, + ShellPipeReader::OsPipe(pipe) => { + pipe.read(&mut buffer).into_diagnostic()? + } + ShellPipeReader::StdFile(file) => { + file.read(&mut buffer).into_diagnostic()? + } }; if size == 0 { break; } - writer.write_all(&buffer[0..size])?; + writer.write_all(&buffer[0..size]).into_diagnostic()?; if flush { - writer.flush()?; + writer.flush().into_diagnostic()?; } } Ok(()) @@ -437,8 +443,8 @@ impl ShellPipeReader { pub fn read(&mut self, buf: &mut [u8]) -> Result { match self { - ShellPipeReader::OsPipe(pipe) => pipe.read(buf).map_err(|e| e.into()), - ShellPipeReader::StdFile(file) => file.read(buf).map_err(|e| e.into()), + ShellPipeReader::OsPipe(pipe) => pipe.read(buf).into_diagnostic(), + ShellPipeReader::StdFile(file) => file.read(buf).into_diagnostic(), } } } @@ -502,19 +508,19 @@ impl ShellPipeWriter { pub fn write_all(&mut self, bytes: &[u8]) -> Result<()> { match self { - Self::OsPipe(pipe) => pipe.write_all(bytes)?, - Self::StdFile(file) => file.write_all(bytes)?, + Self::OsPipe(pipe) => pipe.write_all(bytes).into_diagnostic()?, + Self::StdFile(file) => file.write_all(bytes).into_diagnostic()?, // For both stdout & stderr, we want to flush after each // write in order to bypass Rust's internal buffer. Self::Stdout => { let mut stdout = std::io::stdout().lock(); - stdout.write_all(bytes)?; - stdout.flush()?; + stdout.write_all(bytes).into_diagnostic()?; + stdout.flush().into_diagnostic()?; } Self::Stderr => { let mut stderr = std::io::stderr().lock(); - stderr.write_all(bytes)?; - stderr.flush()?; + stderr.write_all(bytes).into_diagnostic()?; + stderr.flush().into_diagnostic()?; } Self::Null => {} } @@ -600,7 +606,7 @@ impl ArithmeticResult { changes: new_changes, }) } - _ => Err(anyhow::anyhow!( + _ => Err(miette!( "Invalid arithmetic result type for post-increment: {}", self )), @@ -620,7 +626,7 @@ impl ArithmeticResult { changes: new_changes, }) } - _ => Err(anyhow::anyhow!( + _ => Err(miette!( "Invalid arithmetic result type for post-increment: {}", self )), @@ -657,7 +663,7 @@ impl ArithmeticResult { changes: new_changes, }) } - _ => Err(anyhow::anyhow!( + _ => Err(miette!( "Invalid arithmetic result type for pre-increment: {}", self )), @@ -692,7 +698,7 @@ impl ArithmeticResult { changes: new_changes, }) } - _ => Err(anyhow::anyhow!( + _ => Err(miette!( "Invalid arithmetic result type for pre-increment: {}", self )), @@ -710,14 +716,14 @@ impl ArithmeticResult { .checked_add(*rhs) .map(ArithmeticValue::Integer) .ok_or_else(|| { - anyhow::anyhow!("Integer overflow: {} + {}", lhs, rhs) + miette::miette!("Integer overflow: {} + {}", lhs, rhs) })?, (ArithmeticValue::Float(lhs), ArithmeticValue::Float(rhs)) => { let sum = lhs + rhs; if sum.is_finite() { ArithmeticValue::Float(sum) } else { - return Err(anyhow::anyhow!("Float overflow: {} + {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} + {}", lhs, rhs)); } } (ArithmeticValue::Integer(lhs), ArithmeticValue::Float(rhs)) @@ -726,7 +732,7 @@ impl ArithmeticResult { if sum.is_finite() { ArithmeticValue::Float(sum) } else { - return Err(anyhow::anyhow!("Float overflow: {} + {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} + {}", lhs, rhs)); } } }; @@ -749,14 +755,14 @@ impl ArithmeticResult { .checked_sub(*rhs) .map(ArithmeticValue::Integer) .ok_or_else(|| { - anyhow::anyhow!("Integer overflow: {} - {}", lhs, rhs) + miette::miette!("Integer overflow: {} - {}", lhs, rhs) })?, (ArithmeticValue::Float(lhs), ArithmeticValue::Float(rhs)) => { let diff = lhs - rhs; if diff.is_finite() { ArithmeticValue::Float(diff) } else { - return Err(anyhow::anyhow!("Float overflow: {} - {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} - {}", lhs, rhs)); } } (ArithmeticValue::Integer(lhs), ArithmeticValue::Float(rhs)) => { @@ -764,7 +770,7 @@ impl ArithmeticResult { if diff.is_finite() { ArithmeticValue::Float(diff) } else { - return Err(anyhow::anyhow!("Float overflow: {} - {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} - {}", lhs, rhs)); } } (ArithmeticValue::Float(lhs), ArithmeticValue::Integer(rhs)) => { @@ -772,7 +778,7 @@ impl ArithmeticResult { if diff.is_finite() { ArithmeticValue::Float(diff) } else { - return Err(anyhow::anyhow!("Float overflow: {} - {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} - {}", lhs, rhs)); } } }; @@ -795,14 +801,14 @@ impl ArithmeticResult { .checked_mul(*rhs) .map(ArithmeticValue::Integer) .ok_or_else(|| { - anyhow::anyhow!("Integer overflow: {} * {}", lhs, rhs) + miette::miette!("Integer overflow: {} * {}", lhs, rhs) })?, (ArithmeticValue::Float(lhs), ArithmeticValue::Float(rhs)) => { let product = lhs * rhs; if product.is_finite() { ArithmeticValue::Float(product) } else { - return Err(anyhow::anyhow!("Float overflow: {} * {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} * {}", lhs, rhs)); } } (ArithmeticValue::Integer(lhs), ArithmeticValue::Float(rhs)) @@ -811,7 +817,7 @@ impl ArithmeticResult { if product.is_finite() { ArithmeticValue::Float(product) } else { - return Err(anyhow::anyhow!("Float overflow: {} * {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} * {}", lhs, rhs)); } } }; @@ -832,46 +838,46 @@ impl ArithmeticResult { let result = match (&self.value, &other.value) { (ArithmeticValue::Integer(lhs), ArithmeticValue::Integer(rhs)) => { if *rhs == 0 { - return Err(anyhow::anyhow!("Division by zero: {} / {}", lhs, rhs)); + return Err(miette::miette!("Division by zero: {} / {}", lhs, rhs)); } lhs .checked_div(*rhs) .map(ArithmeticValue::Integer) .ok_or_else(|| { - anyhow::anyhow!("Integer overflow: {} / {}", lhs, rhs) + miette::miette!("Integer overflow: {} / {}", lhs, rhs) })? } (ArithmeticValue::Float(lhs), ArithmeticValue::Float(rhs)) => { if *rhs == 0.0 { - return Err(anyhow::anyhow!("Division by zero: {} / {}", lhs, rhs)); + return Err(miette::miette!("Division by zero: {} / {}", lhs, rhs)); } let quotient = lhs / rhs; if quotient.is_finite() { ArithmeticValue::Float(quotient) } else { - return Err(anyhow::anyhow!("Float overflow: {} / {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} / {}", lhs, rhs)); } } (ArithmeticValue::Integer(lhs), ArithmeticValue::Float(rhs)) => { if *rhs == 0.0 { - return Err(anyhow::anyhow!("Division by zero: {} / {}", lhs, rhs)); + return Err(miette::miette!("Division by zero: {} / {}", lhs, rhs)); } let quotient = *lhs as f64 / rhs; if quotient.is_finite() { ArithmeticValue::Float(quotient) } else { - return Err(anyhow::anyhow!("Float overflow: {} / {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} / {}", lhs, rhs)); } } (ArithmeticValue::Float(lhs), ArithmeticValue::Integer(rhs)) => { if *rhs == 0 { - return Err(anyhow::anyhow!("Division by zero: {} / {}", lhs, rhs)); + return Err(miette::miette!("Division by zero: {} / {}", lhs, rhs)); } let quotient = lhs / *rhs as f64; if quotient.is_finite() { ArithmeticValue::Float(quotient) } else { - return Err(anyhow::anyhow!("Float overflow: {} / {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} / {}", lhs, rhs)); } } }; @@ -892,46 +898,46 @@ impl ArithmeticResult { let result = match (&self.value, &other.value) { (ArithmeticValue::Integer(lhs), ArithmeticValue::Integer(rhs)) => { if *rhs == 0 { - return Err(anyhow::anyhow!("Modulo by zero: {} % {}", lhs, rhs)); + return Err(miette::miette!("Modulo by zero: {} % {}", lhs, rhs)); } lhs .checked_rem(*rhs) .map(ArithmeticValue::Integer) .ok_or_else(|| { - anyhow::anyhow!("Integer overflow: {} % {}", lhs, rhs) + miette::miette!("Integer overflow: {} % {}", lhs, rhs) })? } (ArithmeticValue::Float(lhs), ArithmeticValue::Float(rhs)) => { if *rhs == 0.0 { - return Err(anyhow::anyhow!("Modulo by zero: {} % {}", lhs, rhs)); + return Err(miette::miette!("Modulo by zero: {} % {}", lhs, rhs)); } let remainder = lhs % rhs; if remainder.is_finite() { ArithmeticValue::Float(remainder) } else { - return Err(anyhow::anyhow!("Float overflow: {} % {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} % {}", lhs, rhs)); } } (ArithmeticValue::Integer(lhs), ArithmeticValue::Float(rhs)) => { if *rhs == 0.0 { - return Err(anyhow::anyhow!("Modulo by zero: {} % {}", lhs, rhs)); + return Err(miette::miette!("Modulo by zero: {} % {}", lhs, rhs)); } let remainder = *lhs as f64 % rhs; if remainder.is_finite() { ArithmeticValue::Float(remainder) } else { - return Err(anyhow::anyhow!("Float overflow: {} % {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} % {}", lhs, rhs)); } } (ArithmeticValue::Float(lhs), ArithmeticValue::Integer(rhs)) => { if *rhs == 0 { - return Err(anyhow::anyhow!("Modulo by zero: {} % {}", lhs, rhs)); + return Err(miette::miette!("Modulo by zero: {} % {}", lhs, rhs)); } let remainder = lhs % *rhs as f64; if remainder.is_finite() { ArithmeticValue::Float(remainder) } else { - return Err(anyhow::anyhow!("Float overflow: {} % {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} % {}", lhs, rhs)); } } }; @@ -956,14 +962,14 @@ impl ArithmeticResult { if result.is_finite() { ArithmeticValue::Float(result) } else { - return Err(anyhow::anyhow!("Float overflow: {} ** {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} ** {}", lhs, rhs)); } } else { lhs .checked_pow(*rhs as u32) .map(ArithmeticValue::Integer) .ok_or_else(|| { - anyhow::anyhow!("Integer overflow: {} ** {}", lhs, rhs) + miette::miette!("Integer overflow: {} ** {}", lhs, rhs) })? } } @@ -972,7 +978,7 @@ impl ArithmeticResult { if result.is_finite() { ArithmeticValue::Float(result) } else { - return Err(anyhow::anyhow!("Float overflow: {} ** {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} ** {}", lhs, rhs)); } } (ArithmeticValue::Integer(lhs), ArithmeticValue::Float(rhs)) => { @@ -980,7 +986,7 @@ impl ArithmeticResult { if result.is_finite() { ArithmeticValue::Float(result) } else { - return Err(anyhow::anyhow!("Float overflow: {} ** {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} ** {}", lhs, rhs)); } } (ArithmeticValue::Float(lhs), ArithmeticValue::Integer(rhs)) => { @@ -988,7 +994,7 @@ impl ArithmeticResult { if result.is_finite() { ArithmeticValue::Float(result) } else { - return Err(anyhow::anyhow!("Float overflow: {} ** {}", lhs, rhs)); + return Err(miette::miette!("Float overflow: {} ** {}", lhs, rhs)); } } }; @@ -1009,7 +1015,7 @@ impl ArithmeticResult { let result = match (&self.value, &other.value) { (ArithmeticValue::Integer(lhs), ArithmeticValue::Integer(rhs)) => { if *rhs < 0 { - return Err(anyhow::anyhow!( + return Err(miette::miette!( "Negative shift amount: {} << {}", lhs, rhs @@ -1019,11 +1025,11 @@ impl ArithmeticResult { .checked_shl(*rhs as u32) .map(ArithmeticValue::Integer) .ok_or_else(|| { - anyhow::anyhow!("Integer overflow: {} << {}", lhs, rhs) + miette::miette!("Integer overflow: {} << {}", lhs, rhs) })? } _ => { - return Err(anyhow::anyhow!( + return Err(miette::miette!( "Invalid arithmetic result types for left shift: {} << {}", self, other @@ -1047,7 +1053,7 @@ impl ArithmeticResult { let result = match (&self.value, &other.value) { (ArithmeticValue::Integer(lhs), ArithmeticValue::Integer(rhs)) => { if *rhs < 0 { - return Err(anyhow::anyhow!( + return Err(miette::miette!( "Negative shift amount: {} >> {}", lhs, rhs @@ -1057,11 +1063,11 @@ impl ArithmeticResult { .checked_shr(*rhs as u32) .map(ArithmeticValue::Integer) .ok_or_else(|| { - anyhow::anyhow!("Integer underflow: {} >> {}", lhs, rhs) + miette::miette!("Integer underflow: {} >> {}", lhs, rhs) })? } _ => { - return Err(anyhow::anyhow!( + return Err(miette::miette!( "Invalid arithmetic result types for right shift: {} >> {}", self, other @@ -1087,7 +1093,7 @@ impl ArithmeticResult { ArithmeticValue::Integer(lhs & rhs) } _ => { - return Err(anyhow::anyhow!( + return Err(miette::miette!( "Invalid arithmetic result types for bitwise AND: {} & {}", self, other @@ -1113,7 +1119,7 @@ impl ArithmeticResult { ArithmeticValue::Integer(lhs | rhs) } _ => { - return Err(anyhow::anyhow!( + return Err(miette::miette!( "Invalid arithmetic result types for bitwise OR: {} | {}", self, other @@ -1139,7 +1145,7 @@ impl ArithmeticResult { ArithmeticValue::Integer(lhs ^ rhs) } _ => { - return Err(anyhow::anyhow!( + return Err(miette::miette!( "Invalid arithmetic result types for bitwise XOR: {} ^ {}", self, other diff --git a/crates/shell/Cargo.toml b/crates/shell/Cargo.toml index e134ed6..ee5449c 100644 --- a/crates/shell/Cargo.toml +++ b/crates/shell/Cargo.toml @@ -23,7 +23,6 @@ path = "src/main.rs" [features] [dependencies] -anyhow = "1.0.87" clap = { version = "4.5.17", features = ["derive"] } deno_task_shell = { path = "../deno_task_shell", features = ["shell"] } futures = "0.3.30" @@ -33,7 +32,15 @@ uu_ls = "0.0.27" dirs = "5.0.1" which = "6.0.3" uu_uname = "0.0.27" +uu_touch = "0.0.27" uu_date = "0.0.27" +miette = { version = "7.2.0", features = ["fancy"] } +filetime = "0.2.25" +chrono = "0.4.38" +parse_datetime = "0.6.0" +dtparse = "2.0.1" +windows-sys = "0.59.0" +ctrlc = "3.4.5" [package.metadata.release] # Dont publish the binary diff --git a/crates/shell/src/commands/mod.rs b/crates/shell/src/commands/mod.rs index f8dff0a..86f16db 100644 --- a/crates/shell/src/commands/mod.rs +++ b/crates/shell/src/commands/mod.rs @@ -8,10 +8,12 @@ use uu_ls::uumain as uu_ls; use crate::execute; pub mod date; +pub mod touch; pub mod uname; pub mod which; pub use date::DateCommand; +pub use touch::TouchCommand; pub use uname::UnameCommand; pub use which::WhichCommand; @@ -46,6 +48,10 @@ pub fn get_commands() -> HashMap> { "uname".to_string(), Rc::new(UnameCommand) as Rc, ), + ( + "touch".to_string(), + Rc::new(TouchCommand) as Rc, + ), ( "date".to_string(), Rc::new(DateCommand) as Rc, diff --git a/crates/shell/src/commands/touch.rs b/crates/shell/src/commands/touch.rs new file mode 100644 index 0000000..87c9481 --- /dev/null +++ b/crates/shell/src/commands/touch.rs @@ -0,0 +1,346 @@ +use std::{ + ffi::OsString, + fs::{self, OpenOptions}, + io, + path::{Path, PathBuf}, +}; + +use chrono::{DateTime, Duration, Local, NaiveDateTime, TimeZone, Timelike}; +use deno_task_shell::{ExecuteResult, ShellCommand, ShellCommandContext}; +use filetime::{set_file_times, set_symlink_file_times, FileTime}; +use futures::future::LocalBoxFuture; +use miette::{miette, IntoDiagnostic, Result}; +use uu_touch::{options, uu_app as uu_touch}; + +static ARG_FILES: &str = "files"; + +pub struct TouchCommand; + +impl ShellCommand for TouchCommand { + fn execute(&self, mut context: ShellCommandContext) -> LocalBoxFuture<'static, ExecuteResult> { + Box::pin(futures::future::ready(match execute_touch(&mut context) { + Ok(_) => ExecuteResult::from_exit_code(0), + Err(e) => { + let _ = context.stderr.write_all(format!("{:?}", e).as_bytes()); + ExecuteResult::from_exit_code(1) + } + })) + } +} + +fn execute_touch(context: &mut ShellCommandContext) -> Result<()> { + let matches = uu_touch() + .override_usage("touch [OPTION]...") + .no_binary_name(true) + .try_get_matches_from(&context.args) + .into_diagnostic()?; + + let files = match matches.get_many::(ARG_FILES) { + Some(files) => files.map(|file| { + let path = PathBuf::from(file); + if path.is_absolute() { + path + } else { + context.state.cwd().join(path) + } + }), + None => { + return Err(miette!( + "missing file operand\nTry 'touch --help' for more information." + )) + } + }; + + let (mut atime, mut mtime) = match ( + matches.get_one::(options::sources::REFERENCE), + matches.get_one::(options::sources::DATE), + ) { + (Some(reference), Some(date)) => { + let reference_path = PathBuf::from(reference); + let reference_path = if reference_path.is_absolute() { + reference_path + } else { + context.state.cwd().join(reference_path) + }; + let (atime, mtime) = stat(&reference_path, !matches.get_flag(options::NO_DEREF))?; + let atime = filetime_to_datetime(&atime) + .ok_or_else(|| miette!("Could not process the reference access time"))?; + let mtime = filetime_to_datetime(&mtime) + .ok_or_else(|| miette!("Could not process the reference modification time"))?; + Ok((parse_date(atime, date)?, parse_date(mtime, date)?)) + } + (Some(reference), None) => { + let reference_path = PathBuf::from(reference); + let reference_path = if reference_path.is_absolute() { + reference_path + } else { + context.state.cwd().join(reference_path) + }; + stat(&reference_path, !matches.get_flag(options::NO_DEREF)) + } + (None, Some(date)) => { + let timestamp = parse_date(Local::now(), date)?; + Ok((timestamp, timestamp)) + } + (None, None) => { + let timestamp = if let Some(ts) = matches.get_one::(options::sources::TIMESTAMP) + { + parse_timestamp(ts)? + } else { + datetime_to_filetime(&Local::now()) + }; + Ok((timestamp, timestamp)) + } + } + .map_err(|e| miette!("{}", e))?; + + for filename in files { + let pathbuf = if filename.to_str() == Some("-") { + pathbuf_from_stdout()? + } else { + filename + }; + + let path = pathbuf.as_path(); + + let metadata_result = if matches.get_flag(options::NO_DEREF) { + path.symlink_metadata() + } else { + path.metadata() + }; + + if let Err(e) = metadata_result { + if e.kind() != std::io::ErrorKind::NotFound { + return Err(miette!("setting times of {}: {}", path.display(), e)); + } + + if matches.get_flag(options::NO_CREATE) { + continue; + } + + if matches.get_flag(options::NO_DEREF) { + let _ = context.stderr.write_all( + format!( + "setting times of {:?}: No such file or directory", + path.display() + ) + .as_bytes(), + ); + continue; + } + + OpenOptions::new() + .create(true) + .truncate(false) + .write(true) + .open(path) + .map_err(|e| match e.kind() { + io::ErrorKind::NotFound => { + miette!( + "cannot touch {}: {}", + path.display(), + "No such file or directory".to_string() + ) + } + _ => miette!("cannot touch {}: {}", path.display(), e), + })?; + + // Minor optimization: if no reference time was specified, we're done. + if !matches.contains_id(options::SOURCES) { + continue; + } + } + + if matches.get_flag(options::ACCESS) + || matches.get_flag(options::MODIFICATION) + || matches.contains_id(options::TIME) + { + let st = stat(path, !matches.get_flag(options::NO_DEREF))?; + let time = matches + .get_one::(options::TIME) + .map(|s| s.as_str()) + .unwrap_or(""); + + if !(matches.get_flag(options::ACCESS) + || time.contains(&"access".to_owned()) + || time.contains(&"atime".to_owned()) + || time.contains(&"use".to_owned())) + { + atime = st.0; + } + + if !(matches.get_flag(options::MODIFICATION) + || time.contains(&"modify".to_owned()) + || time.contains(&"mtime".to_owned())) + { + mtime = st.1; + } + } + + // sets the file access and modification times for a file or a symbolic link. + // The filename, access time (atime), and modification time (mtime) are provided as inputs. + + // If the filename is not "-", indicating a special case for touch -h -, + // the code checks if the NO_DEREF flag is set, which means the user wants to + // set the times for a symbolic link itself, rather than the file it points to. + if path.to_string_lossy() == "-" { + set_file_times(path, atime, mtime) + } else if matches.get_flag(options::NO_DEREF) { + set_symlink_file_times(path, atime, mtime) + } else { + set_file_times(path, atime, mtime) + } + .map_err(|e| miette!("setting times of {}: {}", path.display(), e))?; + } + + Ok(()) +} + +fn stat(path: &Path, follow: bool) -> Result<(FileTime, FileTime)> { + let metadata = if follow { + fs::metadata(path).or_else(|_| fs::symlink_metadata(path)) + } else { + fs::symlink_metadata(path) + } + .map_err(|e| miette!("failed to get attributes of {}: {}", path.display(), e))?; + + Ok(( + FileTime::from_last_access_time(&metadata), + FileTime::from_last_modification_time(&metadata), + )) +} + +fn filetime_to_datetime(ft: &FileTime) -> Option> { + Some(DateTime::from_timestamp(ft.unix_seconds(), ft.nanoseconds())?.into()) +} + +fn parse_timestamp(s: &str) -> Result { + let now = Local::now(); + let parsed = if s.len() == 15 && s.contains('.') { + // Handle the specific format "202401010000.00" + NaiveDateTime::parse_from_str(s, "%Y%m%d%H%M.%S") + .map_err(|_| miette!("invalid date format '{}'", s))? + } else { + dtparse::parse(s) + .map(|(dt, _)| dt) + .map_err(|_| miette!("invalid date format '{}'", s))? + }; + + let local = now + .timezone() + .from_local_datetime(&parsed) + .single() + .ok_or_else(|| miette!("invalid date '{}'", s))?; + + // Handle leap seconds + let local = if parsed.second() == 59 && s.ends_with(".60") { + local + Duration::seconds(1) + } else { + local + }; + + // Check for daylight saving time issues + if (local + Duration::hours(1) - Duration::hours(1)).hour() != local.hour() { + return Err(miette!("invalid date '{}'", s)); + } + + Ok(datetime_to_filetime(&local)) +} + +// TODO: this may be a good candidate to put in fsext.rs +/// Returns a PathBuf to stdout. +/// +/// On Windows, uses GetFinalPathNameByHandleW to attempt to get the path +/// from the stdout handle. +fn pathbuf_from_stdout() -> Result { + #[cfg(all(unix, not(target_os = "android")))] + { + Ok(PathBuf::from("/dev/stdout")) + } + #[cfg(target_os = "android")] + { + Ok(PathBuf::from("/proc/self/fd/1")) + } + #[cfg(windows)] + { + use std::os::windows::prelude::AsRawHandle; + use windows_sys::Win32::Foundation::{ + GetLastError, ERROR_INVALID_PARAMETER, ERROR_NOT_ENOUGH_MEMORY, ERROR_PATH_NOT_FOUND, + HANDLE, MAX_PATH, + }; + use windows_sys::Win32::Storage::FileSystem::{ + GetFinalPathNameByHandleW, FILE_NAME_OPENED, + }; + + let handle = std::io::stdout().lock().as_raw_handle() as HANDLE; + let mut file_path_buffer: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize]; + + // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlea#examples + // SAFETY: We transmute the handle to be able to cast *mut c_void into a + // HANDLE (i32) so rustc will let us call GetFinalPathNameByHandleW. The + // reference example code for GetFinalPathNameByHandleW implies that + // it is safe for us to leave lpszfilepath uninitialized, so long as + // the buffer size is correct. We know the buffer size (MAX_PATH) at + // compile time. MAX_PATH is a small number (260) so we can cast it + // to a u32. + let ret = unsafe { + GetFinalPathNameByHandleW( + handle, + file_path_buffer.as_mut_ptr(), + file_path_buffer.len() as u32, + FILE_NAME_OPENED, + ) + }; + + let buffer_size = match ret { + ERROR_PATH_NOT_FOUND | ERROR_NOT_ENOUGH_MEMORY | ERROR_INVALID_PARAMETER => { + return Err(miette!("GetFinalPathNameByHandleW failed with code {ret}")) + } + 0 => { + return Err(miette!( + "GetFinalPathNameByHandleW failed with code {}", + // SAFETY: GetLastError is thread-safe and has no documented memory unsafety. + unsafe { GetLastError() } + )); + } + e => e as usize, + }; + + // Don't include the null terminator + Ok(String::from_utf16(&file_path_buffer[0..buffer_size]) + .map_err(|e| miette!("Generated path is not valid UTF-16: {e}"))? + .into()) + } +} + +fn parse_date(ref_time: DateTime, s: &str) -> Result { + // Using the dtparse crate for more robust date parsing + + match dtparse::parse(s) { + Ok((naive_dt, offset)) => { + let dt = offset.map_or_else( + || Local.from_local_datetime(&naive_dt).unwrap(), + |off| DateTime::::from_naive_utc_and_offset(naive_dt, off), + ); + Ok(datetime_to_filetime(&dt)) + } + Err(_) => { + // Fallback to parsing Unix timestamp if dtparse fails + if let Some(stripped) = s.strip_prefix('@') { + stripped + .parse::() + .map(|ts| FileTime::from_unix_time(ts, 0)) + .map_err(|_| miette!("Unable to parse date: {s}")) + } else { + // Use ref_time as a base for relative date parsing + parse_datetime::parse_datetime_at_date(ref_time, s) + .map(|dt| datetime_to_filetime(&dt)) + .map_err(|_| miette!("Unable to parse date: {s}")) + } + } + } +} + +fn datetime_to_filetime(dt: &DateTime) -> FileTime { + FileTime::from_unix_time(dt.timestamp(), dt.timestamp_subsec_nanos()) +} diff --git a/crates/shell/src/completion.rs b/crates/shell/src/completion.rs index fec1dae..5410797 100644 --- a/crates/shell/src/completion.rs +++ b/crates/shell/src/completion.rs @@ -62,6 +62,9 @@ fn complete_filenames(_is_start: bool, word: &str, matches: &mut Vec) { // Determine the full directory path to search let search_dir = if dir_path.starts_with('/') { dir_path.to_string() + } else if let Some(stripped) = dir_path.strip_prefix('~') { + let home_dir = dirs::home_dir().unwrap(); + format!("{}{}", home_dir.display(), stripped) } else { format!("./{}", dir_path) }; diff --git a/crates/shell/src/execute.rs b/crates/shell/src/execute.rs index 0fbd05d..f52d299 100644 --- a/crates/shell/src/execute.rs +++ b/crates/shell/src/execute.rs @@ -1,10 +1,10 @@ -use anyhow::Context; use deno_task_shell::{ execute_sequential_list, AsyncCommandBehavior, ExecuteResult, ShellPipeReader, ShellPipeWriter, ShellState, }; +use miette::{Context, IntoDiagnostic}; -pub async fn execute_inner(text: &str, state: ShellState) -> anyhow::Result { +pub async fn execute_inner(text: &str, state: ShellState) -> miette::Result { let list = deno_task_shell::parser::parse(text); let mut stderr = ShellPipeWriter::stderr(); @@ -30,14 +30,16 @@ pub async fn execute_inner(text: &str, state: ShellState) -> anyhow::Result anyhow::Result { +pub async fn execute(text: &str, state: &mut ShellState) -> miette::Result { let result = execute_inner(text, state.clone()).await?; match result { ExecuteResult::Continue(exit_code, changes, _) => { // set CWD to the last command's CWD state.apply_changes(&changes); - std::env::set_current_dir(state.cwd()).context("Failed to set CWD")?; + std::env::set_current_dir(state.cwd()) + .into_diagnostic() + .context("Failed to set CWD")?; Ok(exit_code) } ExecuteResult::Exit(_, _) => Ok(0), diff --git a/crates/shell/src/main.rs b/crates/shell/src/main.rs index b6c3be3..0556b26 100644 --- a/crates/shell/src/main.rs +++ b/crates/shell/src/main.rs @@ -1,10 +1,11 @@ use std::path::Path; use std::path::PathBuf; -use anyhow::Context; use clap::Parser; use deno_task_shell::parser::debug_parse; use deno_task_shell::ShellState; +use miette::Context; +use miette::IntoDiagnostic; use rustyline::error::ReadlineError; use rustyline::{CompletionType, Config, Editor}; @@ -19,6 +20,14 @@ struct Options { /// The path to the file that should be executed file: Option, + /// Continue in interactive mode after the file has been executed + #[clap(long)] + interact: bool, + + /// Do not source ~/.shellrc on startup + #[clap(long)] + norc: bool, + #[clap(short, long)] debug: bool, } @@ -29,28 +38,46 @@ fn init_state() -> ShellState { ShellState::new(env_vars, &cwd, commands::get_commands()) } -async fn interactive() -> anyhow::Result<()> { +async fn interactive(state: Option, norc: bool) -> miette::Result<()> { let config = Config::builder() .history_ignore_space(true) - .completion_type(CompletionType::Circular) + .completion_type(CompletionType::List) .build(); - let mut rl = Editor::with_config(config)?; + ctrlc::set_handler(move || { + println!("Received Ctrl+C"); + }) + .expect("Error setting Ctrl-C handler"); + + let mut rl = Editor::with_config(config).into_diagnostic()?; let helper = helper::ShellPromptHelper::default(); rl.set_helper(Some(helper)); - let mut state = init_state(); + let mut state = state.unwrap_or_else(init_state); + + let home = dirs::home_dir().ok_or(miette::miette!("Couldn't get home directory"))?; - let home = dirs::home_dir().context("Couldn't get home directory")?; + // Load .shell_history let history_file: PathBuf = [home.as_path(), Path::new(".shell_history")] .iter() .collect(); if Path::new(history_file.as_path()).exists() { rl.load_history(history_file.as_path()) + .into_diagnostic() .context("Failed to read the command history")?; } + // Load ~/.shellrc + let shellrc_file: PathBuf = [home.as_path(), Path::new(".shellrc")].iter().collect(); + if !norc && Path::new(shellrc_file.as_path()).exists() { + let line = "source '".to_owned() + shellrc_file.to_str().unwrap() + "'"; + let prev_exit_code = execute(&line, &mut state) + .await + .context("Failed to source ~/.shellrc")?; + state.set_last_command_exit_code(prev_exit_code); + } + let mut _prev_exit_code = 0; loop { // Reset cancellation flag @@ -59,9 +86,9 @@ async fn interactive() -> anyhow::Result<()> { // Display the prompt and read a line let readline = { let cwd = state.cwd().to_string_lossy().to_string(); - let home_str = home - .to_str() - .context("Couldn't convert home directory path to UTF-8 string")?; + let home_str = home.to_str().ok_or(miette::miette!( + "Couldn't convert home directory path to UTF-8 string" + ))?; if !state.last_command_cd() { state.update_git_branch(); } @@ -96,7 +123,7 @@ async fn interactive() -> anyhow::Result<()> { match readline { Ok(line) => { // Add the line to history - rl.add_history_entry(line.as_str())?; + rl.add_history_entry(line.as_str()).into_diagnostic()?; // Process the input (here we just echo it back) let prev_exit_code = execute(&line, &mut state) @@ -126,13 +153,14 @@ async fn interactive() -> anyhow::Result<()> { } } rl.save_history(history_file.as_path()) + .into_diagnostic() .context("Failed to write the command history")?; Ok(()) } #[tokio::main] -async fn main() -> anyhow::Result<()> { +async fn main() -> miette::Result<()> { let options = Options::parse(); if let Some(file) = options.file { @@ -143,8 +171,11 @@ async fn main() -> anyhow::Result<()> { return Ok(()); } execute(&script_text, &mut state).await?; + if options.interact { + interactive(Some(state), options.norc).await?; + } } else { - interactive().await?; + interactive(None, options.norc).await?; } Ok(()) diff --git a/crates/tests/Cargo.toml b/crates/tests/Cargo.toml index 6aa7577..3e67bae 100644 --- a/crates/tests/Cargo.toml +++ b/crates/tests/Cargo.toml @@ -6,9 +6,10 @@ edition = "2021" [dependencies] deno_task_shell = { path = "../deno_task_shell", features = ["shell"] } shell = { path = "../shell" } -anyhow = "1.0.87" futures = "0.3.30" tokio = { version = "1.40.0", features = ["full"] } +dirs = "5.0.1" +miette = "7.2.0" [dev-dependencies] pretty_assertions = "1.0.0" diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 8c638bf..a489b98 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -74,6 +74,12 @@ async fn commands() { .run() .await; + TestBuilder::new() + .command(r#"FOO=1; echo "$FOO""#) + .assert_stdout("1\n") + .run() + .await; + TestBuilder::new() .command("echo 'a/b'/c") .assert_stdout("a/b/c\n") @@ -227,9 +233,16 @@ async fn pipeline() { .run() .await; + // TODO: implement tee in shell and then enable this test + // TestBuilder::new() + // .command(r#"echo 1 | tee output.txt"#) + // .assert_stdout("1\n") + // .assert_file_equals("output.txt", "1\n") + // .run() + // .await; + TestBuilder::new() - .command(r#"echo 1 | tee output.txt"#) - .assert_stdout("1\n") + .command(r#"echo 1 | cat > output.txt"#) .assert_file_equals("output.txt", "1\n") .run() .await; @@ -776,7 +789,6 @@ async fn uname() { TestBuilder::new() .command("uname") .assert_exit_code(0) - .check_stderr(false) .check_stdout(false) .run() .await; @@ -911,6 +923,165 @@ async fn date() { .await; } +#[tokio::test] +async fn if_clause() { + TestBuilder::new() + .command(r#"FOO=2; if [[ $FOO == 1 ]]; then echo "FOO is 1"; elif [[ $FOO -eq 2 ]]; then echo "FOO is 2"; else echo "FOO is not 1 or 2"; fi"#) + .assert_stdout("FOO is 2\n") + .run() + .await; + TestBuilder::new() + .command(r#"FOO=3; if [[ $FOO == 1 ]]; then echo "FOO is 1"; elif [[ $FOO -eq 2 ]]; then echo "FOO is 2"; else echo "FOO is not 1 or 2"; fi"#) + .assert_stdout("FOO is not 1 or 2\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=1; if [[ $FOO == 1 ]]; then echo "FOO is 1"; elif [[ $FOO -eq 2 ]]; then echo "FOO is 2"; else echo "FOO is not 1 or 2"; fi"#) + .assert_stdout("FOO is 1\n") + .run() + .await; + + TestBuilder::new() + .script_file("../../scripts/if_else.sh") + .assert_exit_code(0) + .assert_stdout("FOO is 2\n") + .assert_stdout("FOO is 2\n") + .assert_stdout("FOO is 2\n") + .assert_stdout("FOO is 2\n") + .run() + .await; +} + +#[tokio::test] +async fn touch() { + TestBuilder::new() + .command("touch file.txt") + .assert_exists("file.txt") + .check_stdout(false) + .run() + .await; + + TestBuilder::new() + .command("touch -m file.txt") + .assert_exists("file.txt") + .run() + .await; + + TestBuilder::new() + .command("touch -c nonexistent.txt") + .assert_not_exists("nonexistent.txt") + .run() + .await; + + TestBuilder::new() + .command("touch file1.txt file2.txt") + .assert_exists("file1.txt") + .assert_exists("file2.txt") + .run() + .await; + + TestBuilder::new() + .command("touch -d 'Tue Feb 20 14:30:00 2024' posix_locale.txt") + .assert_exists("posix_locale.txt") + .run() + .await; + + TestBuilder::new() + .command("touch -d '2024-02-20' iso_8601.txt") + .assert_exists("iso_8601.txt") + .run() + .await; + + TestBuilder::new() + .command("touch -t 202402201430.00 yyyymmddhhmmss.txt") + .assert_exists("yyyymmddhhmmss.txt") + .run() + .await; + + TestBuilder::new() + .command("touch -d '2024-02-20 14:30:00.000000' yyyymmddhhmmss_ms.txt") + .assert_exists("yyyymmddhhmmss_ms.txt") + .run() + .await; + + TestBuilder::new() + .command("touch -d '2024-02-20 14:30' yyyy_mm_dd_hh_mm.txt") + .assert_exists("yyyy_mm_dd_hh_mm.txt") + .run() + .await; + + TestBuilder::new() + .command("touch -t 202402201430 yyyymmddhhmm.txt") + .assert_exists("yyyymmddhhmm.txt") + .run() + .await; + + TestBuilder::new() + .command("touch -d '2024-02-20 14:30 +0000' yyyymmddhhmm_offset.txt") + .assert_exists("yyyymmddhhmm_offset.txt") + .run() + .await; + + TestBuilder::new() + .command("touch file.txt && touch -r file.txt reference.txt") + .assert_exists("reference.txt") + .run() + .await; + // Test for non-existent file with -c option + TestBuilder::new() + .command("touch -c nonexistent.txt") + .assert_not_exists("nonexistent.txt") + .run() + .await; + + // Test for invalid date format + TestBuilder::new() + .command("touch -d 'invalid date' invalid_date.txt") + .assert_stderr_contains("Unable to parse date: invalid date\n") + .assert_exit_code(1) + .run() + .await; + + // Test for invalid timestamp format + TestBuilder::new() + .command("touch -t 9999999999 invalid_timestamp.txt") + .assert_stderr_contains("invalid date format '9999999999'\n") + .assert_exit_code(1) + .run() + .await; + + TestBuilder::new() + .command("touch $TEMP_DIR/absolute_path.txt") + .assert_exists("$TEMP_DIR/absolute_path.txt") + .run() + .await; + + TestBuilder::new() + .command("touch $TEMP_DIR/non_existent_dir/non_existent.txt") + .assert_stderr_contains("No such file or directory") + .assert_exit_code(1) + .run() + .await; + + // TODO: implement ln in shell and then enable this test + // // Test with -h option on a symlink + // TestBuilder::new() + // .command("touch original.txt && ln -s original.txt symlink.txt && touch -h symlink.txt") + // .assert_exists("symlink.txt") + // .run() + // .await; + + // Test with multiple files, including one that doesn't exist + TestBuilder::new() + .command("touch existing.txt && touch existing.txt nonexistent.txt another_existing.txt") + .assert_exists("existing.txt") + .assert_exists("nonexistent.txt") + .assert_exists("another_existing.txt") + .run() + .await; +} + #[cfg(test)] fn no_such_file_error_text() -> &'static str { if cfg!(windows) { diff --git a/crates/tests/src/test_builder.rs b/crates/tests/src/test_builder.rs index 4b1ad3c..e45720f 100644 --- a/crates/tests/src/test_builder.rs +++ b/crates/tests/src/test_builder.rs @@ -1,6 +1,6 @@ // Copyright 2018-2024 the Deno authors. MIT license. -use anyhow::Context; use futures::future::LocalBoxFuture; +use miette::IntoDiagnostic; use pretty_assertions::assert_eq; use std::collections::HashMap; use std::fs; @@ -65,6 +65,7 @@ pub struct TestBuilder { expected_exit_code: i32, expected_stderr: String, expected_stdout: String, + expected_stderr_contains: String, assertions: Vec, assert_stdout: bool, assert_stderr: bool, @@ -101,9 +102,10 @@ impl TestBuilder { expected_exit_code: 0, expected_stderr: Default::default(), expected_stdout: Default::default(), + expected_stderr_contains: Default::default(), assertions: Default::default(), assert_stdout: true, - assert_stderr: true, + assert_stderr: false, } } @@ -128,6 +130,11 @@ impl TestBuilder { self } + pub fn script_file(&mut self, path: &str) -> &mut Self { + self.command(fs::read_to_string(path).unwrap().as_str()); + self + } + pub fn stdin(&mut self, stdin: &str) -> &mut Self { self.stdin = stdin.as_bytes().to_vec(); self @@ -163,6 +170,15 @@ impl TestBuilder { pub fn assert_stderr(&mut self, output: &str) -> &mut Self { self.expected_stderr.push_str(output); + self.assert_stderr = true; + self.expected_stderr_contains.clear(); + self + } + + pub fn assert_stderr_contains(&mut self, output: &str) -> &mut Self { + self.expected_stderr_contains.push_str(output); + self.assert_stderr = false; + self.expected_stderr.clear(); self } @@ -176,15 +192,16 @@ impl TestBuilder { self } - pub fn check_stderr(&mut self, check_stderr: bool) -> &mut Self { - self.assert_stderr = check_stderr; - self - } - pub fn assert_exists(&mut self, path: &str) -> &mut Self { self.ensure_temp_dir(); - self.assertions - .push(TestAssertion::FileExists(path.to_string())); + let temp_dir = if let Some(temp_dir) = &self.temp_dir { + temp_dir.cwd.display().to_string() + } else { + "NO_TEMP_DIR".to_string() + }; + self.assertions.push(TestAssertion::FileExists( + path.to_string().replace("$TEMP_DIR", &temp_dir), + )); self } @@ -205,6 +222,8 @@ impl TestBuilder { } pub async fn run(&mut self) { + std::env::set_var("NO_GRAPHICS", "1"); + let list = parse(&self.command).unwrap(); let cwd = if let Some(temp_dir) = &self.temp_dir { temp_dir.cwd.clone() @@ -218,6 +237,7 @@ impl TestBuilder { let (stderr, stderr_handle) = get_output_writer_and_handle(); let local_set = tokio::task::LocalSet::new(); + self.env_var("TEMP_DIR", &cwd.display().to_string()); let state = ShellState::new( self.env_vars.clone(), &cwd, @@ -231,13 +251,25 @@ impl TestBuilder { } else { "NO_TEMP_DIR".to_string() }; + let stderr_output = stderr_handle.await.unwrap(); if self.assert_stderr { assert_eq!( - stderr_handle.await.unwrap(), + stderr_output, self.expected_stderr.replace("$TEMP_DIR", &temp_dir), "\n\nFailed for: {}", self.command ); + } else if !self.expected_stderr_contains.is_empty() { + assert!( + stderr_output.contains( + &self + .expected_stderr_contains + .replace("$TEMP_DIR", &temp_dir) + ), + "\n\nFailed for: {}\nExpected stderr to contain: {}", + self.command, + self.expected_stderr_contains + ); } if self.assert_stdout { assert_eq!( @@ -256,8 +288,10 @@ impl TestBuilder { for assertion in &self.assertions { match assertion { TestAssertion::FileExists(path) => { + let path_to_check = cwd.join(path); + assert!( - cwd.join(path).exists(), + path_to_check.exists(), "\n\nFailed for: {}\nExpected '{}' to exist.", self.command, path, @@ -273,7 +307,7 @@ impl TestBuilder { } TestAssertion::FileTextEquals(path, text) => { let actual_text = std::fs::read_to_string(cwd.join(path)) - .with_context(|| format!("Error reading {path}")) + .into_diagnostic() .unwrap(); assert_eq!( &actual_text, text, diff --git a/scripts/if_else.sh b/scripts/if_else.sh index f3fd04f..5da3368 100644 --- a/scripts/if_else.sh +++ b/scripts/if_else.sh @@ -1,8 +1,41 @@ FOO=2 -if [[ $FOO -eq 1 ]] then +if [[ $FOO -eq 1 ]]; +then + echo "FOO is 1"; +elif [[ $FOO -eq 2 ]]; +then + echo "FOO is 2"; +else + echo "FOO is not 1 or 2"; +fi + +FOO=2 +if [[ $FOO -eq 1 ]]; then echo "FOO is 1" -elif [[ $FOO -eq 2 ]] then +elif [[ $FOO -eq 2 ]]; then echo "FOO is 2" else echo "FOO is not 1 or 2" +fi + +FOO=2 +if [[ $FOO -eq 1 ]]; +then + echo "FOO is 1"; +elif [[ $FOO -eq 2 ]]; +then + echo "FOO is 2"; +else + echo "FOO is not 1 or 2"; +fi + +FOO=2 +if [[ $FOO -eq 1 ]] +then + echo "FOO is 1"; +elif [[ $FOO -eq 2 ]] +then + echo "FOO is 2"; +else + echo "FOO is not 1 or 2"; fi \ No newline at end of file