main.rs 17 KB

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