use std::io::{self, Write};
use std::path::PathBuf;
use std::time::Instant;
use crate::index::Index;
use crate::path_util::path_bytes;
use crate::{Config, IndexError};
use super::post_filter::apply_post_filters;
use super::render::build_effective_pattern;
pub(super) use super::scope::collect_scoped_paths;
use super::scope::{explicit_path_specs, search_options, sort_and_dedup_matches};
pub(super) use super::search_args::SearchArgs;
pub(super) fn cmd_search(config: Config, args: &SearchArgs) -> i32 {
if let Err((spec, msg)) = super::scope::validate_globs(&args.globs) {
eprintln!("st: invalid glob '{spec}': {msg}");
return 2;
}
let index = match Index::open(config.clone()) {
Ok(idx) => idx,
Err(IndexError::IndexNotFound(dir)) => {
return super::fallback::handle_missing_index(&config, args, &dir);
}
Err(e) => {
eprintln!("st: {e}");
return 2;
}
};
let needs_async_catchup = super::catchup::run_bounded_auto_update(&index, &config, args.quiet);
let exit_code = run_and_render(&index, &config, args);
if needs_async_catchup {
super::catchup::maybe_spawn_async_catchup(&config);
}
exit_code
}
fn run_and_render(index: &Index, config: &Config, args: &SearchArgs) -> i32 {
let output_args = args.with_effective_output_defaults(config);
#[cfg(feature = "symbols")]
if args.sym.is_some() || args.refs.is_some() {
if let Some(code) = super::sym::reject_sym_refs_conflicts(&output_args) {
return code;
}
}
#[cfg(feature = "symbols")]
if args.sym.is_some() {
if let Some(code) = super::sym::reject_incompatible_symbol_flags(&output_args) {
return code;
}
}
let search_start = Instant::now();
if output_args.invert_match {
return handle_output_code(super::render::render_invert_match(
index,
config,
&output_args,
));
}
let outcome = match run_search(index, config, args) {
Ok(r) => r,
Err(e) => {
eprintln!("st: {e}");
return 2;
}
};
let files = outcome.files;
let results = outcome.matches;
let elapsed = search_start.elapsed();
if output_args.search_stats {
let matched_files: std::collections::BTreeSet<_> =
results.iter().map(|m| &m.path).collect();
eprintln!(
"Elapsed: {:.6}s, Matches: {}, Files with matches: {}",
elapsed.as_secs_f64(),
results.len(),
matched_files.len()
);
}
if output_args.files_without_match {
let stdout = io::stdout();
let mut out = stdout.lock();
let sep = if output_args.null { b'\0' } else { b'\n' };
let matched: std::collections::BTreeSet<_> =
results.iter().map(|m| m.path.clone()).collect();
let mut found_any = false;
for path in collect_scoped_paths(index, config, &output_args) {
if matched.contains(&path) {
continue;
}
found_any = true;
if output_args.quiet {
break;
}
let result = out
.write_all(path_bytes(&path).as_ref())
.and_then(|_| out.write_all(&[sep]));
if let Err(err) = result {
return handle_output(err);
}
}
return if found_any { 0 } else { 1 };
}
if results.is_empty() && output_args.json {
if let Err(err) = super::render::render_json(index, config, &results, &files, &output_args)
{
return handle_output(err);
}
return 1;
}
if results.is_empty() {
return 1;
}
if output_args.quiet {
return 0;
}
if output_args.files_with_matches {
let stdout = io::stdout();
let mut out = stdout.lock();
let sep = if output_args.null { b'\0' } else { b'\n' };
let mut seen = std::collections::BTreeSet::new();
for m in &results {
seen.insert(m.path.clone());
}
for path in &seen {
let result = out
.write_all(path_bytes(path).as_ref())
.and_then(|_| out.write_all(&[sep]));
if let Err(err) = result {
return handle_output(err);
}
}
return 0;
}
if output_args.count_matches || (output_args.count && output_args.only_matching) {
return handle_output_code(super::render::render_count_matches(
config,
&results,
&output_args,
));
}
if output_args.count {
let stdout = io::stdout();
let mut out = stdout.lock();
let mut counts: std::collections::BTreeMap<PathBuf, usize> =
std::collections::BTreeMap::new();
for m in &results {
*counts.entry(m.path.clone()).or_default() += 1;
}
for (path, n) in &counts {
let result = if output_args.no_filename {
writeln!(out, "{n}")
} else {
let count_sep = if output_args.null { b'\0' } else { b':' };
out.write_all(path_bytes(path).as_ref())
.and_then(|_| out.write_all(&[count_sep]))
.and_then(|_| writeln!(out, "{n}"))
};
if let Err(err) = result {
return handle_output(err);
}
}
return 0;
}
let has_context = output_args.after_context > 0 || output_args.before_context > 0;
let render = if output_args.json {
super::render::render_json(index, config, &results, &files, &output_args)
} else if output_args.vimgrep {
super::render::render_vimgrep(config, &results, &output_args)
} else if output_args.only_matching {
super::render::render_only_matching(config, &results, &files, &output_args)
} else if has_context {
super::render::render_with_context(config, &results, &files, &output_args)
} else if output_args.heading {
super::render::render_heading(&results, &output_args)
} else {
super::render::render_flat(&results, &output_args)
};
if let Err(err) = render {
return handle_output(err);
}
0
}
fn handle_output_code(result: io::Result<i32>) -> i32 {
result.unwrap_or_else(handle_output)
}
fn handle_output(err: io::Error) -> i32 {
if err.kind() == io::ErrorKind::BrokenPipe {
0
} else {
eprintln!("st: {err}");
2
}
}
pub(super) fn run_search(
index: &Index,
config: &Config,
args: &SearchArgs,
) -> Result<crate::search::SearchOutcome, crate::IndexError> {
use crate::search::{MatchedFile, SearchOutcome};
use std::collections::HashMap;
#[cfg(feature = "symbols")]
if let Some(name) = &args.sym {
return Ok(SearchOutcome {
matches: index.search_symbols(name, args.sym_kind.as_deref())?,
files: HashMap::new(),
});
}
#[cfg(feature = "symbols")]
if let Some(name) = &args.refs {
let explicit_specs = explicit_path_specs(&config.repo_root, &args.paths);
let results = index.search_references(name, args.sym_kind.as_deref())?;
return Ok(SearchOutcome {
matches: apply_post_filters(results, args, &explicit_specs),
files: HashMap::new(),
});
}
let (routing_pattern, verify_pattern) = build_effective_pattern(args);
let explicit_specs = explicit_path_specs(&config.repo_root, &args.paths);
let make_opts = |path_filter: Option<String>| {
let mut opts = search_options(args, path_filter);
opts.verify_pattern = verify_pattern.clone();
if args.count || args.files_with_matches || args.files_without_match {
assert!(
opts.max_results.is_none(),
"max_results must be None in count/files-with-matches/files-without-match modes to avoid truncation bugs"
);
}
opts
};
let (results, files): (Vec<crate::SearchMatch>, HashMap<PathBuf, MatchedFile>) =
if explicit_specs.is_empty() {
let out = index.search_with_content(&routing_pattern, &make_opts(None))?;
(out.matches, out.files)
} else {
let mut merged = Vec::new();
let mut files: HashMap<PathBuf, MatchedFile> = HashMap::new();
for spec in &explicit_specs {
let out = index
.search_with_content(&routing_pattern, &make_opts(Some(spec.path_filter())))?;
merged.extend(out.matches);
for (p, mf) in out.files {
files.entry(p).or_insert(mf);
}
}
(sort_and_dedup_matches(merged), files)
};
Ok(SearchOutcome {
matches: apply_post_filters(results, args, &explicit_specs),
files,
})
}