use super::commands::classify_wrapped;
use super::{ClassifyError, ShellContext, Sink};
const FIND_EXEC: &[&str] = &["-exec", "-execdir", "-ok", "-okdir"];
const OUTPUT_SHORT: &[(&str, &str)] = &[("curl", "-o"), ("wget", "-O")];
pub(super) fn classify(
argv: &[String],
program: &str,
ctx: &ShellContext<'_>,
sink: &mut Sink,
depth: u8,
) -> Result<(), ClassifyError> {
if program.starts_with("python")
&& matches!(argv.get(1..3), Some([m, venv]) if m == "-m" && venv == "venv")
{
for dir in argv[3..].iter().filter(|a| !a.starts_with('-')) {
sink.write(ctx, dir)?;
}
}
if program == "gofmt" {
gofmt(argv, ctx, sink)?;
}
super::upload::classify(argv, program, ctx, sink)?;
super::in_place::classify(argv, program, ctx, sink)?;
let mut i = 1;
while let Some(arg) = argv.get(i) {
i += 1;
if program == "find" && FIND_EXEC.contains(&arg.as_str()) {
let rest = &argv[i..];
let end = rest
.iter()
.position(|a| a == ";" || a == "+")
.unwrap_or(rest.len());
if end > 0 {
classify_wrapped(&rest[..end], ctx, sink, depth)?;
}
i += end + 1;
continue;
}
let short = OUTPUT_SHORT
.iter()
.any(|(p, flag)| *p == program && arg == flag);
let target = match arg
.strip_prefix("--output=")
.or_else(|| arg.strip_prefix("--output-document="))
{
Some(value) => Some(value),
None if arg == "--output" || arg == "--output-document" || short => {
argv.get(i).map(String::as_str)
}
None => None,
};
if let Some(path) = target.filter(|p| !p.is_empty() && *p != "-") {
sink.write(ctx, path)?;
}
}
Ok(())
}
const FD_FLAGS: &str = "HIuisgFalLp01q";
const FD_LONG_FLAGS: &[&str] = &[
"--hidden",
"--no-ignore",
"--no-ignore-vcs",
"--unrestricted",
"--ignore-case",
"--case-sensitive",
"--glob",
"--regex",
"--fixed-strings",
"--absolute-path",
"--list-details",
"--follow",
"--full-path",
"--print0",
"--quiet",
"--prune",
"--show-errors",
"--one-file-system",
];
pub(super) fn fd_pattern(argv: &[String], program: &str) -> Option<usize> {
if program != "fd" {
return None;
}
let at = argv.iter().skip(1).position(|a| !a.starts_with('-'))? + 1;
let flag = |a: &String| {
FD_LONG_FLAGS.contains(&a.as_str())
|| a.strip_prefix('-')
.is_some_and(|f| !f.is_empty() && f.chars().all(|c| FD_FLAGS.contains(c)))
};
argv[1..at].iter().all(flag).then_some(at)
}
const GREP_FLAGS: &str = "abcEFGhHiIlLnoPqrRsTUvVwxyzZ";
const GREP_LONG_FLAGS: &[&str] = &[
"--count",
"--ignore-case",
"--no-ignore-case",
"--line-number",
"--recursive",
"--dereference-recursive",
"--files-with-matches",
"--files-without-match",
"--only-matching",
"--quiet",
"--silent",
"--no-messages",
"--invert-match",
"--word-regexp",
"--line-regexp",
"--with-filename",
"--no-filename",
"--extended-regexp",
"--fixed-strings",
"--basic-regexp",
"--perl-regexp",
"--null",
"--null-data",
"--text",
"--byte-offset",
"--initial-tab",
"--color",
"--colour",
];
pub(super) fn grep_pattern(argv: &[String], program: &str) -> Option<usize> {
if !matches!(program, "grep" | "egrep" | "fgrep") {
return None;
}
let end = argv.iter().position(|a| a == "--").unwrap_or(argv.len());
let names_pattern = |a: &String| match a.strip_prefix("--") {
Some(long) => {
let name = long.split('=').next().unwrap_or(long);
name.is_empty() || "regexp".starts_with(name) || "file".starts_with(name)
}
None => a.len() > 1 && a.starts_with('-') && a[1..].contains(['e', 'f']),
};
if argv[1..end].iter().any(names_pattern) {
return None;
}
let at = argv[1..end]
.iter()
.position(|a| !a.starts_with('-'))
.map(|i| i + 1)
.or_else(|| (end + 1 < argv.len()).then_some(end + 1))?;
let flag = |a: &String| {
a == "--"
|| GREP_LONG_FLAGS.contains(&a.as_str())
|| (a.starts_with("--") && a.contains('='))
|| a.strip_prefix('-')
.is_some_and(|f| !f.is_empty() && f.chars().all(|c| GREP_FLAGS.contains(c)))
};
argv[1..at].iter().all(flag).then_some(at)
}
pub(super) fn only_describes(argv: &[String], program: &str) -> bool {
program == "command"
&& argv[1..]
.iter()
.take_while(|a| a.starts_with('-') && *a != "--")
.any(|a| a.contains(['v', 'V']))
}
fn gofmt(argv: &[String], ctx: &ShellContext<'_>, sink: &mut Sink) -> Result<(), ClassifyError> {
let rewrites = argv[1..].iter().any(|a| go_flag(a) == Some("w"));
let mut words = argv[1..].iter();
while let Some(arg) = words.next() {
match go_flag(arg) {
Some("cpuprofile") => {
let value = arg
.split_once('=')
.map(|(_, v)| v)
.or_else(|| words.next().map(String::as_str));
if let Some(file) = value {
sink.write(ctx, file)?;
}
}
None if rewrites => sink.write(ctx, arg)?,
Some(_) | None => {}
}
}
Ok(())
}
fn go_flag(arg: &str) -> Option<&str> {
let flag = arg.strip_prefix("--").or_else(|| arg.strip_prefix('-'))?;
Some(flag.split_once('=').map_or(flag, |(name, _)| name))
}