use crate::error::Result;
use crate::types::{CliAnalysis, NoArgsBehavior};
use lazy_static::lazy_static;
use regex::Regex;
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Duration;
lazy_static! {
static ref USAGE_LINE: Regex = Regex::new(r"(?i)^\s*usage:\s+(.+)$").unwrap();
static ref INTERACTIVE_TOOLS: Vec<&'static str> = vec![
"psql", "mysql", "redis-cli", "mongo", "mongosh", "sqlite3",
"python", "python3", "node", "irb", "php", "R", "julia",
"gdb", "lldb", "ghci", "erl", "iex",
];
}
pub struct BehaviorInferrer;
impl BehaviorInferrer {
pub fn new() -> Self {
Self
}
pub fn infer_no_args_behavior(&self, analysis: &CliAnalysis) -> NoArgsBehavior {
if self.is_interactive_tool(&analysis.binary_name) {
log::info!(
"Inferred no-args behavior: Interactive (known REPL: {})",
analysis.binary_name
);
return NoArgsBehavior::Interactive;
}
if let Ok(Some(exit_code)) = self.execute_and_measure(&analysis.binary_path) {
let behavior = match exit_code {
0 => NoArgsBehavior::ShowHelp,
1 | 2 => NoArgsBehavior::RequireSubcommand,
_ => NoArgsBehavior::ShowHelp, };
log::info!(
"Inferred no-args behavior: {:?} (from execution: exit {})",
behavior,
exit_code
);
return behavior;
}
if let Some(pattern) = self.extract_usage_pattern(&analysis.help_output) {
log::debug!("Extracted usage pattern: {}", pattern);
if self.requires_subcommand_from_usage(&pattern) {
log::info!("Inferred no-args behavior: RequireSubcommand (from Usage pattern)");
return NoArgsBehavior::RequireSubcommand;
}
if self.is_optional_only_from_usage(&pattern) {
log::info!("Inferred no-args behavior: ShowHelp (from Usage pattern)");
return NoArgsBehavior::ShowHelp;
}
}
if !analysis.subcommands.is_empty() {
log::info!(
"Inferred no-args behavior: RequireSubcommand (has {} subcommands)",
analysis.subcommands.len()
);
return NoArgsBehavior::RequireSubcommand;
}
log::info!("Inferred no-args behavior: ShowHelp (default)");
NoArgsBehavior::ShowHelp
}
fn execute_and_measure(&self, binary_path: &Path) -> Result<Option<i32>> {
log::debug!(
"Executing binary to measure no-args behavior: {:?}",
binary_path
);
let mut child = Command::new(binary_path)
.stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .env("NO_COLOR", "1") .env("TERM", "dumb") .spawn()?;
use wait_timeout::ChildExt;
match child.wait_timeout(Duration::from_secs(1))? {
Some(status) => {
let exit_code = status.code().unwrap_or(0);
log::debug!("Binary exited with code: {}", exit_code);
Ok(Some(exit_code))
}
None => {
log::debug!("Binary execution timed out (likely interactive tool)");
let _ = child.kill();
let _ = child.wait();
Ok(None)
}
}
}
fn extract_usage_pattern(&self, help_output: &str) -> Option<String> {
for line in help_output.lines() {
if let Some(cap) = USAGE_LINE.captures(line.trim()) {
return Some(cap[1].to_string());
}
}
None
}
fn requires_subcommand_from_usage(&self, pattern: &str) -> bool {
let pattern_lower = pattern.to_lowercase();
if pattern_lower.contains("<subcommand>") || pattern_lower.contains("<command>") {
return true;
}
if pattern_lower.contains(" command") || pattern_lower.contains(" subcommand") {
if !pattern.contains("[command]") && !pattern.contains("[subcommand]") {
return true;
}
}
false
}
fn is_optional_only_from_usage(&self, pattern: &str) -> bool {
let parts: Vec<&str> = pattern.split_whitespace().collect();
if parts.len() <= 1 {
return true; }
let args = &parts[1..].join(" ");
args.trim_start().starts_with('[')
}
fn is_interactive_tool(&self, binary_name: &str) -> bool {
INTERACTIVE_TOOLS
.iter()
.any(|&name| binary_name.contains(name))
}
}
impl Default for BehaviorInferrer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Subcommand;
use std::path::PathBuf;
fn create_mock_analysis(
binary_name: &str,
help_output: &str,
subcommands: Vec<&str>,
) -> CliAnalysis {
let mut analysis = CliAnalysis::new(
PathBuf::from(format!("/usr/bin/{}", binary_name)),
binary_name.to_string(),
help_output.to_string(),
);
analysis.subcommands = subcommands
.iter()
.map(|name| Subcommand {
name: name.to_string(),
description: None,
options: vec![],
required_args: vec![],
subcommands: vec![],
depth: 0,
})
.collect();
analysis
}
#[test]
fn test_infer_require_subcommand_from_usage() {
let inferrer = BehaviorInferrer::new();
let help_output = "Usage: git <SUBCOMMAND>\n\nAvailable commands:\n clone\n pull";
let analysis = create_mock_analysis("git", help_output, vec!["clone", "pull"]);
let behavior = inferrer.infer_no_args_behavior(&analysis);
assert_eq!(behavior, NoArgsBehavior::RequireSubcommand);
}
#[test]
fn test_infer_require_subcommand_from_command_pattern() {
let inferrer = BehaviorInferrer::new();
let help_output = "Usage: docker COMMAND\n\nCommands:\n run\n build";
let analysis = create_mock_analysis("docker", help_output, vec!["run", "build"]);
let behavior = inferrer.infer_no_args_behavior(&analysis);
assert_eq!(behavior, NoArgsBehavior::RequireSubcommand);
}
#[test]
fn test_infer_show_help_from_usage() {
let inferrer = BehaviorInferrer::new();
let help_output = "Usage: backup-suite [OPTIONS]\n\nOptions:\n --help";
let analysis = create_mock_analysis("backup-suite", help_output, vec![]);
let behavior = inferrer.infer_no_args_behavior(&analysis);
assert_eq!(behavior, NoArgsBehavior::ShowHelp);
}
#[test]
fn test_infer_require_subcommand_from_subcommands() {
let inferrer = BehaviorInferrer::new();
let help_output = "A CLI tool\n\nCommands:\n start\n stop";
let analysis = create_mock_analysis("service", help_output, vec!["start", "stop"]);
let behavior = inferrer.infer_no_args_behavior(&analysis);
assert_eq!(behavior, NoArgsBehavior::RequireSubcommand);
}
#[test]
fn test_infer_interactive_psql() {
let inferrer = BehaviorInferrer::new();
let help_output = "Usage: psql [OPTIONS]\n\nOptions:\n --help";
let analysis = create_mock_analysis("psql", help_output, vec![]);
let behavior = inferrer.infer_no_args_behavior(&analysis);
assert_eq!(behavior, NoArgsBehavior::Interactive);
}
#[test]
fn test_infer_interactive_python() {
let inferrer = BehaviorInferrer::new();
let help_output = "Usage: python [OPTIONS]\n\nOptions:\n -h";
let analysis = create_mock_analysis("python3", help_output, vec![]);
let behavior = inferrer.infer_no_args_behavior(&analysis);
assert_eq!(behavior, NoArgsBehavior::Interactive);
}
#[test]
fn test_default_to_show_help() {
let inferrer = BehaviorInferrer::new();
let help_output = "A simple tool\n\nOptions:\n --verbose";
let analysis = create_mock_analysis("unknown-tool", help_output, vec![]);
let behavior = inferrer.infer_no_args_behavior(&analysis);
assert_eq!(behavior, NoArgsBehavior::ShowHelp);
}
#[test]
fn test_extract_usage_pattern() {
let inferrer = BehaviorInferrer::new();
let help = "Usage: git <SUBCOMMAND>\n\nOptions:";
let pattern = inferrer.extract_usage_pattern(help);
assert_eq!(pattern, Some("git <SUBCOMMAND>".to_string()));
let help2 = "usage: backup-suite [OPTIONS]";
let pattern2 = inferrer.extract_usage_pattern(help2);
assert_eq!(pattern2, Some("backup-suite [OPTIONS]".to_string()));
}
#[test]
fn test_requires_subcommand_from_usage() {
let inferrer = BehaviorInferrer::new();
assert!(inferrer.requires_subcommand_from_usage("git <SUBCOMMAND>"));
assert!(inferrer.requires_subcommand_from_usage("docker <COMMAND>"));
assert!(inferrer.requires_subcommand_from_usage("cli COMMAND"));
assert!(!inferrer.requires_subcommand_from_usage("cli [OPTIONS]"));
assert!(!inferrer.requires_subcommand_from_usage("cli [command]"));
}
#[test]
fn test_is_optional_only_from_usage() {
let inferrer = BehaviorInferrer::new();
assert!(inferrer.is_optional_only_from_usage("backup-suite [OPTIONS]"));
assert!(inferrer.is_optional_only_from_usage("tool [options] [file]"));
assert!(!inferrer.is_optional_only_from_usage("tool <FILE> [OPTIONS]"));
assert!(!inferrer.is_optional_only_from_usage("tool COMMAND"));
}
#[test]
fn test_is_interactive_tool() {
let inferrer = BehaviorInferrer::new();
assert!(inferrer.is_interactive_tool("psql"));
assert!(inferrer.is_interactive_tool("python3"));
assert!(inferrer.is_interactive_tool("node"));
assert!(inferrer.is_interactive_tool("mysql"));
assert!(!inferrer.is_interactive_tool("git"));
assert!(!inferrer.is_interactive_tool("backup-suite"));
}
}