about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2023-08-25 22:31:16 -0500
committergennyble <gen@nyble.dev>2023-08-25 22:31:16 -0500
commitbf50edc9462d5144d989124b5317bbe2481f12e1 (patch)
tree4f69d37a715b0277245af0c6e15feed44433d4fd
parent8ad14b138f5f8f5ad37592aba172594eb106ee76 (diff)
downloadwhenwasit-bf50edc9462d5144d989124b5317bbe2481f12e1.tar.gz
whenwasit-bf50edc9462d5144d989124b5317bbe2481f12e1.zip
add json output
-rw-r--r--.whenwasit8
-rw-r--r--src/main.rs120
2 files changed, 101 insertions, 27 deletions
diff --git a/.whenwasit b/.whenwasit
index 1877fb3..b2e7823 100644
--- a/.whenwasit
+++ b/.whenwasit
@@ -2,9 +2,9 @@
 Cargo.toml,1692916976,1693007199,1693007319
 .whenwasit-ignore,1692930937,1692937439,1693004271
 Cargo.lock,1692916991,1692927476,1692931525
-readme.md,1692917240,1693017166,1693017167
-.whenwasit,1692931791,1693017222,1693007319
-.gitignore,1692916976,1693017172,1693017172
+readme.md,1692917240,1693017166,1693017222
+.whenwasit,1692931791,1693020676,1693017222
+.gitignore,1692916976,1693017172,1693017222
 .rustfmt.toml,1692917231,1692917231,1692919305
 pre-commit,1692931609,1692931684,1692931791
 .github,1693005489,1693005489,1693005673
@@ -13,4 +13,4 @@ pre-commit,1692931609,1692931684,1692931791
 .vscode,1693017160,1693017160,1693017160
 .vscode/settings.json,1693017160,1693017166,1693017166
 src,1692916976,1692916976,1692917221
-src/main.rs,1692916976,1693004226,1693004271
+src/main.rs,1692916976,1693020614,1693020618
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, &times)
+				row(ctx, entry_path, &times)
 			}
 		};
 	}
@@ -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);
 		}
 	}