pub mod args;
mod bench;
mod commands;
mod manage;
mod render;
mod scope;
mod search;
use std::path::PathBuf;
use clap::Parser;
use crate::Config;
pub use args::{Cli, ManageCommand};
use bench::cmd_bench_search;
use manage::{cmd_index, cmd_status, cmd_type_list, cmd_update};
use scope::cmd_files;
use search::{cmd_search, SearchArgs};
pub fn run() -> i32 {
let cli = Cli::parse();
let mut config = resolve_config(&cli);
config.verbose = cli.verbose || cli.debug;
match cli.command {
Some(ManageCommand::Index {
force,
stats,
quiet,
}) => cmd_index(config, force, stats, quiet),
Some(ManageCommand::Status { json }) => cmd_status(config, json),
Some(ManageCommand::Update { flush, quiet }) => cmd_update(config, flush, quiet),
Some(ManageCommand::BenchSearch {
queries,
iterations,
warmups,
}) => cmd_bench_search(config, &queries, iterations, warmups),
None => {
if cli.type_list {
return cmd_type_list();
}
if cli.files {
return cmd_files(config, &cli);
}
let pattern = match cli.pattern.or(cli.regexp) {
Some(p) => p,
None => {
eprintln!("st: a pattern is required (try `st --help`)");
return 2;
}
};
if cli.pcre2 {
eprintln!("st: --pcre2 is not supported; using default regex engine");
}
let ignore_case = if cli.smart_case && !cli.case_sensitive && !cli.ignore_case {
!pattern.chars().any(|c| c.is_uppercase())
} else {
cli.ignore_case
};
let heading = cli.heading || cli.pretty;
let line_number = cli.line_number || cli.pretty;
let ctx = cli.context.unwrap_or(0);
let search_args = SearchArgs {
pattern,
paths: cli.paths,
fixed_strings: cli.fixed_strings,
ignore_case,
word_regexp: cli.word_regexp,
line_regexp: cli.line_regexp,
line_number,
with_filename: cli.with_filename,
invert_match: cli.invert_match,
files_with_matches: cli.files_with_matches,
files_without_match: cli.files_without_match,
count: cli.count,
count_matches: cli.count_matches,
max_count: cli.max_count,
quiet: cli.quiet,
only_matching: cli.only_matching,
json: cli.json,
heading,
no_line_number: cli.no_line_number,
no_filename: cli.no_filename,
after_context: cli.after_context.unwrap_or(ctx),
before_context: cli.before_context.unwrap_or(ctx),
file_type: cli.file_type,
type_not: cli.type_not,
glob: cli.glob,
column: cli.column || cli.vimgrep,
vimgrep: cli.vimgrep,
replace: cli.replace,
null: cli.null,
context_separator: cli.context_separator,
byte_offset: cli.byte_offset,
trim: cli.trim,
max_columns: cli.max_columns,
search_stats: cli.search_stats,
max_depth: cli.max_depth,
};
cmd_search(config, &search_args)
}
}
}
const MAX_FILE_SIZE_CEILING: u64 = 1_073_741_824;
fn resolve_config(cli: &Cli) -> Config {
let repo_root = cli
.repo_root
.clone()
.or_else(detect_repo_root)
.unwrap_or_else(|| PathBuf::from("."));
let index_dir = {
let raw = cli
.index_dir
.clone()
.unwrap_or_else(|| repo_root.join(".syntext"));
if let Err(msg) = validate_index_dir(&raw) {
eprintln!("{msg}");
std::process::exit(2);
}
raw
};
let max_file_size = parse_max_file_size();
Config {
max_file_size,
max_segments: 10,
index_dir,
repo_root,
verbose: false,
strict_permissions: true,
}
}
fn parse_max_file_size() -> u64 {
clamp_max_file_size(
std::env::var("SYNTEXT_MAX_FILE_SIZE")
.ok()
.and_then(|v| v.parse::<u64>().ok()),
)
}
fn clamp_max_file_size(raw: Option<u64>) -> u64 {
raw.unwrap_or(10 * 1024 * 1024).min(MAX_FILE_SIZE_CEILING)
}
fn detect_repo_root() -> Option<PathBuf> {
let mut dir = std::env::current_dir().ok()?;
loop {
if dir.join(".git").exists() {
return Some(dir);
}
if !dir.pop() {
return None;
}
}
}
#[cfg(unix)]
fn validate_index_dir(index_dir: &std::path::Path) -> Result<(), String> {
if !index_dir.is_absolute() {
return Ok(());
}
const SENSITIVE_PREFIXES: &[&str] = &[
"/etc",
"/usr",
"/bin",
"/sbin",
"/lib",
"/lib64",
"/sys",
"/proc",
"/dev",
"/boot",
"/root",
"/System",
"/Library",
"/private/etc",
"/private/var/root",
];
let dir_str = index_dir.to_string_lossy();
if let Some(matched) = overlaps_sensitive_prefix(&dir_str, SENSITIVE_PREFIXES, '/') {
return Err(format!(
"st: refusing --index-dir '{}': overlaps system path '{matched}'; \
use a path under the repository or a user-owned directory",
index_dir.display(),
));
}
Ok(())
}
#[cfg(not(unix))]
fn validate_index_dir(index_dir: &std::path::Path) -> Result<(), String> {
if !index_dir.is_absolute() {
return Ok(());
}
let mut sensitive: Vec<String> = Vec::new();
for var in ["SYSTEMROOT", "PROGRAMFILES", "PROGRAMFILES(X86)", "PROGRAMDATA"] {
if let Some(val) = std::env::var_os(var) {
let lower = val.to_string_lossy().to_lowercase().replace('/', "\\");
if !sensitive.contains(&lower) {
sensitive.push(lower);
}
}
}
for fb in [
"c:\\windows",
"c:\\program files",
"c:\\program files (x86)",
"c:\\programdata",
] {
let s = fb.to_string();
if !sensitive.contains(&s) {
sensitive.push(s);
}
}
let dir_lower = index_dir.to_string_lossy().to_lowercase().replace('/', "\\");
let prefixes: Vec<&str> = sensitive.iter().map(|s| s.as_str()).collect();
if let Some(matched) = overlaps_sensitive_prefix(&dir_lower, &prefixes, '\\') {
return Err(format!(
"st: refusing --index-dir '{}': overlaps system path '{matched}'; \
use a path under the repository or a user-owned directory",
index_dir.display(),
));
}
Ok(())
}
fn overlaps_sensitive_prefix<'a>(dir: &str, prefixes: &[&'a str], sep: char) -> Option<&'a str> {
for &prefix in prefixes {
if dir == prefix || dir.starts_with(&format!("{prefix}{sep}")) {
return Some(prefix);
}
}
None
}
#[cfg(test)]
mod tests;