options.rs 12.8 KB
Newer Older
B
Ben S 已提交
1
use dir::Dir;
B
Ben S 已提交
2
use file::File;
3
use column::{Column, SizeFormat};
B
Ben S 已提交
4
use column::Column::*;
5
use output::View;
B
Ben S 已提交
6 7 8
use term::dimensions;

use std::ascii::AsciiExt;
B
Ben S 已提交
9
use std::cmp::Ordering;
10
use std::fmt;
B
Ben S 已提交
11

12 13 14
use getopts;
use natord;

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

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

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

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

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

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

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

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

B
Ben S 已提交
76
    /// Display the files using this Option's View.
B
Ben S 已提交
77 78
    pub fn view(&self, dir: Option<&Dir>, files: Vec<File>) {
        self.view.view(dir, files)
B
Ben S 已提交
79 80
    }

B
Ben S 已提交
81
    /// Transform the files (sorting, reversing, filtering) before listing them.
B
Ben S 已提交
82 83 84 85 86
    pub fn transform_files<'a>(&self, mut files: Vec<File<'a>>) -> Vec<File<'a>> {

        if !self.show_invisibles {
            files = files.into_iter().filter(|f| !f.is_dotfile()).collect();
        }
87 88 89

        match self.sort_field {
            SortField::Unsorted => {},
B
Ben S 已提交
90
            SortField::Name => files.sort_by(|a, b| natord::compare(&*a.name, &*b.name)),
91 92 93
            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| {
B
Ben S 已提交
94 95 96 97 98 99
                if a.ext.cmp(&b.ext) == Ordering::Equal {
                    Ordering::Equal
                }
                else {
                    a.name.to_ascii_lowercase().cmp(&b.name.to_ascii_lowercase())
                }
100
            }),
B
Ben S 已提交
101
        }
102 103 104 105 106 107 108 109

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

        files
    }
}
B
Ben S 已提交
110

111 112 113 114 115 116 117
/// What to do when encountering a directory?
#[derive(PartialEq, Debug, Copy)]
pub enum DirAction {
    AsFile, List, Recurse
}

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

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),
B
Benjamin Sago 已提交
175 176 177
            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),
B
Ben S 已提交
178 179 180 181 182 183
        }
    }
}

/// Turns the Getopts results object into a View object.
fn view(matches: &getopts::Matches) -> Result<View, Misfire> {
184 185
    if matches.opt_present("long") {
        if matches.opt_present("across") {
B
Ben S 已提交
186
            Err(Misfire::Useless("across", true, "long"))
B
Ben S 已提交
187
        }
188
        else if matches.opt_present("oneline") {
B
Benjamin Sago 已提交
189
            Err(Misfire::Useless("oneline", true, "long"))
B
Ben S 已提交
190
        }
191
        else {
B
Ben S 已提交
192
            Ok(View::Details(try!(Columns::new(matches)), matches.opt_present("header")))
193
        }
194 195
    }
    else if matches.opt_present("binary") {
B
Ben S 已提交
196
        Err(Misfire::Useless("binary", false, "long"))
197 198
    }
    else if matches.opt_present("bytes") {
B
Ben S 已提交
199
        Err(Misfire::Useless("bytes", false, "long"))
200
    }
201 202 203 204 205 206 207 208 209 210 211 212
    else if matches.opt_present("inode") {
        Err(Misfire::Useless("inode", false, "long"))
    }
    else if matches.opt_present("links") {
        Err(Misfire::Useless("links", false, "long"))
    }
    else if matches.opt_present("header") {
        Err(Misfire::Useless("header", false, "long"))
    }
    else if matches.opt_present("blocks") {
        Err(Misfire::Useless("blocks", false, "long"))
    }
213 214
    else if matches.opt_present("oneline") {
        if matches.opt_present("across") {
B
Ben S 已提交
215
            Err(Misfire::Useless("across", true, "oneline"))
216 217
        }
        else {
218
            Ok(View::Lines)
219
        }
220 221 222 223 224
    }
    else {
        match dimensions() {
            None => Ok(View::Lines),
            Some((width, _)) => Ok(View::Grid(matches.opt_present("across"), width)),
B
Ben S 已提交
225
        }
226 227
    }
}
B
Ben S 已提交
228

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

234
    match (binary, bytes) {
B
Ben S 已提交
235
        (true,  true ) => Err(Misfire::Conflict("binary", "bytes")),
236 237 238 239 240 241
        (true,  false) => Ok(SizeFormat::BinaryBytes),
        (false, true ) => Ok(SizeFormat::JustBytes),
        (false, false) => Ok(SizeFormat::DecimalBytes),
    }
}

