options.rs 10.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;
12
use std::fmt;
B
Ben S 已提交
13

B
Ben S 已提交
14
use self::Misfire::*;
15

B
Ben S 已提交
16 17
/// The *Options* struct represents a parsed version of the user's
/// command-line options.
18
#[derive(PartialEq, Debug)]
B
Ben S 已提交
19
pub struct Options {
B
Ben S 已提交
20
    pub list_dirs: bool,
21
    pub path_strs: Vec<String>,
B
Benjamin Sago 已提交
22 23 24
    reverse: bool,
    show_invisibles: bool,
    sort_field: SortField,
B
Ben S 已提交
25 26 27
    pub view: View,
}

B
Ben S 已提交
28
impl Options {
B
Ben S 已提交
29 30 31

    /// Call getopts on the given slice of command-line strings.
    pub fn getopts(args: &[String]) -> Result<Options, Misfire> {
32
        let opts = &[
B
Ben S 已提交
33 34 35
            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 已提交
36
            getopts::optflag("B", "bytes",     "list file sizes in bytes, without prefixes"),
37
            getopts::optflag("d", "list-dirs", "list directories as regular files"),
B
Ben S 已提交
38 39 40 41 42 43 44 45 46
            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 已提交
47
            getopts::optflag("?", "help",      "show list of command-line options"),
B
Ben S 已提交
48
        ];
B
Ben S 已提交
49

50
        let matches = match getopts::getopts(args, opts) {
51
            Ok(m) => m,
B
Ben S 已提交
52
            Err(e) => return Err(Misfire::InvalidOptions(e)),
B
Ben S 已提交
53
        };
B
Ben S 已提交
54

B
Ben S 已提交
55
        if matches.opt_present("help") {
B
Ben S 已提交
56
            return Err(Misfire::Help(getopts::usage("Usage:\n  exa [options] [files...]", opts)));
B
Ben S 已提交
57
        }
B
Ben S 已提交
58

59 60 61 62 63
        let sort_field = match matches.opt_str("sort") {
            Some(word) => try!(SortField::from_word(word)),
            None => SortField::Name,
        };

B
Ben S 已提交
64
        Ok(Options {
65 66 67 68
            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"),
69
            sort_field:      sort_field,
70
            view:            try!(view(&matches)),
71
        })
B
Ben S 已提交
72
    }
B
Ben S 已提交
73

B
Benjamin Sago 已提交
74 75 76 77
    pub fn path_strings(&self) -> Iter<String> {
        self.path_strs.iter()
    }

B
Ben S 已提交
78
    /// Transform the files somehow before listing them.
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
    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 已提交
94
        }
95 96 97 98 99 100 101 102 103 104 105

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

        files
    }

    fn should_display(&self, f: &File) -> bool {
        if self.show_invisibles {
            true
B
Ben S 已提交
106 107
        }
        else {
108
            !f.name.as_slice().starts_with(".")
B
Ben S 已提交
109 110
        }
    }
111
}
B
Ben S 已提交
112

B
Ben S 已提交
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
/// User-supplied field to sort by
#[derive(PartialEq, Debug)]
pub enum SortField {
    Unsorted, Name, Extension, Size, FileInode
}

impl Copy for SortField { }

impl SortField {

    /// Find which field to use based on a user-supplied word.
    fn from_word(word: String) -> Result<SortField, Misfire> {
        match word.as_slice() {
            "name"  => Ok(SortField::Name),
            "size"  => Ok(SortField::Size),
            "ext"   => Ok(SortField::Extension),
            "none"  => Ok(SortField::Unsorted),
            "inode" => Ok(SortField::FileInode),
            field   => Err(SortField::none(field))
        }
    }

    /// How to display an error when the word didn't match with anything.
    fn none(field: &str) -> Misfire {
        Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--sort {}", field)))
    }
}

/// One of these things could happen instead of listing files.
#[derive(PartialEq, Debug)]
pub enum Misfire {

    /// The getopts crate didn't like these arguments.
    InvalidOptions(getopts::Fail),

    /// The user asked for help. This isn't strictly an error, which is why
    /// this enum isn't named Error!
    Help(String),

    /// Two options were given that conflict with one another
    Conflict(&'static str, &'static str),

    /// An option was given that does nothing when another one either is or
    /// isn't present.
    Useless(&'static str, bool, &'static str),
}

impl Misfire {
    /// The OS return code this misfire should signify.
    pub fn error_code(&self) -> isize {
        if let Help(_) = *self { 2 }
                          else { 3 }
    }
}

impl fmt::Display for Misfire {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            InvalidOptions(ref e) => write!(f, "{}", e),
            Help(ref text)        => write!(f, "{}", text),
            Conflict(a, b)        => write!(f, "Option --{} conflicts with option {}", a, b),
            Useless(a, false, b)  => write!(f, "Option --{} is useless without option --{}", a, b),
            Useless(a, true, b)   => write!(f, "Option --{} is useless given option --{}", a, b),
        }
    }
}

