use std::path::Path;
#[derive(Debug, PartialEq, Eq)]
pub enum Route {
Compress(Vec<String>),
Unchanged,
}
pub fn route(command: &str, cwd: &Path) -> Route {
if bypass_active(cwd) || has_shell_syntax(command) {
return Route::Unchanged;
}
let Some(words) = shlex::split(command) else {
return Route::Unchanged;
};
let Some((program, args)) = words.split_first() else {
return Route::Unchanged;
};
if program.contains('/')
|| program.contains('=')
|| program == "bctx"
|| !reads_builds_or_tests(program, args)
|| crate::commands::run::is_streaming(program, args)
|| !crate::commands::run::mesh().compresses(program, args)
{
return Route::Unchanged;
}
Route::Compress(words)
}
fn reads_builds_or_tests(program: &str, args: &[String]) -> bool {
let words: Vec<&str> = args.iter().map(String::as_str).collect();
let first = words.first().copied();
let second = words.get(1).copied();
let has = |flag: &str| words.contains(&flag);
match program {
"git" => matches!(
git_subcommand(&words),
Some(
"log"
| "status"
| "diff"
| "show"
| "blame"
| "shortlog"
| "ls-files"
| "grep"
| "describe"
| "rev-parse"
)
),
"cargo" => matches!(first, Some("build" | "check" | "test" | "clippy" | "tree")),
"npm" | "pnpm" | "yarn" | "bun" => match first {
Some("test") => true,
Some("run") => matches!(
second,
Some("test" | "lint" | "build" | "typecheck" | "check")
),
_ => false,
},
"go" => matches!(first, Some("test" | "vet" | "build" | "list")),
"pytest" | "jest" | "vitest" | "mocha" | "tsc" | "mypy" | "flake8" | "pylint" => true,
"eslint" => !has("--fix"),
"ruff" => first == Some("check") && !has("--fix"),
"python" | "python3" => {
first == Some("-m") && matches!(second, Some("pytest" | "unittest" | "mypy"))
}
"kubectl" => match first {
Some("get") => !has("-w") && !has("--watch"),
Some("logs") => !has("-f") && !has("--follow"),
Some("describe" | "top" | "explain" | "version" | "api-resources") => true,
_ => false,
},
"docker" => match first {
Some("logs") => !has("-f") && !has("--follow"),
Some("ps" | "images" | "inspect" | "version" | "info") => true,
_ => false,
},
"terraform" => matches!(first, Some("plan" | "show" | "validate" | "output")),
"gh" => matches!(second, Some("list" | "view" | "checks" | "diff" | "status")),
"aws" => second.is_some_and(|op| {
op.starts_with("describe-") || op.starts_with("list-") || op.starts_with("get-")
}),
"ls" | "tree" | "grep" | "rg" | "wc" | "du" | "df" => true,
"find" => !words.iter().any(|w| {
matches!(
*w,
"-delete"
| "-exec"
| "-execdir"
| "-ok"
| "-okdir"
| "-fprint"
| "-fprintf"
| "-fls"
)
}),
_ => false,
}
}
fn git_subcommand<'a>(words: &[&'a str]) -> Option<&'a str> {
let mut rest = words.iter();
while let Some(word) = rest.next() {
match *word {
"-C" | "-c" | "--git-dir" | "--work-tree" | "--namespace" => {
rest.next();
}
w if w.starts_with('-') => {}
w => return Some(w),
}
}
None
}
pub fn bypass_active(cwd: &Path) -> bool {
std::env::var("BCTX_BYPASS").as_deref() == Ok("1")
|| cwd.ancestors().any(|dir| dir.join(".bctx-bypass").exists())
}
fn has_shell_syntax(command: &str) -> bool {
if command.contains('`') || command.contains("$(") {
return true;
}
let (mut single, mut double, mut escaped) = (false, false, false);
for c in command.chars() {
if escaped {
escaped = false;
continue;
}
match c {
'\\' if !single => escaped = true,
'\'' if !double => single = !single,
'"' if !single => double = !double,
'|' | '&' | ';' | '<' | '>' | '(' | ')' | '\n' | '\r' if !single && !double => {
return true
}
_ => {}
}
}
single || double
}
#[cfg(test)]
mod tests {
use super::*;
fn compresses(command: &str) -> bool {
matches!(route(command, &std::env::temp_dir()), Route::Compress(_))
}
#[test]
fn a_simple_command_bctx_compresses_goes_through_bctx() {
assert!(compresses("git log --oneline"));
assert!(compresses("git log --grep 'a | b; c > d'"));
assert!(compresses(r#"git log --grep "x;y""#));
}
#[test]
fn only_commands_that_read_build_or_test_are_rewritten() {
for c in [
"git push --force",
"git reset --hard HEAD~1",
"git commit -m x",
"git checkout main",
"git clean -fd",
"git branch -D old",
"npm install left-pad",
"npm publish",
"cargo run",
"cargo install ripgrep",
"cargo publish",
"kubectl delete pod web",
"kubectl apply -f deploy.yaml",
"kubectl get pods -w",
"terraform apply",
"terraform destroy",
"docker run alpine",
"docker rm web",
"rm -rf build",
"env TARGET=build rm -rf build",
"export FOO=bar",
"find . -name x -delete",
"eslint --fix .",
] {
assert!(!compresses(c), "{c:?}");
}
for c in [
"git status",
"git diff",
"git show HEAD",
"git -C repo log --oneline",
"cargo test",
"cargo build",
"npm test",
"kubectl get pods",
"docker ps",
] {
assert!(compresses(c), "{c:?}");
}
}
#[test]
fn pipes_lists_redirects_and_substitutions_stay_as_written() {
for c in [
"git log | head",
"git diff | patch -p1",
"git add . && git commit -m x",
"git status; ls",
"git log > out.txt",
"git apply < fix.patch",
"git log &",
"(git log)",
"git log $(git rev-parse HEAD)",
"git log `git rev-parse HEAD`",
"git log \"$(date)\"",
"git log\ngit status",
] {
assert!(!compresses(c), "{c:?}");
}
}
#[test]
fn env_prefixes_paths_and_bctx_itself_stay_as_written() {
for c in [
"GIT_PAGER=cat git log",
"/usr/bin/git log",
"./git log",
"bctx git log",
] {
assert!(!compresses(c), "{c:?}");
}
}
#[test]
fn programs_without_a_compressor_and_streaming_commands_stay_as_written() {
for c in [
"vim notes.md",
"npm run dev",
"cargo watch -x test",
"kubectl logs -f web",
] {
assert!(!compresses(c), "{c:?}");
}
}
#[test]
fn empty_and_unbalanced_commands_stay_as_written() {
for c in ["", " ", "git log 'oops", "git log \"oops"] {
assert!(!compresses(c), "{c:?}");
}
}
#[test]
fn a_bypass_file_in_any_parent_keeps_commands_as_written() {
let root = tempfile::tempdir().unwrap();
let nested = root.path().join("a/b");
std::fs::create_dir_all(&nested).unwrap();
std::fs::write(root.path().join(".bctx-bypass"), "").unwrap();
assert!(bypass_active(&nested));
assert_eq!(route("git log", &nested), Route::Unchanged);
}
}