use std::io::{self, IsTerminal, Write};
use std::path::PathBuf;
use std::time::Instant;
use crate::index::Index;
use crate::path_util::path_bytes;
use crate::Config;
pub(super) use super::scope::collect_scoped_paths;
use super::scope::{
explicit_path_specs, matches_any_explicit_path, matches_optional_glob, path_depth,
search_options, shows_filename_by_default, sort_and_dedup_matches, truncate_matches_per_file,
};
#[derive(Clone)]
pub(super) struct SearchArgs {
pub pattern: String,
pub paths: Vec<PathBuf>,
pub fixed_strings: bool,
pub ignore_case: bool,
pub word_regexp: bool,
pub line_regexp: bool,
pub line_number: bool,
pub with_filename: bool,
pub invert_match: bool,
pub files_with_matches: bool,
pub files_without_match: bool,
pub count: bool,
pub count_matches: bool,
pub max_count: Option<usize>,
pub quiet: bool,
pub only_matching: bool,
pub json: bool,
pub heading: bool,
pub no_line_number: bool,
pub no_filename: bool,
pub after_context: usize,
pub before_context: usize,
pub file_types: Vec<String>,
pub type_nots: Vec<String>,
pub globs: Vec<String>,
pub column: bool,
pub vimgrep: bool,
pub replace: Option<String>,
pub null: bool,
pub context_separator: String,
pub byte_offset: bool,
pub trim: bool,
pub max_columns: Option<usize>,
pub search_stats: bool,
pub max_depth: Option<usize>,
}
impl Default for SearchArgs {
fn default() -> Self {
Self {
pattern: String::new(),
paths: Vec::new(),
fixed_strings: false,
ignore_case: false,
word_regexp: false,
line_regexp: false,
line_number: false,
with_filename: false,
invert_match: false,
files_with_matches: false,
files_without_match: false,
count: false,
count_matches: false,
max_count: None,
quiet: false,
only_matching: false,
json: false,
heading: false,
no_line_number: false,
no_filename: false,
after_context: 0,
before_context: 0,
file_types: Vec::new(),
type_nots: Vec::new(),
globs: Vec::new(),
column: false,
vimgrep: false,
replace: None,
null: false,
context_separator: "--".to_string(),
byte_offset: false,
trim: false,
max_columns: None,
search_stats: false,
max_depth: None,
}
}
}
pub(super) fn cmd_search(config: Config, args: &SearchArgs) -> i32 {
let index = match Index::open(config.clone()) {
Ok(idx) => idx,
Err(e) => {
eprintln!("st: {e}");
return 2;
}
};
let output_args = args.with_effective_output_defaults(&config);
let search_start = Instant::now();
if output_args.invert_match {
return handle_output_code(super::render::render_invert_match(
&index,
&config,
&output_args,
));
}
let results = match run_search(&index, &config, args) {
Ok(r) => r,
Err(e) => {
eprintln!("st: {e}");
return 2;
}
};
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 results.is_empty() && output_args.json {
if let Err(err) = super::render::render_json(&index, &config, &results, &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.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;
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 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 {
out.write_all(path_bytes(path).as_ref())
.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, &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, &output_args)
} else if has_context {
super::render::render_with_context(&config, &results, &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 {
match result {
Ok(code) => code,
Err(err) => handle_output(err),
}
}
fn handle_output(err: io::Error) -> i32 {
if err.kind() == io::ErrorKind::BrokenPipe {
0
} else {
eprintln!("st: {err}");
2
}
}
pub(super) fn build_effective_pattern(args: &SearchArgs) -> String {
let pat = if args.fixed_strings {
regex::escape(&args.pattern)
} else {
args.pattern.clone()
};
if args.line_regexp {
format!("^(?:{pat})$")
} else if args.word_regexp {
format!(r"\b{pat}\b")
} else {
pat
}
}
pub(super) fn run_search(
index: &Index,
config: &Config,
args: &SearchArgs,
) -> Result<Vec<crate::SearchMatch>, crate::IndexError> {
let effective_pattern = build_effective_pattern(args);
let explicit_specs = explicit_path_specs(&config.repo_root, &args.paths);
let mut results = if explicit_specs.is_empty() {
index.search(&effective_pattern, &search_options(args, None))?
} else {
let mut merged = Vec::new();
for spec in &explicit_specs {
merged.extend(index.search(
&effective_pattern,
&search_options(args, Some(spec.path_filter())),
)?);
}
sort_and_dedup_matches(merged)
};
if !explicit_specs.is_empty()
|| !args.file_types.is_empty()
|| !args.type_nots.is_empty()
|| !args.globs.is_empty()
{
results.retain(|m| {
matches_any_explicit_path(&m.path, &explicit_specs)
&& matches_optional_glob(&m.path, &args.file_types, &args.type_nots, &args.globs)
});
}
if let Some(depth) = args.max_depth {
results.retain(|m| path_depth(&m.path) <= depth);
}
if let Some(limit) = args.max_count {
results = truncate_matches_per_file(results, limit);
}
Ok(results)
}
impl SearchArgs {
fn with_effective_output_defaults(&self, config: &Config) -> Self {
let mut effective = self.clone();
let stdout_is_tty = io::stdout().is_terminal();
if !self.line_number && !self.no_line_number {
effective.no_line_number = !stdout_is_tty;
}
if self.with_filename {
effective.no_filename = false;
} else if !self.no_filename {
effective.no_filename = !shows_filename_by_default(config, &self.paths);
}
effective
}
}