242 243 244 245 246 247 248 249 250 251 252 253
fn dir_action(matches: &getopts::Matches) -> Result<DirAction, Misfire> {
    let recurse = matches.opt_present("recurse");
    let list    = matches.opt_present("list-dirs");

    match (recurse, list) {
        (true,  true ) => Err(Misfire::Conflict("recurse", "list-dirs")),
        (true,  false) => Ok(DirAction::Recurse),
        (false, true ) => Ok(DirAction::AsFile),
        (false, false) => Ok(DirAction::List),
    }
}

B
Ben S 已提交
254 255 256 257 258 259 260 261
#[derive(PartialEq, Copy, Debug)]
pub struct Columns {
    size_format: SizeFormat,
    inode: bool,
    links: bool,
    blocks: bool,
    group: bool,
}
B
Ben S 已提交
262

B
Ben S 已提交
263 264 265 266 267 268 269 270 271
impl Columns {
    pub fn new(matches: &getopts::Matches) -> Result<Columns, Misfire> {
        Ok(Columns {
            size_format: try!(file_size(matches)),
            inode:  matches.opt_present("inode"),
            links:  matches.opt_present("links"),
            blocks: matches.opt_present("blocks"),
            group:  matches.opt_present("group"),
        })
B
Ben S 已提交
272 273
    }

B
Ben S 已提交
274 275
    pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
        let mut columns = vec![];
276

B
Ben S 已提交
277 278 279
        if self.inode {
            columns.push(Inode);
        }
B
Ben S 已提交
280

B
Ben S 已提交
281
        columns.push(Permissions);
282

B
Ben S 已提交
283 284 285
        if self.links {
            columns.push(HardLinks);
        }
286

B
Ben S 已提交
287
        columns.push(FileSize(self.size_format));
288

B
Ben S 已提交
289 290 291
        if self.blocks {
            columns.push(Blocks);
        }
292

B
Ben S 已提交
293 294 295 296 297 298 299 300 301 302 303 304 305
        columns.push(User);

        if self.group {
            columns.push(Group);
        }

        if cfg!(feature="git") {
            if let Some(d) = dir {
                if d.has_git_repo() {
                    columns.push(GitStatus);
                }
            }
        }
306

B
Ben S 已提交
307 308 309
        columns.push(FileName);
        columns
    }
B
Ben S 已提交
310
}
311 312 313 314

#[cfg(test)]
mod test {
    use super::Options;
B
Ben S 已提交
315 316
    use super::Misfire;
    use super::Misfire::*;
317

318 319
    use std::fmt;

B
Ben S 已提交
320 321
    fn is_helpful(misfire: Result<Options, Misfire>) -> bool {
        match misfire {
322 323 324 325 326
            Err(Help(_)) => true,
            _            => false,
        }
    }

327 328 329 330 331 332
    impl fmt::Display for Options {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "{:?}", self)
        }
    }

333 334 335 336 337 338 339 340 341 342 343 344 345 346
    #[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() {
B
Ben S 已提交
347
        let opts = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]).unwrap();
B
Ben S 已提交
348 349
        let args: Vec<String> = opts.path_strs;
        assert_eq!(args, vec![ "this file".to_string(), "that file".to_string() ])
350 351 352 353
    }

    #[test]
    fn no_args() {
B
Ben S 已提交
354
        let opts = Options::getopts(&[]).unwrap();
B
Ben S 已提交
355 356
        let args: Vec<String> = opts.path_strs;
        assert_eq!(args, vec![ ".".to_string() ])
357 358 359
    }

    #[test]
360 361
    fn file_sizes() {
        let opts = Options::getopts(&[ "--long".to_string(), "--binary".to_string(), "--bytes".to_string() ]);
B
Ben S 已提交
362
        assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
363 364 365 366 367
    }

    #[test]
    fn just_binary() {
        let opts = Options::getopts(&[ "--binary".to_string() ]);
B
Ben S 已提交
368
        assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
369
    }
370 371 372 373

    #[test]
    fn just_bytes() {
        let opts = Options::getopts(&[ "--bytes".to_string() ]);
B
Ben S 已提交
374
        assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
375 376 377 378 379
    }

    #[test]
    fn long_across() {
        let opts = Options::getopts(&[ "--long".to_string(), "--across".to_string() ]);
B
Ben S 已提交
380
        assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long"))
381 382 383 384 385
    }

    #[test]
    fn oneline_across() {
        let opts = Options::getopts(&[ "--oneline".to_string(), "--across".to_string() ]);
B
Ben S 已提交
386
        assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline"))
387
    }
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411

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

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

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

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