summaryrefslogtreecommitdiffstats
path: root/src/options/mod.rs
blob: 324bfe83e9d3254f1da729e27e352af0973cb177 (plain)
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
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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
//! Parsing command-line strings into exa options.
//!
//! This module imports exa’s configuration types, such as `View` (the details
//! of displaying multiple files) and `DirAction` (what to do when encountering
//! a directory), and implements `deduce` methods on them so they can be
//! configured using command-line options.
//!
//!
//! ## Useless and overridden options
//!
//! Let’s say exa was invoked with just one argument: `exa --inode`. The
//! `--inode` option is used in the details view, where it adds the inode
//! column to the output. But because the details view is *only* activated with
//! the `--long` argument, adding `--inode` without it would not have any
//! effect.
//!
//! For a long time, exa’s philosophy was that the user should be warned
//! whenever they could be mistaken like this. If you tell exa to display the
//! inode, and it *doesn’t* display the inode, isn’t that more annoying than
//! having it throw an error back at you?
//!
//! However, this doesn’t take into account *configuration*. Say a user wants
//! to configure exa so that it lists inodes in the details view, but otherwise
//! functions normally. A common way to do this for command-line programs is to
//! define a shell alias that specifies the details they want to use every
//! time. For the inode column, the alias would be:
//!
//! `alias exa="exa --inode"`
//!
//! Using this alias means that although the inode column will be shown in the
//! details view, you’re now *only* allowed to use the details view, as any
//! other view type will result in an error. Oops!
//!
//! Another example is when an option is specified twice, such as `exa
//! --sort=Name --sort=size`. Did the user change their mind about sorting, and
//! accidentally specify the option twice?
//!
//! Again, exa rejected this case, throwing an error back to the user instead
//! of trying to guess how they want their output sorted. And again, this
//! doesn’t take into account aliases being used to set defaults. A user who
//! wants their files to be sorted case-insensitively may configure their shell
//! with the following:
//!
//! `alias exa="exa --sort=Name"`
//!
//! Just like the earlier example, the user now can’t use any other sort order,
//! because exa refuses to guess which one they meant. It’s *more* annoying to
//! have to go back and edit the command than if there were no error.
//!
//! Fortunately, there’s a heuristic for telling which options came from an
//! alias and which came from the actual command-line: aliased options are
//! nearer the beginning of the options array, and command-line options are
//! nearer the end. This means that after the options have been parsed, exa
//! needs to traverse them *backwards* to find the last-most-specified one.
//!
//! For example, invoking exa with `exa --sort=size` when that alias is present
//! would result in a full command-line of:
//!
//! `exa --sort=Name --sort=size`
//!
//! `--sort=size` should override `--sort=Name` because it’s closer to the end
//! of the arguments array. In fact, because there’s no way to tell where the
//! arguments came from — it’s just a heuristic — this will still work even
//! if no aliases are being used!
//!
//! Finally, this isn’t just useful when options could override each other.
//! Creating an alias `exal="exa --long --inode --header"` then invoking `exal
//! --grid --long` shouldn’t complain about `--long` being given twice when
//! it’s clear what the user wants.


use std::ffi::OsStr;

use crate::fs::dir_action::DirAction;
use crate::fs::filter::{FileFilter, GitIgnore};
use crate::output::{View, Mode, details, grid_details};

mod dir_action;
mod filter;
mod flags;
mod style;
mod view;

mod error;
pub use self::error::OptionsError;

mod help;
use self::help::HelpString;

mod parser;
use self::parser::MatchedFlags;

pub mod vars;
pub use self::vars::Vars;

mod version;
use self::version::VersionString;


/// These **options** represent a parsed, error-checked versions of the
/// user’s command-line options.
#[derive(Debug)]
pub struct Options {

    /// The action to perform when encountering a directory rather than a
    /// regular file.
    pub dir_action: DirAction,

    /// How to sort and filter files before outputting them.
    pub filter: FileFilter,

    /// The type of output to use (lines, grid, or details).
    pub view: View,
}

impl Options {

