a_rusty_cli/
lib.rs

1use std::{collections::HashMap, fmt};
2#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3pub enum OptValue {
4    STRING(String),
5    INT64(i64),
6    UINT64(u64),
7    BOOL(bool),
8}
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct Opt {
12    short: String,
13    long: String,
14    value: OptValue,
15    description: String,
16}
17
18impl Opt {
19    pub fn new(short: &str, long: &str, value: OptValue, description: &str) -> Opt {
20        Opt {
21            short: short.to_string(),
22            long: long.to_string(),
23            value: value,
24            description: description.to_string(),
25        }
26    }
27}
28
29#[derive(Debug, Clone)]
30pub struct CommandError {}
31
32impl CommandError {
33    pub fn new() -> CommandError {
34        CommandError {}
35    }
36}
37
38impl fmt::Display for CommandError {
39    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
40        write!(f, "CommandError")
41    }
42}
43
44pub trait Command {
45    fn opts(&self) -> Vec<Opt> {
46        vec![]
47    }
48    fn run(&self, args: Vec<String>, opts: HashMap<Opt, OptValue>) -> Result<(), CommandError>;
49}
50
51pub struct CommandManager {
52    commands: HashMap<Vec<String>, Box<dyn Command + 'static>>,
53}
54
55pub fn find_opt(opts: HashMap<Opt, OptValue>, key: String) -> Option<OptValue> {
56    for (opt, value) in opts.iter() {
57        if key.eq(format!("--{}", opt.long).as_str()) {
58            return Some(value.clone());
59        }
60
61        if key.eq(format!("-{}", opt.short).as_str()) {
62            return Some(value.clone());
63        }
64    }
65    None
66}
67
68impl CommandManager {
69    pub fn new() -> CommandManager {
70        CommandManager {
71            commands: HashMap::new(),
72        }
73    }
74
75    pub fn add_command(&mut self, name: String, command: Box<dyn Command>) {
76        let name = name.split_whitespace().map(|s| s.to_string()).collect();
77        self.commands.insert(name, command);
78    }
79
80    fn get_command(&self, args: Vec<String>) -> Option<&dyn Command> {
81        // Collect all the keys in the hashmap and sort them by length
82        let keys: Vec<&Vec<String>> = self.commands.keys().collect();
83
84        for key in keys.iter().copied() {
85            if key.len() > args.len() {
86                println!("Key is longer than args");
87                continue;
88            }
89
90            let mut matched = true;
91
92            let args_without_options: Vec<String> = args
93                .iter()
94                .filter(|arg| !arg.starts_with("-"))
95                .map(|arg| arg.clone())
96                .collect();
97
98            for i in 0..key.len() {
99                if !key[i].eq(&args_without_options[i]) {
100                    println!(
101                        "Key does not match {:?} != {:?}",
102                        key[i], args_without_options[i]
103                    );
104                    matched = false;
105                    break;
106                }
107            }
108
109            if matched {
110                println!("Matched key: {:?}", key);
111                return Some(self.commands.get(key).unwrap().as_ref());
112            }
113        }
114
115        println!("No command matched");
116
117        None
118    }
119
120    fn print_help(&self) {
121        println!("Available commands:");
122        for (key, command) in self.commands.iter() {
123            println!("{}", key.join(" "));
124
125            for opt in command.opts() {
126                println!(
127                    "  -{}, --{}: {}",
128                    opt.short, opt.long, opt.description
129                );
130            }
131        }
132    }
133
134    fn parse_opts(&self, args: Vec<String>, opts: Vec<Opt>) -> HashMap<Opt, OptValue> {
135        let mut result = HashMap::new();
136
137        for (pos, arg) in args.iter().enumerate() {
138            if !arg.starts_with("-") {
139                continue;
140            }
141
142
143            let opt_result = opts.iter().find(|opt| {
144                arg.eq(format!("-{}", opt.short).as_str())
145                    || arg.eq(format!("--{}", opt.long).as_str())
146            });
147
148            if opt_result.is_none() {
149                println!("No opt found for {:?}", arg);
150                continue;
151            }
152
153            let opt = opt_result.unwrap();
154
155            match &opt.value {
156                OptValue::INT64(default_val) => {
157                    let value = args
158                        .get(pos + 1)
159                        .unwrap_or(&"".to_string())
160                        .parse::<i64>()
161                        .unwrap_or(*default_val);
162                    result.insert(opt.clone(), OptValue::INT64(value));
163                }
164                OptValue::UINT64(default_value) => {
165                    let value = args
166                        .get(pos + 1)
167                        .unwrap_or(&"".to_string())
168                        .parse::<u64>()
169                        .unwrap_or(*default_value);
170                    result.insert(opt.clone(), OptValue::UINT64(value));
171                }
172                OptValue::STRING(default_value) => {
173                    let value = args.get(pos + 1).unwrap_or(&default_value).clone();
174                    result.insert(opt.clone(), OptValue::STRING(value.clone()));
175                }
176                OptValue::BOOL(_) => {
177                    let value = args
178                        .get(pos + 1)
179                        .unwrap_or(&"".to_string())
180                        .parse::<bool>()
181                        .unwrap_or(true);
182                    result.insert(opt.clone(), OptValue::BOOL(value));
183                }
184            }
185        }
186
187        result
188    }
189
190    pub fn run(&self, args: Vec<String>) -> Result<(), CommandError> {
191        if args.len() == 0  {
192            self.print_help();
193            return Ok(());
194        }
195
196        if  args[0].eq("--help") || args[0].eq("-h") {
197            self.print_help();
198            return Ok(());
199        }
200
201        let command = self.get_command(args.clone()).ok_or(CommandError::new())?;
202
203        let opts = self.parse_opts(args.clone(), command.opts());
204
205        return command.run(args, opts);
206    }
207}