rusht/escape/
namesafe_args.rs

1use ::clap::builder::BoolishValueParser;
2use ::clap::builder::TypedValueParser;
3use ::clap::Parser;
4use ::std::str::FromStr;
5
6use ::clap::ArgAction;
7
8#[derive(Parser, Debug)]
9#[command(
10    name = "namesafe",
11    about = "Convert each line to a string that is safe for names (no whitespace or special characters, not too long)."
12)]
13pub struct NamesafeArgs {
14    /// Allow non-ascii letters (but no non-letter symbols).
15    #[arg(action = ArgAction::SetTrue, value_parser = BoolishValueParser::new().map(Charset::from_allow), short = 'u', long = "allow-unicode")]
16    // #value_parser = Charset::from_allow,
17    pub charset: Charset,
18    /// In which cases to include a hash in the name ([a]lways, [c]hanged, too-[l]ong, [n]ever).
19    #[arg(
20        short = 'x',
21        long = "hash",
22        default_value = "changed",  //TODO @mverleg: not sure why Default impl doesn't work
23    )]
24    pub hash_policy: HashPolicy,
25    /// Maximum number of characters in the cleaned line (min 8).
26    #[arg(short = 'l', long = "max-length", default_value = "32")]
27    pub max_length: u32,
28    /// If the line appears to contain an filename extension (max 4 chars), preserve it.
29    #[arg(short = 'e', long = "extension")]
30    pub keep_extension: bool,
31    /// If the command has to be shortened, keep the end part instead of the start.
32    #[arg(short = 'E', long = "keep-tail")]
33    pub keep_tail: bool,
34    /// Do not fail if there are no input lines.
35    #[arg(short = '0', long = "allow-empty", conflicts_with = "input")]
36    pub allow_empty: bool,
37    /// Turn all capital letters to lowercase.
38    #[arg(short = 'c', long = "lowercase")]
39    pub lowercase: bool,
40    /// Allow dashes and underscores at the start or end of names
41    #[arg(short = 'C', long)]
42    pub allow_outer_connector: bool,
43    #[arg(short = 'i', long)]
44    /// If this string is provided, do matching on that and ignore stdin.
45    pub input: Option<String>,
46    /// Expect exactly one input line. Fail if more. Fail if fewer unless --allow_empty.
47    #[arg(short = '1', long = "single", conflicts_with = "input")]
48    pub single_line: bool,
49    /// Only allow one separator, instead of both - and _
50    #[arg(short = 'S', long)]
51    pub separator: Option<char>,
52}
53//TODO @mverleg: when to hash? (always, if changed, if too long, never)
54
55#[test]
56fn test_cli_args() {
57    NamesafeArgs::try_parse_from(&["cmd", "-l", "16", "-x", "a", "--keep-tail"]).unwrap();
58}
59
60#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
61pub enum HashPolicy {
62    Always,
63    #[default]
64    Changed,
65    TooLong,
66    Never,
67}
68
69impl HashPolicy {
70    pub fn should_hash(&self, was_changed: bool, was_too_long: bool) -> bool {
71        match self {
72            HashPolicy::Always => true,
73            HashPolicy::Changed => was_changed || was_too_long,
74            HashPolicy::TooLong => was_too_long,
75            HashPolicy::Never => false,
76        }
77    }
78}
79
80impl FromStr for HashPolicy {
81    type Err = String;
82
83    fn from_str(text: &str) -> Result<Self, Self::Err> {
84        Ok(match text.to_lowercase().as_str() {
85            "always" | "a" => HashPolicy::Always,
86            "changed" | "c" => HashPolicy::Changed,
87            "too-long" | "long" | "l" => HashPolicy::TooLong,
88            "never" | "n" => HashPolicy::Never,
89            other => return Err(format!("unknown hash policy: {}", other)),
90        })
91    }
92}
93
94#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
95pub enum Charset {
96    AllowUnicode,
97    #[default]
98    AsciiOnly,
99}
100
101impl Charset {
102    fn from_allow(allow_unicode: bool) -> Self {
103        if allow_unicode {
104            Charset::AllowUnicode
105        } else {
106            Charset::AsciiOnly
107        }
108    }
109
110    pub fn is_allowed(&self, symbol: char, separator: Option<char>) -> bool {
111        let is_sep = match separator {
112            None => symbol == '-' || symbol == '_',
113            Some(sep) => symbol == sep,
114        };
115        if is_sep {
116            return true;
117        }
118        match self {
119            Charset::AllowUnicode => symbol.is_alphanumeric(),
120            Charset::AsciiOnly => {
121                ('a'..='z').contains(&symbol)
122                    || ('A'..='Z').contains(&symbol)
123                    || ('0'..='9').contains(&symbol)
124            }
125        }
126    }
127}
128
129impl Default for NamesafeArgs {
130    fn default() -> Self {
131        NamesafeArgs {
132            charset: Default::default(),
133            hash_policy: Default::default(),
134            max_length: 32,
135            keep_extension: false,
136            keep_tail: false,
137            allow_empty: false,
138            lowercase: false,
139            allow_outer_connector: false,
140            input: None,
141            single_line: false,
142            separator: None,
143        }
144    }
145}