diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/main.rs | 120 |
1 files changed, 97 insertions, 23 deletions
diff --git a/src/main.rs b/src/main.rs index a7c9df7..3912bff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use core::fmt; use std::{ fs::{File, Metadata}, io::{BufRead, BufReader}, + str::FromStr, time::{Duration, SystemTime}, }; @@ -11,6 +12,8 @@ use getopts::Options; struct Context { root: Utf8PathBuf, quiet: bool, + format: Format, + first_line: bool, ignores: Vec<String>, } @@ -19,11 +22,13 @@ impl Context { Self { root, quiet: false, + format: Format::Csv, + first_line: true, ignores: vec![], } } - /// Reads the ignore file. Errors are fail and exit the process. + /// Reads the ignore file. Errors are fatal and exit the process. pub fn ignore_file(&mut self, ignore_path: Utf8PathBuf) { let mut bufread = match File::open(&ignore_path) { Ok(f) => BufReader::new(f), @@ -56,6 +61,17 @@ impl Context { } } + /// Parses the provided string as a foramt or fails, exiting the process. + pub fn format(&mut self, raw: &str) { + match raw.parse() { + Ok(fmt) => self.format = fmt, + Err(()) => { + eprintln!("failed to parse '{raw}' as an output format"); + std::process::exit(1); + } + } + } + pub fn is_file_ignored<P: AsRef<Utf8Path>>(&self, path: P) -> bool { match path.as_ref().strip_prefix(&self.root) { Ok(rel) => { @@ -78,6 +94,24 @@ impl Context { } } +#[derive(Debug, PartialEq, Eq)] +pub enum Format { + Csv, + Json, +} + +impl FromStr for Format { + type Err = (); + + fn from_str(raw: &str) -> Result<Self, Self::Err> { + match raw.trim() { + "csv" | "CSV" => Ok(Self::Csv), + "json" | "JSON" => Ok(Self::Json), + _ => Err(()), + } + } +} + fn print_usage(opts: &Options) { let breif = "Usage: whenwasit [options] PATH"; @@ -90,6 +124,8 @@ fn main() { opts.optopt("", "ignore", "file of paths to ignore, one per line", "PATH"); opts.optflag("h", "help", "print this help thing"); opts.optflag("q", "quiet", "don't print errors to STDERR"); + #[rustfmt::skip] + opts.optopt("f", "format", "output format. defaults to csv.\nformats: [csv, json]", "FORMAT"); let matches = match opts.parse(std::env::args()) { Ok(m) => m, @@ -113,22 +149,34 @@ fn main() { Some(p) => Utf8PathBuf::from(p), }; - let mut context = Context::new(root); + let mut context = Context::new(root.clone()); context.quiet = matches.opt_present("quiet"); + if let Some(fmt) = matches.opt_str("format") { + context.format(&fmt); + } + if let Some(ignore) = matches.opt_str("ignore") { let ignore = Utf8PathBuf::from(ignore); context.ignore_file(ignore); } - process(&context, &context.root) + if context.format == Format::Json { + print!("{{") + } + + process(&mut context, &root); + + if context.format == Format::Json { + print!("}}\n") + } } /// Loop through the provided directory printing CSV rows. The `root_path` is /// used to make printed paths relative to it. /// /// Does two passes: First pass prints files. Second pass recurs, printing directories. -fn process(ctx: &Context, path: &Utf8Path) { +fn process(ctx: &mut Context, path: &Utf8Path) { match std::fs::metadata(&path) { Err(e) => { ctx.print_err(format!("{path}: {e}")); @@ -136,7 +184,7 @@ fn process(ctx: &Context, path: &Utf8Path) { } Ok(meta) => { let this_times = Times::metadata(&meta); - row(&ctx.root, &path, &this_times); + row(ctx, &path, &this_times); } } @@ -171,7 +219,7 @@ fn process(ctx: &Context, path: &Utf8Path) { } let times = Times::metadata(&meta); - row(&ctx.root, entry_path, ×) + row(ctx, entry_path, ×) } }; } @@ -212,37 +260,63 @@ fn process(ctx: &Context, path: &Utf8Path) { } } -fn row<P: AsRef<Utf8Path>>(root: &Utf8Path, path: P, times: &Times) { +fn row<P: AsRef<Utf8Path>>(ctx: &mut Context, path: P, times: &Times) { let relative = path .as_ref() - .strip_prefix(root) + .strip_prefix(&ctx.root) .expect("row wasn't relative to the root; what happened?"); fn time_string(time: Option<Duration>) -> String { time.map(|d| d.as_secs().to_string()).unwrap_or_default() } - println!( - "{},{},{},{}", - escape(relative), - time_string(times.created()), - time_string(times.modified()), - time_string(times.accessed()) - ); + match ctx.format { + Format::Csv => { + println!( + "{},{},{},{}", + escape_csv(relative), + time_string(times.created()), + time_string(times.modified()), + time_string(times.accessed()) + ); + } + Format::Json => { + if !ctx.first_line { + print!(","); + } else { + ctx.first_line = false; + } + + print!( + r#"{{"path": "{}", "btime":{}, "mtime":{}, "atime":{}}}"#, + escape_json(relative), + time_string(times.created()), + time_string(times.modified()), + time_string(times.accessed()) + ); + } + } +} + +fn escape_csv<S: AsRef<str>>(raw: S) -> String { + escape(raw, ',') } -/// Blackslash-escape comma and backslash -fn escape<S: AsRef<str>>(raw: S) -> String { +fn escape_json<S: AsRef<str>>(raw: S) -> String { + escape(raw, '"') +} + +/// Blackslash-escape one char or the backslash itself +fn escape<S: AsRef<str>>(raw: S, esc: char) -> String { let raw = raw.as_ref(); let mut escaped = String::with_capacity(raw.len()); for c in raw.chars() { - match c { - ',' | '\\' => { - escaped.push('\\'); - escaped.push(c); - } - _ => escaped.push(c), + if c == '\\' || c == esc { + escaped.push('\\'); + escaped.push(c); + } else { + escaped.push(c); } } |