1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201

use std::collections::HashMap;
use std::env;
use strsim::levenshtein;
use crate::CliCommand;
use crate::help::CliHelpScreen;
use crate::*;

pub struct CliRouter {
    pub commands: HashMap<String, Box<dyn CliCommand>>,
    pub shortcuts: HashMap<String, String>,
    pub value_flags: HashMap<String, Vec<String>>,
    pub categories: HashMap<String, (String, String)>
}

pub struct CliRequest {
    pub cmd_alias: String,
    pub is_help: bool,
    pub args: Vec<String>,
    pub flags: Vec<String>,
    pub value_flags: HashMap<String, String>,
    pub shortcuts: Vec<String>
}

impl Default for CliRouter {

    fn default() -> Self {
        Self::new()
    }
}

impl CliRouter {

    pub fn new() -> Self {

        Self {
            commands: HashMap::new(),
            shortcuts: HashMap::new(),
            value_flags: HashMap::new(),
            categories: HashMap::new()
        }

    }

    /// Link a struct / impl that inherits the CliRouter trait to a command name.  Takes three arguments, 
        /// the full name of the command, a vector of available shortcuts, and a vector of 
        /// long-form flags (prefixed with dashes (--)) for which a value is expected.
    pub fn add<T: CliCommand + Default + 'static>(&mut self, alias: &str, shortcuts: Vec<&str>, value_flags: Vec<&str>) {

        // Add to list of commands
        let cmd = Box::<T>::default();
        self.commands.insert(alias.to_string(), cmd);

        // Add shortcuts
        for shortcut in shortcuts {
            self.shortcuts.insert(shortcut.to_string(), alias.to_string());
        }

        // Add value flags
        let flags: Vec<String> = value_flags.iter().map(|s| s.to_string()).collect();
        self.value_flags.insert(alias.to_string(), flags);
    }

    /// Taking arguments passed via command line into account,  checks all routes that were added and 
    /// tries to determine the correct impl to execute.  This function is automatically 
    /// executed by the cli_run() function and should never be manually executed.
    pub fn lookup(&self) -> (&Box<dyn CliCommand>, CliRequest) {

        // Get args
        let mut cmdargs: Vec<String> = env::args().collect();
        cmdargs.remove(0);
        if cmdargs.is_empty() {
            cli_error("You did not specify a command to run.  Please specify a command or use 'help' or '-h' to view a list of all available commands.");
        }

        // Blank variables
        let mut extra_args: Vec<String> = Vec::new();
        let mut cmd_alias = String::new();

        // Check if help
        let mut is_help: bool = false;
        if cmdargs[0] == "help" || cmdargs[0] == "-h" {
            is_help = true;
            cmdargs.remove(0);
        }

        // Check for help index
        if is_help && cmdargs.is_empty() {
            CliHelpScreen::render_index(self);
        }

        // Check routing table for command
        loop {

            // Check for zero cmdargs
            if cmdargs.is_empty() && is_help {
                break;
            } else if cmdargs.is_empty() {
                cli_error("No command exists with that name.  Use either 'help' or the -h flag to view a list of all available commands.");
            }
            let alias = cmdargs.join(" ").to_string();

            if self.commands.contains_key(&alias) {
                cmd_alias = alias;
                break;
            } else if self.shortcuts.contains_key(&alias) {
                cmd_alias = self.shortcuts.get(&alias).unwrap().to_string();
                break;
            } else if is_help && self.categories.contains_key(&alias) {
                CliHelpScreen::render_category(self, &alias);
            } else if let Some(found_cmd) = self.lookup_similar(&alias) {
                let confirm_msg = format!("No command with that name exists, but a similar command with the name '{}' does exist.  Is this the command you wish to run?", found_cmd);
                if cli_confirm(&confirm_msg) {
                    cmd_alias = found_cmd.to_string();
                    break;
                }
            }
            extra_args.insert(0, cmdargs.pop().unwrap());
        }

        // Set variables
        let mut args: Vec<String> = Vec::new();
        let mut flags: Vec<String> = Vec::new();
        let mut value_flags: HashMap<String, String> = HashMap::new();
        let flag_values = self.value_flags.get(&cmd_alias).unwrap();

        // Get flags
        while !extra_args.is_empty() {
            let chk_arg = extra_args[0].to_string();    
            extra_args.remove(0);

            if chk_arg.starts_with("--") {
                let arg = chk_arg.trim_start_matches('-').to_string();
                if flag_values.contains(&arg) {
                    value_flags.insert(arg, extra_args[0].to_string());
                extra_args.remove(0);
                } else {
                    flags.push(arg);
                }

            } else if chk_arg.starts_with('-') {
                let arg = chk_arg.trim_start_matches('-').to_string();
                for c in arg.chars() {
                    flags.push(c.to_string());
                }

            } else {
                args.push(chk_arg);
            }
        }

        // Get all shortcuts
        let shortcuts: Vec<String> = self.shortcuts.iter().filter_map(|(key, value)| {
            if *value == cmd_alias {
                Some(key.to_string())
            } else {
                None
            }
        }).collect();

        let cmd = self.commands.get(&cmd_alias).unwrap();
        let req = CliRequest {
            cmd_alias,
            is_help,
            args,
            flags,
            value_flags,
            shortcuts
        };

        (cmd, req)
    }

    /// Never needs to be manually executed, and is used when a full match for the command name can not 
    /// be found.  Uses the levenshtein to see if any commands closely resemble the 
    /// command name given in case of typo.
    fn lookup_similar(&self, chk_cmd: &String) -> Option<&String> {

        let mut distance = 4;
        let mut res = None;

        for cmd in self.commands.keys() {
            let num = levenshtein(chk_cmd, cmd);
            if num < distance {
                distance = num;
                res = Some(cmd)
            }
        }

        res
    }

    /// Add a new top level category that contains CLI commands.  Used for organization and to display proper help screens.
    pub fn add_category(&mut self, alias: &str, name: &str, description: &str) {
        self.categories.insert(alias.to_string(), (name.to_string(), description.to_string()));
    }

}