pub mod types;
pub(crate) mod compound;
pub(crate) mod rules;
pub(crate) mod user_config;
use std::path::PathBuf;
use crate::config;
use compound::{StrippedPipe, has_bare_pipe, split_compound, strip_env_prefix, strip_simple_pipe};
use rules::{apply_rules, should_skip};
use types::{RewriteConfig, RewriteOptions, RewriteRule};
pub use user_config::load_user_config;
const BUILTIN_WRAPPERS: &[(&str, &str)] = &[
(r"^(?:[^\s]*/)?make(\s.*)?$", "make SHELL=tokf{1}"),
(
r"^(?:[^\s]*/)?just(\s.*)?$",
"just --shell tokf --shell-arg -cu{1}",
),
];
fn build_wrapper_rules() -> Vec<RewriteRule> {
BUILTIN_WRAPPERS
.iter()
.map(|(pattern, replace)| RewriteRule {
match_pattern: (*pattern).to_string(),
replace: (*replace).to_string(),
})
.collect()
}
#[cfg(test)]
pub(crate) fn build_rules_from_filters(search_dirs: &[PathBuf]) -> Vec<RewriteRule> {
build_rules_from_filters_with_options(search_dirs, &RewriteOptions::default())
}
fn build_rules_from_filters_with_options(
search_dirs: &[PathBuf],
options: &RewriteOptions,
) -> Vec<RewriteRule> {
let mut rules = Vec::new();
let mut seen_patterns: std::collections::HashSet<String> = std::collections::HashSet::new();
let Ok(filters) = config::cache::discover_with_cache(search_dirs) else {
return rules;
};
let run_prefix = if options.no_mask_exit_code {
"tokf run --no-mask-exit-code"
} else {
"tokf run"
};
for filter in filters {
for pattern in filter.config.command.patterns() {
if !seen_patterns.insert(pattern.clone()) {
continue;
}
let regex_str = config::command_pattern_to_regex(pattern);
rules.push(RewriteRule {
match_pattern: regex_str,
replace: format!("{run_prefix} {{0}}"),
});
}
}
rules
}
pub fn rewrite(command: &str, verbose: bool) -> String {
rewrite_with_options(command, verbose, &RewriteOptions::default())
}
pub fn rewrite_with_options(command: &str, verbose: bool, options: &RewriteOptions) -> String {
let user_config = load_user_config().unwrap_or_default();
rewrite_with_config_and_options(
command,
&user_config,
&config::default_search_dirs(),
verbose,
options,
)
}
struct SegmentRules<'a> {
wrapper: &'a [RewriteRule],
filter: &'a [RewriteRule],
options: &'a RewriteOptions,
}
fn rewrite_segment(
segment: &str,
rules: &SegmentRules<'_>,
strip_pipes: bool,
prefer_less: bool,
verbose: bool,
) -> String {
let (env_prefix, cmd_owned) =
strip_env_prefix(segment).unwrap_or_else(|| (String::new(), segment.to_string()));
let cmd = cmd_owned.as_str();
let wrapper_result = apply_rules(rules.wrapper, cmd);
if wrapper_result != cmd {
if verbose {
eprintln!("[tokf] wrapper rewrite: task runner shell override");
}
return format!("{env_prefix}{wrapper_result}");
}
if has_bare_pipe(cmd) {
if strip_pipes && let Some(StrippedPipe { base, suffix }) = strip_simple_pipe(cmd) {
let rewritten = apply_rules(rules.filter, &base);
if rewritten != base {
if verbose {
eprintln!("[tokf] stripped pipe — tokf filter provides structured output");
}
let injected =
inject_pipe_flags_with_options(&rewritten, &suffix, prefer_less, rules.options);
return format!("{env_prefix}{injected}");
}
}
if verbose {
eprintln!("[tokf] skipping rewrite: command contains a pipe");
}
return segment.to_string();
}
let result = apply_rules(rules.filter, cmd);
if result == cmd {
segment.to_string()
} else {
format!("{env_prefix}{result}")
}
}
#[cfg(test)]
fn inject_pipe_flags(rewritten: &str, suffix: &str, prefer_less: bool) -> String {
inject_pipe_flags_with_options(rewritten, suffix, prefer_less, &RewriteOptions::default())
}
fn inject_pipe_flags_with_options(
rewritten: &str,
suffix: &str,
prefer_less: bool,
options: &RewriteOptions,
) -> String {
rewritten.strip_prefix("tokf run ").map_or_else(
|| rewritten.to_string(),
|rest| {
let rest = rest.strip_prefix("--no-mask-exit-code ").unwrap_or(rest);
let escaped = suffix.replace('\'', "'\\''");
let prefer_flag = if prefer_less { " --prefer-less" } else { "" };
let mask_flag = if options.no_mask_exit_code {
" --no-mask-exit-code"
} else {
""
};
format!("tokf run{mask_flag} --baseline-pipe '{escaped}'{prefer_flag} {rest}")
},
)
}
fn should_skip_effective(command: &str, user_patterns: &[String]) -> bool {
if should_skip(command, user_patterns) {
return true;
}
strip_env_prefix(command).is_some_and(|(_, cmd)| should_skip(&cmd, &[]))
}
pub(crate) fn rewrite_with_config(
command: &str,
user_config: &RewriteConfig,
search_dirs: &[PathBuf],
verbose: bool,
) -> String {
rewrite_with_config_and_options(
command,
user_config,
search_dirs,
verbose,
&RewriteOptions::default(),
)
}
pub(crate) fn rewrite_with_config_and_options(
command: &str,
user_config: &RewriteConfig,
search_dirs: &[PathBuf],
verbose: bool,
options: &RewriteOptions,
) -> String {
let user_skip_patterns = user_config
.skip
.as_ref()
.map_or(&[] as &[String], |s| &s.patterns);
let strip_pipes = user_config.pipe.as_ref().is_none_or(|p| p.strip);
let prefer_less = user_config.pipe.as_ref().is_some_and(|p| p.prefer_less);
if should_skip_effective(command, user_skip_patterns) {
return command.to_string();
}
let user_result = apply_rules(&user_config.rewrite, command);
if user_result != command {
return user_result;
}
let wrapper_rules = build_wrapper_rules();
let filter_rules = build_rules_from_filters_with_options(search_dirs, options);
let rules = SegmentRules {
wrapper: &wrapper_rules,
filter: &filter_rules,
options,
};
let segments = split_compound(command);
if segments.len() == 1 {
return rewrite_segment(command, &rules, strip_pipes, prefer_less, verbose);
}
let mut changed = false;
let mut out = String::with_capacity(command.len() + segments.len() * 9);
for (seg, sep) in &segments {
let trimmed = seg.trim();
let rewritten = if trimmed.is_empty() || should_skip_effective(trimmed, user_skip_patterns)
{
trimmed.to_string()
} else {
let r = rewrite_segment(trimmed, &rules, strip_pipes, prefer_less, verbose);
if r != trimmed {
changed = true;
}
r
};
out.push_str(&rewritten);
out.push_str(sep);
}
if changed { out } else { command.to_string() }
}
#[cfg(test)]
mod compound_tests;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_compound;
#[cfg(test)]
mod tests_env;
#[cfg(test)]
mod tests_pipe;