use std::io::Write;
use std::path::PathBuf;
use std::process::ExitCode;
use blue_lang_mondou::{Engine, Hit, Query, Reaches};
use blue_lang_pkg::load_path::Environment;
use crate::CliError;
pub fn run(query: &str, paths: &[PathBuf], json: bool) -> Result<ExitCode, CliError> {
let text = query_text(query)?;
let q = Query::parse(&text).map_err(|e| CliError::Pkg(e.to_string()))?;
let files = files(paths);
let mut engine = Engine::with_scopes(Box::new(Environment::from_env()));
let mut out = std::io::stdout().lock();
let (mut hits, mut skipped) = (0usize, 0usize);
for path in &files {
let source = std::fs::read_to_string(path).map_err(|source| CliError::Read {
path: path.display().to_string(),
source,
})?;
let id = path.display().to_string();
engine.set_document(&id, Some(path.clone()), &source);
if let Some(why) = unanswerable(&engine, &id) {
eprintln!("blue query: {id}: skipped, {why}");
skipped += 1;
engine.close_document(&id);
continue;
}
let found = engine
.query(&id, &q)
.map_err(|e| CliError::Pkg(format!("{id}: {e}")))?;
for h in &found {
let line = if json {
h.json(&id).to_string()
} else {
text_line(&id, h, &q)
};
let _ = writeln!(out, "{line}");
}
hits += found.len();
engine.close_document(&id);
}
eprintln!(
"{hits} hit(s) in {} file(s){}",
files.len() - skipped,
if skipped > 0 {
format!(", {skipped} skipped")
} else {
String::new()
}
);
Ok(if skipped == 0 {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
fn query_text(query: &str) -> Result<String, CliError> {
let trimmed = query.trim_start();
if trimmed.starts_with('(') || trimmed.starts_with('`') {
return Ok(query.to_string());
}
let cwd = std::env::current_dir().map_err(|source| CliError::Read {
path: ".".into(),
source,
})?;
let Some(bluefile) = cwd
.ancestors()
.map(|d| d.join(blue_lang_pkg::MANIFEST_FILE))
.find(|f| f.is_file())
else {
return Err(CliError::Pkg(format!(
"`{query}` is not a query (one starts with `(`) and no Bluefile above {} names queries",
cwd.display()
)));
};
let src = std::fs::read_to_string(&bluefile).map_err(|source| CliError::Read {
path: bluefile.display().to_string(),
source,
})?;
let project = blue_lang_pkg::read_bluefile(&src)?.project;
project.queries.get(query).cloned().ok_or_else(|| {
CliError::Pkg(format!(
"{} declares no query `{query}`; it declares: {}",
bluefile.display(),
if project.queries.is_empty() {
"none".to_string()
} else {
project
.queries
.keys()
.cloned()
.collect::<Vec<_>>()
.join(", ")
}
))
})
}
fn files(paths: &[PathBuf]) -> Vec<PathBuf> {
let dot = [PathBuf::from(".")];
let paths = if paths.is_empty() { &dot[..] } else { paths };
let mut out = Vec::new();
for p in paths {
if p.is_dir() {
out.extend(blue_lang_pkg::corpus::files(p));
} else {
out.push(p.clone());
}
}
out.dedup();
out
}
fn unanswerable(engine: &Engine, id: &str) -> Option<String> {
let a = engine.analysis(id)?;
match (a.parsed.as_ref(), &a.checked) {
(Err(e), _) => Some(format!("it does not parse: {e}")),
(_, Some(Err(e))) => Some(format!("its imports do not load: {e}")),
_ => None,
}
}
fn text_line(file: &str, h: &Hit, q: &Query) -> String {
let mut line = format!("{file}:{}:{}: {}", h.line, h.column, h.definition);
if !h.bindings.is_empty() {
let bound: Vec<String> = h
.bindings
.iter()
.map(|(n, v)| format!("{n} = {v}"))
.collect();
line.push_str(" ");
line.push_str(&bound.join(", "));
}
for (cap, via) in &h.reach {
let named = q.reaches().iter().any(|r| match r {
Reaches::Any => true,
Reaches::One(c) => c == cap,
});
if named {
line.push_str(&format!(" {} via {}", cap.label(), via.join(" -> ")));
}
}
line
}