options.rs 18.6 KB
Newer Older
B
Ben S 已提交
1
use dir::Dir;
B
Ben S 已提交
2
use file::File;
3
use column::Column;
B
Ben S 已提交
4
use column::Column::*;
5
use output::{Grid, Details};
B
Ben S 已提交
6
use term::dimensions;
N
nwin 已提交
7
use attr;
B
Ben S 已提交
8 9

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

13 14 15
use getopts;
use natord;

B
Ben S 已提交
16 17
use datetime::local::{LocalDateTime, DatePiece};

B
Ben S 已提交
18
use self::Misfire::*;
19

B
Ben S 已提交
20 21
/// The *Options* struct represents a parsed version of the user's
/// command-line options.
22
#[derive(PartialEq, Debug, Copy)]
B
Ben S 已提交
23
pub struct Options {
24
    pub dir_action: DirAction,
25
    pub filter: FileFilter,
26
    pub view: View,
27 28 29 30
}

#[derive(PartialEq, Debug, Copy)]
pub struct FileFilter {
B
Benjamin Sago 已提交
31 32 33
    reverse: bool,
    show_invisibles: bool,
    sort_field: SortField,
B
Ben S 已提交
34 35
}

36
#[derive(PartialEq, Debug, Copy)]
37 38 39 40 41 42
pub enum View {
    Details(Details),
    Lines,
    Grid(Grid),
}

B
Ben S 已提交
43
impl Options {
B
Ben S 已提交
44 45

    /// Call getopts on the given slice of command-line strings.
46
    pub fn getopts(args: &[String]) -> Result<(Options, Vec<String>), Misfire> {
B
Ben S 已提交
47
        let mut opts = getopts::Options::new();
N
nwin 已提交
48 49 50 51 52
        if attr::feature_implemented() {
            opts.optflag("@", "extended",
                         "display extended attribute keys and sizes in long (-l) output"
            );  
        }
B
Ben S 已提交
53 54 55 56 57 58 59 60 61
        opts.optflag("1", "oneline",   "display one entry per line");
        opts.optflag("a", "all",       "show dot-files");
        opts.optflag("b", "binary",    "use binary prefixes in file sizes");
        opts.optflag("B", "bytes",     "list file sizes in bytes, without prefixes");
        opts.optflag("d", "list-dirs", "list directories as regular files");
        opts.optflag("g", "group",     "show group as well as user");
        opts.optflag("h", "header",    "show a header row at the top");
        opts.optflag("H", "links",     "show number of hard links");
        opts.optflag("i", "inode",     "show each file's inode number");
B
Ben S 已提交
62
        opts.optflag("l", "long",      "display extended details and attributes");
63
        opts.optflag("m", "modified",  "display timestamp of most recent modification");
B
Ben S 已提交
64 65 66 67
        opts.optflag("r", "reverse",   "reverse order of files");
        opts.optflag("R", "recurse",   "recurse into directories");
        opts.optopt ("s", "sort",      "field to sort by", "WORD");
        opts.optflag("S", "blocks",    "show number of file system blocks");
B
Ben S 已提交
68
        opts.optopt ("t", "time",      "which timestamp to show for a file", "WORD");
B
Ben S 已提交
69
        opts.optflag("T", "tree",      "recurse into subdirectories in a tree view");
70 71
        opts.optflag("u", "accessed",  "display timestamp of last access for a file");
        opts.optflag("U", "created",   "display timestamp of creation for a file");
B
Ben S 已提交
72 73
        opts.optflag("x", "across",    "sort multi-column view entries across");
        opts.optflag("?", "help",      "show list of command-line options");
B
Ben S 已提交
74

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

B
Ben S 已提交
80
        if matches.opt_present("help") {
B
Ben S 已提交
81
            return Err(Misfire::Help(opts.usage("Usage:\n  exa [options] [files...]")));
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,
        };

89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
        let filter = FileFilter {
            reverse:         matches.opt_present("reverse"),
            show_invisibles: matches.opt_present("all"),
            sort_field:      sort_field,
        };

        let path_strs = if matches.free.is_empty() {
            vec![ ".".to_string() ]
        }
        else {
            matches.free.clone()
        };

        Ok((Options {
            dir_action: try!(DirAction::deduce(&matches)),
            view:       try!(View::deduce(&matches, filter)),
            filter:     filter,
        }, path_strs))
B
Ben S 已提交
107
    }
B
Ben S 已提交
108

B
Ben S 已提交
109
    pub fn transform_files<'a>(&self, files: &mut Vec<File<'a>>) {
110 111 112
        self.filter.transform_files(files)
    }
}
B
Ben S 已提交
113

