use crate::utils::project;
use anyhow::{Context, Result};
use std::process::{Command, Stdio};
pub fn run(patterns: &[String]) -> Result<()> {
let project_type = project::detect()?.context("No uv/poetry/cargo project found")?;
match project_type {
project::ProjectType::Uv => {
show_uv(patterns)?;
}
project::ProjectType::Poetry => {
show_poetry(patterns)?;
}
project::ProjectType::Cargo => {
show_cargo(patterns)?;
}
}
Ok(())
}
fn show_uv(patterns: &[String]) -> Result<()> {
let mut cmd = Command::new("uv");
cmd.args(["pip", "list"]);
if patterns.is_empty() {
let status = cmd.status().context("Failed to run uv pip list")?;
std::process::exit(status.code().unwrap_or(1));
} else {
let output = cmd
.stdout(Stdio::piped())
.output()
.context("Failed to run uv pip list")?;
if !output.status.success() {
anyhow::bail!("uv pip list failed");
}
let stdout = String::from_utf8(output.stdout)?;
let pattern = patterns.join("|");
for line in stdout.lines() {
if regex_match_case_insensitive(line, &pattern) {
println!("{}", line);
}
}
}
Ok(())
}
fn show_poetry(patterns: &[String]) -> Result<()> {
let mut cmd = Command::new("poetry");
cmd.arg("show");
if patterns.is_empty() {
let status = cmd.status().context("Failed to run poetry show")?;
std::process::exit(status.code().unwrap_or(1));
} else {
let output = cmd
.stdout(Stdio::piped())
.output()
.context("Failed to run poetry show")?;
if !output.status.success() {
anyhow::bail!("poetry show failed");
}
let stdout = String::from_utf8(output.stdout)?;
let pattern = patterns.join("|");
for line in stdout.lines() {
if regex_match_case_insensitive(line, &pattern) {
println!("{}", line);
}
}
}
Ok(())
}
fn show_cargo(patterns: &[String]) -> Result<()> {
let mut cmd = Command::new("cargo");
cmd.args(["tree"]);
if patterns.is_empty() {
let status = cmd.status().context("Failed to run cargo tree")?;
std::process::exit(status.code().unwrap_or(1));
} else {
let output = cmd
.stdout(Stdio::piped())
.output()
.context("Failed to run cargo tree")?;
if !output.status.success() {
anyhow::bail!("cargo tree failed");
}
let stdout = String::from_utf8(output.stdout)?;
let pattern = patterns.join("|");
for line in stdout.lines() {
if regex_match_case_insensitive(line, &pattern) {
println!("{}", line);
}
}
}
Ok(())
}
fn regex_match_case_insensitive(text: &str, pattern: &str) -> bool {
use regex::Regex;
let escaped = regex::escape(pattern);
match Regex::new(&format!("(?i){}", escaped)) {
Ok(re) => re.is_match(text),
Err(_) => text.to_lowercase().contains(&pattern.to_lowercase()),
}
}
#[cfg(test)]
fn join_patterns(patterns: &[String]) -> String {
patterns.join("|")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_regex_match_case_insensitive_exact() {
assert!(regex_match_case_insensitive("clap", "clap"));
assert!(regex_match_case_insensitive("CLAP", "clap"));
assert!(regex_match_case_insensitive("clap", "CLAP"));
}
#[test]
fn test_regex_match_case_insensitive_partial() {
assert!(regex_match_case_insensitive("clap-derive", "clap"));
assert!(regex_match_case_insensitive("anyhow", "any"));
assert!(regex_match_case_insensitive("regex", "reg"));
}
#[test]
fn test_regex_match_case_insensitive_no_match() {
assert!(!regex_match_case_insensitive("clap", "nonexistent"));
assert!(!regex_match_case_insensitive("anyhow", "clap"));
}
#[test]
fn test_regex_match_case_insensitive_special_chars() {
assert!(regex_match_case_insensitive("test.package", "test.package"));
assert!(regex_match_case_insensitive("test+package", "test+package"));
assert!(regex_match_case_insensitive("test*package", "test*package"));
}
#[test]
fn test_regex_match_case_insensitive_multiple_patterns() {
let text = "clap v4.5.54";
assert!(regex_match_case_insensitive(text, "clap"));
assert!(regex_match_case_insensitive(text, "v4"));
assert!(!regex_match_case_insensitive(text, "nonexistent"));
}
#[test]
fn test_regex_match_case_insensitive_empty_pattern() {
assert!(regex_match_case_insensitive("anything", ""));
assert!(regex_match_case_insensitive("", ""));
}
#[test]
fn test_regex_match_case_insensitive_invalid_regex_fallback() {
let text = "test package";
assert!(regex_match_case_insensitive(text, "test"));
assert!(regex_match_case_insensitive(text, "package"));
}
#[test]
fn test_join_patterns_single() {
let patterns = vec!["clap".to_string()];
assert_eq!(join_patterns(&patterns), "clap");
}
#[test]
fn test_join_patterns_multiple() {
let patterns = vec!["clap".to_string(), "anyhow".to_string()];
assert_eq!(join_patterns(&patterns), "clap|anyhow");
}
#[test]
fn test_join_patterns_empty() {
let patterns = vec![];
assert_eq!(join_patterns(&patterns), "");
}
#[test]
fn test_join_patterns_three() {
let patterns = vec![
"clap".to_string(),
"anyhow".to_string(),
"regex".to_string(),
];
assert_eq!(join_patterns(&patterns), "clap|anyhow|regex");
}
#[test]
fn test_regex_match_with_joined_patterns() {
let patterns = vec!["clap".to_string(), "anyhow".to_string()];
let pattern = join_patterns(&patterns);
assert!(regex_match_case_insensitive("clap v4.5", "clap"));
assert!(regex_match_case_insensitive("anyhow v1.0", "anyhow"));
assert!(!regex_match_case_insensitive("regex v1.0", "clap"));
assert!(pattern.contains("clap"));
assert!(pattern.contains("anyhow"));
}
#[test]
fn test_regex_match_case_insensitive_newlines() {
assert!(regex_match_case_insensitive("clap", "clap"));
}
#[test]
fn test_regex_match_with_pipe_separator() {
let patterns = vec!["clap".to_string(), "anyhow".to_string()];
let pattern = join_patterns(&patterns);
assert_eq!(pattern, "clap|anyhow");
assert!(regex_match_case_insensitive("clap v4.5", "clap"));
assert!(regex_match_case_insensitive("anyhow v1.0", "anyhow"));
}
#[test]
fn test_regex_match_multiple_patterns_in_text() {
let text = "clap v4.5.54 anyhow v1.0";
assert!(regex_match_case_insensitive(text, "clap"));
assert!(regex_match_case_insensitive(text, "anyhow"));
assert!(regex_match_case_insensitive(text, "v4"));
}
#[test]
fn test_regex_match_empty_string() {
assert!(regex_match_case_insensitive("", ""));
assert!(regex_match_case_insensitive("test", ""));
}
#[test]
fn test_regex_match_numbers() {
assert!(regex_match_case_insensitive("clap v4.5.54", "4.5"));
assert!(regex_match_case_insensitive("anyhow 1.0.0", "1.0"));
}
#[test]
fn test_regex_match_hyphens() {
assert!(regex_match_case_insensitive(
"test-package v1.0",
"test-package"
));
assert!(regex_match_case_insensitive("test-package v1.0", "test"));
assert!(regex_match_case_insensitive("test-package v1.0", "package"));
}
}