main.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  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::env;
  24. use std::ffi::{OsStr, OsString};
  25. use std::io::{self, stdin, ErrorKind, IsTerminal, Read, Write};
  26. use std::path::{Component, PathBuf};
  27. use std::process::exit;
  28. use nu_ansi_term::{AnsiStrings as ANSIStrings, Style};
  29. use crate::fs::feature::git::GitCache;
  30. use crate::fs::filter::GitIgnore;
  31. use crate::fs::{Dir, File};
  32. use crate::options::stdin::FilesInput;
  33. use crate::options::{vars, Options, OptionsResult, Vars};
  34. use crate::output::{details, escape, file_name, grid, grid_details, lines, Mode, View};
  35. use crate::theme::Theme;
  36. use log::*;
  37. mod fs;
  38. mod info;
  39. mod logger;
  40. mod options;
  41. mod output;
  42. mod theme;
  43. fn main() {
  44. #[cfg(unix)]
  45. unsafe {
  46. libc::signal(libc::SIGPIPE, libc::SIG_DFL);
  47. }
  48. logger::configure(env::var_os(vars::EZA_DEBUG).or_else(|| env::var_os(vars::EXA_DEBUG)));
  49. let stdout_istty = io::stdout().is_terminal();
  50. let mut input = String::new();
  51. let args: Vec<_> = env::args_os().skip(1).collect();
  52. match Options::parse(args.iter().map(std::convert::AsRef::as_ref), &LiveVars) {
  53. OptionsResult::Ok(options, mut input_paths) => {
  54. // List the current directory by default.
  55. // (This has to be done here, otherwise git_options won’t see it.)
  56. if input_paths.is_empty() {
  57. match &options.stdin {
  58. FilesInput::Args => {
  59. input_paths = vec![OsStr::new(".")];
  60. }
  61. FilesInput::Stdin(separator) => {
  62. stdin()
  63. .read_to_string(&mut input)
  64. .expect("Failed to read from stdin");
  65. input_paths.extend(
  66. input
  67. .split(&separator.clone().into_string().unwrap_or("\n".to_string()))
  68. .map(std::ffi::OsStr::new)
  69. .filter(|s| !s.is_empty())
  70. .collect::<Vec<_>>(),
  71. );
  72. }
  73. }
  74. }
  75. let git = git_options(&options, &input_paths);
  76. let writer = io::stdout();
  77. let git_repos = git_repos(&options, &input_paths);
  78. let console_width = options.view.width.actual_terminal_width();
  79. let theme = options.theme.to_theme(stdout_istty);
  80. let exa = Exa {
  81. options,
  82. writer,
  83. input_paths,
  84. theme,
  85. console_width,
  86. git,
  87. git_repos,
  88. };
  89. info!("matching on exa.run");
  90. match exa.run() {
  91. Ok(exit_status) => {
  92. trace!("exa.run: exit Ok(exit_status)");
  93. exit(exit_status);
  94. }
  95. Err(e) if e.kind() == ErrorKind::BrokenPipe => {
  96. warn!("Broken pipe error: {e}");
  97. exit(exits::SUCCESS);
  98. }
  99. Err(e) => {
  100. eprintln!("{e}");
  101. trace!("exa.run: exit RUNTIME_ERROR");
  102. exit(exits::RUNTIME_ERROR);
  103. }
  104. }
  105. }
  106. OptionsResult::Help(help_text) => {
  107. print!("{help_text}");
  108. }
  109. OptionsResult::Version(version_str) => {
  110. print!("{version_str}");
  111. }
  112. OptionsResult::InvalidOptions(error) => {
  113. eprintln!("eza: {error}");
  114. if let Some(s) = error.suggestion() {
  115. eprintln!("{s}");
  116. }
  117. exit(exits::OPTIONS_ERROR);
  118. }
  119. }
  120. }
  121. /// The main program wrapper.
  122. pub struct Exa<'args> {
  123. /// List of command-line options, having been successfully parsed.
  124. pub options: Options,
  125. /// The output handle that we write to.
  126. pub writer: io::Stdout,
  127. /// List of the free command-line arguments that should correspond to file
  128. /// names (anything that isn’t an option).
  129. pub input_paths: Vec<&'args OsStr>,
  130. /// The theme that has been configured from the command-line options and
  131. /// environment variables. If colours are disabled, this is a theme with
  132. /// every style set to the default.
  133. pub theme: Theme,
  134. /// The detected width of the console. This is used to determine which
  135. /// view to use.
  136. pub console_width: Option<usize>,
  137. /// A global Git cache, if the option was passed in.
  138. /// This has to last the lifetime of the program, because the user might
  139. /// want to list several directories in the same repository.
  140. pub git: Option<GitCache>,
  141. pub git_repos: bool,
  142. }
  143. /// The “real” environment variables type.
  144. /// Instead of just calling `var_os` from within the options module,
  145. /// the method of looking up environment variables has to be passed in.
  146. struct LiveVars;
  147. impl Vars for LiveVars {
  148. fn get(&self, name: &'static str) -> Option<OsString> {
  149. env::var_os(name)
  150. }
  151. }
  152. /// Create a Git cache populated with the arguments that are going to be
  153. /// listed before they’re actually listed, if the options demand it.
  154. fn git_options(options: &Options, args: &[&OsStr]) -> Option<GitCache> {
  155. if options.should_scan_for_git() {
  156. Some(args.iter().map(PathBuf::from).collect())
  157. } else {
  158. None
  159. }
  160. }
  161. #[cfg(not(feature = "git"))]
  162. fn git_repos(_options: &Options, _args: &[&OsStr]) -> bool {
  163. return false;
  164. }
  165. #[cfg(feature = "git")]
  166. fn get_files_in_dir(paths: &mut Vec<PathBuf>, path: PathBuf) {
  167. let temp_paths = if path.is_dir() {
  168. match path.read_dir() {
  169. Err(_) => {
  170. vec![path]
  171. }
  172. Ok(d) => d
  173. .filter_map(|entry| entry.ok().map(|e| e.path()))
  174. .collect::<Vec<PathBuf>>(),
  175. }
  176. } else {
  177. vec![path]
  178. };
  179. paths.extend(temp_paths);
  180. }
  181. #[cfg(feature = "git")]
  182. fn git_repos(options: &Options, args: &[&OsStr]) -> bool {
  183. let option_enabled = match options.view.mode {
  184. Mode::Details(details::Options {
  185. table: Some(ref table),
  186. ..
  187. })
  188. | Mode::GridDetails(grid_details::Options {
  189. details:
  190. details::Options {
  191. table: Some(ref table),
  192. ..
  193. },
  194. ..
  195. }) => table.columns.subdir_git_repos || table.columns.subdir_git_repos_no_stat,
  196. _ => false,
  197. };
  198. if option_enabled {
  199. let paths: Vec<PathBuf> = args.iter().map(PathBuf::from).collect::<Vec<PathBuf>>();
  200. let mut files: Vec<PathBuf> = Vec::new();
  201. for path in paths {
  202. get_files_in_dir(&mut files, path);
  203. }
  204. let repos: Vec<bool> = files
  205. .iter()
  206. .map(git2::Repository::open)
  207. .map(|repo| repo.is_ok())
  208. .collect();
  209. repos.contains(&true)
  210. } else {
  211. false
  212. }
  213. }
  214. impl<'args> Exa<'args> {
  215. /// # Errors
  216. ///
  217. /// Will return `Err` if printing to stderr fails.
  218. pub fn run(mut self) -> io::Result<i32> {
  219. debug!("Running with options: {:#?}", self.options);
  220. let mut files = Vec::new();
  221. let mut dirs = Vec::new();
  222. let mut exit_status = 0;
  223. for file_path in &self.input_paths {
  224. match File::from_args(
  225. PathBuf::from(file_path),
  226. None,
  227. None,
  228. self.options.view.deref_links,
  229. self.options.view.total_size,
  230. ) {
  231. Err(e) => {
  232. exit_status = 2;
  233. writeln!(io::stderr(), "{file_path:?}: {e}")?;
  234. }
  235. Ok(f) => {
  236. if f.points_to_directory() && !self.options.dir_action.treat_dirs_as_files() {
  237. trace!("matching on to_dir");
  238. match f.to_dir() {
  239. Ok(d) => dirs.push(d),
  240. Err(e) if e.kind() == ErrorKind::PermissionDenied => {
  241. eprintln!("{file_path:?}: {e}");
  242. exit(exits::PERMISSION_DENIED);
  243. }
  244. Err(e) => writeln!(io::stderr(), "{file_path:?}: {e}")?,
  245. }
  246. } else {
  247. files.push(f);
  248. }
  249. }
  250. }
  251. }
  252. // We want to print a directory’s name before we list it, *except* in
  253. // the case where it’s the only directory, *except* if there are any
  254. // files to print as well. (It’s a double negative)
  255. let no_files = files.is_empty();
  256. let is_only_dir = dirs.len() == 1 && no_files;
  257. self.options.filter.filter_argument_files(&mut files);
  258. self.print_files(None, files)?;
  259. self.print_dirs(dirs, no_files, is_only_dir, exit_status)
  260. }
  261. fn print_dirs(
  262. &mut self,
  263. dir_files: Vec<Dir>,
  264. mut first: bool,
  265. is_only_dir: bool,
  266. exit_status: i32,
  267. ) -> io::Result<i32> {
  268. let View {
  269. file_style: file_name::Options { quote_style, .. },
  270. ..
  271. } = self.options.view;
  272. for dir in dir_files {
  273. // Put a gap between directories, or between the list of files and
  274. // the first directory.
  275. if first {
  276. first = false;
  277. } else {
  278. writeln!(&mut self.writer)?;
  279. }
  280. if !is_only_dir {
  281. let mut bits = Vec::new();
  282. escape(
  283. dir.path.display().to_string(),
  284. &mut bits,
  285. Style::default(),
  286. Style::default(),
  287. quote_style,
  288. );
  289. writeln!(&mut self.writer, "{}:", ANSIStrings(&bits))?;
  290. }
  291. let mut children = Vec::new();
  292. let git_ignore = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
  293. for file in dir.files(
  294. self.options.filter.dot_filter,
  295. self.git.as_ref(),
  296. git_ignore,
  297. self.options.view.deref_links,
  298. self.options.view.total_size,
  299. ) {
  300. match file {
  301. Ok(file) => children.push(file),
  302. Err((path, e)) => writeln!(io::stderr(), "[{}: {}]", path.display(), e)?,
  303. }
  304. }
  305. self.options.filter.filter_child_files(&mut children);
  306. self.options.filter.sort_files(&mut children);
  307. if let Some(recurse_opts) = self.options.dir_action.recurse_options() {
  308. let depth = dir
  309. .path
  310. .components()
  311. .filter(|&c| c != Component::CurDir)
  312. .count()
  313. + 1;
  314. if !recurse_opts.tree && !recurse_opts.is_too_deep(depth) {
  315. let mut child_dirs = Vec::new();
  316. for child_dir in children
  317. .iter()
  318. .filter(|f| f.is_directory() && !f.is_all_all)
  319. {
  320. match child_dir.to_dir() {
  321. Ok(d) => child_dirs.push(d),
  322. Err(e) => {
  323. writeln!(io::stderr(), "{}: {}", child_dir.path.display(), e)?;
  324. }
  325. }
  326. }
  327. self.print_files(Some(&dir), children)?;
  328. match self.print_dirs(child_dirs, false, false, exit_status) {
  329. Ok(_) => (),
  330. Err(e) => return Err(e),
  331. }
  332. continue;
  333. }
  334. }
  335. self.print_files(Some(&dir), children)?;
  336. }
  337. Ok(exit_status)
  338. }
  339. /// Prints the list of files using whichever view is selected.
  340. fn print_files(&mut self, dir: Option<&Dir>, files: Vec<File<'_>>) -> io::Result<()> {
  341. if files.is_empty() {
  342. return Ok(());
  343. }
  344. let theme = &self.theme;
  345. let View {
  346. ref mode,
  347. ref file_style,
  348. ..
  349. } = self.options.view;
  350. match (mode, self.console_width) {
  351. (Mode::Grid(ref opts), Some(console_width)) => {
  352. let filter = &self.options.filter;
  353. let r = grid::Render {
  354. files,
  355. theme,
  356. file_style,
  357. opts,
  358. console_width,
  359. filter,
  360. };
  361. r.render(&mut self.writer)
  362. }
  363. (Mode::Grid(_), None) | (Mode::Lines, _) => {
  364. let filter = &self.options.filter;
  365. let r = lines::Render {
  366. files,
  367. theme,
  368. file_style,
  369. filter,
  370. };
  371. r.render(&mut self.writer)
  372. }
  373. (Mode::Details(ref opts), _) => {
  374. let filter = &self.options.filter;
  375. let recurse = self.options.dir_action.recurse_options();
  376. let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
  377. let git = self.git.as_ref();
  378. let git_repos = self.git_repos;
  379. let r = details::Render {
  380. dir,
  381. files,
  382. theme,
  383. file_style,
  384. opts,
  385. recurse,
  386. filter,
  387. git_ignoring,
  388. git,
  389. git_repos,
  390. };
  391. r.render(&mut self.writer)
  392. }
  393. (Mode::GridDetails(ref opts), Some(console_width)) => {
  394. let details = &opts.details;
  395. let row_threshold = opts.row_threshold;
  396. let filter = &self.options.filter;
  397. let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
  398. let git = self.git.as_ref();
  399. let git_repos = self.git_repos;
  400. let r = grid_details::Render {
  401. dir,
  402. files,
  403. theme,
  404. file_style,
  405. details,
  406. filter,
  407. row_threshold,
  408. git_ignoring,
  409. git,
  410. console_width,
  411. git_repos,
  412. };
  413. r.render(&mut self.writer)
  414. }
  415. (Mode::GridDetails(ref opts), None) => {
  416. let opts = &opts.to_details_options();
  417. let filter = &self.options.filter;
  418. let recurse = self.options.dir_action.recurse_options();
  419. let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
  420. let git = self.git.as_ref();
  421. let git_repos = self.git_repos;
  422. let r = details::Render {
  423. dir,
  424. files,
  425. theme,
  426. file_style,
  427. opts,
  428. recurse,
  429. filter,
  430. git_ignoring,
  431. git,
  432. git_repos,
  433. };
  434. r.render(&mut self.writer)
  435. }
  436. }
  437. }
  438. }
  439. mod exits {
  440. /// Exit code for when exa runs OK.
  441. pub const SUCCESS: i32 = 0;
  442. /// Exit code for when there was at least one I/O error during execution.
  443. pub const RUNTIME_ERROR: i32 = 1;
  444. /// Exit code for when the command-line options are invalid.
  445. pub const OPTIONS_ERROR: i32 = 3;
  446. /// Exit code for missing file permissions
  447. pub const PERMISSION_DENIED: i32 = 13;
  448. }