114
impl FileFilter {
B
Ben S 已提交
115
    /// Transform the files (sorting, reversing, filtering) before listing them.
B
Ben S 已提交
116
    pub fn transform_files<'a>(&self, files: &mut Vec<File<'a>>) {
B
Ben S 已提交
117 118

        if !self.show_invisibles {
B
Ben S 已提交
119
            files.retain(|f| !f.is_dotfile());
B
Ben S 已提交
120
        }
121 122 123

        match self.sort_field {
            SortField::Unsorted => {},
B
Ben S 已提交
124
            SortField::Name => files.sort_by(|a, b| natord::compare(&*a.name, &*b.name)),
125 126 127
            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 已提交
128 129 130 131 132 133
                if a.ext.cmp(&b.ext) == Ordering::Equal {
                    Ordering::Equal
                }
                else {
                    a.name.to_ascii_lowercase().cmp(&b.name.to_ascii_lowercase())
                }
134
            }),
B
Ben S 已提交
135 136 137
            SortField::ModifiedDate => files.sort_by(|a, b| a.stat.modified.cmp(&b.stat.modified)),
            SortField::AccessedDate => files.sort_by(|a, b| a.stat.accessed.cmp(&b.stat.accessed)),
            SortField::CreatedDate  => files.sort_by(|a, b| a.stat.created.cmp(&b.stat.created)),
B
Ben S 已提交
138
        }
139 140 141 142 143 144

        if self.reverse {
            files.reverse();
        }
    }
}
B
Ben S 已提交
145

146
/// User-supplied field to sort by.
B
Ben S 已提交
147
#[derive(PartialEq, Debug, Copy)]
B
Ben S 已提交
148
pub enum SortField {
B
Ben S 已提交
149 150
    Unsorted, Name, Extension, Size, FileInode,
    ModifiedDate, AccessedDate, CreatedDate,
B
Ben S 已提交
151 152 153 154 155 156 157
}

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() {
B
Ben S 已提交
158 159 160 161 162 163 164 165 166
            "name" | "filename"  => Ok(SortField::Name),
            "size" | "filesize"  => Ok(SortField::Size),
            "ext"  | "extension" => Ok(SortField::Extension),
            "mod"  | "modified"  => Ok(SortField::ModifiedDate),
            "acc"  | "accessed"  => Ok(SortField::AccessedDate),
            "cr"   | "created"   => Ok(SortField::CreatedDate),
            "none"               => Ok(SortField::Unsorted),
            "inode"              => Ok(SortField::FileInode),
            field                => Err(SortField::none(field))
B
Ben S 已提交
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
        }
    }

    /// 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.
B
Ben S 已提交
197
    pub fn error_code(&self) -> i32 {
B
Ben S 已提交
198 199 200 201 202 203 204 205 206 207
        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 已提交
208 209 210
            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 已提交
211 212 213 214
        }
    }
}

215 216 217 218 219 220 221 222 223 224 225 226
impl View {
    pub fn deduce(matches: &getopts::Matches, filter: FileFilter) -> Result<View, Misfire> {
        if matches.opt_present("long") {
            if matches.opt_present("across") {
                Err(Misfire::Useless("across", true, "long"))
            }
            else if matches.opt_present("oneline") {
                Err(Misfire::Useless("oneline", true, "long"))
            }
            else {
                let details = Details {
                        columns: try!(Columns::deduce(matches)),
227
                        header: matches.opt_present("header"),
228
                        tree: matches.opt_present("recurse"),
N
nwin 已提交
229
                        ext_attr: attr::feature_implemented() && matches.opt_present("extended"),
230 231 232 233 234
                        filter: filter,
                };

                Ok(View::Details(details))
            }
B
Ben S 已提交
235
        }
236 237
        else if matches.opt_present("binary") {
            Err(Misfire::Useless("binary", false, "long"))
B
Ben S 已提交
238
        }
239 240
        else if matches.opt_present("bytes") {
            Err(Misfire::Useless("bytes", false, "long"))
241
        }
242 243
        else if matches.opt_present("inode") {
            Err(Misfire::Useless("inode", false, "long"))
244
        }
245 246
        else if matches.opt_present("links") {
            Err(Misfire::Useless("links", false, "long"))
247
        }
248 249 250 251 252 253
        else if matches.opt_present("header") {
            Err(Misfire::Useless("header", false, "long"))
        }
        else if matches.opt_present("blocks") {
            Err(Misfire::Useless("blocks", false, "long"))
        }
B
Ben S 已提交
254 255 256
        else if matches.opt_present("time") {
            Err(Misfire::Useless("time", false, "long"))
        }
B
Ben S 已提交
257 258 259
        else if matches.opt_present("tree") {
            Err(Misfire::Useless("tree", false, "long"))
        }
N
nwin 已提交
260 261 262
        else if attr::feature_implemented() && matches.opt_present("extended") {
            Err(Misfire::Useless("extended", false, "long"))
        }
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
        else if matches.opt_present("oneline") {
            if matches.opt_present("across") {
                Err(Misfire::Useless("across", true, "oneline"))
            }
            else {
                Ok(View::Lines)
            }
        }
        else {
            if let Some((width, _)) = dimensions() {
                let grid = Grid {
                    across: matches.opt_present("across"),
                    console_width: width
                };

                Ok(View::Grid(grid))
            }
            else {
                // If the terminal width couldn't be matched for some reason, such
                // as the program's stdout being connected to a file, then
                // fallback to the lines view.
                Ok(View::Lines)
            }
B
Ben S 已提交
286
        }
287 288
    }
}
B
Ben S 已提交
289