/// Turns the Getopts results object into a View object.
fn view(matches: &getopts::Matches) -> Result<View, Misfire> {
182 183
    if matches.opt_present("long") {
        if matches.opt_present("across") {
B
Ben S 已提交
184
            Err(Misfire::Useless("across", true, "long"))
B
Ben S 已提交
185
        }
186
        else if matches.opt_present("oneline") {
B
Ben S 已提交
187
            Err(Misfire::Useless("across", true, "long"))
B
Ben S 已提交
188
        }
189 190
        else {
            Ok(View::Details(try!(columns(matches)), matches.opt_present("header")))
191
        }
192 193
    }
    else if matches.opt_present("binary") {
B
Ben S 已提交
194
        Err(Misfire::Useless("binary", false, "long"))
195 196
    }
    else if matches.opt_present("bytes") {
B
Ben S 已提交
197
        Err(Misfire::Useless("bytes", false, "long"))
198 199 200
    }
    else if matches.opt_present("oneline") {
        if matches.opt_present("across") {
B
Ben S 已提交
201
            Err(Misfire::Useless("across", true, "oneline"))
202 203
        }
        else {
204
            Ok(View::Lines)
205
        }
206 207 208 209 210
    }
    else {
        match dimensions() {
            None => Ok(View::Lines),
            Some((width, _)) => Ok(View::Grid(matches.opt_present("across"), width)),
B
Ben S 已提交
211
        }
212 213
    }
}
B
Ben S 已提交
214

B
Ben S 已提交
215 216
/// Finds out which file size the user has asked for.
fn file_size(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
217 218
    let binary = matches.opt_present("binary");
    let bytes = matches.opt_present("bytes");
B
Ben S 已提交
219

220
    match (binary, bytes) {
B
Ben S 已提交
221
        (true,  true ) => Err(Misfire::Conflict("binary", "bytes")),
222 223 224 225 226 227
        (true,  false) => Ok(SizeFormat::BinaryBytes),
        (false, true ) => Ok(SizeFormat::JustBytes),
        (false, false) => Ok(SizeFormat::DecimalBytes),
    }
}

B
Ben S 已提交
228 229 230
/// Turns the Getopts results object into a list of columns for the columns
/// view, depending on the passed-in command-line arguments.
fn columns(matches: &getopts::Matches) -> Result<Vec<Column>, Misfire> {
231
    let mut columns = vec![];
B
Ben S 已提交
232

233 234
    if matches.opt_present("inode") {
        columns.push(Inode);
B
Ben S 已提交
235 236
    }

237 238 239 240
    columns.push(Permissions);

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

B
Ben S 已提交
243
    // Fail early here if two file size flags are given
244
    columns.push(FileSize(try!(file_size(matches))));
245

246 247 248
    if matches.opt_present("blocks") {
        columns.push(Blocks);
    }
249

250
    columns.push(User);
251

252 253
    if matches.opt_present("group") {
        columns.push(Group);
254
    }
255 256 257

    columns.push(FileName);
    Ok(columns)
B
Ben S 已提交
258
}
259 260 261 262

#[cfg(test)]
mod test {
    use super::Options;
B
Ben S 已提交
263 264
    use super::Misfire;
    use super::Misfire::*;
265

266 267
    use std::fmt;

B
Ben S 已提交
268 269
    fn is_helpful(misfire: Result<Options, Misfire>) -> bool {
        match misfire {
270 271 272 273 274
            Err(Help(_)) => true,
            _            => false,
        }
    }

275 276 277 278 279 280
    impl fmt::Display for Options {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "{:?}", self)
        }
    }

281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
    #[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]
306 307
    fn file_sizes() {
        let opts = Options::getopts(&[ "--long".to_string(), "--binary".to_string(), "--bytes".to_string() ]);
B
Ben S 已提交
308
        assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
309 310 311 312 313
    }

    #[test]
    fn just_binary() {
        let opts = Options::getopts(&[ "--binary".to_string() ]);
B
Ben S 已提交
314
        assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
315
    }
316 317 318 319

    #[test]
    fn just_bytes() {
        let opts = Options::getopts(&[ "--bytes".to_string() ]);
B
Ben S 已提交
320
        assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
321 322 323 324 325
    }

    #[test]
    fn long_across() {
        let opts = Options::getopts(&[ "--long".to_string(), "--across".to_string() ]);
B
Ben S 已提交
326
        assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long"))
327 328 329 330 331
    }

    #[test]
    fn oneline_across() {
        let opts = Options::getopts(&[ "--oneline".to_string(), "--across".to_string() ]);
B
Ben S 已提交
332
        assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline"))
333
    }
334
}