options.rs 8.6 KB
Newer Older
B
Ben S 已提交
1
extern crate getopts;
2
extern crate natord;
B
Ben S 已提交
3

B
Ben S 已提交
4
use file::File;
5
use column::{Column, SizeFormat};
B
Ben S 已提交
6
use column::Column::*;
7
use output::View;
B
Ben S 已提交
8 9 10
use term::dimensions;

use std::ascii::AsciiExt;
B
Benjamin Sago 已提交
11
use std::slice::Iter;
B
Ben S 已提交
12

13
#[derive(PartialEq, Show)]
B
Ben S 已提交
14
pub enum SortField {
B
Ben S 已提交
15
    Unsorted, Name, Extension, Size, FileInode
B
Ben S 已提交
16 17
}

B
Ben S 已提交
18 19
impl Copy for SortField { }

B
Ben S 已提交
20
impl SortField {
21
    fn from_word(word: String) -> Result<SortField, Error> {
B
Ben S 已提交
22
        match word.as_slice() {
23 24 25 26 27 28
            "name"  => Ok(SortField::Name),
            "size"  => Ok(SortField::Size),
            "ext"   => Ok(SortField::Extension),
            "none"  => Ok(SortField::Unsorted),
            "inode" => Ok(SortField::FileInode),
            field   => Err(no_sort_field(field))
B
Ben S 已提交
29 30 31 32
        }
    }
}

33 34 35 36
fn no_sort_field(field: &str) -> Error {
    Error::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--sort {}", field)))
}

37
#[derive(PartialEq, Show)]
B
Ben S 已提交
38
pub struct Options {
B
Ben S 已提交
39
    pub list_dirs: bool,
40
    pub path_strs: Vec<String>,
B
Benjamin Sago 已提交
41 42 43
    reverse: bool,
    show_invisibles: bool,
    sort_field: SortField,
B
Ben S 已提交
44 45 46
    pub view: View,
}

47
#[derive(PartialEq, Show)]
48 49 50
pub enum Error {
    InvalidOptions(getopts::Fail),
    Help(String),
51 52
    Conflict(&'static str, &'static str),
    Useless(&'static str, bool, &'static str),
53 54
}

B
Ben S 已提交
55
impl Options {
56
    pub fn getopts(args: &[String]) -> Result<Options, Error> {
57
        let opts = &[
B
Ben S 已提交
58 59 60
            getopts::optflag("1", "oneline",   "display one entry per line"),
            getopts::optflag("a", "all",       "show dot-files"),
            getopts::optflag("b", "binary",    "use binary prefixes in file sizes"),
B
Ben S 已提交
61
            getopts::optflag("B", "bytes",     "list file sizes in bytes, without prefixes"),
62
            getopts::optflag("d", "list-dirs", "list directories as regular files"),
B
Ben S 已提交
63 64 65 66 67 68 69 70 71
            getopts::optflag("g", "group",     "show group as well as user"),
            getopts::optflag("h", "header",    "show a header row at the top"),
            getopts::optflag("H", "links",     "show number of hard links"),
            getopts::optflag("l", "long",      "display extended details and attributes"),
            getopts::optflag("i", "inode",     "show each file's inode number"),
            getopts::optflag("r", "reverse",   "reverse order of files"),
            getopts::optopt ("s", "sort",      "field to sort by", "WORD"),
            getopts::optflag("S", "blocks",    "show number of file system blocks"),
            getopts::optflag("x", "across",    "sort multi-column view entries across"),
B
Ben S 已提交
72
            getopts::optflag("?", "help",      "show list of command-line options"),
B
Ben S 已提交
73
        ];
B
Ben S 已提交
74

75
        let matches = match getopts::getopts(args, opts) {
76
            Ok(m) => m,
77
            Err(e) => return Err(Error::InvalidOptions(e)),
B
Ben S 已提交
78
        };
B
Ben S 已提交
79

B
Ben S 已提交
80
        if matches.opt_present("help") {
81
            return Err(Error::Help(getopts::usage("Usage:\n  exa [options] [files...]", opts)));
B
Ben S 已提交
82
        }
B
Ben S 已提交
83

84 85 86 87 88
        let sort_field = match matches.opt_str("sort") {
            Some(word) => try!(SortField::from_word(word)),
            None => SortField::Name,
        };

B
Ben S 已提交
89
        Ok(Options {
90 91 92 93
            list_dirs:       matches.opt_present("list-dirs"),
            path_strs:       if matches.free.is_empty() { vec![ ".".to_string() ] } else { matches.free.clone() },
            reverse:         matches.opt_present("reverse"),
            show_invisibles: matches.opt_present("all"),
94
            sort_field:      sort_field,
95
            view:            try!(view(&matches)),
96
        })
B
Ben S 已提交
97
    }
B
Ben S 已提交
98

B
Benjamin Sago 已提交
99 100 101 102
    pub fn path_strings(&self) -> Iter<String> {
        self.path_strs.iter()
    }

103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
    pub fn transform_files<'a>(&self, unordered_files: Vec<File<'a>>) -> Vec<File<'a>> {
        let mut files: Vec<File<'a>> = unordered_files.into_iter()
            .filter(|f| self.should_display(f))
            .collect();

        match self.sort_field {
            SortField::Unsorted => {},
            SortField::Name => files.sort_by(|a, b| natord::compare(a.name.as_slice(), b.name.as_slice())),
            SortField::Size => files.sort_by(|a, b| a.stat.size.cmp(&b.stat.size)),
            SortField::FileInode => files.sort_by(|a, b| a.stat.unstable.inode.cmp(&b.stat.unstable.inode)),
            SortField::Extension => files.sort_by(|a, b| {
                let exts  = a.ext.clone().map(|e| e.to_ascii_lowercase()).cmp(&b.ext.clone().map(|e| e.to_ascii_lowercase()));
                let names = a.name.to_ascii_lowercase().cmp(&b.name.to_ascii_lowercase());
                exts.cmp(&names)
            }),
B
Ben S 已提交
118
        }
