use crate::lang::{LANGUAGE_SPECS, Language};
use crate::{Target, TargetAddress};
use anyhow::{Context, Result, bail};
use argh::FromArgs;
use std::path::{Path, PathBuf};
#[derive(Debug, FromArgs)]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct Cli {
#[argh(switch, short = 'V')]
pub(crate) version: bool,
#[argh(subcommand)]
pub(crate) command: Option<Command>,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand)]
pub(crate) enum Command {
Detect(FileCommand),
Read(ReadCommand),
Map(MapCommand),
Symbol(SymbolCommand),
Identify(IdentifyCommand),
Definition(DefinitionCommand),
References(ReferencesCommand),
Search(SearchCommand),
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "file")]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct FileCommand {
#[argh(positional)]
pub(crate) target: Option<String>,
#[argh(switch)]
pub(crate) stdin: bool,
#[argh(option)]
pub(crate) path: Option<PathBuf>,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "read")]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct ReadCommand {
#[argh(positional)]
pub(crate) target: Option<String>,
#[argh(switch)]
pub(crate) stdin: bool,
#[argh(option)]
pub(crate) path: Option<PathBuf>,
#[argh(option)]
pub(crate) start: Option<usize>,
#[argh(option)]
pub(crate) end: Option<usize>,
#[argh(option)]
pub(crate) offset: Option<usize>,
#[argh(option)]
pub(crate) limit: Option<usize>,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "map")]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct MapCommand {
#[argh(positional)]
pub(crate) target: Option<String>,
#[argh(switch)]
pub(crate) stdin: bool,
#[argh(option)]
pub(crate) path: Option<PathBuf>,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "symbol")]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct SymbolCommand {
#[argh(positional)]
pub(crate) args: Vec<String>,
#[argh(switch)]
pub(crate) stdin: bool,
#[argh(option)]
pub(crate) path: Option<PathBuf>,
#[argh(option)]
pub(crate) line: Option<usize>,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "identify")]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct IdentifyCommand {
#[argh(positional)]
pub(crate) target: Option<String>,
#[argh(switch)]
pub(crate) stdin: bool,
#[argh(option)]
pub(crate) path: Option<PathBuf>,
#[argh(option)]
pub(crate) line: Option<usize>,
#[argh(option)]
pub(crate) column: Option<usize>,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "definition")]
#[argh(help_triggers("-h", "--help"))]
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct DefinitionCommand {
#[argh(positional)]
pub(crate) target: PathBuf,
#[argh(positional)]
pub(crate) name: Option<String>,
#[argh(switch)]
pub(crate) stdin: bool,
#[argh(switch)]
pub(crate) compact: bool,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
#[argh(switch, short = 'c')]
pub(crate) cached: bool,
#[argh(switch, short = 'o')]
pub(crate) others: bool,
#[argh(switch, short = 'i')]
pub(crate) ignored: bool,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "references")]
#[argh(help_triggers("-h", "--help"))]
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct ReferencesCommand {
#[argh(positional)]
pub(crate) target: PathBuf,
#[argh(positional)]
pub(crate) name: String,
#[argh(switch)]
pub(crate) compact: bool,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
#[argh(switch, short = 'c')]
pub(crate) cached: bool,
#[argh(switch, short = 'o')]
pub(crate) others: bool,
#[argh(switch, short = 'i')]
pub(crate) ignored: bool,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "search")]
#[argh(help_triggers("-h", "--help"))]
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct SearchCommand {
#[argh(positional)]
pub(crate) target: PathBuf,
#[argh(positional)]
pub(crate) pattern: String,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
#[argh(switch, short = 'c')]
pub(crate) cached: bool,
#[argh(switch, short = 'o')]
pub(crate) others: bool,
#[argh(switch, short = 'i')]
pub(crate) ignored: bool,
}
pub(crate) fn parse_language(value: &str) -> std::result::Result<Language, String> {
let alias = value.to_ascii_lowercase().replace(['-', '_'], "");
if alias == "unknown" {
return Ok(Language::Unknown);
}
LANGUAGE_SPECS
.iter()
.find_map(|spec| {
spec.aliases
.contains(&alias.as_str())
.then_some(spec.language)
})
.ok_or_else(|| format!("unknown language: {value}"))
}
pub(crate) fn parse_input_target(
target: Option<&str>,
stdin: bool,
path: Option<&Path>,
) -> Result<Target> {
parse_input_target_with(target, stdin, path, parse_target)
}
pub(crate) fn parse_symbol_input_target(
target: Option<&str>,
stdin: bool,
path: Option<&Path>,
) -> Result<Target> {
parse_input_target_with(target, stdin, path, parse_symbol_target)
}
fn parse_input_target_with(
target: Option<&str>,
stdin: bool,
path: Option<&Path>,
parse: fn(&str) -> Result<Target>,
) -> Result<Target> {
if stdin {
if target.is_some() {
bail!("target cannot be combined with --stdin");
}
let path = path.context("--stdin requires --path")?;
return Ok(Target {
path: path.to_path_buf(),
address: None,
});
}
if path.is_some() {
bail!("--path requires --stdin");
}
parse(target.context("target required")?)
}
pub(crate) fn symbol_args(args: &[String], stdin: bool) -> Result<(Option<&str>, Option<&str>)> {
match (stdin, args) {
(true, []) => Ok((None, None)),
(true, [address]) => Ok((None, Some(address.as_str()))),
(true, _) => bail!("symbol with --stdin accepts at most one qualified name argument"),
(false, [target]) => Ok((Some(target.as_str()), None)),
(false, [target, address]) => Ok((Some(target.as_str()), Some(address.as_str()))),
(false, []) => bail!("target required"),
(false, _) => bail!("symbol accepts at most target and qualified name arguments"),
}
}
fn parse_target(value: &str) -> Result<Target> {
if value.is_empty() {
bail!("target must not be empty");
}
if let Some((path, suffix)) = value.rsplit_once(':') {
if path.is_empty() {
bail!("target path must not be empty");
}
if suffix.chars().all(|ch| ch.is_ascii_digit()) {
let line = suffix
.parse::<usize>()
.with_context(|| format!("invalid target line: {suffix}"))?;
if line == 0 {
bail!("target line must be greater than zero");
}
return Ok(Target {
path: PathBuf::from(path),
address: Some(TargetAddress::Line(line)),
});
}
if is_line_hash(suffix) {
return Ok(Target {
path: PathBuf::from(path),
address: Some(TargetAddress::Hash(suffix.to_ascii_lowercase())),
});
}
}
Ok(Target {
path: PathBuf::from(value),
address: None,
})
}
fn parse_symbol_target(value: &str) -> Result<Target> {
let target = parse_target(value)?;
if target.address.is_some() || Path::new(value).exists() {
return Ok(target);
}
let Some((path, symbol)) = value.rsplit_once(':') else {
return Ok(target);
};
if path.is_empty() || symbol.is_empty() {
return Ok(target);
}
Ok(Target {
path: PathBuf::from(path),
address: Some(TargetAddress::Symbol(symbol.to_owned())),
})
}
fn is_line_hash(value: &str) -> bool {
value.len() == 3 && value.chars().all(|ch| ch.is_ascii_hexdigit())
}