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