119 120 121 122 123 124 125 126 127 128 129

        if self.reverse {
            files.reverse();
        }

        files
    }

    fn should_display(&self, f: &File) -> bool {
        if self.show_invisibles {
            true
B
Ben S 已提交
130 131
        }
        else {
132
            !f.name.as_slice().starts_with(".")
B
Ben S 已提交
133 134
        }
    }
135
}
B
Ben S 已提交
136

137 138 139 140
fn view(matches: &getopts::Matches) -> Result<View, Error> {
    if matches.opt_present("long") {
        if matches.opt_present("across") {
            Err(Error::Useless("across", true, "long"))
B
Ben S 已提交
141
        }
142 143
        else if matches.opt_present("oneline") {
            Err(Error::Useless("across", true, "long"))
B
Ben S 已提交
144
        }
145 146
        else {
            Ok(View::Details(try!(columns(matches)), matches.opt_present("header")))
147
        }
148 149 150 151 152 153 154 155 156 157
    }
    else if matches.opt_present("binary") {
        Err(Error::Useless("binary", false, "long"))
    }
    else if matches.opt_present("bytes") {
        Err(Error::Useless("bytes", false, "long"))
    }
    else if matches.opt_present("oneline") {
        if matches.opt_present("across") {
            Err(Error::Useless("across", true, "oneline"))
158 159
        }
        else {
160
            Ok(View::Lines)
161
        }
162 163 164 165 166
    }
    else {
        match dimensions() {
            None => Ok(View::Lines),
            Some((width, _)) => Ok(View::Grid(matches.opt_present("across"), width)),
B
Ben S 已提交
167
        }
168 169
    }
}
B
Ben S 已提交
170

171 172 173
fn file_size(matches: &getopts::Matches) -> Result<SizeFormat, Error> {
    let binary = matches.opt_present("binary");
    let bytes = matches.opt_present("bytes");
B
Ben S 已提交
174

175 176 177 178 179 180 181 182 183 184
    match (binary, bytes) {
        (true,  true ) => Err(Error::Conflict("binary", "bytes")),
        (true,  false) => Ok(SizeFormat::BinaryBytes),
        (false, true ) => Ok(SizeFormat::JustBytes),
        (false, false) => Ok(SizeFormat::DecimalBytes),
    }
}

fn columns(matches: &getopts::Matches) -> Result<Vec<Column>, Error> {
    let mut columns = vec![];
B
Ben S 已提交
185

186 187
    if matches.opt_present("inode") {
        columns.push(Inode);
B
Ben S 已提交
188 189
    }

190 191 192 193
    columns.push(Permissions);

    if matches.opt_present("links") {
        columns.push(HardLinks);
B
Ben S 已提交
194
    }
B
Ben S 已提交
195

196
    columns.push(FileSize(try!(file_size(matches))));
197

198 199 200
    if matches.opt_present("blocks") {
        columns.push(Blocks);
    }
201

202
    columns.push(User);
203

204 205
    if matches.opt_present("group") {
        columns.push(Group);
206
    }
207 208 209

    columns.push(FileName);
    Ok(columns)
B
Ben S 已提交
210
}
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249

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

    fn is_helpful(error: Result<Options, Error>) -> bool {
        match error {
            Err(Help(_)) => true,
            _            => false,
        }
    }

    #[test]
    fn help() {
        let opts = Options::getopts(&[ "--help".to_string() ]);
        assert!(is_helpful(opts))
    }

    #[test]
    fn help_with_file() {
        let opts = Options::getopts(&[ "--help".to_string(), "me".to_string() ]);
        assert!(is_helpful(opts))
    }

    #[test]
    fn files() {
        let opts = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]);
        assert_eq!(opts.unwrap().path_strs, vec![ "this file".to_string(), "that file".to_string() ])
    }

    #[test]
    fn no_args() {
        let opts = Options::getopts(&[]);
        assert_eq!(opts.unwrap().path_strs, vec![ ".".to_string() ])
    }

    #[test]
250 251 252 253 254 255 256 257 258
    fn file_sizes() {
        let opts = Options::getopts(&[ "--long".to_string(), "--binary".to_string(), "--bytes".to_string() ]);
        assert_eq!(opts.unwrap_err(), Error::Conflict("binary", "bytes"))
    }

    #[test]
    fn just_binary() {
        let opts = Options::getopts(&[ "--binary".to_string() ]);
        assert_eq!(opts.unwrap_err(), Error::Useless("binary", false, "long"))
259
    }
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279

    #[test]
    fn just_bytes() {
        let opts = Options::getopts(&[ "--bytes".to_string() ]);
        assert_eq!(opts.unwrap_err(), Error::Useless("bytes", false, "long"))
    }

    #[test]
    fn long_across() {
        let opts = Options::getopts(&[ "--long".to_string(), "--across".to_string() ]);
        assert_eq!(opts.unwrap_err(), Error::Useless("across", true, "long"))
    }

    #[test]
    fn oneline_across() {
        let opts = Options::getopts(&[ "--oneline".to_string(), "--across".to_string() ]);
        assert_eq!(opts.unwrap_err(), Error::Useless("across", true, "oneline"))
    }


280
}