rusht/escape/
namesafe_args.rs1use ::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 #[arg(action = ArgAction::SetTrue, value_parser = BoolishValueParser::new().map(Charset::from_allow), short = 'u', long = "allow-unicode")]
16 pub charset: Charset,
18 #[arg(
20 short = 'x',
21 long = "hash",
22 default_value = "changed", )]
24 pub hash_policy: HashPolicy,
25 #[arg(short = 'l', long = "max-length", default_value = "32")]
27 pub max_length: u32,
28 #[arg(short = 'e', long = "extension")]
30 pub keep_extension: bool,
31 #[arg(short = 'E', long = "keep-tail")]
33 pub keep_tail: bool,
34 #[arg(short = '0', long = "allow-empty", conflicts_with = "input")]
36 pub allow_empty: bool,
37 #[arg(short = 'c', long = "lowercase")]
39 pub lowercase: bool,
40 #[arg(short = 'C', long)]
42 pub allow_outer_connector: bool,
43 #[arg(short = 'i', long)]
44 pub input: Option<String>,
46 #[arg(short = '1', long = "single", conflicts_with = "input")]
48 pub single_line: bool,
49 #[arg(short = 'S', long)]
51 pub separator: Option<char>,
52}
53#[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}