1use std::cmp::Ordering;
2use std::collections::HashMap;
3use std::ffi::CString;
4use std::fs::{self, DirEntry, Metadata};
5use std::io::{self, BufWriter, Write};
6use std::os::unix::fs::MetadataExt;
7use std::path::{Path, PathBuf};
8use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
9use std::time::SystemTime;
10
11static IS_C_LOCALE: AtomicBool = AtomicBool::new(false);
14
15pub fn detect_c_locale() {
18 let lc = unsafe { libc::setlocale(libc::LC_COLLATE, std::ptr::null()) };
19 if lc.is_null() {
20 IS_C_LOCALE.store(true, AtomicOrdering::Relaxed);
21 return;
22 }
23 let s = unsafe { std::ffi::CStr::from_ptr(lc) }.to_bytes();
24 let is_c = s == b"C" || s == b"POSIX";
25 IS_C_LOCALE.store(is_c, AtomicOrdering::Relaxed);
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum SortBy {
35 Name,
36 Size,
37 Time,
38 Extension,
39 Version,
40 None,
41 Width,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum OutputFormat {
47 Long,
48 SingleColumn,
49 Columns,
50 Comma,
51 Across,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum ColorMode {
57 Always,
58 Auto,
59 Never,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum TimeField {
65 Mtime,
66 Atime,
67 Ctime,
68 Birth,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum TimeStyle {
74 FullIso,
75 LongIso,
76 Iso,
77 Locale,
78 Custom(String),
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum IndicatorStyle {
84 None,
85 Slash,
86 FileType,
87 Classify,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum ClassifyMode {
93 Always,
94 Auto,
95 Never,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum QuotingStyle {
101 Literal,
102 Locale,
103 Shell,
104 ShellAlways,
105 ShellEscape,
106 ShellEscapeAlways,
107 C,
108 Escape,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum HyperlinkMode {
114 Always,
115 Auto,
116 Never,
117}
118
119#[derive(Debug, Clone)]
121pub struct LsConfig {
122 pub all: bool,
123 pub almost_all: bool,
124 pub long_format: bool,
125 pub human_readable: bool,
126 pub si: bool,
127 pub reverse: bool,
128 pub recursive: bool,
129 pub sort_by: SortBy,
130 pub format: OutputFormat,
131 pub classify: ClassifyMode,
132 pub color: ColorMode,
133 pub group_directories_first: bool,
134 pub show_inode: bool,
135 pub show_size: bool,
136 pub show_owner: bool,
137 pub show_group: bool,
138 pub numeric_ids: bool,
139 pub dereference: bool,
140 pub directory: bool,
141 pub time_field: TimeField,
142 pub time_style: TimeStyle,
143 pub ignore_patterns: Vec<String>,
144 pub ignore_backups: bool,
145 pub width: usize,
146 pub quoting_style: QuotingStyle,
147 pub hide_control_chars: bool,
148 pub kibibytes: bool,
149 pub indicator_style: IndicatorStyle,
150 pub tab_size: usize,
151 pub hyperlink: HyperlinkMode,
152 pub context: bool,
153 pub literal: bool,
154}
155
156impl Default for LsConfig {
157 fn default() -> Self {
158 LsConfig {
159 all: false,
160 almost_all: false,
161 long_format: false,
162 human_readable: false,
163 si: false,
164 reverse: false,
165 recursive: false,
166 sort_by: SortBy::Name,
167 format: OutputFormat::Columns,
168 classify: ClassifyMode::Never,
169 color: ColorMode::Auto,
170 group_directories_first: false,
171 show_inode: false,
172 show_size: false,
173 show_owner: true,
174 show_group: true,
175 numeric_ids: false,
176 dereference: false,
177 directory: false,
178 time_field: TimeField::Mtime,
179 time_style: TimeStyle::Locale,
180 ignore_patterns: Vec::new(),
181 ignore_backups: false,
182 width: 80,
183 quoting_style: QuotingStyle::Literal,
184 hide_control_chars: false,
185 kibibytes: false,
186 indicator_style: IndicatorStyle::None,
187 tab_size: 8,
188 hyperlink: HyperlinkMode::Never,
189 context: false,
190 literal: false,
191 }
192 }
193}
194
195#[derive(Debug, Clone)]
201pub struct ColorDb {
202 pub map: HashMap<String, String>,
203 pub dir: String,
204 pub link: String,
205 pub exec: String,
206 pub pipe: String,
207 pub socket: String,
208 pub block_dev: String,
209 pub char_dev: String,
210 pub orphan: String,
211 pub setuid: String,
212 pub setgid: String,
213 pub sticky: String,
214 pub other_writable: String,
215 pub sticky_other_writable: String,
216 pub reset: String,
217}
218
219impl Default for ColorDb {
220 fn default() -> Self {
221 ColorDb {
222 map: HashMap::new(),
223 dir: "\x1b[01;34m".to_string(), link: "\x1b[01;36m".to_string(), exec: "\x1b[01;32m".to_string(), pipe: "\x1b[33m".to_string(), socket: "\x1b[01;35m".to_string(), block_dev: "\x1b[01;33m".to_string(), char_dev: "\x1b[01;33m".to_string(), orphan: "\x1b[01;31m".to_string(), setuid: "\x1b[37;41m".to_string(), setgid: "\x1b[30;43m".to_string(), sticky: "\x1b[37;44m".to_string(), other_writable: "\x1b[34;42m".to_string(), sticky_other_writable: "\x1b[30;42m".to_string(), reset: "\x1b[0m".to_string(),
237 }
238 }
239}
240
241impl ColorDb {
242 pub fn from_env() -> Self {
244 let mut db = ColorDb::default();
245 if let Ok(val) = std::env::var("LS_COLORS") {
246 for item in val.split(':') {
247 if let Some((key, code)) = item.split_once('=') {
248 let esc = format!("\x1b[{}m", code);
249 match key {
250 "di" => db.dir = esc,
251 "ln" => db.link = esc,
252 "ex" => db.exec = esc,
253 "pi" | "fi" if key == "pi" => db.pipe = esc,
254 "so" => db.socket = esc,
255 "bd" => db.block_dev = esc,
256 "cd" => db.char_dev = esc,
257 "or" => db.orphan = esc,
258 "su" => db.setuid = esc,
259 "sg" => db.setgid = esc,
260 "st" => db.sticky = esc,
261 "ow" => db.other_writable = esc,
262 "tw" => db.sticky_other_writable = esc,
263 "rs" => db.reset = esc,
264 _ => {
265 if key.starts_with('*') {
266 db.map.insert(key[1..].to_string(), esc);
267 }
268 }
269 }
270 }
271 }
272 }
273 db
274 }
275
276 fn color_for(&self, entry: &FileEntry) -> &str {
278 let mode = entry.mode;
279 let ft = mode & (libc::S_IFMT as u32);
280
281 if ft == libc::S_IFLNK as u32 {
283 if entry.link_target_ok {
284 return &self.link;
285 } else {
286 return &self.orphan;
287 }
288 }
289
290 if ft == libc::S_IFDIR as u32 {
292 let sticky = mode & (libc::S_ISVTX as u32) != 0;
293 let ow = mode & (libc::S_IWOTH as u32) != 0;
294 if sticky && ow {
295 return &self.sticky_other_writable;
296 }
297 if ow {
298 return &self.other_writable;
299 }
300 if sticky {
301 return &self.sticky;
302 }
303 return &self.dir;
304 }
305
306 if ft == libc::S_IFIFO as u32 {
308 return &self.pipe;
309 }
310 if ft == libc::S_IFSOCK as u32 {
311 return &self.socket;
312 }
313 if ft == libc::S_IFBLK as u32 {
314 return &self.block_dev;
315 }
316 if ft == libc::S_IFCHR as u32 {
317 return &self.char_dev;
318 }
319
320 if mode & (libc::S_ISUID as u32) != 0 {
322 return &self.setuid;
323 }
324 if mode & (libc::S_ISGID as u32) != 0 {
325 return &self.setgid;
326 }
327
328 if let Some(ext_pos) = entry.name.rfind('.') {
330 let ext = &entry.name[ext_pos..];
331 if let Some(c) = self.map.get(ext) {
332 return c;
333 }
334 }
335
336 if ft == libc::S_IFREG as u32
338 && mode & (libc::S_IXUSR as u32 | libc::S_IXGRP as u32 | libc::S_IXOTH as u32) != 0
339 {
340 return &self.exec;
341 }
342
343 ""
344 }
345}
346
347#[derive(Debug, Clone)]
353pub struct FileEntry {
354 pub name: String,
355 pub path: PathBuf,
356 pub sort_key: CString,
358 pub ino: u64,
359 pub nlink: u64,
360 pub mode: u32,
361 pub uid: u32,
362 pub gid: u32,
363 pub size: u64,
364 pub blocks: u64,
365 pub mtime: i64,
366 pub mtime_nsec: i64,
367 pub atime: i64,
368 pub atime_nsec: i64,
369 pub ctime: i64,
370 pub ctime_nsec: i64,
371 pub rdev_major: u32,
372 pub rdev_minor: u32,
373 pub is_dir: bool,
374 pub link_target: Option<String>,
375 pub link_target_ok: bool,
376}
377
378impl FileEntry {
379 fn from_dir_entry(de: &DirEntry, config: &LsConfig) -> io::Result<Self> {
381 let name = de.file_name().to_string_lossy().into_owned();
382 let path = de.path();
383
384 let meta = if config.dereference {
385 fs::metadata(&path).or_else(|_| fs::symlink_metadata(&path))?
386 } else {
387 fs::symlink_metadata(&path)?
388 };
389
390 Self::from_metadata(name, path, &meta, config)
391 }
392
393 pub fn from_path_with_name(name: String, path: &Path, config: &LsConfig) -> io::Result<Self> {
396 let meta = if config.dereference {
397 fs::metadata(path).or_else(|_| fs::symlink_metadata(path))?
398 } else {
399 fs::symlink_metadata(path)?
400 };
401 Self::from_metadata(name, path.to_path_buf(), &meta, config)
402 }
403
404 fn from_metadata(
405 name: String,
406 path: PathBuf,
407 meta: &Metadata,
408 _config: &LsConfig,
409 ) -> io::Result<Self> {
410 let file_type = meta.file_type();
411 let is_symlink = file_type.is_symlink();
412
413 let (link_target, link_target_ok) = if is_symlink {
414 match fs::read_link(&path) {
415 Ok(target) => {
416 let ok = fs::metadata(&path).is_ok();
417 (Some(target.to_string_lossy().into_owned()), ok)
418 }
419 Err(_) => (None, false),
420 }
421 } else {
422 (None, true)
423 };
424
425 let rdev = meta.rdev();
426 let sort_key = CString::new(name.as_str()).unwrap_or_default();
427
428 Ok(FileEntry {
429 name,
430 path,
431 sort_key,
432 ino: meta.ino(),
433 nlink: meta.nlink(),
434 mode: meta.mode(),
435 uid: meta.uid(),
436 gid: meta.gid(),
437 size: meta.size(),
438 blocks: meta.blocks(),
439 mtime: meta.mtime(),
440 mtime_nsec: meta.mtime_nsec(),
441 atime: meta.atime(),
442 atime_nsec: meta.atime_nsec(),
443 ctime: meta.ctime(),
444 ctime_nsec: meta.ctime_nsec(),
445 rdev_major: ((rdev >> 8) & 0xfff) as u32,
446 rdev_minor: (rdev & 0xff) as u32,
447 is_dir: meta.is_dir(),
448 link_target,
449 link_target_ok,
450 })
451 }
452
453 fn time_secs(&self, field: TimeField) -> i64 {
455 match field {
456 TimeField::Mtime => self.mtime,
457 TimeField::Atime => self.atime,
458 TimeField::Ctime | TimeField::Birth => self.ctime,
459 }
460 }
461
462 fn time_nsec(&self, field: TimeField) -> i64 {
463 match field {
464 TimeField::Mtime => self.mtime_nsec,
465 TimeField::Atime => self.atime_nsec,
466 TimeField::Ctime | TimeField::Birth => self.ctime_nsec,
467 }
468 }
469
470 fn extension(&self) -> &str {
472 match self.name.rfind('.') {
473 Some(pos) if pos > 0 => &self.name[pos + 1..],
474 _ => "",
475 }
476 }
477
478 fn is_directory(&self) -> bool {
480 self.is_dir
481 }
482
483 fn indicator(&self, style: IndicatorStyle) -> &'static str {
485 let ft = self.mode & (libc::S_IFMT as u32);
486 match style {
487 IndicatorStyle::None => "",
488 IndicatorStyle::Slash => {
489 if ft == libc::S_IFDIR as u32 {
490 "/"
491 } else {
492 ""
493 }
494 }
495 IndicatorStyle::FileType => match ft {
496 x if x == libc::S_IFDIR as u32 => "/",
497 x if x == libc::S_IFLNK as u32 => "@",
498 x if x == libc::S_IFIFO as u32 => "|",
499 x if x == libc::S_IFSOCK as u32 => "=",
500 _ => "",
501 },
502 IndicatorStyle::Classify => match ft {
503 x if x == libc::S_IFDIR as u32 => "/",
504 x if x == libc::S_IFLNK as u32 => "@",
505 x if x == libc::S_IFIFO as u32 => "|",
506 x if x == libc::S_IFSOCK as u32 => "=",
507 _ => {
508 if ft == libc::S_IFREG as u32
509 && self.mode
510 & (libc::S_IXUSR as u32 | libc::S_IXGRP as u32 | libc::S_IXOTH as u32)
511 != 0
512 {
513 "*"
514 } else {
515 ""
516 }
517 }
518 },
519 }
520 }
521
522 fn display_width(&self, config: &LsConfig) -> usize {
524 let quoted = quote_name(&self.name, config);
525 let ind = self.indicator(config.indicator_style);
526 quoted.len() + ind.len()
527 }
528}
529
530pub fn quote_name(name: &str, config: &LsConfig) -> String {
536 match config.quoting_style {
537 QuotingStyle::Literal => {
538 if config.hide_control_chars {
539 hide_control(name)
540 } else {
541 name.to_string()
542 }
543 }
544 QuotingStyle::Escape => escape_name(name),
545 QuotingStyle::C => c_quote(name),
546 QuotingStyle::Shell => shell_quote(name, false, false),
547 QuotingStyle::ShellAlways => shell_quote(name, true, false),
548 QuotingStyle::ShellEscape => shell_quote(name, false, true),
549 QuotingStyle::ShellEscapeAlways => shell_quote(name, true, true),
550 QuotingStyle::Locale => locale_quote(name),
551 }
552}
553
554fn hide_control(name: &str) -> String {
555 name.chars()
556 .map(|c| if c.is_control() { '?' } else { c })
557 .collect()
558}
559
560fn escape_name(name: &str) -> String {
561 let mut out = String::with_capacity(name.len());
562 for c in name.chars() {
563 match c {
564 '\\' => out.push_str("\\\\"),
565 '\n' => out.push_str("\\n"),
566 '\r' => out.push_str("\\r"),
567 '\t' => out.push_str("\\t"),
568 c if c.is_control() => {
569 out.push_str(&format!("\\{:03o}", c as u32));
570 }
571 c => out.push(c),
572 }
573 }
574 out
575}
576
577fn c_quote(name: &str) -> String {
578 let mut out = String::with_capacity(name.len() + 2);
579 out.push('"');
580 for c in name.chars() {
581 match c {
582 '"' => out.push_str("\\\""),
583 '\\' => out.push_str("\\\\"),
584 '\n' => out.push_str("\\n"),
585 '\r' => out.push_str("\\r"),
586 '\t' => out.push_str("\\t"),
587 '\x07' => out.push_str("\\a"),
588 '\x08' => out.push_str("\\b"),
589 '\x0C' => out.push_str("\\f"),
590 '\x0B' => out.push_str("\\v"),
591 c if c.is_control() => {
592 out.push_str(&format!("\\{:03o}", c as u32));
593 }
594 c => out.push(c),
595 }
596 }
597 out.push('"');
598 out
599}
600
601fn shell_quote(name: &str, always: bool, escape: bool) -> String {
602 let needs_quoting = name.is_empty()
603 || name
604 .chars()
605 .any(|c| " \t\n'\"\\|&;()<>!$`#~{}[]?*".contains(c) || c.is_control());
606
607 if !needs_quoting && !always {
608 return name.to_string();
609 }
610
611 if escape {
612 let has_control = name.chars().any(|c| c.is_control());
614 if has_control {
615 let mut out = String::with_capacity(name.len() + 4);
616 out.push_str("$'");
617 for c in name.chars() {
618 match c {
619 '\'' => out.push_str("\\'"),
620 '\\' => out.push_str("\\\\"),
621 '\n' => out.push_str("\\n"),
622 '\r' => out.push_str("\\r"),
623 '\t' => out.push_str("\\t"),
624 c if c.is_control() => {
625 out.push_str(&format!("\\{:03o}", c as u32));
626 }
627 c => out.push(c),
628 }
629 }
630 out.push('\'');
631 return out;
632 }
633 }
634
635 let mut out = String::with_capacity(name.len() + 2);
637 out.push('\'');
638 for c in name.chars() {
639 if c == '\'' {
640 out.push_str("'\\''");
641 } else {
642 out.push(c);
643 }
644 }
645 out.push('\'');
646 out
647}
648
649fn locale_quote(name: &str) -> String {
650 let mut out = String::with_capacity(name.len() + 2);
652 out.push('\u{2018}');
653 for c in name.chars() {
654 match c {
655 '\\' => out.push_str("\\\\"),
656 '\n' => out.push_str("\\n"),
657 '\r' => out.push_str("\\r"),
658 '\t' => out.push_str("\\t"),
659 c if c.is_control() => {
660 out.push_str(&format!("\\{:03o}", c as u32));
661 }
662 c => out.push(c),
663 }
664 }
665 out.push('\u{2019}');
666 out
667}
668
669pub(crate) fn version_cmp(a: &str, b: &str) -> Ordering {
675 let ab = a.as_bytes();
676 let bb = b.as_bytes();
677 let mut ai = 0;
678 let mut bi = 0;
679 while ai < ab.len() && bi < bb.len() {
680 let ac = ab[ai];
681 let bc = bb[bi];
682 if ac.is_ascii_digit() && bc.is_ascii_digit() {
683 let a_start = ai;
685 let b_start = bi;
686 while ai < ab.len() && ab[ai] == b'0' {
687 ai += 1;
688 }
689 while bi < bb.len() && bb[bi] == b'0' {
690 bi += 1;
691 }
692 let a_num_start = ai;
693 let b_num_start = bi;
694 while ai < ab.len() && ab[ai].is_ascii_digit() {
695 ai += 1;
696 }
697 while bi < bb.len() && bb[bi].is_ascii_digit() {
698 bi += 1;
699 }
700 let a_len = ai - a_num_start;
701 let b_len = bi - b_num_start;
702 if a_len != b_len {
703 return a_len.cmp(&b_len);
704 }
705 let ord = ab[a_num_start..ai].cmp(&bb[b_num_start..bi]);
706 if ord != Ordering::Equal {
707 return ord;
708 }
709 let a_zeros = a_num_start - a_start;
711 let b_zeros = b_num_start - b_start;
712 if a_zeros != b_zeros {
713 return a_zeros.cmp(&b_zeros);
714 }
715 } else {
716 let ord = ac.cmp(&bc);
717 if ord != Ordering::Equal {
718 return ord;
719 }
720 ai += 1;
721 bi += 1;
722 }
723 }
724 ab.len().cmp(&bb.len())
725}
726
727fn sort_entries(entries: &mut [FileEntry], config: &LsConfig) {
728 if config.group_directories_first {
729 entries.sort_by(|a, b| {
731 let a_dir = a.is_directory();
732 let b_dir = b.is_directory();
733 match (a_dir, b_dir) {
734 (true, false) => Ordering::Less,
735 (false, true) => Ordering::Greater,
736 _ => compare_entries(a, b, config),
737 }
738 });
739 } else {
740 entries.sort_by(|a, b| compare_entries(a, b, config));
741 }
742}
743
744#[inline]
748fn locale_cmp_cstr(a: &CString, b: &CString) -> Ordering {
749 if IS_C_LOCALE.load(AtomicOrdering::Relaxed) {
750 a.as_bytes().cmp(b.as_bytes())
751 } else {
752 let result = unsafe { libc::strcoll(a.as_ptr(), b.as_ptr()) };
753 result.cmp(&0)
754 }
755}
756
757fn locale_cmp(a: &str, b: &str) -> Ordering {
759 if IS_C_LOCALE.load(AtomicOrdering::Relaxed) {
760 a.cmp(b)
761 } else {
762 let ca = CString::new(a).unwrap_or_default();
763 let cb = CString::new(b).unwrap_or_default();
764 let result = unsafe { libc::strcoll(ca.as_ptr(), cb.as_ptr()) };
765 result.cmp(&0)
766 }
767}
768
769fn compare_entries(a: &FileEntry, b: &FileEntry, config: &LsConfig) -> Ordering {
770 let ord = match config.sort_by {
772 SortBy::Name => locale_cmp_cstr(&a.sort_key, &b.sort_key),
773 SortBy::Size => {
774 let size_ord = b.size.cmp(&a.size);
775 if size_ord == Ordering::Equal {
776 locale_cmp_cstr(&a.sort_key, &b.sort_key)
777 } else {
778 size_ord
779 }
780 }
781 SortBy::Time => {
782 let ta = a.time_secs(config.time_field);
783 let tb = b.time_secs(config.time_field);
784 let ord = tb.cmp(&ta);
785 if ord == Ordering::Equal {
786 let na = a.time_nsec(config.time_field);
787 let nb = b.time_nsec(config.time_field);
788 let nsec_ord = nb.cmp(&na);
789 if nsec_ord == Ordering::Equal {
790 locale_cmp_cstr(&a.sort_key, &b.sort_key)
791 } else {
792 nsec_ord
793 }
794 } else {
795 ord
796 }
797 }
798 SortBy::Extension => {
799 let ea = a.extension();
800 let eb = b.extension();
801 let ord = locale_cmp(ea, eb);
802 if ord == Ordering::Equal {
803 locale_cmp_cstr(&a.sort_key, &b.sort_key)
804 } else {
805 ord
806 }
807 }
808 SortBy::Version => version_cmp(&a.name, &b.name),
809 SortBy::None => Ordering::Equal,
810 SortBy::Width => {
811 let wa = a.display_width(config);
812 let wb = b.display_width(config);
813 wa.cmp(&wb)
814 }
815 };
816
817 if config.reverse { ord.reverse() } else { ord }
818}
819
820pub fn format_permissions(mode: u32) -> String {
826 let mut s = String::with_capacity(10);
827
828 s.push(match mode & (libc::S_IFMT as u32) {
830 x if x == libc::S_IFDIR as u32 => 'd',
831 x if x == libc::S_IFLNK as u32 => 'l',
832 x if x == libc::S_IFBLK as u32 => 'b',
833 x if x == libc::S_IFCHR as u32 => 'c',
834 x if x == libc::S_IFIFO as u32 => 'p',
835 x if x == libc::S_IFSOCK as u32 => 's',
836 _ => '-',
837 });
838
839 s.push(if mode & (libc::S_IRUSR as u32) != 0 {
841 'r'
842 } else {
843 '-'
844 });
845 s.push(if mode & (libc::S_IWUSR as u32) != 0 {
846 'w'
847 } else {
848 '-'
849 });
850 s.push(if mode & (libc::S_ISUID as u32) != 0 {
851 if mode & (libc::S_IXUSR as u32) != 0 {
852 's'
853 } else {
854 'S'
855 }
856 } else if mode & (libc::S_IXUSR as u32) != 0 {
857 'x'
858 } else {
859 '-'
860 });
861
862 s.push(if mode & (libc::S_IRGRP as u32) != 0 {
864 'r'
865 } else {
866 '-'
867 });
868 s.push(if mode & (libc::S_IWGRP as u32) != 0 {
869 'w'
870 } else {
871 '-'
872 });
873 s.push(if mode & (libc::S_ISGID as u32) != 0 {
874 if mode & (libc::S_IXGRP as u32) != 0 {
875 's'
876 } else {
877 'S'
878 }
879 } else if mode & (libc::S_IXGRP as u32) != 0 {
880 'x'
881 } else {
882 '-'
883 });
884
885 s.push(if mode & (libc::S_IROTH as u32) != 0 {
887 'r'
888 } else {
889 '-'
890 });
891 s.push(if mode & (libc::S_IWOTH as u32) != 0 {
892 'w'
893 } else {
894 '-'
895 });
896 s.push(if mode & (libc::S_ISVTX as u32) != 0 {
897 if mode & (libc::S_IXOTH as u32) != 0 {
898 't'
899 } else {
900 'T'
901 }
902 } else if mode & (libc::S_IXOTH as u32) != 0 {
903 'x'
904 } else {
905 '-'
906 });
907
908 s
909}
910
911pub fn format_size(size: u64, human: bool, si: bool, kibibytes: bool) -> String {
917 if human || si {
918 let base: f64 = if si { 1000.0 } else { 1024.0 };
919 let suffixes = ["", "K", "M", "G", "T", "P", "E"];
920
921 if size == 0 {
922 return "0".to_string();
923 }
924
925 let mut val = size as f64;
926 let mut idx = 0;
927 while val >= base && idx < suffixes.len() - 1 {
928 val /= base;
929 idx += 1;
930 }
931
932 if idx == 0 {
933 format!("{}", size)
934 } else if val >= 10.0 {
935 format!("{:.0}{}", val, suffixes[idx])
936 } else {
937 format!("{:.1}{}", val, suffixes[idx])
938 }
939 } else if kibibytes {
940 let blocks_k = (size + 1023) / 1024;
942 format!("{}", blocks_k)
943 } else {
944 format!("{}", size)
945 }
946}
947
948pub fn format_blocks(blocks_512: u64, human: bool, si: bool, kibibytes: bool) -> String {
950 let bytes = blocks_512 * 512;
951 if human || si {
952 format_size(bytes, human, si, false)
953 } else if kibibytes {
954 let k = (bytes + 1023) / 1024;
955 format!("{}", k)
956 } else {
957 let k = (bytes + 1023) / 1024;
959 format!("{}", k)
960 }
961}
962
963pub fn format_time(secs: i64, nsec: i64, style: &TimeStyle) -> String {
969 let now_sys = SystemTime::now();
971 let now_secs = now_sys
972 .duration_since(SystemTime::UNIX_EPOCH)
973 .map(|d| d.as_secs() as i64)
974 .unwrap_or(0);
975 let six_months_ago = now_secs - 6 * 30 * 24 * 3600;
976
977 let tm = time_from_epoch(secs);
979
980 match style {
981 TimeStyle::FullIso => {
982 format!(
983 "{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {}",
984 tm.year,
985 tm.month,
986 tm.day,
987 tm.hour,
988 tm.min,
989 tm.sec,
990 nsec,
991 format_tz_offset(tm.utc_offset_secs)
992 )
993 }
994 TimeStyle::LongIso => {
995 format!(
996 "{:04}-{:02}-{:02} {:02}:{:02}",
997 tm.year, tm.month, tm.day, tm.hour, tm.min
998 )
999 }
1000 TimeStyle::Iso => {
1001 if secs > six_months_ago && secs <= now_secs {
1002 format!("{:02}-{:02} {:02}:{:02}", tm.month, tm.day, tm.hour, tm.min)
1003 } else {
1004 format!("{:02}-{:02} {:04}", tm.month, tm.day, tm.year)
1005 }
1006 }
1007 TimeStyle::Locale | TimeStyle::Custom(_) => {
1008 let month_names = [
1009 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
1010 ];
1011 let mon = if tm.month >= 1 && tm.month <= 12 {
1012 month_names[(tm.month - 1) as usize]
1013 } else {
1014 "???"
1015 };
1016
1017 if secs > six_months_ago && secs <= now_secs {
1018 format!("{} {:>2} {:02}:{:02}", mon, tm.day, tm.hour, tm.min)
1019 } else {
1020 format!("{} {:>2} {:04}", mon, tm.day, tm.year)
1021 }
1022 }
1023 }
1024}
1025
1026fn format_tz_offset(offset_secs: i32) -> String {
1027 let sign = if offset_secs >= 0 { '+' } else { '-' };
1028 let abs = offset_secs.unsigned_abs();
1029 let hours = abs / 3600;
1030 let mins = (abs % 3600) / 60;
1031 format!("{}{:02}{:02}", sign, hours, mins)
1032}
1033
1034struct BrokenDownTime {
1035 year: i32,
1036 month: u32,
1037 day: u32,
1038 hour: u32,
1039 min: u32,
1040 sec: u32,
1041 utc_offset_secs: i32,
1042}
1043
1044fn time_from_epoch(secs: i64) -> BrokenDownTime {
1046 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
1047 let time_t = secs as libc::time_t;
1048 unsafe {
1049 libc::localtime_r(&time_t, &mut tm);
1050 }
1051 BrokenDownTime {
1052 year: tm.tm_year + 1900,
1053 month: (tm.tm_mon + 1) as u32,
1054 day: tm.tm_mday as u32,
1055 hour: tm.tm_hour as u32,
1056 min: tm.tm_min as u32,
1057 sec: tm.tm_sec as u32,
1058 utc_offset_secs: tm.tm_gmtoff as i32,
1059 }
1060}
1061
1062fn lookup_user(uid: u32) -> String {
1069 use std::cell::RefCell;
1070 thread_local! {
1071 static CACHE: RefCell<HashMap<u32, String>> = RefCell::new(HashMap::new());
1072 }
1073 CACHE.with(|c| {
1074 let mut cache = c.borrow_mut();
1075 if let Some(name) = cache.get(&uid) {
1076 return name.clone();
1077 }
1078 let name = lookup_user_uncached(uid);
1079 cache.insert(uid, name.clone());
1080 name
1081 })
1082}
1083
1084fn lookup_user_uncached(uid: u32) -> String {
1085 let mut buf = vec![0u8; 1024];
1086 let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
1087 let mut result: *mut libc::passwd = std::ptr::null_mut();
1088 let ret = unsafe {
1089 libc::getpwuid_r(
1090 uid,
1091 &mut pwd,
1092 buf.as_mut_ptr() as *mut libc::c_char,
1093 buf.len(),
1094 &mut result,
1095 )
1096 };
1097 if ret == 0 && !result.is_null() {
1098 let cstr = unsafe { std::ffi::CStr::from_ptr(pwd.pw_name) };
1099 cstr.to_string_lossy().into_owned()
1100 } else {
1101 uid.to_string()
1102 }
1103}
1104
1105fn lookup_group(gid: u32) -> String {
1107 use std::cell::RefCell;
1108 thread_local! {
1109 static CACHE: RefCell<HashMap<u32, String>> = RefCell::new(HashMap::new());
1110 }
1111 CACHE.with(|c| {
1112 let mut cache = c.borrow_mut();
1113 if let Some(name) = cache.get(&gid) {
1114 return name.clone();
1115 }
1116 let name = lookup_group_uncached(gid);
1117 cache.insert(gid, name.clone());
1118 name
1119 })
1120}
1121
1122fn lookup_group_uncached(gid: u32) -> String {
1123 let mut buf = vec![0u8; 1024];
1124 let mut grp: libc::group = unsafe { std::mem::zeroed() };
1125 let mut result: *mut libc::group = std::ptr::null_mut();
1126 let ret = unsafe {
1127 libc::getgrgid_r(
1128 gid,
1129 &mut grp,
1130 buf.as_mut_ptr() as *mut libc::c_char,
1131 buf.len(),
1132 &mut result,
1133 )
1134 };
1135 if ret == 0 && !result.is_null() {
1136 let cstr = unsafe { std::ffi::CStr::from_ptr(grp.gr_name) };
1137 cstr.to_string_lossy().into_owned()
1138 } else {
1139 gid.to_string()
1140 }
1141}
1142
1143pub fn glob_match(pattern: &str, name: &str) -> bool {
1149 let pat = pattern.as_bytes();
1150 let txt = name.as_bytes();
1151 let mut pi = 0;
1152 let mut ti = 0;
1153 let mut star_p = usize::MAX;
1154 let mut star_t = 0;
1155
1156 while ti < txt.len() {
1157 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
1158 pi += 1;
1159 ti += 1;
1160 } else if pi < pat.len() && pat[pi] == b'*' {
1161 star_p = pi;
1162 star_t = ti;
1163 pi += 1;
1164 } else if star_p != usize::MAX {
1165 pi = star_p + 1;
1166 star_t += 1;
1167 ti = star_t;
1168 } else {
1169 return false;
1170 }
1171 }
1172 while pi < pat.len() && pat[pi] == b'*' {
1173 pi += 1;
1174 }
1175 pi == pat.len()
1176}
1177
1178fn should_ignore(name: &str, config: &LsConfig) -> bool {
1179 if config.ignore_backups && name.ends_with('~') {
1180 return true;
1181 }
1182 for pat in &config.ignore_patterns {
1183 if glob_match(pat, name) {
1184 return true;
1185 }
1186 }
1187 false
1188}
1189
1190pub fn read_entries(path: &Path, config: &LsConfig) -> io::Result<Vec<FileEntry>> {
1196 let mut entries = Vec::new();
1197
1198 if config.all {
1199 if let Ok(e) = FileEntry::from_path_with_name(".".to_string(), path, config) {
1201 entries.push(e);
1202 }
1203 let parent = path.parent().unwrap_or(path);
1204 if let Ok(e) = FileEntry::from_path_with_name("..".to_string(), parent, config) {
1205 entries.push(e);
1206 }
1207 }
1208
1209 for entry in fs::read_dir(path)? {
1210 let entry = entry?;
1211 let name = entry.file_name().to_string_lossy().into_owned();
1212
1213 if !config.all && !config.almost_all && name.starts_with('.') {
1215 continue;
1216 }
1217 if config.almost_all && (name == "." || name == "..") {
1218 continue;
1219 }
1220
1221 if should_ignore(&name, config) {
1223 continue;
1224 }
1225
1226 match FileEntry::from_dir_entry(&entry, config) {
1227 Ok(fe) => entries.push(fe),
1228 Err(e) => {
1229 eprintln!("ls: cannot access '{}': {}", entry.path().display(), e);
1230 }
1231 }
1232 }
1233
1234 Ok(entries)
1235}
1236
1237fn print_long(
1243 out: &mut impl Write,
1244 entries: &[FileEntry],
1245 config: &LsConfig,
1246 color_db: Option<&ColorDb>,
1247) -> io::Result<()> {
1248 if entries.is_empty() {
1249 return Ok(());
1250 }
1251
1252 let max_nlink = entries
1254 .iter()
1255 .map(|e| count_digits(e.nlink))
1256 .max()
1257 .unwrap_or(1);
1258 let max_owner = if config.show_owner {
1259 entries
1260 .iter()
1261 .map(|e| {
1262 if config.numeric_ids {
1263 e.uid.to_string().len()
1264 } else {
1265 lookup_user(e.uid).len()
1266 }
1267 })
1268 .max()
1269 .unwrap_or(0)
1270 } else {
1271 0
1272 };
1273 let max_group = if config.show_group {
1274 entries
1275 .iter()
1276 .map(|e| {
1277 if config.numeric_ids {
1278 e.gid.to_string().len()
1279 } else {
1280 lookup_group(e.gid).len()
1281 }
1282 })
1283 .max()
1284 .unwrap_or(0)
1285 } else {
1286 0
1287 };
1288
1289 let has_device = entries.iter().any(|e| {
1291 let ft = e.mode & (libc::S_IFMT as u32);
1292 ft == libc::S_IFBLK as u32 || ft == libc::S_IFCHR as u32
1293 });
1294 let max_size = if has_device {
1295 entries
1297 .iter()
1298 .map(|e| {
1299 let ft = e.mode & (libc::S_IFMT as u32);
1300 if ft == libc::S_IFBLK as u32 || ft == libc::S_IFCHR as u32 {
1301 format!("{}, {}", e.rdev_major, e.rdev_minor).len()
1302 } else {
1303 format_size(e.size, config.human_readable, config.si, config.kibibytes).len()
1304 }
1305 })
1306 .max()
1307 .unwrap_or(1)
1308 } else {
1309 entries
1310 .iter()
1311 .map(|e| format_size(e.size, config.human_readable, config.si, config.kibibytes).len())
1312 .max()
1313 .unwrap_or(1)
1314 };
1315
1316 let max_inode = if config.show_inode {
1317 entries
1318 .iter()
1319 .map(|e| count_digits(e.ino))
1320 .max()
1321 .unwrap_or(1)
1322 } else {
1323 0
1324 };
1325
1326 let max_blocks = if config.show_size {
1327 entries
1328 .iter()
1329 .map(|e| {
1330 format_blocks(e.blocks, config.human_readable, config.si, config.kibibytes).len()
1331 })
1332 .max()
1333 .unwrap_or(1)
1334 } else {
1335 0
1336 };
1337
1338 for entry in entries {
1339 if config.show_inode {
1341 write!(out, "{:>width$} ", entry.ino, width = max_inode)?;
1342 }
1343
1344 if config.show_size {
1346 let bs = format_blocks(
1347 entry.blocks,
1348 config.human_readable,
1349 config.si,
1350 config.kibibytes,
1351 );
1352 write!(out, "{:>width$} ", bs, width = max_blocks)?;
1353 }
1354
1355 write!(out, "{} ", format_permissions(entry.mode))?;
1357
1358 write!(out, "{:>width$} ", entry.nlink, width = max_nlink)?;
1360
1361 if config.show_owner {
1363 let owner = if config.numeric_ids {
1364 entry.uid.to_string()
1365 } else {
1366 lookup_user(entry.uid)
1367 };
1368 write!(out, "{:<width$} ", owner, width = max_owner)?;
1369 }
1370
1371 if config.show_group {
1373 let group = if config.numeric_ids {
1374 entry.gid.to_string()
1375 } else {
1376 lookup_group(entry.gid)
1377 };
1378 write!(out, "{:<width$} ", group, width = max_group)?;
1379 }
1380
1381 let ft = entry.mode & (libc::S_IFMT as u32);
1383 if ft == libc::S_IFBLK as u32 || ft == libc::S_IFCHR as u32 {
1384 let dev = format!("{}, {}", entry.rdev_major, entry.rdev_minor);
1385 write!(out, "{:>width$} ", dev, width = max_size)?;
1386 } else {
1387 let sz = format_size(
1388 entry.size,
1389 config.human_readable,
1390 config.si,
1391 config.kibibytes,
1392 );
1393 write!(out, "{:>width$} ", sz, width = max_size)?;
1394 }
1395
1396 let ts = format_time(
1398 entry.time_secs(config.time_field),
1399 entry.time_nsec(config.time_field),
1400 &config.time_style,
1401 );
1402 write!(out, "{} ", ts)?;
1403
1404 let quoted = quote_name(&entry.name, config);
1406 if let Some(db) = color_db {
1407 let c = db.color_for(entry);
1408 if c.is_empty() {
1409 write!(out, "{}", quoted)?;
1410 } else {
1411 write!(out, "{}{}{}", c, quoted, db.reset)?;
1412 }
1413 } else {
1414 write!(out, "{}", quoted)?;
1415 }
1416
1417 let ind = entry.indicator(config.indicator_style);
1419 if !ind.is_empty() {
1420 write!(out, "{}", ind)?;
1421 }
1422
1423 if let Some(ref target) = entry.link_target {
1425 write!(out, " -> {}", target)?;
1426 }
1427
1428 writeln!(out)?;
1429 }
1430
1431 Ok(())
1432}
1433
1434fn count_digits(n: u64) -> usize {
1435 if n == 0 {
1436 return 1;
1437 }
1438 let mut count = 0;
1439 let mut v = n;
1440 while v > 0 {
1441 count += 1;
1442 v /= 10;
1443 }
1444 count
1445}
1446
1447fn print_columns(
1453 out: &mut impl Write,
1454 entries: &[FileEntry],
1455 config: &LsConfig,
1456 color_db: Option<&ColorDb>,
1457) -> io::Result<()> {
1458 if entries.is_empty() {
1459 return Ok(());
1460 }
1461
1462 let term_width = config.width;
1463 let tab = config.tab_size;
1464
1465 let items: Vec<(String, usize, &FileEntry)> = entries
1467 .iter()
1468 .map(|e| {
1469 let quoted = quote_name(&e.name, config);
1470 let ind = e.indicator(config.indicator_style);
1471 let display = format!("{}{}", quoted, ind);
1472 let w = display.len();
1473 (display, w, e)
1474 })
1475 .collect();
1476
1477 let max_inode_w = if config.show_inode {
1478 entries
1479 .iter()
1480 .map(|e| count_digits(e.ino))
1481 .max()
1482 .unwrap_or(1)
1483 } else {
1484 0
1485 };
1486 let max_blocks_w = if config.show_size {
1487 entries
1488 .iter()
1489 .map(|e| {
1490 format_blocks(e.blocks, config.human_readable, config.si, config.kibibytes).len()
1491 })
1492 .max()
1493 .unwrap_or(1)
1494 } else {
1495 0
1496 };
1497
1498 let prefix_width = if config.show_inode && config.show_size {
1499 max_inode_w + 1 + max_blocks_w + 1
1500 } else if config.show_inode {
1501 max_inode_w + 1
1502 } else if config.show_size {
1503 max_blocks_w + 1
1504 } else {
1505 0
1506 };
1507
1508 let n = items.len();
1510 let max_name_width = items.iter().map(|(_, w, _)| *w).max().unwrap_or(0);
1511 let col_width_raw = max_name_width + prefix_width;
1512
1513 let col_width = if tab > 0 {
1515 ((col_width_raw + tab) / tab) * tab
1516 } else {
1517 col_width_raw + 2
1518 };
1519
1520 if col_width == 0 || col_width >= term_width {
1521 return print_single_column(out, entries, config, color_db);
1523 }
1524
1525 let num_cols = std::cmp::max(1, term_width / col_width);
1526 let num_rows = (n + num_cols - 1) / num_cols;
1527
1528 for row in 0..num_rows {
1529 let mut col = 0;
1530 loop {
1531 let idx = col * num_rows + row;
1532 if idx >= n {
1533 break;
1534 }
1535
1536 let (ref display, w, entry) = items[idx];
1537 let is_last_col = col + 1 >= num_cols || (col + 1) * num_rows + row >= n;
1538
1539 if config.show_inode {
1541 write!(out, "{:>width$} ", entry.ino, width = max_inode_w)?;
1542 }
1543 if config.show_size {
1545 let bs = format_blocks(
1546 entry.blocks,
1547 config.human_readable,
1548 config.si,
1549 config.kibibytes,
1550 );
1551 write!(out, "{:>width$} ", bs, width = max_blocks_w)?;
1552 }
1553
1554 if let Some(db) = color_db {
1556 let c = db.color_for(entry);
1557 let quoted = quote_name(&entry.name, config);
1558 let ind = entry.indicator(config.indicator_style);
1559 if c.is_empty() {
1560 write!(out, "{}{}", quoted, ind)?;
1561 } else {
1562 write!(out, "{}{}{}{}", c, quoted, db.reset, ind)?;
1563 }
1564 } else {
1565 write!(out, "{}", display)?;
1566 }
1567
1568 if !is_last_col {
1569 let name_w = w + prefix_width;
1571 let padding = if col_width > name_w {
1572 col_width - name_w
1573 } else {
1574 2
1575 };
1576 for _ in 0..padding {
1577 write!(out, " ")?;
1578 }
1579 }
1580
1581 col += 1;
1582 }
1583 writeln!(out)?;
1584 }
1585
1586 Ok(())
1587}
1588
1589fn print_single_column(
1594 out: &mut impl Write,
1595 entries: &[FileEntry],
1596 config: &LsConfig,
1597 color_db: Option<&ColorDb>,
1598) -> io::Result<()> {
1599 let max_inode_w = if config.show_inode {
1600 entries
1601 .iter()
1602 .map(|e| count_digits(e.ino))
1603 .max()
1604 .unwrap_or(1)
1605 } else {
1606 0
1607 };
1608 let max_blocks_w = if config.show_size {
1609 entries
1610 .iter()
1611 .map(|e| {
1612 format_blocks(e.blocks, config.human_readable, config.si, config.kibibytes).len()
1613 })
1614 .max()
1615 .unwrap_or(1)
1616 } else {
1617 0
1618 };
1619
1620 for entry in entries {
1621 if config.show_inode {
1622 write!(out, "{:>width$} ", entry.ino, width = max_inode_w)?;
1623 }
1624 if config.show_size {
1625 let bs = format_blocks(
1626 entry.blocks,
1627 config.human_readable,
1628 config.si,
1629 config.kibibytes,
1630 );
1631 write!(out, "{:>width$} ", bs, width = max_blocks_w)?;
1632 }
1633
1634 let quoted = quote_name(&entry.name, config);
1635 if let Some(db) = color_db {
1636 let c = db.color_for(entry);
1637 if c.is_empty() {
1638 write!(out, "{}", quoted)?;
1639 } else {
1640 write!(out, "{}{}{}", c, quoted, db.reset)?;
1641 }
1642 } else {
1643 write!(out, "{}", quoted)?;
1644 }
1645
1646 let ind = entry.indicator(config.indicator_style);
1647 if !ind.is_empty() {
1648 write!(out, "{}", ind)?;
1649 }
1650
1651 writeln!(out)?;
1652 }
1653 Ok(())
1654}
1655
1656pub fn print_comma(
1661 out: &mut impl Write,
1662 entries: &[FileEntry],
1663 config: &LsConfig,
1664 color_db: Option<&ColorDb>,
1665) -> io::Result<()> {
1666 for (i, entry) in entries.iter().enumerate() {
1667 if i > 0 {
1668 write!(out, ", ")?;
1669 }
1670 let quoted = quote_name(&entry.name, config);
1671 if let Some(db) = color_db {
1672 let c = db.color_for(entry);
1673 if c.is_empty() {
1674 write!(out, "{}", quoted)?;
1675 } else {
1676 write!(out, "{}{}{}", c, quoted, db.reset)?;
1677 }
1678 } else {
1679 write!(out, "{}", quoted)?;
1680 }
1681 let ind = entry.indicator(config.indicator_style);
1682 if !ind.is_empty() {
1683 write!(out, "{}", ind)?;
1684 }
1685 }
1686 if !entries.is_empty() {
1687 writeln!(out)?;
1688 }
1689 Ok(())
1690}
1691
1692fn print_total(out: &mut impl Write, entries: &[FileEntry], config: &LsConfig) -> io::Result<()> {
1697 let total_blocks: u64 = entries.iter().map(|e| e.blocks).sum();
1698 let formatted = format_blocks(
1699 total_blocks,
1700 config.human_readable,
1701 config.si,
1702 config.kibibytes,
1703 );
1704 writeln!(out, "total {}", formatted)
1705}
1706
1707pub fn ls_dir(
1713 out: &mut impl Write,
1714 path: &Path,
1715 config: &LsConfig,
1716 color_db: Option<&ColorDb>,
1717 show_header: bool,
1718) -> io::Result<bool> {
1719 if show_header {
1720 writeln!(out, "{}:", path.display())?;
1721 }
1722
1723 let mut entries = read_entries(path, config)?;
1724 sort_entries(&mut entries, config);
1725
1726 if config.long_format || config.show_size {
1728 print_total(out, &entries, config)?;
1729 }
1730
1731 match config.format {
1732 OutputFormat::Long => print_long(out, &entries, config, color_db)?,
1733 OutputFormat::SingleColumn => print_single_column(out, &entries, config, color_db)?,
1734 OutputFormat::Columns | OutputFormat::Across => {
1735 print_columns(out, &entries, config, color_db)?
1736 }
1737 OutputFormat::Comma => print_comma(out, &entries, config, color_db)?,
1738 }
1739
1740 if config.recursive {
1742 let dirs: Vec<PathBuf> = entries
1743 .iter()
1744 .filter(|e| {
1745 e.is_directory()
1746 && e.name != "."
1747 && e.name != ".."
1748 && (e.mode & (libc::S_IFMT as u32)) != libc::S_IFLNK as u32
1749 })
1750 .map(|e| e.path.clone())
1751 .collect();
1752
1753 for dir in dirs {
1754 writeln!(out)?;
1755 ls_dir(out, &dir, config, color_db, true)?;
1756 }
1757 }
1758
1759 Ok(true)
1760}
1761
1762pub fn ls_main(paths: &[String], config: &LsConfig) -> io::Result<bool> {
1766 let stdout = io::stdout();
1767 let is_tty = atty_stdout();
1768 #[cfg(target_os = "linux")]
1773 if !is_tty {
1774 unsafe {
1775 libc::fcntl(1, 1031 , 4096i32)
1776 };
1777 }
1778 let buf_cap = if is_tty { 64 * 1024 } else { 4 * 1024 };
1779 let mut out = BufWriter::with_capacity(buf_cap, stdout.lock());
1780
1781 let color_db = match config.color {
1782 ColorMode::Always => Some(ColorDb::from_env()),
1783 ColorMode::Auto => {
1784 if atty_stdout() {
1785 Some(ColorDb::from_env())
1786 } else {
1787 None
1788 }
1789 }
1790 ColorMode::Never => None,
1791 };
1792
1793 let mut had_error = false;
1794
1795 let mut file_args: Vec<FileEntry> = Vec::new();
1797 let mut dir_args: Vec<PathBuf> = Vec::new();
1798
1799 for p in paths {
1800 let path = PathBuf::from(p);
1801 let meta_result = if config.dereference {
1802 fs::metadata(&path).or_else(|_| fs::symlink_metadata(&path))
1803 } else {
1804 fs::symlink_metadata(&path)
1805 };
1806
1807 match meta_result {
1808 Ok(meta) => {
1809 if config.directory || !meta.is_dir() {
1810 match FileEntry::from_path_with_name(p.to_string(), &path, config) {
1811 Ok(fe) => file_args.push(fe),
1812 Err(e) => {
1813 eprintln!("ls: cannot access '{}': {}", p, e);
1814 had_error = true;
1815 }
1816 }
1817 } else {
1818 dir_args.push(path);
1819 }
1820 }
1821 Err(e) => {
1822 eprintln!(
1823 "ls: cannot access '{}': {}",
1824 p,
1825 crate::common::io_error_msg(&e)
1826 );
1827 had_error = true;
1828 }
1829 }
1830 }
1831
1832 sort_entries(&mut file_args, config);
1834
1835 if !file_args.is_empty() {
1837 match config.format {
1838 OutputFormat::Long => print_long(&mut out, &file_args, config, color_db.as_ref())?,
1839 OutputFormat::SingleColumn => {
1840 print_single_column(&mut out, &file_args, config, color_db.as_ref())?
1841 }
1842 OutputFormat::Columns | OutputFormat::Across => {
1843 print_columns(&mut out, &file_args, config, color_db.as_ref())?
1844 }
1845 OutputFormat::Comma => print_comma(&mut out, &file_args, config, color_db.as_ref())?,
1846 }
1847 }
1848
1849 dir_args.sort_by(|a, b| {
1851 let an = a.to_string_lossy();
1852 let bn = b.to_string_lossy();
1853 let ord = locale_cmp(&an, &bn);
1854 if config.reverse { ord.reverse() } else { ord }
1855 });
1856
1857 let show_header =
1858 dir_args.len() > 1 || (!file_args.is_empty() && !dir_args.is_empty()) || config.recursive;
1859
1860 for (i, dir) in dir_args.iter().enumerate() {
1861 if i > 0 || !file_args.is_empty() {
1862 writeln!(out)?;
1863 }
1864 match ls_dir(&mut out, dir, config, color_db.as_ref(), show_header) {
1865 Ok(_) => {}
1866 Err(e) if e.kind() == io::ErrorKind::BrokenPipe => return Err(e),
1867 Err(e) => {
1868 eprintln!(
1869 "ls: cannot open directory '{}': {}",
1870 dir.display(),
1871 crate::common::io_error_msg(&e)
1872 );
1873 had_error = true;
1874 }
1875 }
1876 }
1877
1878 out.flush()?;
1879
1880 Ok(!had_error)
1881}
1882
1883pub fn atty_stdout() -> bool {
1885 unsafe { libc::isatty(1) != 0 }
1886}
1887
1888pub fn collect_entries(path: &Path, config: &LsConfig) -> io::Result<Vec<FileEntry>> {
1894 let mut entries = read_entries(path, config)?;
1895 sort_entries(&mut entries, config);
1896 Ok(entries)
1897}
1898
1899pub fn render_long(entries: &[FileEntry], config: &LsConfig) -> io::Result<String> {
1901 let mut buf = Vec::new();
1902 print_long(&mut buf, entries, config, None)?;
1903 Ok(String::from_utf8_lossy(&buf).into_owned())
1904}
1905
1906pub fn render_single_column(entries: &[FileEntry], config: &LsConfig) -> io::Result<String> {
1908 let mut buf = Vec::new();
1909 print_single_column(&mut buf, entries, config, None)?;
1910 Ok(String::from_utf8_lossy(&buf).into_owned())
1911}
1912
1913pub fn render_dir(path: &Path, config: &LsConfig) -> io::Result<String> {
1915 let mut buf = Vec::new();
1916 ls_dir(&mut buf, path, config, None, false)?;
1917 Ok(String::from_utf8_lossy(&buf).into_owned())
1918}