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
use clap::{App, Arg};

/// Struct to hold command line arguments
pub struct Options {
    pub max_depth: Option<usize>,
    pub follow_sym_links: bool,
    pub show_hidden: bool,
    pub root: Option<String>,
    pub dir_only: bool,
    pub pattern: Option<String>,
    pub extension: Option<String>,
    pub line_count: bool,
    pub no_colours: bool,
}

impl Options {
    pub fn new(
        root: Option<String>,
        max_depth: Option<usize>,
        follow_sym_links: bool,
        show_hidden: bool,
        dir_only: bool,
        pattern: Option<String>,
        extension: Option<String>,
        line_count: bool,
        no_colours: bool,
    ) -> Options {
        Options {
            root,
            max_depth,
            follow_sym_links,
            show_hidden,
            dir_only,
            pattern,
            extension,
            line_count,
            no_colours,
        }
    }
}

/// parses command line arguments using clap and returns an Option struct encapsulating those arguments.
pub fn parse_args() -> Options {
    let matches = App::new("tre")
        .version("1.0")
        .author("Jacky Zhen. <me@jackyzhen.com>, Tom Arrell. <me@tom.arrell.com>")
        .about("List contents of directories in a tree-like format.")
        .arg(
            Arg::with_name("path")
                .help("Start from specific path.")
                .index(1)
                .takes_value(true),
        ).arg(
            Arg::with_name("directories")
                .short("d")
                .long("directories")
                .help("List directories only.")
                .takes_value(false),
        ).arg(
            Arg::with_name("all")
                .short("a")
                .long("all")
                .help("All files are printed. By default tre does not print hidden files (those beginning with a dot '.').  In no event does tree print the file system constructs `.' (current directory) and `..' (previous directory).")
                .takes_value(false),
        ).arg(
            Arg::with_name("symbolic")
                .short("s")
                .long("symbolic")
                .help("Follows symbolic links if they point to directories, as if they were directories")
                .takes_value(false),
        ).arg(
            Arg::with_name("level")
                .short("l")
                .long("level")
                .help("Max display depth of the directory tree.")
                .validator(is_numeric)
                .takes_value(true),
        ).arg(
            Arg::with_name("pattern")
                .short("p")
                .long("pattern")
                .help("filter with a name pattern.")
                .takes_value(true),
        ).arg(
            Arg::with_name("extension")
                .short("e")
                .long("extension")
                .help("filter with a file extension.")
                .takes_value(true),
        ).arg(
            Arg::with_name("linecount")
                .short("c")
                .long("linecount")
                .help("count and display lines of files.")
                .takes_value(false),
        ).arg(
            Arg::with_name("nocolours")
                .short("n")
                .long("nocolours")
                .help("output without colours.")
                .takes_value(false),
        ).get_matches();

    let max_depth: Option<usize> = match matches.value_of("level") {
        Some(l) => Some(l.parse().unwrap()),
        None => None,
    };

    Options::new(
        matches.value_of("path").map(|s| s.to_string()),
        max_depth,
        matches.is_present("symbolic"),
        matches.is_present("all"),
        matches.is_present("directories"),
        matches.value_of("pattern").map(|s| s.to_string()),
        matches.value_of("extension").map(|s| s.to_string()),
        matches.is_present("linecount"),
        matches.is_present("nocolours"),
    )
}

fn is_numeric(val: String) -> Result<(), String> {
    match val.parse::<usize>() {
        Ok(_) => Ok(()),
        Err(_) => Err(String::from("Value wasn't a valid number!")),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn is_numeric_works() {
        assert_eq!(is_numeric(String::from("2")), Ok(()));
    }

    #[test]
    fn is_numeric_works_zero() {
        assert_eq!(is_numeric(String::from("0")), Ok(()));
    }

    #[test]
    fn is_numeric_works_invalid() {
        let error = String::from("Value wasn't a valid number!");
        assert_eq!(is_numeric(String::from("low")), Err(error));
    }
}