use crate::models::{ScannedFlag, ValueType};
pub fn recover_values_from_descriptions(flags: &mut [ScannedFlag]) {
for flag in flags.iter_mut() {
if flag.value_name.is_some() || flag.value_type != ValueType::Boolean {
continue;
}
let Some((placeholder, rest)) = split_leading_placeholder(&flag.description) else {
continue;
};
flag.value_name = Some(placeholder.clone());
flag.description = rest;
flag.value_type = infer_value_type(&placeholder);
}
}
const PLACEHOLDER_WORDS: &[&str] = &[
"ARG",
"CHAR",
"CMD",
"COMMAND",
"COUNT",
"DATE",
"DIR",
"DIRECTORY",
"EXPR",
"FILE",
"FILENAME",
"FORMAT",
"HOST",
"ID",
"INT",
"KEY",
"LEVEL",
"LIST",
"MODE",
"N",
"NAME",
"NUM",
"NUMBER",
"PATH",
"PATTERN",
"PORT",
"PREFIX",
"RANGE",
"SECONDS",
"SIZE",
"SPEC",
"STRING",
"SUFFIX",
"TEXT",
"TYPE",
"URI",
"URL",
"USER",
"VALUE",
"WHEN",
"WIDTH",
"WORD",
];
fn split_leading_placeholder(description: &str) -> Option<(String, String)> {
let trimmed = description.trim_start();
if trimmed.is_empty() {
return None;
}
if let Some(rest) = trimmed.strip_prefix('<') {
let (inner, tail) = rest.split_once('>')?;
if inner.is_empty() {
return None;
}
return Some((inner.to_string(), tail.trim_start().to_string()));
}
let (first, tail) = match trimmed.split_once(char::is_whitespace) {
Some((first, tail)) => (first, tail.trim_start()),
None => (trimmed, ""),
};
if is_mixed_bracket_placeholder(first) {
return Some((strip_brackets(first), tail.to_string()));
}
if !tail.is_empty() && PLACEHOLDER_WORDS.contains(&first) {
return Some((first.to_string(), tail.to_string()));
}
None
}
fn is_mixed_bracket_placeholder(token: &str) -> bool {
if !token.contains('[') || !token.contains(']') {
return false;
}
let mut depth = 0i32;
let mut outside = 0usize;
for ch in token.chars() {
match ch {
'[' => depth += 1,
']' => depth = (depth - 1).max(0),
_ if depth == 0 => outside += 1,
_ => {}
}
}
outside > 0
}
fn strip_brackets(token: &str) -> String {
token.replace(['[', ']'], "")
}
pub fn infer_value_type(placeholder: &str) -> ValueType {
let lowered = placeholder.to_ascii_lowercase();
match lowered.as_str() {
"seconds" | "fractional seconds" | "secs" | "milliseconds" | "ms" => ValueType::Float,
"float" | "decimal" => ValueType::Float,
"num" | "number" | "count" | "n" | "port" | "int" | "integer" | "size" => {
ValueType::Integer
}
"file" | "filename" | "file name" | "path" | "dir" | "directory" => ValueType::Path,
"url" | "uri" => ValueType::Url,
_ => ValueType::String,
}
}
pub fn flag_value_type(value_name: Option<&str>) -> ValueType {
match value_name {
None => ValueType::Boolean,
Some(placeholder) => infer_value_type(placeholder),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn boolean_flag(long: &str, description: &str) -> ScannedFlag {
ScannedFlag {
long_name: Some(long.to_string()),
description: description.to_string(),
value_type: ValueType::Boolean,
..Default::default()
}
}
#[test]
fn test_recover_values_types_angle_bracket_placeholder() {
let mut flags = vec![boolean_flag(
"--max-time",
"<fractional seconds> Maximum time in seconds that you allow each transfer to take.",
)];
recover_values_from_descriptions(&mut flags);
assert_eq!(flags[0].value_name.as_deref(), Some("fractional seconds"));
assert_eq!(flags[0].value_type, ValueType::Float);
assert!(
flags[0].description.starts_with("Maximum time"),
"placeholder must leave the description: {:?}",
flags[0].description
);
}
#[test]
fn test_recover_values_types_bracketed_placeholder() {
let mut flags = vec![boolean_flag(
"--proxy",
"[protocol://]host[:port] Use this proxy.",
)];
recover_values_from_descriptions(&mut flags);
assert_eq!(flags[0].value_name.as_deref(), Some("protocol://host:port"));
assert_eq!(flags[0].value_type, ValueType::String);
assert_eq!(flags[0].description, "Use this proxy.");
}
#[test]
fn test_recover_values_types_caps_placeholder() {
let mut flags = vec![boolean_flag("--output", "FILE Write output here.")];
recover_values_from_descriptions(&mut flags);
assert_eq!(flags[0].value_name.as_deref(), Some("FILE"));
assert_eq!(flags[0].value_type, ValueType::Path);
}
#[test]
fn test_recover_values_leaves_prose_descriptions_alone() {
let mut flags = vec![
boolean_flag("--verbose", "Make the operation more talkative."),
boolean_flag("--ssl", "SSL"),
boolean_flag("--http2", "HTTP/2 Use HTTP/2."),
];
recover_values_from_descriptions(&mut flags);
for flag in &flags {
assert_eq!(
flag.value_type,
ValueType::Boolean,
"boolean flag retyped from prose: {flag:?}"
);
assert!(flag.value_name.is_none(), "{flag:?}");
}
}
#[test]
fn test_recover_values_leaves_platform_tagged_boolean_switches_alone() {
let mut flags = vec![
boolean_flag(
"--archive-clear",
"[WIN32] Once archive is created, clear the archive bits of files.",
),
boolean_flag(
"--datafork",
"[MacOS] Include only data-fork of files zipped.",
),
boolean_flag("--echo-wid", "GTK GUI only: Echo the Window ID on stdout."),
boolean_flag(
"-noprebind",
"LD_PREBIND is no longer supported as a way to enable prebinding.",
),
boolean_flag("--experimental", "[experimental] Enable the new resolver."),
];
recover_values_from_descriptions(&mut flags);
for flag in &flags {
assert_eq!(
flag.value_type,
ValueType::Boolean,
"a documentation tag is not a value placeholder: {flag:?}"
);
assert!(flag.value_name.is_none(), "{flag:?}");
}
}
#[test]
fn test_recover_values_still_takes_a_mixed_bracket_placeholder() {
let mut flags = vec![
boolean_flag("--proxy", "[protocol://]host[:port] Use this proxy."),
boolean_flag("-D", "[bind_address:]port Dynamic port forwarding."),
];
recover_values_from_descriptions(&mut flags);
assert_eq!(flags[0].value_name.as_deref(), Some("protocol://host:port"));
assert_eq!(flags[1].value_name.as_deref(), Some("bind_address:port"));
}
#[test]
fn test_flag_value_type_is_the_scanner_wide_table() {
assert_eq!(flag_value_type(None), ValueType::Boolean);
assert_eq!(flag_value_type(Some("FILE")), ValueType::Path);
assert_eq!(flag_value_type(Some("file")), ValueType::Path);
assert_eq!(flag_value_type(Some("SIZE")), ValueType::Integer);
assert_eq!(flag_value_type(Some("FLOAT")), ValueType::Float);
assert_eq!(flag_value_type(Some("DECIMAL")), ValueType::Float);
assert_eq!(flag_value_type(Some("INTEGER")), ValueType::Integer);
assert_eq!(flag_value_type(Some("SECONDS")), ValueType::Float);
assert_eq!(flag_value_type(Some("URL")), ValueType::Url);
assert_eq!(flag_value_type(Some("WIDGET")), ValueType::String);
}
#[test]
fn test_infer_value_type_matches_durations_as_whole_words() {
assert_eq!(infer_value_type("time"), ValueType::String);
assert_eq!(infer_value_type("datetime"), ValueType::String);
assert_eq!(infer_value_type("timestamp"), ValueType::String);
assert_eq!(infer_value_type("seconds"), ValueType::Float);
assert_eq!(infer_value_type("fractional seconds"), ValueType::Float);
assert_eq!(infer_value_type("milliseconds"), ValueType::Float);
}
#[test]
fn test_recover_values_does_not_override_a_parsed_value_name() {
let mut flag = boolean_flag("--output", "FILE Write output here.");
flag.value_name = Some("TARGET".to_string());
flag.value_type = ValueType::Path;
let mut flags = vec![flag];
recover_values_from_descriptions(&mut flags);
assert_eq!(flags[0].value_name.as_deref(), Some("TARGET"));
assert_eq!(flags[0].description, "FILE Write output here.");
}
}