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/// Deletes one path after stripping the first ':' suffix segment.
17///
18/// # Errors
19/// - Metadata retrieval or deletion fails.
20fn process(file: &str) -> rootcause::Result<()> {
21    let trimmed = before_first_colon(file);
22    let path = Path::new(&trimmed);
23
24    let metadata: Metadata = path.symlink_metadata().map_err(|error| {
25        eprintln!(
26            "Cannot read metadata of path={} error={}",
27            path.display(),
28            format!("{error:?}").red()
29        );
30        report!(error)
31    })?;
32
33    let ft = metadata.file_type();
34    if ft.is_file() || ft.is_symlink() {
35        std::fs::remove_file(path).inspect_err(|err| {
36            eprintln!(
37                "Cannot delete file={} error={}",
38                path.display(),
39                format!("{err:?}").red()
40            );
41        })?;
42        return Ok(());
43    }
44    if ft.is_dir() {
45        std::fs::remove_dir_all(path).inspect_err(|err| {
46            eprintln!(
47                "Cannot delete dir={} error={}",
48                path.display(),
49                format!("{err:?}").red()
50            );
51        })?;
52        return Ok(());
53    }
54    bail!("{}", format!("Not found path={}", path.display()).red())
55}
56
57/// Strips suffix beginning at first ':'.
58fn before_first_colon(s: &str) -> &str {
59    s.split_once(':').map_or(s, |(before, _)| before)
60}
61
62/// Remove files or directories passed as CLI args (recursive for dirs).
63fn main() {
64    let files = ytil_sys::cli::get();
65
66    if files.has_help() {
67        println!("{}", include_str!("../help.txt"));
68        return;
69    }
70
71    if files.is_empty() {
72        println!("Nothing done");
73    }
74
75    let mut any_errors = false;
76    for file in &files {
77        if process(file).is_err() {
78            any_errors = true;
79        }
80    }
81
82    if any_errors {
83        std::process::exit(1);
84    }
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 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}