From 508505a96c3679e41018104c0bea358ebeef90c8 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Sun, 22 Sep 2024 13:16:23 -0400 Subject: [PATCH 01/24] Add support for test --- Cargo.lock | 93 ++++++++++++++++++++++++++++++ crates/shell/Cargo.toml | 3 +- crates/shell/src/commands/mod.rs | 6 ++ crates/shell/src/commands/touch.rs | 31 ++++++++++ crates/tests/file.txt | 0 crates/tests/src/lib.rs | 10 ++++ crates/tests/src/test_builder.rs | 2 + 7 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 crates/shell/src/commands/touch.rs create mode 100644 crates/tests/file.txt diff --git a/Cargo.lock b/Cargo.lock index 8a68334..38a5458 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -392,6 +401,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" @@ -625,6 +646,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags", "libc", + "redox_syscall", ] [[package]] @@ -695,6 +717,12 @@ dependencies = [ "syn", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -737,6 +765,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.49.0" @@ -830,6 +868,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse_datetime" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8720474e3dd4af20cea8716703498b9f3b690f318fa9d9d9e2e38eaf44b96d0" +dependencies = [ + "chrono", + "nom", + "regex", +] + [[package]] name = "path-dedot" version = "3.1.1" @@ -988,6 +1037,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1108,6 +1186,7 @@ dependencies = [ "rustyline", "tokio", "uu_ls", + "uu_touch", "uu_uname", "which", ] @@ -1366,6 +1445,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" diff --git a/crates/shell/Cargo.toml b/crates/shell/Cargo.toml index 78797fe..48795ec 100644 --- a/crates/shell/Cargo.toml +++ b/crates/shell/Cargo.toml @@ -33,7 +33,8 @@ uu_ls = "0.0.27" dirs = "5.0.1" which = "6.0.3" uu_uname = "0.0.27" +uu_touch = "0.0.27" [package.metadata.release] # Dont publish the binary -release = false \ No newline at end of file +release = false diff --git a/crates/shell/src/commands/mod.rs b/crates/shell/src/commands/mod.rs index 810485b..e7361ba 100644 --- a/crates/shell/src/commands/mod.rs +++ b/crates/shell/src/commands/mod.rs @@ -8,9 +8,11 @@ use uu_ls::uumain as uu_ls; use crate::execute; pub mod uname; +pub mod touch; pub mod which; pub use uname::UnameCommand; +pub use touch::TouchCommand; pub use which::WhichCommand; pub struct LsCommand; @@ -44,6 +46,10 @@ pub fn get_commands() -> HashMap> { "uname".to_string(), Rc::new(UnameCommand) as Rc, ), + ( + "touch".to_string(), + Rc::new(TouchCommand) as Rc, + ), ]) } diff --git a/crates/shell/src/commands/touch.rs b/crates/shell/src/commands/touch.rs new file mode 100644 index 0000000..1388493 --- /dev/null +++ b/crates/shell/src/commands/touch.rs @@ -0,0 +1,31 @@ +use std::ffi::OsString; + +use deno_task_shell::{ExecuteResult, ShellCommand, ShellCommandContext}; +use futures::future::LocalBoxFuture; +use uu_touch::uumain as uu_touch; + +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(exit_code) => ExecuteResult::from_exit_code(exit_code), + })) + } +} + +fn execute_touch(context: &mut ShellCommandContext) -> Result<(), i32> { + let mut args: Vec = vec![OsString::from("touch")]; + + context + .args + .iter() + .for_each(|arg| args.push(OsString::from(arg))); + + let exit_code = uu_touch(args.into_iter()); + if exit_code != 0 { + return Err(exit_code); + } + Ok(()) +} \ No newline at end of file diff --git a/crates/tests/file.txt b/crates/tests/file.txt new file mode 100644 index 0000000..e69de29 diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 7eb8367..9764cf1 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -870,6 +870,16 @@ async fn arithmetic() { .await; } +#[tokio::test] +async fn touch() { + TestBuilder::new() + .command("touch file.txt") + .assert_exists("file.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..d5e06bb 100644 --- a/crates/tests/src/test_builder.rs +++ b/crates/tests/src/test_builder.rs @@ -256,6 +256,8 @@ impl TestBuilder { for assertion in &self.assertions { match assertion { TestAssertion::FileExists(path) => { + println!("path: {}", path); + println!("cwd: {:?}", cwd.join(path)); assert!( cwd.join(path).exists(), "\n\nFailed for: {}\nExpected '{}' to exist.", From 404c90f9067d22531f6b1d9a3f50b2bf250feb4d Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Sun, 22 Sep 2024 14:08:57 -0400 Subject: [PATCH 02/24] Added tests --- crates/shell/src/commands/mod.rs | 4 ++-- crates/shell/src/commands/touch.rs | 30 ++++++++++++++++++++++++++++-- crates/tests/file.txt | 0 crates/tests/src/lib.rs | 30 ++++++++++++++++++++++++++++++ crates/tests/src/test_builder.rs | 2 -- 5 files changed, 60 insertions(+), 6 deletions(-) delete mode 100644 crates/tests/file.txt diff --git a/crates/shell/src/commands/mod.rs b/crates/shell/src/commands/mod.rs index e7361ba..9694cb2 100644 --- a/crates/shell/src/commands/mod.rs +++ b/crates/shell/src/commands/mod.rs @@ -7,12 +7,12 @@ use uu_ls::uumain as uu_ls; use crate::execute; -pub mod uname; pub mod touch; +pub mod uname; pub mod which; -pub use uname::UnameCommand; pub use touch::TouchCommand; +pub use uname::UnameCommand; pub use which::WhichCommand; pub struct LsCommand; diff --git a/crates/shell/src/commands/touch.rs b/crates/shell/src/commands/touch.rs index 1388493..fa626e9 100644 --- a/crates/shell/src/commands/touch.rs +++ b/crates/shell/src/commands/touch.rs @@ -1,4 +1,4 @@ -use std::ffi::OsString; +use std::{ffi::OsString, path::Path}; use deno_task_shell::{ExecuteResult, ShellCommand, ShellCommandContext}; use futures::future::LocalBoxFuture; @@ -23,9 +23,35 @@ fn execute_touch(context: &mut ShellCommandContext) -> Result<(), i32> { .iter() .for_each(|arg| args.push(OsString::from(arg))); + let mut new_args = Vec::new(); + let mut skip_next = false; + + for (index, arg) in args[1..].iter().enumerate() { + if skip_next { + skip_next = false; + continue; + } + + if arg.to_str().map_or(false, |s| s == "-t" || s == "-d") && index + 1 < args[1..].len() { + new_args.push(arg.clone()); + new_args.push(args[index + 2].clone()); + skip_next = true; + } else if !arg.to_str().map_or(false, |s| s.starts_with('-')) { + new_args.push(if Path::new(arg).is_absolute() { + arg.clone() + } else { + context.state.cwd().join(arg).into_os_string() + }); + } else { + new_args.push(arg.clone()); + } + } + + args.splice(1.., new_args); + let exit_code = uu_touch(args.into_iter()); if exit_code != 0 { return Err(exit_code); } Ok(()) -} \ No newline at end of file +} diff --git a/crates/tests/file.txt b/crates/tests/file.txt deleted file mode 100644 index e69de29..0000000 diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 9764cf1..210506b 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -878,6 +878,36 @@ async fn touch() { .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 -t 202401010000.00 timestamp.txt") + .assert_exists("timestamp.txt") + .run() + .await; + + TestBuilder::new() + .command("touch file.txt && touch -r file.txt reference.txt") + .assert_exists("reference.txt") + .run() + .await; } #[cfg(test)] diff --git a/crates/tests/src/test_builder.rs b/crates/tests/src/test_builder.rs index d5e06bb..4b1ad3c 100644 --- a/crates/tests/src/test_builder.rs +++ b/crates/tests/src/test_builder.rs @@ -256,8 +256,6 @@ impl TestBuilder { for assertion in &self.assertions { match assertion { TestAssertion::FileExists(path) => { - println!("path: {}", path); - println!("cwd: {:?}", cwd.join(path)); assert!( cwd.join(path).exists(), "\n\nFailed for: {}\nExpected '{}' to exist.", From ad2bf31acc58f5177c0cae9ea42aaa3f32933d34 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Sun, 22 Sep 2024 14:16:12 -0400 Subject: [PATCH 03/24] run fmt --- crates/shell/src/commands/mod.rs | 2 +- crates/tests/src/lib.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/shell/src/commands/mod.rs b/crates/shell/src/commands/mod.rs index d2ea317..86f16db 100644 --- a/crates/shell/src/commands/mod.rs +++ b/crates/shell/src/commands/mod.rs @@ -12,8 +12,8 @@ pub mod touch; pub mod uname; pub mod which; -pub use touch::TouchCommand; pub use date::DateCommand; +pub use touch::TouchCommand; pub use uname::UnameCommand; pub use which::WhichCommand; diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 1d9497d..66c62e0 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -878,8 +878,8 @@ async fn date() { .check_stdout(false) .run() .await; - - TestBuilder::new() + + TestBuilder::new() .command("date +%Y-%m-%d") .assert_exit_code(0) .check_stdout(false) @@ -895,7 +895,7 @@ async fn touch() { .check_stdout(false) .run() .await; - + TestBuilder::new() .command("touch -m file.txt") .assert_exists("file.txt") From 4fa753e8f88d86029387296fdc111c7af22daee9 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Mon, 23 Sep 2024 23:17:58 -0400 Subject: [PATCH 04/24] Moved touch stuff to shell side --- Cargo.lock | 83 +++++++- crates/shell/Cargo.toml | 4 + crates/shell/src/commands/touch.rs | 305 ++++++++++++++++++++++++++--- 3 files changed, 353 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ccb10a..f4d5a55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -282,6 +282,20 @@ dependencies = [ "typenum", ] +[[package]] +name = "datetime" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c3f7a77f3e57fedf80e09136f2d8777ebf621207306f6d96d610af048354bc" +dependencies = [ + "iso8601", + "libc", + "locale", + "pad", + "redox_syscall 0.1.57", + "winapi", +] + [[package]] name = "deno_task_shell" version = "0.17.0" @@ -509,7 +523,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", - "version_check", + "version_check 0.9.5", ] [[package]] @@ -602,6 +616,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "iso8601" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e86914a73535f3f541a765adea0a9fafcf53fa6adb73662c4988fd9233766f" +dependencies = [ + "nom 4.2.3", +] + [[package]] name = "itertools" version = "0.13.0" @@ -646,7 +669,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags", "libc", - "redox_syscall", + "redox_syscall 0.5.4", ] [[package]] @@ -655,6 +678,15 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "locale" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd" +dependencies = [ + "libc", +] + [[package]] name = "lock_api" version = "0.4.12" @@ -765,6 +797,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + [[package]] name = "nom" version = "7.1.3" @@ -845,6 +887,15 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" +[[package]] +name = "pad" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3" +dependencies = [ + "unicode-width", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -863,7 +914,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.4", "smallvec", "windows-targets 0.52.6", ] @@ -875,7 +926,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8720474e3dd4af20cea8716703498b9f3b690f318fa9d9d9e2e38eaf44b96d0" dependencies = [ "chrono", - "nom", + "nom 7.1.3", "regex", ] @@ -1017,6 +1068,12 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + [[package]] name = "redox_syscall" version = "0.5.4" @@ -1180,10 +1237,14 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "datetime", "deno_task_shell", "dirs", + "filetime", "futures", + "miette", "rustyline", + "thiserror", "tokio", "uu_date", "uu_ls", @@ -1325,18 +1386,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", @@ -1533,6 +1594,12 @@ dependencies = [ "ansi-width", ] +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + [[package]] name = "version_check" version = "0.9.5" diff --git a/crates/shell/Cargo.toml b/crates/shell/Cargo.toml index 9a5d699..c651bdd 100644 --- a/crates/shell/Cargo.toml +++ b/crates/shell/Cargo.toml @@ -35,6 +35,10 @@ 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"] } +thiserror = "1.0.64" +filetime = "0.2.25" +datetime = "0.5.2" [package.metadata.release] # Dont publish the binary diff --git a/crates/shell/src/commands/touch.rs b/crates/shell/src/commands/touch.rs index fa626e9..b243ebb 100644 --- a/crates/shell/src/commands/touch.rs +++ b/crates/shell/src/commands/touch.rs @@ -1,8 +1,12 @@ -use std::{ffi::OsString, path::Path}; +use std::{ffi::OsString, fs::{self, File}, path::{Path, PathBuf}}; use deno_task_shell::{ExecuteResult, ShellCommand, ShellCommandContext}; use futures::future::LocalBoxFuture; -use uu_touch::uumain as uu_touch; +use uu_touch::{uu_app as uu_touch, options}; +use miette::{miette, Result, IntoDiagnostic}; +use filetime::{set_file_times, set_symlink_file_times, FileTime}; + +static ARG_FILES: &str = "files"; pub struct TouchCommand; @@ -10,48 +14,287 @@ 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(exit_code) => ExecuteResult::from_exit_code(exit_code), + Err(e) => { + let _ = context.stderr.write_all(format!("{:#}", e).as_bytes()); + ExecuteResult::from_exit_code(1) + }, })) } } -fn execute_touch(context: &mut ShellCommandContext) -> Result<(), i32> { - let mut args: Vec = vec![OsString::from("touch")]; +fn execute_touch(context: &mut ShellCommandContext) -> Result<()> { + let matches = uu_touch().try_get_matches_from(context.args.clone()).into_diagnostic()?; - context - .args - .iter() - .for_each(|arg| args.push(OsString::from(arg))); + 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 new_args = Vec::new(); - let mut skip_next = false; + let (atime, mtime) = match ( + matches.get_one::(options::sources::REFERENCE), + matches.get_one::(options::sources::DATE), + ) { + (Some(reference), Some(date)) => { + let (atime, mtime) = stat(Path::new(&reference), !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) => { + stat(Path::new(&reference), !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 { + // FIXME: find a way to avoid having to clone the path + let pathbuf = if filename.to_string_lossy() == "-" { + pathbuf_from_stdout() + .into_diagnostic()? + } else { + PathBuf::from(filename) + }; + + let path = pathbuf.as_path(); + + let metadata_result = if matches.get_flag(options::NO_DEREF) { + path.symlink_metadata() + } else { + path.metadata() + }; - for (index, arg) in args[1..].iter().enumerate() { - if skip_next { - skip_next = false; - continue; + if let Err(e) = metadata_result { + if e.kind() != std::io::ErrorKind::NotFound { + return Err(miette!("setting times of {}: {}", filename.to_string_lossy(), e)); + } + + if matches.get_flag(options::NO_CREATE) { + continue; + } + + if matches.get_flag(options::NO_DEREF) { + context.stderr.write_all(format!("setting times of {}: No such file or directory", filename.to_string_lossy()).as_bytes()) + .map_err(|e| miette!("Failed to write to stderr: {}", e))?; + continue; + } + + File::create(path) + .into_diagnostic() + .map_err(|e| 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 arg.to_str().map_or(false, |s| s == "-t" || s == "-d") && index + 1 < args[1..].len() { - new_args.push(arg.clone()); - new_args.push(args[index + 2].clone()); - skip_next = true; - } else if !arg.to_str().map_or(false, |s| s.starts_with('-')) { - new_args.push(if Path::new(arg).is_absolute() { - arg.clone() - } else { - context.state.cwd().join(arg).into_os_string() - }); + 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 filename.to_string_lossy() == "-" { + filetime::set_file_times(path, atime, mtime) + } else if matches.get_flag(options::NO_DEREF) { + set_symlink_file_times(path, atime, mtime) } else { - new_args.push(arg.clone()); + 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 current_year = || Local::now().year(); + + let (format, ts) = match s.chars().count() { + 15 => (YYYYMMDDHHMM_DOT_SS, s.to_owned()), + 12 => (YYYYMMDDHHMM, s.to_owned()), + // If we don't add "20", we have insufficient information to parse + 13 => (YYYYMMDDHHMM_DOT_SS, format!("20{}", s)), + 10 => (YYYYMMDDHHMM, format!("20{}", s)), + 11 => (YYYYMMDDHHMM_DOT_SS, format!("{}{}", current_year(), s)), + 8 => (YYYYMMDDHHMM, format!("{}{}", current_year(), s)), + _ => { + return Err(miette!("invalid date format '{}'", s)); + } + }; - args.splice(1.., new_args); + let local = NaiveDateTime::parse_from_str(&ts, format) + .map_err(|_| miette!("invalid date ts format '{}'", s))?; + let mut local = match chrono::Local.from_local_datetime(&local) { + LocalResult::Single(dt) => dt, + _ => { + return Err(miette!( + "invalid date ts format '{}'", s, + )) + } + }; - let exit_code = uu_touch(args.into_iter()); - if exit_code != 0 { - return Err(exit_code); + // Chrono caps seconds at 59, but 60 is valid. It might be a leap second + // or wrap to the next minute. But that doesn't really matter, because we + // only care about the timestamp anyway. + // Tested in gnu/tests/touch/60-seconds + if local.second() == 59 && ts.ends_with(".60") { + local += Duration::try_seconds(1).unwrap(); + } + + // Due to daylight saving time switch, local time can jump from 1:59 AM to + // 3:00 AM, in which case any time between 2:00 AM and 2:59 AM is not + // valid. If we are within this jump, chrono takes the offset from before + // the jump. If we then jump forward an hour, we get the new corrected + // offset. Jumping back will then now correctly take the jump into account. + let local2 = local + Duration::try_hours(1).unwrap() - Duration::try_hours(1).unwrap(); + if local.hour() != local2.hour() { + return Err(miette!( + "invalid date format '{}'", s, + )); + } + + let local = FileTime::from_unix_time(local.timestamp(), local.timestamp_subsec_nanos()); + Ok(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(USimpleError::new( + 1, + format!("GetFinalPathNameByHandleW failed with code {ret}"), + )) + } + 0 => { + return Err(USimpleError::new( + 1, + format!( + "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| USimpleError::new(1, e.to_string()))? + .into()) } - Ok(()) } From 74ecc51a81426cff3008cc695662033fce27ed6b Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Thu, 26 Sep 2024 02:00:57 -0400 Subject: [PATCH 05/24] Redo testing by moving functions on touch.rs to shell --- Cargo.lock | 33 ++++++ crates/shell/Cargo.toml | 3 + crates/shell/src/commands/touch.rs | 179 ++++++++++++++++++----------- crates/tests/src/lib.rs | 46 +++++++- 4 files changed, 189 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4d5a55..0e7eac9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,12 @@ version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "ascii_tree" version = "0.1.1" @@ -197,7 +203,9 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -358,6 +366,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" @@ -1123,6 +1143,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" @@ -1236,13 +1266,16 @@ name = "shell" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "clap", "datetime", "deno_task_shell", "dirs", + "dtparse", "filetime", "futures", "miette", + "parse_datetime", "rustyline", "thiserror", "tokio", diff --git a/crates/shell/Cargo.toml b/crates/shell/Cargo.toml index c651bdd..b79d6bf 100644 --- a/crates/shell/Cargo.toml +++ b/crates/shell/Cargo.toml @@ -39,6 +39,9 @@ miette = { version = "7.2.0", features = ["fancy"] } thiserror = "1.0.64" filetime = "0.2.25" datetime = "0.5.2" +chrono = "0.4.38" +parse_datetime = "0.6.0" +dtparse = "2.0.1" [package.metadata.release] # Dont publish the binary diff --git a/crates/shell/src/commands/touch.rs b/crates/shell/src/commands/touch.rs index b243ebb..91a6499 100644 --- a/crates/shell/src/commands/touch.rs +++ b/crates/shell/src/commands/touch.rs @@ -1,10 +1,15 @@ -use std::{ffi::OsString, fs::{self, File}, path::{Path, PathBuf}}; +use std::{ + ffi::OsString, + fs::{self, File}, + path::{Path, PathBuf}, +}; +use chrono::{DateTime, Duration, Local, NaiveDateTime, TimeZone, Timelike}; use deno_task_shell::{ExecuteResult, ShellCommand, ShellCommandContext}; -use futures::future::LocalBoxFuture; -use uu_touch::{uu_app as uu_touch, options}; -use miette::{miette, Result, IntoDiagnostic}; 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"; @@ -17,13 +22,17 @@ impl ShellCommand for TouchCommand { 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().try_get_matches_from(context.args.clone()).into_diagnostic()?; + 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| { @@ -35,26 +44,38 @@ fn execute_touch(context: &mut ShellCommandContext) -> Result<()> { } }), None => { - return Err(miette!("missing file operand\nTry 'touch --help' for more information.")); + return Err(miette!( + "missing file operand\nTry 'touch --help' for more information." + )) } }; - let (atime, mtime) = match ( + let (mut atime, mut mtime) = match ( matches.get_one::(options::sources::REFERENCE), matches.get_one::(options::sources::DATE), ) { (Some(reference), Some(date)) => { - let (atime, mtime) = stat(Path::new(&reference), !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") - })?; + 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) => { - stat(Path::new(&reference), !matches.get_flag(options::NO_DEREF)) + 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)?; @@ -69,15 +90,14 @@ fn execute_touch(context: &mut ShellCommandContext) -> Result<()> { }; Ok((timestamp, timestamp)) } - }.map_err(|e| miette!("{}", e))?; + } + .map_err(|e| miette!("{}", e))?; for filename in files { - // FIXME: find a way to avoid having to clone the path - let pathbuf = if filename.to_string_lossy() == "-" { - pathbuf_from_stdout() - .into_diagnostic()? + let pathbuf = if filename.to_str() == Some("-") { + pathbuf_from_stdout()? } else { - PathBuf::from(filename) + filename }; let path = pathbuf.as_path(); @@ -90,7 +110,7 @@ fn execute_touch(context: &mut ShellCommandContext) -> Result<()> { if let Err(e) = metadata_result { if e.kind() != std::io::ErrorKind::NotFound { - return Err(miette!("setting times of {}: {}", filename.to_string_lossy(), e)); + return Err(miette!("setting times of {}: {}", path.display(), e)); } if matches.get_flag(options::NO_CREATE) { @@ -98,8 +118,13 @@ fn execute_touch(context: &mut ShellCommandContext) -> Result<()> { } if matches.get_flag(options::NO_DEREF) { - context.stderr.write_all(format!("setting times of {}: No such file or directory", filename.to_string_lossy()).as_bytes()) - .map_err(|e| miette!("Failed to write to stderr: {}", e))?; + let _ = context.stderr.write_all( + format!( + "setting times of {:?}: No such file or directory", + path.display() + ) + .as_bytes(), + ); continue; } @@ -145,15 +170,15 @@ fn execute_touch(context: &mut ShellCommandContext) -> Result<()> { // 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 filename.to_string_lossy() == "-" { - filetime::set_file_times(path, atime, mtime) + 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(()) } @@ -177,54 +202,36 @@ fn filetime_to_datetime(ft: &FileTime) -> Option> { } fn parse_timestamp(s: &str) -> Result { - let current_year = || Local::now().year(); - - let (format, ts) = match s.chars().count() { - 15 => (YYYYMMDDHHMM_DOT_SS, s.to_owned()), - 12 => (YYYYMMDDHHMM, s.to_owned()), - // If we don't add "20", we have insufficient information to parse - 13 => (YYYYMMDDHHMM_DOT_SS, format!("20{}", s)), - 10 => (YYYYMMDDHHMM, format!("20{}", s)), - 11 => (YYYYMMDDHHMM_DOT_SS, format!("{}{}", current_year(), s)), - 8 => (YYYYMMDDHHMM, format!("{}{}", current_year(), s)), - _ => { - return Err(miette!("invalid date format '{}'", s)); - } + 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 = NaiveDateTime::parse_from_str(&ts, format) - .map_err(|_| miette!("invalid date ts format '{}'", s))?; - let mut local = match chrono::Local.from_local_datetime(&local) { - LocalResult::Single(dt) => dt, - _ => { - return Err(miette!( - "invalid date ts format '{}'", s, - )) - } - }; + let local = now + .timezone() + .from_local_datetime(&parsed) + .single() + .ok_or_else(|| miette!("invalid date '{}'", s))?; - // Chrono caps seconds at 59, but 60 is valid. It might be a leap second - // or wrap to the next minute. But that doesn't really matter, because we - // only care about the timestamp anyway. - // Tested in gnu/tests/touch/60-seconds - if local.second() == 59 && ts.ends_with(".60") { - local += Duration::try_seconds(1).unwrap(); - } + // Handle leap seconds + let local = if parsed.second() == 59 && s.ends_with(".60") { + local + Duration::seconds(1) + } else { + local + }; - // Due to daylight saving time switch, local time can jump from 1:59 AM to - // 3:00 AM, in which case any time between 2:00 AM and 2:59 AM is not - // valid. If we are within this jump, chrono takes the offset from before - // the jump. If we then jump forward an hour, we get the new corrected - // offset. Jumping back will then now correctly take the jump into account. - let local2 = local + Duration::try_hours(1).unwrap() - Duration::try_hours(1).unwrap(); - if local.hour() != local2.hour() { - return Err(miette!( - "invalid date format '{}'", s, - )); + // Check for daylight saving time issues + if (local + Duration::hours(1) - Duration::hours(1)).hour() != local.hour() { + return Err(miette!("invalid date '{}'", s)); } - let local = FileTime::from_unix_time(local.timestamp(), local.timestamp_subsec_nanos()); - Ok(local) + Ok(datetime_to_filetime(&local)) } // TODO: this may be a good candidate to put in fsext.rs @@ -298,3 +305,35 @@ fn pathbuf_from_stdout() -> Result { .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/tests/src/lib.rs b/crates/tests/src/lib.rs index 66c62e0..821dc20 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -916,8 +916,50 @@ async fn touch() { .await; TestBuilder::new() - .command("touch -t 202401010000.00 timestamp.txt") - .assert_exists("timestamp.txt") + .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:00' yyyymmddhhmms.txt") + .assert_exists("yyyymmddhhmms.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; From 77a489b054fa7af4676c410eb80574e58d68b78d Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Thu, 26 Sep 2024 02:44:59 -0400 Subject: [PATCH 06/24] Increase coverage --- Cargo.lock | 1 + crates/shell/src/commands/touch.rs | 2 +- crates/tests/Cargo.toml | 1 + crates/tests/src/lib.rs | 58 ++++++++++++++++++++++++++---- crates/tests/src/test_builder.rs | 37 ++++++++++++++----- 5 files changed, 83 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e7eac9..7d4aaf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1399,6 +1399,7 @@ version = "0.1.0" dependencies = [ "anyhow", "deno_task_shell", + "dirs", "futures", "pretty_assertions", "shell", diff --git a/crates/shell/src/commands/touch.rs b/crates/shell/src/commands/touch.rs index 91a6499..44b8bfb 100644 --- a/crates/shell/src/commands/touch.rs +++ b/crates/shell/src/commands/touch.rs @@ -20,7 +20,7 @@ impl ShellCommand for TouchCommand { 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()); + let _ = context.stderr.write_all(format!("{:?}", e).as_bytes()); ExecuteResult::from_exit_code(1) } })) diff --git a/crates/tests/Cargo.toml b/crates/tests/Cargo.toml index 6aa7577..b0215d1 100644 --- a/crates/tests/Cargo.toml +++ b/crates/tests/Cargo.toml @@ -9,6 +9,7 @@ shell = { path = "../shell" } anyhow = "1.0.87" futures = "0.3.30" tokio = { version = "1.40.0", features = ["full"] } +dirs = "5.0.1" [dev-dependencies] pretty_assertions = "1.0.0" diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 821dc20..175c1a0 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -776,7 +776,6 @@ async fn uname() { TestBuilder::new() .command("uname") .assert_exit_code(0) - .check_stderr(false) .check_stdout(false) .run() .await; @@ -939,12 +938,6 @@ async fn touch() { .run() .await; - TestBuilder::new() - .command("touch -d '2024-02-20 14:30:00' yyyymmddhhmms.txt") - .assert_exists("yyyymmddhhmms.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") @@ -968,6 +961,57 @@ async fn touch() { .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 ~/absolute_path.txt") + .assert_exists("~/absolute_path.txt") + .run() + .await; + + TestBuilder::new() + .command("touch /non_existent_dir/non_existent.txt") + .assert_stderr_contains("No such file or directory") + .assert_exit_code(1) + .run() + .await; + + // 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)] diff --git a/crates/tests/src/test_builder.rs b/crates/tests/src/test_builder.rs index 4b1ad3c..122318c 100644 --- a/crates/tests/src/test_builder.rs +++ b/crates/tests/src/test_builder.rs @@ -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, } } @@ -163,6 +165,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,11 +187,6 @@ 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 @@ -231,13 +237,21 @@ 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), + "\n\nFailed for: {}\nExpected stderr to contain: {}", + self.command, + self.expected_stderr_contains + ); } if self.assert_stdout { assert_eq!( @@ -256,8 +270,15 @@ impl TestBuilder { for assertion in &self.assertions { match assertion { TestAssertion::FileExists(path) => { + let path_to_check = if path.starts_with('/') { + PathBuf::from(path) + } else if path.starts_with("~/") { + dirs::home_dir().unwrap().join(path.strip_prefix("~/").unwrap()) + } else { + cwd.join(path) + }; assert!( - cwd.join(path).exists(), + path_to_check.exists(), "\n\nFailed for: {}\nExpected '{}' to exist.", self.command, path, From 135b3c67b7d8cab7a78aa077e7c398517a7f1f8f Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Thu, 26 Sep 2024 02:45:14 -0400 Subject: [PATCH 07/24] run fmt --- crates/tests/src/test_builder.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/tests/src/test_builder.rs b/crates/tests/src/test_builder.rs index 122318c..c46fd3b 100644 --- a/crates/tests/src/test_builder.rs +++ b/crates/tests/src/test_builder.rs @@ -273,7 +273,9 @@ impl TestBuilder { let path_to_check = if path.starts_with('/') { PathBuf::from(path) } else if path.starts_with("~/") { - dirs::home_dir().unwrap().join(path.strip_prefix("~/").unwrap()) + dirs::home_dir() + .unwrap() + .join(path.strip_prefix("~/").unwrap()) } else { cwd.join(path) }; From 3b97729bb6502f04606e8375c7ca0b66987d8380 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Thu, 26 Sep 2024 03:06:33 -0400 Subject: [PATCH 08/24] Remove extra crates --- Cargo.lock | 73 +++-------------------------------------- crates/shell/Cargo.toml | 2 -- 2 files changed, 4 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d4aaf1..d31055a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -290,20 +290,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "datetime" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c3f7a77f3e57fedf80e09136f2d8777ebf621207306f6d96d610af048354bc" -dependencies = [ - "iso8601", - "libc", - "locale", - "pad", - "redox_syscall 0.1.57", - "winapi", -] - [[package]] name = "deno_task_shell" version = "0.17.0" @@ -543,7 +529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", - "version_check 0.9.5", + "version_check", ] [[package]] @@ -636,15 +622,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "iso8601" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e86914a73535f3f541a765adea0a9fafcf53fa6adb73662c4988fd9233766f" -dependencies = [ - "nom 4.2.3", -] - [[package]] name = "itertools" version = "0.13.0" @@ -689,7 +666,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags", "libc", - "redox_syscall 0.5.4", + "redox_syscall", ] [[package]] @@ -698,15 +675,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "locale" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd" -dependencies = [ - "libc", -] - [[package]] name = "lock_api" version = "0.4.12" @@ -817,16 +785,6 @@ dependencies = [ "libc", ] -[[package]] -name = "nom" -version = "4.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" -dependencies = [ - "memchr", - "version_check 0.1.5", -] - [[package]] name = "nom" version = "7.1.3" @@ -907,15 +865,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" -[[package]] -name = "pad" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3" -dependencies = [ - "unicode-width", -] - [[package]] name = "parking_lot" version = "0.12.3" @@ -934,7 +883,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.4", + "redox_syscall", "smallvec", "windows-targets 0.52.6", ] @@ -946,7 +895,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8720474e3dd4af20cea8716703498b9f3b690f318fa9d9d9e2e38eaf44b96d0" dependencies = [ "chrono", - "nom 7.1.3", + "nom", "regex", ] @@ -1088,12 +1037,6 @@ dependencies = [ "nibble_vec", ] -[[package]] -name = "redox_syscall" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" - [[package]] name = "redox_syscall" version = "0.5.4" @@ -1268,7 +1211,6 @@ dependencies = [ "anyhow", "chrono", "clap", - "datetime", "deno_task_shell", "dirs", "dtparse", @@ -1277,7 +1219,6 @@ dependencies = [ "miette", "parse_datetime", "rustyline", - "thiserror", "tokio", "uu_date", "uu_ls", @@ -1628,12 +1569,6 @@ dependencies = [ "ansi-width", ] -[[package]] -name = "version_check" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" - [[package]] name = "version_check" version = "0.9.5" diff --git a/crates/shell/Cargo.toml b/crates/shell/Cargo.toml index b79d6bf..327a47e 100644 --- a/crates/shell/Cargo.toml +++ b/crates/shell/Cargo.toml @@ -36,9 +36,7 @@ uu_uname = "0.0.27" uu_touch = "0.0.27" uu_date = "0.0.27" miette = { version = "7.2.0", features = ["fancy"] } -thiserror = "1.0.64" filetime = "0.2.25" -datetime = "0.5.2" chrono = "0.4.38" parse_datetime = "0.6.0" dtparse = "2.0.1" From 211b393fd67428aacd2c91de4e757ea7f7034412 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Thu, 26 Sep 2024 12:42:23 -0400 Subject: [PATCH 09/24] Add support for windows --- crates/shell/Cargo.toml | 1 + crates/shell/src/commands/touch.rs | 18 ++++++------------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/crates/shell/Cargo.toml b/crates/shell/Cargo.toml index 327a47e..8fa58c1 100644 --- a/crates/shell/Cargo.toml +++ b/crates/shell/Cargo.toml @@ -40,6 +40,7 @@ filetime = "0.2.25" chrono = "0.4.38" parse_datetime = "0.6.0" dtparse = "2.0.1" +windows-sys = "0.59.0" [package.metadata.release] # Dont publish the binary diff --git a/crates/shell/src/commands/touch.rs b/crates/shell/src/commands/touch.rs index 44b8bfb..79b6ead 100644 --- a/crates/shell/src/commands/touch.rs +++ b/crates/shell/src/commands/touch.rs @@ -281,19 +281,13 @@ fn pathbuf_from_stdout() -> Result { let buffer_size = match ret { ERROR_PATH_NOT_FOUND | ERROR_NOT_ENOUGH_MEMORY | ERROR_INVALID_PARAMETER => { - return Err(USimpleError::new( - 1, - format!("GetFinalPathNameByHandleW failed with code {ret}"), - )) + return Err(miette!("GetFinalPathNameByHandleW failed with code {ret}")) } 0 => { - return Err(USimpleError::new( - 1, - format!( - "GetFinalPathNameByHandleW failed with code {}", - // SAFETY: GetLastError is thread-safe and has no documented memory unsafety. - unsafe { GetLastError() } - ), + return Err(miette!( + "GetFinalPathNameByHandleW failed with code {}", + // SAFETY: GetLastError is thread-safe and has no documented memory unsafety. + unsafe { GetLastError() } )); } e => e as usize, @@ -301,7 +295,7 @@ fn pathbuf_from_stdout() -> Result { // Don't include the null terminator Ok(String::from_utf16(&file_path_buffer[0..buffer_size]) - .map_err(|e| USimpleError::new(1, e.to_string()))? + .map_err(|e| miette!("GetFinalPathNameByHandleW failed with code {ret}"))? .into()) } } From 06bb7728921f3e772ec7ee9a25a3a6bd35895212 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Thu, 26 Sep 2024 12:42:49 -0400 Subject: [PATCH 10/24] Update lockfile --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index d31055a..c36ed25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1225,6 +1225,7 @@ dependencies = [ "uu_touch", "uu_uname", "which", + "windows-sys 0.59.0", ] [[package]] From 5368061788c57c573fcd10348113b5a3629a2a47 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Thu, 26 Sep 2024 12:48:30 -0400 Subject: [PATCH 11/24] Fix error message --- crates/shell/src/commands/touch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/shell/src/commands/touch.rs b/crates/shell/src/commands/touch.rs index 79b6ead..4526229 100644 --- a/crates/shell/src/commands/touch.rs +++ b/crates/shell/src/commands/touch.rs @@ -295,7 +295,7 @@ fn pathbuf_from_stdout() -> Result { // Don't include the null terminator Ok(String::from_utf16(&file_path_buffer[0..buffer_size]) - .map_err(|e| miette!("GetFinalPathNameByHandleW failed with code {ret}"))? + .map_err(|e| miette!("Generated path is not valid UTF-16: {e}"))? .into()) } } From 5de60018e0d248bc35b0836acd85fc4afadad060 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Thu, 26 Sep 2024 19:06:01 -0400 Subject: [PATCH 12/24] Update test --- crates/tests/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 175c1a0..1b5dbc5 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -991,7 +991,7 @@ async fn touch() { .await; TestBuilder::new() - .command("touch /non_existent_dir/non_existent.txt") + .command("touch ~/non_existent_dir/non_existent.txt") .assert_stderr_contains("No such file or directory") .assert_exit_code(1) .run() From 76c5c9a2b14d2721ccaa0d1228682b4a0503cdf7 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Fri, 27 Sep 2024 11:55:44 -0400 Subject: [PATCH 13/24] Using TEMPDIR for absolute path instead of ~ --- crates/tests/src/lib.rs | 6 +++--- crates/tests/src/test_builder.rs | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 1b5dbc5..8efe800 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -985,13 +985,13 @@ async fn touch() { .await; TestBuilder::new() - .command("touch ~/absolute_path.txt") - .assert_exists("~/absolute_path.txt") + .command("touch $TEMP_DIR/absolute_path.txt") + .assert_exists("$TEMP_DIR/absolute_path.txt") .run() .await; TestBuilder::new() - .command("touch ~/non_existent_dir/non_existent.txt") + .command("touch $TEMP_DIR/non_existent_dir/non_existent.txt") .assert_stderr_contains("No such file or directory") .assert_exit_code(1) .run() diff --git a/crates/tests/src/test_builder.rs b/crates/tests/src/test_builder.rs index c46fd3b..14215d7 100644 --- a/crates/tests/src/test_builder.rs +++ b/crates/tests/src/test_builder.rs @@ -117,6 +117,8 @@ impl TestBuilder { fn get_temp_dir(&mut self) -> &mut TempDir { if self.temp_dir.is_none() { self.temp_dir = Some(TempDir::new()); + let temp_dir_string = self.temp_dir_path().display().to_string(); + self.env_vars.insert("TEMP_DIR".to_string(), temp_dir_string); } self.temp_dir.as_mut().unwrap() } @@ -189,8 +191,13 @@ impl TestBuilder { pub fn assert_exists(&mut self, path: &str) -> &mut Self { self.ensure_temp_dir(); + 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())); + .push(TestAssertion::FileExists(path.to_string().replace("$TEMP_DIR", &temp_dir))); self } @@ -247,7 +254,7 @@ impl TestBuilder { ); } else if !self.expected_stderr_contains.is_empty() { assert!( - stderr_output.contains(&self.expected_stderr_contains), + 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 @@ -270,15 +277,8 @@ impl TestBuilder { for assertion in &self.assertions { match assertion { TestAssertion::FileExists(path) => { - let path_to_check = if path.starts_with('/') { - PathBuf::from(path) - } else if path.starts_with("~/") { - dirs::home_dir() - .unwrap() - .join(path.strip_prefix("~/").unwrap()) - } else { - cwd.join(path) - }; + let path_to_check = cwd.join(path); + assert!( path_to_check.exists(), "\n\nFailed for: {}\nExpected '{}' to exist.", From 596aa66ac3e855d80b9244a1ffd373dd435dd947 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Fri, 27 Sep 2024 11:56:43 -0400 Subject: [PATCH 14/24] run fmt --- crates/tests/src/test_builder.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/tests/src/test_builder.rs b/crates/tests/src/test_builder.rs index 14215d7..8e4c73f 100644 --- a/crates/tests/src/test_builder.rs +++ b/crates/tests/src/test_builder.rs @@ -118,7 +118,8 @@ impl TestBuilder { if self.temp_dir.is_none() { self.temp_dir = Some(TempDir::new()); let temp_dir_string = self.temp_dir_path().display().to_string(); - self.env_vars.insert("TEMP_DIR".to_string(), temp_dir_string); + self.env_vars + .insert("TEMP_DIR".to_string(), temp_dir_string); } self.temp_dir.as_mut().unwrap() } @@ -191,13 +192,14 @@ impl TestBuilder { pub fn assert_exists(&mut self, path: &str) -> &mut Self { self.ensure_temp_dir(); - let temp_dir = if let Some(temp_dir) = &self.temp_dir { + 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.assertions.push(TestAssertion::FileExists( + path.to_string().replace("$TEMP_DIR", &temp_dir), + )); self } @@ -254,7 +256,11 @@ impl TestBuilder { ); } else if !self.expected_stderr_contains.is_empty() { assert!( - stderr_output.contains(&self.expected_stderr_contains.replace("$TEMP_DIR", &temp_dir)), + 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 @@ -278,7 +284,7 @@ impl TestBuilder { match assertion { TestAssertion::FileExists(path) => { let path_to_check = cwd.join(path); - + assert!( path_to_check.exists(), "\n\nFailed for: {}\nExpected '{}' to exist.", From af545de9ecdae41d36e0c83e00a3c40762da7cd9 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Fri, 27 Sep 2024 12:33:02 -0400 Subject: [PATCH 15/24] Using OpenOptions instead of File for file creation --- crates/shell/src/commands/touch.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/shell/src/commands/touch.rs b/crates/shell/src/commands/touch.rs index 4526229..4384a84 100644 --- a/crates/shell/src/commands/touch.rs +++ b/crates/shell/src/commands/touch.rs @@ -1,6 +1,6 @@ use std::{ ffi::OsString, - fs::{self, File}, + fs::{self, OpenOptions}, path::{Path, PathBuf}, }; @@ -128,7 +128,7 @@ fn execute_touch(context: &mut ShellCommandContext) -> Result<()> { continue; } - File::create(path) + OpenOptions::new().create(true).truncate(false).write(true).open(path) .into_diagnostic() .map_err(|e| miette!("cannot touch {}: {}", path.display(), e))?; From 46166beb61cb061bc5073126d3fb410f5d854b1b Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Fri, 27 Sep 2024 12:33:21 -0400 Subject: [PATCH 16/24] run fmt --- crates/shell/src/commands/touch.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/shell/src/commands/touch.rs b/crates/shell/src/commands/touch.rs index 4384a84..9561966 100644 --- a/crates/shell/src/commands/touch.rs +++ b/crates/shell/src/commands/touch.rs @@ -128,7 +128,11 @@ fn execute_touch(context: &mut ShellCommandContext) -> Result<()> { continue; } - OpenOptions::new().create(true).truncate(false).write(true).open(path) + OpenOptions::new() + .create(true) + .truncate(false) + .write(true) + .open(path) .into_diagnostic() .map_err(|e| miette!("cannot touch {}: {}", path.display(), e))?; From 07609d3028b8971575299ff8e9134c19df08f100 Mon Sep 17 00:00:00 2001 From: Parsa Bahraminejad Date: Sat, 28 Sep 2024 19:07:06 -0400 Subject: [PATCH 17/24] Updated tests --- crates/tests/src/lib.rs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 8efe800..d4be7e6 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -227,9 +227,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; @@ -990,6 +997,7 @@ async fn touch() { .run() .await; + #[cfg(not(windows))] TestBuilder::new() .command("touch $TEMP_DIR/non_existent_dir/non_existent.txt") .assert_stderr_contains("No such file or directory") @@ -997,13 +1005,22 @@ async fn touch() { .run() .await; - // Test with -h option on a symlink + #[cfg(windows)] TestBuilder::new() - .command("touch original.txt && ln -s original.txt symlink.txt && touch -h symlink.txt") - .assert_exists("symlink.txt") + .command("touch $TEMP_DIR/non_existent_dir/non_existent.txt") + .assert_stderr_contains("The system cannot find the path specified") + .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") From a16399b0ffb4e1049a94646e49ae8744ef9704ec Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Sat, 28 Sep 2024 16:54:07 -0400 Subject: [PATCH 18/24] map error to print the same error on both platforms --- crates/shell/src/commands/touch.rs | 13 +++++++++++-- crates/tests/src/lib.rs | 9 --------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/shell/src/commands/touch.rs b/crates/shell/src/commands/touch.rs index 9561966..87c9481 100644 --- a/crates/shell/src/commands/touch.rs +++ b/crates/shell/src/commands/touch.rs @@ -1,6 +1,7 @@ use std::{ ffi::OsString, fs::{self, OpenOptions}, + io, path::{Path, PathBuf}, }; @@ -133,8 +134,16 @@ fn execute_touch(context: &mut ShellCommandContext) -> Result<()> { .truncate(false) .write(true) .open(path) - .into_diagnostic() - .map_err(|e| miette!("cannot touch {}: {}", path.display(), e))?; + .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) { diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index d4be7e6..6b34db7 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -997,7 +997,6 @@ async fn touch() { .run() .await; - #[cfg(not(windows))] TestBuilder::new() .command("touch $TEMP_DIR/non_existent_dir/non_existent.txt") .assert_stderr_contains("No such file or directory") @@ -1005,14 +1004,6 @@ async fn touch() { .run() .await; - #[cfg(windows)] - TestBuilder::new() - .command("touch $TEMP_DIR/non_existent_dir/non_existent.txt") - .assert_stderr_contains("The system cannot find the path specified") - .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() From 40c93374a7ba59c7afebf5980f54c473b99c8c20 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Sat, 28 Sep 2024 20:53:39 -0400 Subject: [PATCH 19/24] Add tmate for debugging --- .github/workflows/rust-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/rust-tests.yml b/.github/workflows/rust-tests.yml index 729109b..cf66557 100644 --- a/.github/workflows/rust-tests.yml +++ b/.github/workflows/rust-tests.yml @@ -28,6 +28,9 @@ jobs: toolchain: stable profile: minimal override: true + + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 - uses: Swatinem/rust-cache@v2 From c19fc12db3d1e228e8f89da7f03a15eb90326b34 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Sat, 28 Sep 2024 23:30:40 -0400 Subject: [PATCH 20/24] Update tmate github action --- .github/workflows/rust-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/rust-tests.yml b/.github/workflows/rust-tests.yml index cf66557..6706715 100644 --- a/.github/workflows/rust-tests.yml +++ b/.github/workflows/rust-tests.yml @@ -31,6 +31,8 @@ jobs: - name: Setup tmate session uses: mxschmitt/action-tmate@v3 + with: + limit-access-to-actor: false - uses: Swatinem/rust-cache@v2 From 8ffad011fae34d75b12cc5decd1a1b474ebdb730 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Sun, 29 Sep 2024 14:32:04 -0400 Subject: [PATCH 21/24] Add temp_dir correctly to test_builder --- .github/workflows/rust-tests.yml | 5 ----- crates/deno_task_shell/Cargo.toml | 2 +- crates/tests/src/test_builder.rs | 7 +++---- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/rust-tests.yml b/.github/workflows/rust-tests.yml index 6706715..729109b 100644 --- a/.github/workflows/rust-tests.yml +++ b/.github/workflows/rust-tests.yml @@ -28,11 +28,6 @@ jobs: toolchain: stable profile: minimal override: true - - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - with: - limit-access-to-actor: false - uses: Swatinem/rust-cache@v2 diff --git a/crates/deno_task_shell/Cargo.toml b/crates/deno_task_shell/Cargo.toml index 8a3f909..655825e 100644 --- a/crates/deno_task_shell/Cargo.toml +++ b/crates/deno_task_shell/Cargo.toml @@ -28,7 +28,7 @@ pest = { git = "https://github.com/pest-parser/pest.git", branch = "master", fea 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" +miette = { version = "7.2.0", features = ["fancy"] } lazy_static = "1.4.0" [dev-dependencies] diff --git a/crates/tests/src/test_builder.rs b/crates/tests/src/test_builder.rs index bf62348..7b21062 100644 --- a/crates/tests/src/test_builder.rs +++ b/crates/tests/src/test_builder.rs @@ -117,9 +117,6 @@ impl TestBuilder { fn get_temp_dir(&mut self) -> &mut TempDir { if self.temp_dir.is_none() { self.temp_dir = Some(TempDir::new()); - let temp_dir_string = self.temp_dir_path().display().to_string(); - self.env_vars - .insert("TEMP_DIR".to_string(), temp_dir_string); } self.temp_dir.as_mut().unwrap() } @@ -238,6 +235,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, @@ -266,8 +264,9 @@ impl TestBuilder { .expected_stderr_contains .replace("$TEMP_DIR", &temp_dir) ), - "\n\nFailed for: {}\nExpected stderr to contain: {}", + "\n\nFailed for: {}\nExpected stderr to contain: {}, {}", self.command, + stderr_output, self.expected_stderr_contains ); } From 30c1fa12d8b5405b6d18d2759934764b85b20773 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Sun, 29 Sep 2024 15:53:10 -0400 Subject: [PATCH 22/24] Use NO_GRAPHICS for miette erros --- crates/tests/src/test_builder.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/tests/src/test_builder.rs b/crates/tests/src/test_builder.rs index 7b21062..e5b63f2 100644 --- a/crates/tests/src/test_builder.rs +++ b/crates/tests/src/test_builder.rs @@ -222,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() From 7e8d7cc0dfdcd717f97d021b70cedc5bac78daa5 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Sun, 29 Sep 2024 15:55:10 -0400 Subject: [PATCH 23/24] Remove extra print in assert --- crates/tests/src/test_builder.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/tests/src/test_builder.rs b/crates/tests/src/test_builder.rs index e5b63f2..985aa0a 100644 --- a/crates/tests/src/test_builder.rs +++ b/crates/tests/src/test_builder.rs @@ -266,9 +266,8 @@ impl TestBuilder { .expected_stderr_contains .replace("$TEMP_DIR", &temp_dir) ), - "\n\nFailed for: {}\nExpected stderr to contain: {}, {}", + "\n\nFailed for: {}\nExpected stderr to contain: {}", self.command, - stderr_output, self.expected_stderr_contains ); } From 6936a2bc22f5df55841997cd8ac653bc43e19e00 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Sun, 29 Sep 2024 17:18:01 -0400 Subject: [PATCH 24/24] Update duplicate in toml file --- Cargo.lock | 1 - crates/shell/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b316a5..b8ab949 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1239,7 +1239,6 @@ dependencies = [ "futures", "miette", "parse_datetime", - "miette", "rustyline", "tokio", "uu_date", diff --git a/crates/shell/Cargo.toml b/crates/shell/Cargo.toml index e54cd22..ee5449c 100644 --- a/crates/shell/Cargo.toml +++ b/crates/shell/Cargo.toml @@ -41,7 +41,6 @@ parse_datetime = "0.6.0" dtparse = "2.0.1" windows-sys = "0.59.0" ctrlc = "3.4.5" -miette = { version="7.2.0", features = ["fancy"] } [package.metadata.release] # Dont publish the binary