1 #![warn(deprecated_in_future)]
2 #![warn(future_incompatible)]
3 #![warn(nonstandard_style)]
4 #![warn(rust_2018_compatibility)]
5 #![warn(rust_2018_idioms)]
6 #![warn(trivial_casts, trivial_numeric_casts)]
7 #![warn(unused)]
8 
9 #![warn(clippy::all, clippy::pedantic)]
10 #![allow(clippy::enum_glob_use)]
11 #![allow(clippy::find_map)]
12 #![allow(clippy::map_unwrap_or)]
13 #![allow(clippy::match_same_arms)]
14 #![allow(clippy::missing_const_for_fn)]
15 #![allow(clippy::missing_errors_doc)]
16 #![allow(clippy::module_name_repetitions)]
17 #![allow(clippy::must_use_candidate)]
18 #![allow(clippy::non_ascii_literal)]
19 #![allow(clippy::option_if_let_else)]
20 #![allow(clippy::too_many_lines)]
21 #![allow(clippy::unused_self)]
22 #![allow(clippy::wildcard_imports)]
23 
24 use std::env;
25 use std::ffi::{OsStr, OsString};
26 use std::io::{self, Write, ErrorKind};
27 use std::path::{Component, PathBuf};
28 
29 use ansi_term::{ANSIStrings, Style};
30 
31 use log::*;
32 
33 use crate::fs::{Dir, File};
34 use crate::fs::feature::git::GitCache;
35 use crate::fs::filter::GitIgnore;
36 use crate::options::{Options, Vars, vars, OptionsResult};
37 use crate::output::{escape, lines, grid, grid_details, details, View, Mode};
38 use crate::theme::Theme;
39 
40 mod fs;
41 mod info;
42 mod logger;
43 mod options;
44 mod output;
45 mod theme;
46 
47 
main()48 fn main() {
49     use std::process::exit;
50 
51     logger::configure(env::var_os(vars::EXA_DEBUG));
52 
53     let args: Vec<_> = env::args_os().skip(1).collect();
54     match Options::parse(args.iter().map(|e| e.as_ref()), &LiveVars) {
55         OptionsResult::Ok(options, mut input_paths) => {
56 
57             // List the current directory by default.
58             // (This has to be done here, otherwise git_options won’t see it.)
59             if input_paths.is_empty() {
60                 input_paths = vec![ OsStr::new(".") ];
61             }
62 
63             let git = git_options(&options, &input_paths);
64             let writer = io::stdout();
65 
66             let console_width = options.view.width.actual_terminal_width();
67             let theme = options.theme.to_theme(console_width.is_some());
68             let exa = Exa { options, writer, input_paths, theme, console_width, git };
69 
70             match exa.run() {
71                 Ok(exit_status) => {
72                     exit(exit_status);
73                 }
74 
75                 Err(e) if e.kind() == ErrorKind::BrokenPipe => {
76                     warn!("Broken pipe error: {}", e);
77                     exit(exits::SUCCESS);
78                 }
79 
80                 Err(e) => {
81                     eprintln!("{}", e);
82                     exit(exits::RUNTIME_ERROR);
83                 }
84             }
85         }
86 
87         OptionsResult::Help(help_text) => {
88             print!("{}", help_text);
89         }
90 
91         OptionsResult::Version(version_str) => {
92             print!("{}", version_str);
93         }
94 
95         OptionsResult::InvalidOptions(error) => {
96             eprintln!("exa: {}", error);
97 
98             if let Some(s) = error.suggestion() {
99                 eprintln!("{}", s);
100             }
101 
102             exit(exits::OPTIONS_ERROR);
103         }
104     }
105 }
106 
107 
108 /// The main program wrapper.
109 pub struct Exa<'args> {
110 
111     /// List of command-line options, having been successfully parsed.
112     pub options: Options,
113 
114     /// The output handle that we write to.
115     pub writer: io::Stdout,
116 
117     /// List of the free command-line arguments that should correspond to file
118     /// names (anything that isn’t an option).
119     pub input_paths: Vec<&'args OsStr>,
120 
121     /// The theme that has been configured from the command-line options and
122     /// environment variables. If colours are disabled, this is a theme with
123     /// every style set to the default.
124     pub theme: Theme,
125 
126     /// The detected width of the console. This is used to determine which
127     /// view to use.
128     pub console_width: Option<usize>,
129 
130     /// A global Git cache, if the option was passed in.
131     /// This has to last the lifetime of the program, because the user might
132     /// want to list several directories in the same repository.
133     pub git: Option<GitCache>,
134 }
135 
136 /// The “real” environment variables type.
137 /// Instead of just calling `var_os` from within the options module,
138 /// the method of looking up environment variables has to be passed in.
139 struct LiveVars;
140 impl Vars for LiveVars {
get(&self, name: &'static str) -> Option<OsString>141     fn get(&self, name: &'static str) -> Option<OsString> {
142         env::var_os(name)
143     }
144 }
145 
146 /// Create a Git cache populated with the arguments that are going to be
147 /// listed before they’re actually listed, if the options demand it.
git_options(options: &Options, args: &[&OsStr]) -> Option<GitCache>148 fn git_options(options: &Options, args: &[&OsStr]) -> Option<GitCache> {
149     if options.should_scan_for_git() {
150         Some(args.iter().map(PathBuf::from).collect())
151     }
152     else {
153         None
154     }
155 }
156 
157 impl<'args> Exa<'args> {
run(mut self) -> io::Result<i32>158     pub fn run(mut self) -> io::Result<i32> {
159         debug!("Running with options: {:#?}", self.options);
160 
161         let mut files = Vec::new();
162         let mut dirs = Vec::new();
163         let mut exit_status = 0;
164 
165         for file_path in &self.input_paths {
166             match File::from_args(PathBuf::from(file_path), None, None) {
167                 Err(e) => {
168                     exit_status = 2;
169                     writeln!(io::stderr(), "{:?}: {}", file_path, e)?;
170                 }
171 
172                 Ok(f) => {
173                     if f.points_to_directory() && ! self.options.dir_action.treat_dirs_as_files() {
174                         match f.to_dir() {
175                             Ok(d)   => dirs.push(d),
176                             Err(e)  => writeln!(io::stderr(), "{:?}: {}", file_path, e)?,
177                         }
178                     }
179                     else {
180                         files.push(f);
181                     }
182                 }
183             }
184         }
185 
186         // We want to print a directory’s name before we list it, *except* in
187         // the case where it’s the only directory, *except* if there are any
188         // files to print as well. (It’s a double negative)
189 
190         let no_files = files.is_empty();
191         let is_only_dir = dirs.len() == 1 && no_files;
192 
193         self.options.filter.filter_argument_files(&mut files);
194         self.print_files(None, files)?;
195 
196         self.print_dirs(dirs, no_files, is_only_dir, exit_status)
197     }
198 
print_dirs(&mut self, dir_files: Vec<Dir>, mut first: bool, is_only_dir: bool, exit_status: i32) -> io::Result<i32>199     fn print_dirs(&mut self, dir_files: Vec<Dir>, mut first: bool, is_only_dir: bool, exit_status: i32) -> io::Result<i32> {
200         for dir in dir_files {
201 
202             // Put a gap between directories, or between the list of files and
203             // the first directory.
204             if first {
205                 first = false;
206             }
207             else {
208                 writeln!(&mut self.writer)?;
209             }
210 
211             if ! is_only_dir {
212                 let mut bits = Vec::new();
213                 escape(dir.path.display().to_string(), &mut bits, Style::default(), Style::default());
214                 writeln!(&mut self.writer, "{}:", ANSIStrings(&bits))?;
215             }
216 
217             let mut children = Vec::new();
218             let git_ignore = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
219             for file in dir.files(self.options.filter.dot_filter, self.git.as_ref(), git_ignore) {
220                 match file {
221                     Ok(file)        => children.push(file),
222                     Err((path, e))  => writeln!(io::stderr(), "[{}: {}]", path.display(), e)?,
223                 }
224             };
225 
226             self.options.filter.filter_child_files(&mut children);
227             self.options.filter.sort_files(&mut children);
228 
229             if let Some(recurse_opts) = self.options.dir_action.recurse_options() {
230                 let depth = dir.path.components().filter(|&c| c != Component::CurDir).count() + 1;
231                 if ! recurse_opts.tree && ! recurse_opts.is_too_deep(depth) {
232 
233                     let mut child_dirs = Vec::new();
234                     for child_dir in children.iter().filter(|f| f.is_directory() && ! f.is_all_all) {
235                         match child_dir.to_dir() {
236                             Ok(d)   => child_dirs.push(d),
237                             Err(e)  => writeln!(io::stderr(), "{}: {}", child_dir.path.display(), e)?,
238                         }
239                     }
240 
241                     self.print_files(Some(&dir), children)?;
242                     match self.print_dirs(child_dirs, false, false, exit_status) {
243                         Ok(_)   => (),
244                         Err(e)  => return Err(e),
245                     }
246                     continue;
247                 }
248             }
249 
250             self.print_files(Some(&dir), children)?;
251         }
252 
253         Ok(exit_status)
254     }
255 
256     /// Prints the list of files using whichever view is selected.
print_files(&mut self, dir: Option<&Dir>, files: Vec<File<'_>>) -> io::Result<()>257     fn print_files(&mut self, dir: Option<&Dir>, files: Vec<File<'_>>) -> io::Result<()> {
258         if files.is_empty() {
259             return Ok(());
260         }
261 
262         let theme = &self.theme;
263         let View { ref mode, ref file_style, .. } = self.options.view;
264 
265         match (mode, self.console_width) {
266             (Mode::Grid(ref opts), Some(console_width)) => {
267                 let filter = &self.options.filter;
268                 let r = grid::Render { files, theme, file_style, opts, console_width, filter };
269                 r.render(&mut self.writer)
270             }
271 
272             (Mode::Grid(_), None) |
273             (Mode::Lines,   _)    => {
274                 let filter = &self.options.filter;
275                 let r = lines::Render { files, theme, file_style, filter };
276                 r.render(&mut self.writer)
277             }
278 
279             (Mode::Details(ref opts), _) => {
280                 let filter = &self.options.filter;
281                 let recurse = self.options.dir_action.recurse_options();
282 
283                 let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
284                 let git = self.git.as_ref();
285                 let r = details::Render { dir, files, theme, file_style, opts, filter, recurse, git_ignoring, git };
286                 r.render(&mut self.writer)
287             }
288 
289             (Mode::GridDetails(ref opts), Some(console_width)) => {
290                 let grid = &opts.grid;
291                 let details = &opts.details;
292                 let row_threshold = opts.row_threshold;
293 
294                 let filter = &self.options.filter;
295                 let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
296                 let git = self.git.as_ref();
297 
298                 let r = grid_details::Render { dir, files, theme, file_style, grid, details, filter, row_threshold, git_ignoring, git, console_width };
299                 r.render(&mut self.writer)
300             }
301 
302             (Mode::GridDetails(ref opts), None) => {
303                 let opts = &opts.to_details_options();
304                 let filter = &self.options.filter;
305                 let recurse = self.options.dir_action.recurse_options();
306                 let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
307 
308                 let git = self.git.as_ref();
309                 let r = details::Render { dir, files, theme, file_style, opts, filter, recurse, git_ignoring, git };
310                 r.render(&mut self.writer)
311             }
312         }
313     }
314 }
315 
316 
317 mod exits {
318 
319     /// Exit code for when exa runs OK.
320     pub const SUCCESS: i32 = 0;
321 
322     /// Exit code for when there was at least one I/O error during execution.
323     pub const RUNTIME_ERROR: i32 = 1;
324 
325     /// Exit code for when the command-line options are invalid.
326     pub const OPTIONS_ERROR: i32 = 3;
327 }
328