290 291 292 293 294 295
#[derive(PartialEq, Debug, Copy)]
pub enum SizeFormat {
    DecimalBytes,
    BinaryBytes,
    JustBytes,
}
B
Ben S 已提交
296

297 298 299 300 301 302 303 304 305 306 307
impl SizeFormat {
    pub fn deduce(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
        let binary = matches.opt_present("binary");
        let bytes  = matches.opt_present("bytes");

        match (binary, bytes) {
            (true,  true ) => Err(Misfire::Conflict("binary", "bytes")),
            (true,  false) => Ok(SizeFormat::BinaryBytes),
            (false, true ) => Ok(SizeFormat::JustBytes),
            (false, false) => Ok(SizeFormat::DecimalBytes),
        }
308 309 310
    }
}

311 312 313 314 315 316 317 318
#[derive(PartialEq, Debug, Copy)]
pub enum TimeType {
    FileAccessed,
    FileModified,
    FileCreated,
}

impl TimeType {
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
    pub fn header(&self) -> &'static str {
        match *self {
            TimeType::FileAccessed => "Date Accessed",
            TimeType::FileModified => "Date Modified",
            TimeType::FileCreated  => "Date Created",
        }
    }
}

#[derive(PartialEq, Debug, Copy)]
pub struct TimeTypes {
    accessed: bool,
    modified: bool,
    created:  bool,
}

impl TimeTypes {
336 337

    /// Find which field to use based on a user-supplied word.
338
    fn deduce(matches: &getopts::Matches) -> Result<TimeTypes, Misfire> {
339
        let possible_word = matches.opt_str("time");
340 341 342
        let modified = matches.opt_present("modified");
        let created  = matches.opt_present("created");
        let accessed = matches.opt_present("accessed");
343 344

        if let Some(word) = possible_word {
345 346 347 348 349 350 351 352 353 354
            if modified {
                return Err(Misfire::Useless("modified", true, "time"));
            }
            else if created {
                return Err(Misfire::Useless("created", true, "time"));
            }
            else if accessed {
                return Err(Misfire::Useless("accessed", true, "time"));
            }

355
            match word.as_slice() {
356 357 358 359
                "mod" | "modified"  => Ok(TimeTypes { accessed: false, modified: true, created: false }),
                "acc" | "accessed"  => Ok(TimeTypes { accessed: true, modified: false, created: false }),
                "cr"  | "created"   => Ok(TimeTypes { accessed: false, modified: false, created: true }),
                field   => Err(TimeTypes::none(field)),
360 361 362
            }
        }
        else {
363 364 365 366 367 368
            if modified || created || accessed {
                Ok(TimeTypes { accessed: accessed, modified: modified, created: created })
            }
            else {
                Ok(TimeTypes { accessed: false, modified: true, created: false })
            }
369 370 371 372 373 374 375 376
        }
    }

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

378 379 380 381 382 383 384 385 386 387 388 389 390
/// What to do when encountering a directory?
#[derive(PartialEq, Debug, Copy)]
pub enum DirAction {
    AsFile, List, Recurse, Tree
}

impl DirAction {
    pub fn deduce(matches: &getopts::Matches) -> Result<DirAction, Misfire> {
        let recurse = matches.opt_present("recurse");
        let list    = matches.opt_present("list-dirs");
        let tree    = matches.opt_present("tree");

        match (recurse, list, tree) {
B
Ben S 已提交
391
            (false, _,     true ) => Err(Misfire::Useless("tree", false, "recurse")),
392 393 394 395 396 397
            (true,  true,  _    ) => Err(Misfire::Conflict("recurse", "list-dirs")),
            (true,  false, false) => Ok(DirAction::Recurse),
            (true,  false, true ) => Ok(DirAction::Tree),
            (false, true,  _    ) => Ok(DirAction::AsFile),
            (false, false, _    ) => Ok(DirAction::List),
        }
398 399 400
    }
}

B
Ben S 已提交
401 402 403
#[derive(PartialEq, Copy, Debug)]
pub struct Columns {
    size_format: SizeFormat,
404
    time_types: TimeTypes,
B
Ben S 已提交
405 406 407 408 409
    inode: bool,
    links: bool,
    blocks: bool,
    group: bool,
}
B
Ben S 已提交
410

B
Ben S 已提交
411
impl Columns {
412
    pub fn deduce(matches: &getopts::Matches) -> Result<Columns, Misfire> {
B
Ben S 已提交
413
        Ok(Columns {
414
            size_format: try!(SizeFormat::deduce(matches)),
415
            time_types:  try!(TimeTypes::deduce(matches)),
B
Ben S 已提交
416 417 418 419 420
            inode:  matches.opt_present("inode"),
            links:  matches.opt_present("links"),
            blocks: matches.opt_present("blocks"),
            group:  matches.opt_present("group"),
        })
B
Ben S 已提交
421 422
    }

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

B
Ben S 已提交
426 427 428
        if self.inode {
            columns.push(Inode);
        }
B
Ben S 已提交
429

B
Ben S 已提交
430
        columns.push(Permissions);
431

B
Ben S 已提交
432 433 434
        if self.links {
            columns.push(HardLinks);
        }
435

B
Ben S 已提交
436
        columns.push(FileSize(self.size_format));
437

B
Ben S 已提交
438 439 440
        if self.blocks {
            columns.push(Blocks);
        }
441

B
Ben S 已提交
442 443 444 445 446 447
        columns.push(User);

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

B
Ben S 已提交
448 449
        let current_year = LocalDateTime::now().year();

450
        if self.time_types.modified {
B
Ben S 已提交
451
            columns.push(Timestamp(TimeType::FileModified, current_year));
452 453 454
        }

