Skip to main content

rmr/
main.rs

1//! Remove files or directories passed as CLI args (recursive for dirs).
2//!
3//! Strips trailing `:...` suffix from paths before deletion.
4//!
5//! # Errors
6//! - I/O errors from file/directory removal.
7
8use std::fs::Metadata;
9use std::path::Path;
10
11use owo_colors::OwoColorize;
12use rootcause::bail;
13use rootcause::report;
14use ytil_sys::cli::Args;
15
16/// Remove files or directories passed as CLI args (recursive for dirs).
17fn main() {
18    let files = ytil_sys::cli::get();
19
20    if files.has_help() {
21        println!("{}", include_str!("../help.txt"));
22        return;
23    }
24
25    if files.is_empty() {
26        println!("Nothing done");
27    }
28
29    let mut any_errors = false;
30    for file in &files {
31        if process(file).is_err() {
32            any_errors = true;
33        }
34    }
35
36    if any_errors {
37        std::process::exit(1);
38    }
39}
40
41/// Deletes one path after stripping the first ':' suffix segment.
42///
43/// # Errors
44/// - Metadata retrieval or deletion fails.
45fn process(file: &str) -> rootcause::Result<()> {
46    let trimmed = before_first_colon(file);
47    let path = Path::new(&trimmed);
48
49    let metadata: Metadata = path.symlink_metadata().map_err(|error| {
50        eprintln!(
51            "Cannot read metadata of path={} error={}",
52            path.display(),
53            format!("{error:?}").red()
54        );
55        report!(error)
56    })?;
57
58    let ft = metadata.file_type();
59    if ft.is_file() || ft.is_symlink() {
60        std::fs::remove_file(path).inspect_err(|err| {
61            eprintln!(
62                "Cannot delete file={} error={}",
63                path.display(),
64                format!("{err:?}").red()
65            );
66        })?;
67        return Ok(());
68    }
69    if ft.is_dir() {
70        std::fs::remove_dir_all(path).inspect_err(|err| {
71            eprintln!(
72                "Cannot delete dir={} error={}",
73                path.display(),
74                format!("{err:?}").red()
75            );
76        })?;
77        return Ok(());
78    }
79    bail!("{}", format!("Not found path={}", path.display()).red())
80}
81
82/// Strips suffix beginning at first ':'.
83fn before_first_colon(s: &str) -> &str {
84    s.split_once(':').map_or(s, |(before, _)| before)
85}
86
87#[cfg(test)]
88mod tests {
89    use rstest::rstest;
90
91    use super::*;
92
93    #[rstest]
94    #[case::no_colon("hello", "hello")]
95    #[case::single_colon_at_end("alpha:", "alpha")]
96    #[case::multiple_colons("one:two:three", "one")]
97    #[case::colon_at_start(":rest", "")]
98    #[case::only_colon(":", "")]
99    #[case::empty_string("", "")]
100    #[case::unicode_characters("\u{03C0}:\u{03C4}:\u{03C9}", "\u{03C0}")]
101    fn test_before_first_colon_when_various_inputs_strips_after_first_colon_returns_expected(
102        #[case] input: &str,
103        #[case] expected: &str,
104    ) {
105        pretty_assertions::assert_eq!(before_first_colon(input), expected);
106    }
107}