main.rs 18 KB

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