        if self.time_types.created {
B
Ben S 已提交
455
            columns.push(Timestamp(TimeType::FileCreated, current_year));
456 457 458
        }

        if self.time_types.accessed {
B
Ben S 已提交
459
            columns.push(Timestamp(TimeType::FileAccessed, current_year));
460
        }
461

B
Ben S 已提交
462 463 464 465 466 467 468
        if cfg!(feature="git") {
            if let Some(d) = dir {
                if d.has_git_repo() {
                    columns.push(GitStatus);
                }
            }
        }
469

B
Ben S 已提交
470 471
        columns
    }
B
Ben S 已提交
472
}
473 474 475 476

#[cfg(test)]
mod test {
    use super::Options;
B
Ben S 已提交
477 478
    use super::Misfire;
    use super::Misfire::*;
479

480
    fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
B
Ben S 已提交
481
        match misfire {
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500
            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() {
501
        let args = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]).unwrap().1;
B
Ben S 已提交
502
        assert_eq!(args, vec![ "this file".to_string(), "that file".to_string() ])
503 504 505 506
    }

    #[test]
    fn no_args() {
507
        let args = Options::getopts(&[]).unwrap().1;
B
Ben S 已提交
508
        assert_eq!(args, vec![ ".".to_string() ])
509 510 511
    }

    #[test]
512 513
    fn file_sizes() {
        let opts = Options::getopts(&[ "--long".to_string(), "--binary".to_string(), "--bytes".to_string() ]);
B
Ben S 已提交
514
        assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
515 516 517 518 519
    }

    #[test]
    fn just_binary() {
        let opts = Options::getopts(&[ "--binary".to_string() ]);
B
Ben S 已提交
520
        assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
521
    }
522 523 524 525

    #[test]
    fn just_bytes() {
        let opts = Options::getopts(&[ "--bytes".to_string() ]);
B
Ben S 已提交
526
        assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
527 528 529 530 531
    }

    #[test]
    fn long_across() {
        let opts = Options::getopts(&[ "--long".to_string(), "--across".to_string() ]);
B
Ben S 已提交
532
        assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long"))
533 534 535 536 537
    }

    #[test]
    fn oneline_across() {
        let opts = Options::getopts(&[ "--oneline".to_string(), "--across".to_string() ]);
B
Ben S 已提交
538
        assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline"))
539
    }
540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563

    #[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"))
    }
B
Ben S 已提交
564

N
nwin 已提交
565 566 567 568 569 570
    #[test]
    fn extended_without_long() {
        let opts = Options::getopts(&[ "--extended".to_string() ]);
        assert_eq!(opts.unwrap_err(), Misfire::Useless("extended", false, "long"))
    }

B
Ben S 已提交
571 572 573 574 575 576
    #[test]
    fn tree_without_recurse() {
        let opts = Options::getopts(&[ "--tree".to_string() ]);
        assert_eq!(opts.unwrap_err(), Misfire::Useless("tree", false, "recurse"))
    }

577
}