use std::path::{Path, PathBuf};
use anyhow::Result;
use crate::catalog;
use crate::catalog_view::{self, ListStyle};
use crate::cli::{
CatalogCommand, CatalogListArgs, CatalogRelationsArgs, CheckArgs, Cli, Command, DoctorArgs,
InitArgs, SortArgs, WhereArgs,
};
use crate::config::Config;
use crate::doctor::{self, Diagnostic, Filter, Severity};
use crate::format;
use crate::init::{self, InitOptions, InitOutcome};
use crate::lint;
use crate::os_detect::Os;
use crate::path_source::{self, Target};
use crate::report;
use crate::resolve;
use crate::source_match::{self, SourceWarningReason};
use crate::where_cmd::{self, WhereOutcome};
fn read_path_entries(global: &crate::cli::GlobalOpts) -> Vec<String> {
let target: Target = global.target.into();
let path_read = path_source::read_path(target);
if let Some(w) = &path_read.warning {
eprintln!("pathlint: warning: {w}");
}
resolve::split_path(&path_read.value)
}
fn enforce_source_validation(
sources: &std::collections::BTreeMap<String, crate::config::SourceDef>,
os: Os,
) -> Result<()> {
let warnings = source_match::validate_sources(sources, os);
if warnings.is_empty() {
return Ok(());
}
for w in &warnings {
eprintln!(
"pathlint: error: source `{name}` rejected — {reason} ({needle:?})",
name = w.name,
needle = w.needle,
reason = match w.reason {
SourceWarningReason::RootPath => "path expands to filesystem root",
SourceWarningReason::NeedleTooShort => "expanded path is too short to match safely",
},
);
}
anyhow::bail!("{} unsafe source definition(s); aborting", warnings.len());
}
pub fn execute(cli: Cli) -> Result<u8> {
let check_args = match cli.command {
Some(Command::Init(args)) => return execute_init(&args),
Some(Command::Catalog {
action: CatalogCommand::List(args),
}) => return execute_catalog_list(&args, cli.global.rules.as_deref()),
Some(Command::Catalog {
action: CatalogCommand::Relations(args),
}) => return execute_catalog_relations(&args, cli.global.rules.as_deref()),
Some(Command::Doctor(args)) => return execute_doctor(&args, &cli.global),
Some(Command::Where(args)) => return execute_where(&args, &cli.global),
Some(Command::Sort(args)) => return execute_sort(&args, &cli.global),
Some(Command::Check(args)) => args,
None => CheckArgs::default(),
};
let rules_path = locate_rules(cli.global.rules.as_deref())?;
let cfg = match rules_path.as_ref() {
Some(p) => Config::from_path(p)?,
None => Config::default(),
};
if let Err(msg) = catalog::version_check(cfg.require_catalog, catalog::embedded_version()) {
eprintln!("pathlint: {msg}");
return Ok(2);
}
let catalog = catalog::merge_with_user(&cfg.source);
let os = Os::current();
enforce_source_validation(&catalog, os)?;
let path_entries = read_path_entries(&cli.global);
if cli.global.verbose {
if let Some(p) = &rules_path {
eprintln!("pathlint: rules = {}", p.display());
} else {
eprintln!("pathlint: rules = <none — running with empty config>");
}
eprintln!("pathlint: PATH entries ({}):", path_entries.len());
for entry in &path_entries {
eprintln!(" {entry}");
}
}
let outcomes = lint::evaluate(
&cfg.expectations,
&catalog,
os,
|cmd| resolve::resolve(cmd, &path_entries),
lint::check_shape_filesystem,
);
if check_args.json {
let json = format::check_json(&outcomes)?;
println!("{json}");
} else {
let style = report::Style {
no_glyphs: cli.global.no_glyphs,
verbose: cli.global.verbose,
quiet: cli.global.quiet,
explain: check_args.explain,
};
print!("{}", report::render(&outcomes, style));
}
Ok(lint::exit_code(&outcomes))
}
fn execute_doctor(args: &DoctorArgs, global: &crate::cli::GlobalOpts) -> Result<u8> {
let filter = Filter {
include: args.include.clone(),
exclude: args.exclude.clone(),
};
if let Err(msg) = doctor::validate_filter_names(&filter) {
anyhow::bail!(msg);
}
let rules_path = locate_rules(global.rules.as_deref())?;
let cfg = match rules_path.as_ref() {
Some(p) => Config::from_path(p)?,
None => Config::default(),
};
let merged_for_validation = catalog::merge_with_user(&cfg.source);
enforce_source_validation(&merged_for_validation, Os::current())?;
let entries = read_path_entries(global);
let diags = doctor::analyze_real(&entries, Os::current());
let kept = filter.apply(&diags);
if args.json {
let json = format::doctor_json(&kept)?;
println!("{json}");
} else {
let printable: Vec<&Diagnostic> = if global.quiet {
kept.iter()
.copied()
.filter(|d| d.severity == Severity::Error)
.collect()
} else {
kept.clone()
};
for d in &printable {
println!("{}", format::doctor_line(d, &entries));
}
}
Ok(if doctor::has_error(&kept) { 1 } else { 0 })
}
fn execute_catalog_list(args: &CatalogListArgs, explicit_rules: Option<&Path>) -> Result<u8> {
let cfg = match locate_rules(explicit_rules)? {
Some(p) => Config::from_path(&p)?,
None => Config::default(),
};
let merged = catalog::merge_with_user(&cfg.source);
let style = ListStyle {
all_os: args.all,
names_only: args.names_only,
};
if !args.names_only {
println!("# catalog_version = {}", catalog::embedded_version());
}
print!("{}", catalog_view::render(&merged, Os::current(), style));
Ok(0)
}
fn execute_catalog_relations(
args: &CatalogRelationsArgs,
explicit_rules: Option<&Path>,
) -> Result<u8> {
let cfg = match locate_rules(explicit_rules)? {
Some(p) => Config::from_path(&p)?,
None => Config::default(),
};
let relations = catalog::merge_with_user_relations(&cfg.relations);
if let Err(msg) = catalog::check_acyclic(&relations) {
eprintln!("pathlint: {msg}");
return Ok(2);
}
if args.json {
let json = format::relations_json(&relations)?;
println!("{json}");
} else {
println!("{}", format::relations_human(&relations));
}
Ok(0)
}
fn execute_where(args: &WhereArgs, global: &crate::cli::GlobalOpts) -> Result<u8> {
let rules_path = locate_rules(global.rules.as_deref())?;
let cfg = match rules_path.as_ref() {
Some(p) => Config::from_path(p)?,
None => Config::default(),
};
let merged = catalog::merge_with_user(&cfg.source);
enforce_source_validation(&merged, Os::current())?;
let relations = catalog::merge_with_user_relations(&cfg.relations);
let path_entries = read_path_entries(global);
let outcome = where_cmd::locate(&args.command, &merged, &relations, Os::current(), |cmd| {
resolve::resolve(cmd, &path_entries)
});
if args.json {
let json = format::where_json(&args.command, &outcome)?;
println!("{json}");
return Ok(match outcome {
WhereOutcome::NotFound => 1,
WhereOutcome::Found(_) => 0,
});
}
match outcome {
WhereOutcome::NotFound => {
println!("{}", format::where_not_found(&args.command));
Ok(1)
}
WhereOutcome::Found(found) => {
println!("{}", format::where_human(&found));
Ok(0)
}
}
}
fn execute_sort(args: &SortArgs, global: &crate::cli::GlobalOpts) -> Result<u8> {
let rules_path = locate_rules(global.rules.as_deref())?;
let cfg = match rules_path.as_ref() {
Some(p) => crate::config::Config::from_path(p)?,
None => crate::config::Config::default(),
};
let catalog = catalog::merge_with_user(&cfg.source);
enforce_source_validation(&catalog, Os::current())?;
let path_entries = read_path_entries(global);
let relations = catalog::merge_with_user_relations(&cfg.relations);
let plan = crate::sort::sort_path(
&path_entries,
&cfg.expectations,
&catalog,
&relations,
Os::current(),
);
if args.json {
let json = format::sort_json(&plan)?;
println!("{json}");
} else {
println!("{}", format::sort_human(&plan));
}
Ok(0)
}
fn execute_init(args: &InitArgs) -> Result<u8> {
let cwd = std::env::current_dir()?;
let opts = InitOptions {
emit_defaults: args.emit_defaults,
force: args.force,
};
match init::run(&cwd, &opts, Os::current())? {
InitOutcome::Wrote(p) => {
println!("pathlint: wrote {}", p.display());
Ok(0)
}
InitOutcome::AlreadyExists(p) => {
eprintln!(
"pathlint: {} already exists; pass --force to overwrite",
p.display()
);
Ok(1)
}
}
}
fn locate_rules(explicit: Option<&Path>) -> Result<Option<PathBuf>> {
if let Some(p) = explicit {
if !p.is_file() {
anyhow::bail!("--rules path not found: {}", p.display());
}
return Ok(Some(p.to_path_buf()));
}
let local = PathBuf::from("pathlint.toml");
if local.is_file() {
return Ok(Some(local));
}
if let Some(xdg) = xdg_config_path() {
if xdg.is_file() {
return Ok(Some(xdg));
}
}
Ok(None)
}
fn xdg_config_path() -> Option<PathBuf> {
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
return Some(PathBuf::from(xdg).join("pathlint").join("pathlint.toml"));
}
if let Ok(home) = std::env::var("HOME") {
return Some(
PathBuf::from(home)
.join(".config")
.join("pathlint")
.join("pathlint.toml"),
);
}
if let Ok(profile) = std::env::var("USERPROFILE") {
return Some(
PathBuf::from(profile)
.join(".config")
.join("pathlint")
.join("pathlint.toml"),
);
}
None
}