Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ strip = "none"

[workspace.dependencies]
owo-colors = { version = "4.3.0", features = ["supports-colors"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ path = "src/main.rs"
[dependencies]
clap = { version = "4.6.0", features = ["derive"] }
owo-colors.workspace = true
serde_json.workspace = true
unrot_core = { version = "0.1.1", path = "../core" }
114 changes: 102 additions & 12 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clap::{Parser, Subcommand};
use clap::{Parser, Subcommand, ValueEnum};
use owo_colors::{OwoColorize, Stream};
use std::{io::IsTerminal, path::PathBuf};
use unrot_core::{
Expand All @@ -16,8 +16,18 @@ fn main() {
None => cli.no_color,
};

// Configure colors: --no-color, NO_COLOR env, or piped stdout => plain
if no_color || std::env::var("NO_COLOR").is_ok() || !std::io::stdout().is_terminal() {
let format = match &cli.subcommand {
Some(Sub::Scan(s)) => s.format.unwrap_or(OutputFormat::Human),
Some(Sub::Fix(_)) => OutputFormat::Human,
Some(Sub::List(l)) => l.format.unwrap_or(OutputFormat::Human),
None => cli.format.unwrap_or(OutputFormat::Human),
};

let is_json = format == OutputFormat::Json;

// JSON implies no color; also --no-color, NO_COLOR env, or piped stdout => plain
if is_json || no_color || std::env::var("NO_COLOR").is_ok() || !std::io::stdout().is_terminal()
{
owo_colors::set_override(false);
} else {
owo_colors::unset_override();
Expand Down Expand Up @@ -60,18 +70,28 @@ fn main() {

match mode {
Mode::List => {
for link in &broken {
println!(
"{}",
link.link
.display()
.if_supports_color(Stream::Stdout, |v| v.red())
);
if is_json {
let paths: Vec<&std::path::Path> =
broken.iter().map(|b| b.link.as_path()).collect();
let wrapper = serde_json::json!({ "broken_symlinks": paths });
println!("{}", serde_json::to_string_pretty(&wrapper).unwrap());
} else {
for link in &broken {
println!(
"{}",
link.link
.display()
.if_supports_color(Stream::Stdout, |v| v.red())
);
}
}
return;
}
Mode::Scan => {
if broken.is_empty() {
if is_json {
let wrapper = serde_json::json!({ "broken_symlinks": &broken });
println!("{}", serde_json::to_string_pretty(&wrapper).unwrap());
} else if broken.is_empty() {
println!(
"{}",
"no broken symlinks found".if_supports_color(Stream::Stdout, |v| v.green())
Expand All @@ -88,7 +108,12 @@ fn main() {
}
return;
}
Mode::Fix => {}
Mode::Fix => {
if is_json {
eprintln!("--format json is not supported in fix mode");
std::process::exit(1);
}
}
}

let cases: Vec<RepairCase> = broken
Expand Down Expand Up @@ -133,6 +158,12 @@ fn resolve_path(path: &std::path::Path) -> PathBuf {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum OutputFormat {
Human,
Json,
}

enum Mode {
Scan,
Fix,
Expand Down Expand Up @@ -174,6 +205,10 @@ struct Cli {
/// Disable colored output
#[arg(long)]
no_color: bool,

/// Output format (json only valid for scan/list)
#[arg(long, value_enum)]
format: Option<OutputFormat>,
}

#[derive(Subcommand)]
Expand All @@ -200,6 +235,10 @@ struct ScanArgs {
/// Disable colored output
#[arg(long)]
no_color: bool,

/// Output format
#[arg(long, value_enum)]
format: Option<OutputFormat>,
}

#[derive(clap::Args)]
Expand Down Expand Up @@ -240,6 +279,10 @@ struct ListArgs {
/// Disable colored output
#[arg(long)]
no_color: bool,

/// Output format
#[arg(long, value_enum)]
format: Option<OutputFormat>,
}

#[cfg(test)]
Expand Down Expand Up @@ -366,4 +409,51 @@ mod tests {
_ => panic!("expected List subcommand"),
}
}

#[test]
fn unrot_scan_format_json() {
let cli = parse(&["scan", "/tmp", "--format", "json"]).unwrap();
match &cli.subcommand {
Some(Sub::Scan(s)) => assert_eq!(s.format, Some(OutputFormat::Json)),
_ => panic!("expected Scan subcommand"),
}
}

#[test]
fn unrot_scan_format_human() {
let cli = parse(&["scan", "/tmp", "--format", "human"]).unwrap();
match &cli.subcommand {
Some(Sub::Scan(s)) => assert_eq!(s.format, Some(OutputFormat::Human)),
_ => panic!("expected Scan subcommand"),
}
}

#[test]
fn unrot_scan_format_default_is_none() {
let cli = parse(&["scan", "/tmp"]).unwrap();
match &cli.subcommand {
Some(Sub::Scan(s)) => assert_eq!(s.format, None),
_ => panic!("expected Scan subcommand"),
}
}

#[test]
fn unrot_list_format_json() {
let cli = parse(&["list", "/tmp", "--format", "json"]).unwrap();
match &cli.subcommand {
Some(Sub::List(l)) => assert_eq!(l.format, Some(OutputFormat::Json)),
_ => panic!("expected List subcommand"),
}
}

#[test]
fn unrot_default_format_json() {
let cli = parse(&["--format", "json"]).unwrap();
assert_eq!(cli.format, Some(OutputFormat::Json));
}

#[test]
fn unrot_format_invalid_rejected() {
assert!(parse(&["scan", "--format", "xml"]).is_err());
}
}
1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ categories = ["command-line-utilities", "filesystem"]
[dependencies]
walkdir = "2.5.0"
owo-colors.workspace = true
serde.workspace = true

[dev-dependencies]
tempfile = "3"
3 changes: 2 additions & 1 deletion core/src/fuzzy.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use serde::Serialize;
use std::path::{Component, Path, PathBuf};
use walkdir::WalkDir;

Expand Down Expand Up @@ -151,7 +152,7 @@ pub fn find_candidates(
candidates
}

#[derive(Debug)]
#[derive(Debug, Serialize)]
pub struct ScoredCandidate {
pub path: PathBuf,
pub score: f64,
Expand Down
2 changes: 2 additions & 0 deletions core/src/resolver/model.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use serde::Serialize;
use std::{fmt, path::PathBuf};

use crate::fuzzy::ScoredCandidate;
Expand Down Expand Up @@ -40,6 +41,7 @@ impl fmt::Display for Summary {
}
}

#[derive(Serialize)]
pub struct RepairCase {
pub link: PathBuf,
pub original_target: PathBuf,
Expand Down
2 changes: 2 additions & 0 deletions core/src/scanner.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use owo_colors::{OwoColorize, Stream};
use serde::Serialize;
use std::{
fmt, fs,
path::{Path, PathBuf},
Expand Down Expand Up @@ -54,6 +55,7 @@ pub fn find_broken_symlinks(path: &Path, ignore: &[String]) -> Vec<BrokenSymlink
broken_symlinks
}

#[derive(Serialize)]
pub struct BrokenSymlink {
pub link: PathBuf,
pub target: PathBuf,
Expand Down
Loading