main.rs 13 KB

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