    /// Parse the given iterator of command-line strings into an Options
    /// struct and a list of free filenames, using the environment variables
    /// for extra options.
    #[allow(unused_results)]
    pub fn parse<'args, I, V>(args: I, vars: &V) -> OptionsResult<'args>
    where I: IntoIterator<Item = &'args OsStr>,
          V: Vars,
    {
        use crate::options::parser::{Matches, Strictness};

        let strictness = match vars.get(vars::EXA_STRICT) {
            None                         => Strictness::UseLastArguments,
            Some(ref t) if t.is_empty()  => Strictness::UseLastArguments,
            Some(_)                      => Strictness::ComplainAboutRedundantArguments,
        };

        let Matches { flags, frees } = match flags::ALL_ARGS.parse(args, strictness) {
            Ok(m)    => m,
            Err(pe)  => return OptionsResult::InvalidOptions(OptionsError::Parse(pe)),
        };

        if let Some(help) = HelpString::deduce(&flags) {
            return OptionsResult::Help(help);
        }

        if let Some(version) = VersionString::deduce(&flags) {
            return OptionsResult::Version(version);
        }

        match Self::deduce(&flags, vars) {
            Ok(options)  => OptionsResult::Ok(options, frees),
            Err(oe)      => OptionsResult::InvalidOptions(oe),
        }
    }

    /// Whether the View specified in this set of options includes a Git
    /// status column. It’s only worth trying to discover a repository if the
    /// results will end up being displayed.
    pub fn should_scan_for_git(&self) -> bool {
        if self.filter.git_ignore == GitIgnore::CheckAndIgnore {
            return true;
        }

        match self.view.mode {
            Mode::Details(details::Options { table: Some(ref table), .. }) |
            Mode::GridDetails(grid_details::Options { details: details::Options { table: Some(ref table), .. }, .. }) => table.columns.git,
            _ => false,
        }
    }

    /// Determines the complete set of options based on the given command-line
    /// arguments, after they’ve been parsed.
    fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
        let dir_action = DirAction::deduce(matches)?;
        let filter = FileFilter::deduce(matches)?;
        let view = View::deduce(matches, vars)?;

        Ok(Self { dir_action, view, filter })
    }
}


/// The result of the `Options::getopts` function.
#[derive(Debug)]
pub enum OptionsResult<'args> {

    /// The options were parsed successfully.
    Ok(Options, Vec<&'args OsStr>),

    /// There was an error parsing the arguments.
    InvalidOptions(OptionsError),

    /// One of the arguments was `--help`, so display help.
    Help(HelpString),

    /// One of the arguments was `--version`, so display the version number.
    Version(VersionString),
}


#[cfg(test)]
pub mod test {
    use crate::options::parser::{Arg, MatchedFlags};
    use std::ffi::OsStr;

    #[derive(PartialEq, Debug)]
    pub enum Strictnesses {
        Last,
        Complain,
        Both,
    }

    /// This function gets used by the other testing modules.
    /// It can run with one or both strictness values: if told to run with
    /// both, then both should resolve to the same result.
    ///
    /// It returns a vector with one or two elements in.
    /// These elements can then be tested with assert_eq or what have you.
    pub fn parse_for_test<T, F>(inputs: &[&str], args: &'static [&'static Arg], strictnesses: Strictnesses, get: F) -> Vec<T>
    where F: Fn(&MatchedFlags<'_>) -> T
    {
        use self::Strictnesses::*;
        use crate::options::parser::{Args, Strictness};

        let bits = inputs.into_iter().map(OsStr::new).collect::<Vec<_>>();
        let mut result = Vec::new();

        if strictnesses == Last || strictnesses == Both {
            let results = Args(args).parse(bits.clone(), Strictness::UseLastArguments);
            result.push(get(&results.unwrap().flags));
        }

        if strictnesses == Complain || strictnesses == Both {
            let results = Args(args).parse(bits, Strictness::ComplainAboutRedundantArguments);
            result.push(get(&results.unwrap().flags));
        }

        result
    }
}