1use std::io;
4
5use super::{
6 ClassifyMode, ColorMode, HyperlinkMode, IndicatorStyle, LsConfig, OutputFormat, QuotingStyle,
7 SortBy, TimeField, TimeStyle, atty_stdout, ls_main,
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum LsFlavor {
13 Ls,
14 Dir,
15 Vdir,
16}
17
18impl LsFlavor {
19 pub fn name(self) -> &'static str {
20 match self {
21 LsFlavor::Ls => "ls",
22 LsFlavor::Dir => "dir",
23 LsFlavor::Vdir => "vdir",
24 }
25 }
26}
27
28fn get_terminal_width() -> Option<usize> {
29 let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
30 let ret = unsafe { libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) };
31 if ret == 0 && ws.ws_col > 0 {
32 return Some(ws.ws_col as usize);
33 }
34 if let Ok(val) = std::env::var("COLUMNS") {
35 if let Ok(w) = val.parse::<usize>() {
36 return Some(w);
37 }
38 }
39 None
40}
41
42fn take_short_value(
43 bytes: &[u8],
44 pos: usize,
45 args: &mut impl Iterator<Item = std::ffi::OsString>,
46 flag: &str,
47 prog: &str,
48) -> String {
49 if pos < bytes.len() {
50 let full = String::from_utf8_lossy(bytes).into_owned();
51 full[pos..].to_string()
52 } else {
53 args.next()
54 .unwrap_or_else(|| {
55 eprintln!("{}: option requires an argument -- '{}'", prog, flag);
56 std::process::exit(2);
57 })
58 .to_string_lossy()
59 .into_owned()
60 }
61}
62
63fn print_ls_help(flavor: LsFlavor) {
64 let name = flavor.name();
65 let desc = match flavor {
66 LsFlavor::Ls => {
67 "List information about the FILEs (the current directory by default).\n\
68 Sort entries alphabetically if none of -cftuvSUX nor --sort is specified."
69 }
70 LsFlavor::Dir => {
71 "List directory contents.\n\
72 Equivalent to ls -C -b (multi-column format with C-style escapes).\n\
73 All ls options are accepted."
74 }
75 LsFlavor::Vdir => {
76 "List directory contents.\n\
77 Equivalent to ls -l -b (long format with C-style escapes).\n\
78 All ls options are accepted."
79 }
80 };
81 print!(
82 "Usage: {} [OPTION]... [FILE]...\n{}\n\n\
83 \x20 -a, --all do not ignore entries starting with .\n\
84 \x20 -A, --almost-all do not list implied . and ..\n\
85 \x20 -b, --escape print C-style escapes for nongraphic characters\n\
86 \x20 -B, --ignore-backups do not list implied entries ending with ~\n\
87 \x20 -c sort by/show ctime\n\
88 \x20 -C list entries by columns\n\
89 \x20 --color[=WHEN] colorize output; WHEN: always, auto, never\n\
90 \x20 -d, --directory list directories themselves, not their contents\n\
91 \x20 -F, --classify[=WHEN] append indicator (one of */=>@|) to entries\n\
92 \x20 -g like -l, but do not list owner\n\
93 \x20 -G, --no-group in -l listing, don't print group names\n\
94 \x20 --group-directories-first group directories before files\n\
95 \x20 --full-time like -l --time-style=full-iso\n\
96 \x20 -h, --human-readable with -l, print sizes like 1K 234M 2G etc.\n\
97 \x20 -i, --inode print the index number of each file\n\
98 \x20 -I, --ignore=PATTERN do not list entries matching PATTERN\n\
99 \x20 -k, --kibibytes default to 1024-byte blocks\n\
100 \x20 -l use a long listing format\n\
101 \x20 -L, --dereference show info for link references\n\
102 \x20 -m fill width with a comma separated list of entries\n\
103 \x20 -n, --numeric-uid-gid like -l, but list numeric user and group IDs\n\
104 \x20 -N, --literal print entry names without quoting\n\
105 \x20 -o like -l, but do not list group information\n\
106 \x20 -p append / indicator to directories\n\
107 \x20 -q, --hide-control-chars print ? instead of nongraphic characters\n\
108 \x20 -Q, --quote-name enclose entry names in double quotes\n\
109 \x20 -r, --reverse reverse order while sorting\n\
110 \x20 -R, --recursive list subdirectories recursively\n\
111 \x20 -s, --size print the allocated size of each file, in blocks\n\
112 \x20 -S sort by file size, largest first\n\
113 \x20 --si use powers of 1000 not 1024\n\
114 \x20 --sort=WORD sort by WORD: none, size, time, version, extension\n\
115 \x20 -t sort by time, newest first\n\
116 \x20 -T, --tabsize=COLS assume tab stops at each COLS instead of 8\n\
117 \x20 --time=WORD select which time to show/sort by\n\
118 \x20 --time-style=STYLE time display style\n\
119 \x20 -u sort by/show access time\n\
120 \x20 -U do not sort; list entries in directory order\n\
121 \x20 -v natural sort of (version) numbers within text\n\
122 \x20 -w, --width=COLS set output width to COLS\n\
123 \x20 -x list entries by lines instead of by columns\n\
124 \x20 -X sort alphabetically by entry extension\n\
125 \x20 -Z, --context print any security context of each file\n\
126 \x20 -1 list one file per line\n\
127 \x20 --hyperlink[=WHEN] hyperlink file names; WHEN: always, auto, never\n\
128 \x20 --indicator-style=WORD append indicator WORD: none, slash, file-type, classify\n\
129 \x20 --quoting-style=WORD use quoting style WORD for entry names\n\
130 \x20 --help display this help and exit\n\
131 \x20 --version output version information and exit\n",
132 name, desc
133 );
134}
135
136fn next_opt_val(
137 eq_val: Option<&str>,
138 args: &mut impl Iterator<Item = std::ffi::OsString>,
139 prog: &str,
140 opt: &str,
141) -> String {
142 eq_val.map(|v| v.to_string()).unwrap_or_else(|| {
143 args.next()
144 .unwrap_or_else(|| {
145 eprintln!("{}: option '--{}' requires an argument", prog, opt);
146 std::process::exit(2);
147 })
148 .to_string_lossy()
149 .into_owned()
150 })
151}
152
153pub fn parse_ls_args(flavor: LsFlavor) -> (LsConfig, Vec<String>) {
155 let is_tty = atty_stdout();
156 let mut config = LsConfig::default();
157 let mut paths = Vec::new();
158 let prog = flavor.name();
159
160 match flavor {
161 LsFlavor::Ls => {
162 if is_tty {
163 config.format = OutputFormat::Columns;
164 config.hide_control_chars = true;
165 } else {
166 config.format = OutputFormat::SingleColumn;
167 config.color = ColorMode::Never;
168 }
169 }
170 LsFlavor::Dir => {
171 config.format = OutputFormat::Columns;
172 config.quoting_style = QuotingStyle::Escape;
173 if !is_tty {
174 config.color = ColorMode::Never;
175 }
176 }
177 LsFlavor::Vdir => {
178 config.format = OutputFormat::Long;
179 config.long_format = true;
180 config.quoting_style = QuotingStyle::Escape;
181 if !is_tty {
182 config.color = ColorMode::Never;
183 }
184 }
185 }
186
187 if is_tty {
188 if let Some(w) = get_terminal_width() {
189 if w > 0 {
190 config.width = w;
191 }
192 }
193 }
194
195 let mut explicit_format = false;
196 let mut args = std::env::args_os().skip(1);
197 #[allow(clippy::while_let_on_iterator)]
198 while let Some(arg) = args.next() {
199 let bytes = arg.as_encoded_bytes();
200 if bytes == b"--" {
201 for a in args {
202 paths.push(a.to_string_lossy().into_owned());
203 }
204 break;
205 }
206 if bytes.starts_with(b"--") {
207 let s = arg.to_string_lossy();
208 let opt = &s[2..];
209 let (name, eq_val) = if let Some(eq) = opt.find('=') {
210 (&opt[..eq], Some(&opt[eq + 1..]))
211 } else {
212 (opt, None)
213 };
214 match name {
215 "help" => {
216 print_ls_help(flavor);
217 std::process::exit(0);
218 }
219 "version" => {
220 println!("{} (fcoreutils) {}", prog, env!("CARGO_PKG_VERSION"));
221 std::process::exit(0);
222 }
223 "all" => config.all = true,
224 "almost-all" => config.almost_all = true,
225 "escape" => config.quoting_style = QuotingStyle::Escape,
226 "ignore-backups" => config.ignore_backups = true,
227 "directory" => config.directory = true,
228 "classify" => {
229 let mode = eq_val.unwrap_or("always");
230 match mode {
231 "always" | "yes" | "force" => {
232 config.classify = ClassifyMode::Always;
233 config.indicator_style = IndicatorStyle::Classify;
234 }
235 "auto" | "tty" | "if-tty" => {
236 config.classify = ClassifyMode::Auto;
237 if is_tty {
238 config.indicator_style = IndicatorStyle::Classify;
239 }
240 }
241 "never" | "no" | "none" => config.classify = ClassifyMode::Never,
242 _ => {
243 eprintln!("{}: invalid argument '{}' for '--classify'", prog, mode);
244 std::process::exit(2);
245 }
246 }
247 }
248 "no-group" => config.show_group = false,
249 "group-directories-first" => config.group_directories_first = true,
250 "human-readable" => config.human_readable = true,
251 "si" => config.si = true,
252 "inode" => config.show_inode = true,
253 "ignore" => {
254 let val = next_opt_val(eq_val, &mut args, prog, "ignore");
255 config.ignore_patterns.push(val);
256 }
257 "kibibytes" => config.kibibytes = true,
258 "dereference" => config.dereference = true,
259 "numeric-uid-gid" => {
260 config.numeric_ids = true;
261 config.long_format = true;
262 if !explicit_format {
263 config.format = OutputFormat::Long;
264 }
265 }
266 "literal" => {
267 config.literal = true;
268 config.quoting_style = QuotingStyle::Literal;
269 }
270 "hide-control-chars" => config.hide_control_chars = true,
271 "quote-name" => config.quoting_style = QuotingStyle::C,
272 "reverse" => config.reverse = true,
273 "recursive" => config.recursive = true,
274 "size" => config.show_size = true,
275 "context" => config.context = true,
276 "color" => {
277 let val = eq_val.unwrap_or("always");
278 config.color = match val {
279 "always" | "yes" | "force" => ColorMode::Always,
280 "auto" | "tty" | "if-tty" => ColorMode::Auto,
281 "never" | "no" | "none" => ColorMode::Never,
282 _ => {
283 eprintln!("{}: invalid argument '{}' for '--color'", prog, val);
284 std::process::exit(2);
285 }
286 };
287 }
288 "sort" => {
289 let val = next_opt_val(eq_val, &mut args, prog, "sort");
290 config.sort_by = match val.as_str() {
291 "none" => SortBy::None,
292 "size" => SortBy::Size,
293 "time" => SortBy::Time,
294 "version" => SortBy::Version,
295 "extension" => SortBy::Extension,
296 "width" => SortBy::Width,
297 _ => {
298 eprintln!("{}: invalid argument '{}' for '--sort'", prog, val);
299 std::process::exit(2);
300 }
301 };
302 }
303 "time" => {
304 let val = next_opt_val(eq_val, &mut args, prog, "time");
305 config.time_field = match val.as_str() {
306 "atime" | "access" | "use" => TimeField::Atime,
307 "ctime" | "status" => TimeField::Ctime,
308 "birth" | "creation" => TimeField::Birth,
309 "mtime" | "modification" => TimeField::Mtime,
310 _ => {
311 eprintln!("{}: invalid argument '{}' for '--time'", prog, val);
312 std::process::exit(2);
313 }
314 };
315 }
316 "time-style" => {
317 let val = next_opt_val(eq_val, &mut args, prog, "time-style");
318 config.time_style = match val.as_str() {
319 "full-iso" => TimeStyle::FullIso,
320 "long-iso" => TimeStyle::LongIso,
321 "iso" => TimeStyle::Iso,
322 "locale" => TimeStyle::Locale,
323 s if s.starts_with('+') => TimeStyle::Custom(s[1..].to_string()),
324 _ => {
325 eprintln!("{}: invalid argument '{}' for '--time-style'", prog, val);
326 std::process::exit(2);
327 }
328 };
329 }
330 "full-time" => {
331 config.long_format = true;
332 config.format = OutputFormat::Long;
333 explicit_format = true;
334 config.time_style = TimeStyle::FullIso;
335 }
336 "tabsize" => {
337 let val = next_opt_val(eq_val, &mut args, prog, "tabsize");
338 config.tab_size = val.parse().unwrap_or(8);
339 }
340 "width" => {
341 let val = next_opt_val(eq_val, &mut args, prog, "width");
342 config.width = val.parse().unwrap_or(80);
343 }
344 "hyperlink" => {
345 let val = eq_val.unwrap_or("always");
346 config.hyperlink = match val {
347 "always" | "yes" | "force" => HyperlinkMode::Always,
348 "auto" | "tty" | "if-tty" => HyperlinkMode::Auto,
349 "never" | "no" | "none" => HyperlinkMode::Never,
350 _ => {
351 eprintln!("{}: invalid argument '{}' for '--hyperlink'", prog, val);
352 std::process::exit(2);
353 }
354 };
355 }
356 "indicator-style" => {
357 let val = next_opt_val(eq_val, &mut args, prog, "indicator-style");
358 config.indicator_style = match val.as_str() {
359 "none" => IndicatorStyle::None,
360 "slash" => IndicatorStyle::Slash,
361 "file-type" => IndicatorStyle::FileType,
362 "classify" => IndicatorStyle::Classify,
363 _ => {
364 eprintln!(
365 "{}: invalid argument '{}' for '--indicator-style'",
366 prog, val
367 );
368 std::process::exit(2);
369 }
370 };
371 }
372 "quoting-style" => {
373 let val = next_opt_val(eq_val, &mut args, prog, "quoting-style");
374 config.quoting_style = match val.as_str() {
375 "literal" => QuotingStyle::Literal,
376 "locale" => QuotingStyle::Locale,
377 "shell" => QuotingStyle::Shell,
378 "shell-always" => QuotingStyle::ShellAlways,
379 "shell-escape" => QuotingStyle::ShellEscape,
380 "shell-escape-always" => QuotingStyle::ShellEscapeAlways,
381 "c" => QuotingStyle::C,
382 "escape" => QuotingStyle::Escape,
383 _ => {
384 eprintln!("{}: invalid argument '{}' for '--quoting-style'", prog, val);
385 std::process::exit(2);
386 }
387 };
388 }
389 _ => {
390 eprintln!("{}: unrecognized option '--{}'", prog, name);
391 eprintln!("Try '{} --help' for more information.", prog);
392 std::process::exit(2);
393 }
394 }
395 } else if bytes.len() > 1 && bytes[0] == b'-' {
396 let mut i = 1;
397 while i < bytes.len() {
398 match bytes[i] {
399 b'a' => config.all = true,
400 b'A' => config.almost_all = true,
401 b'b' => config.quoting_style = QuotingStyle::Escape,
402 b'B' => config.ignore_backups = true,
403 b'c' => config.time_field = TimeField::Ctime,
404 b'C' => {
405 config.format = OutputFormat::Columns;
406 explicit_format = true;
407 }
408 b'd' => config.directory = true,
409 b'f' => {
410 config.all = true;
411 config.sort_by = SortBy::None;
412 }
413 b'F' => {
414 config.classify = ClassifyMode::Always;
415 config.indicator_style = IndicatorStyle::Classify;
416 }
417 b'g' => {
418 config.long_format = true;
419 config.show_owner = false;
420 if !explicit_format {
421 config.format = OutputFormat::Long;
422 }
423 }
424 b'G' => config.show_group = false,
425 b'h' => config.human_readable = true,
426 b'i' => config.show_inode = true,
427 b'k' => config.kibibytes = true,
428 b'l' => {
429 config.long_format = true;
430 config.format = OutputFormat::Long;
431 explicit_format = true;
432 }
433 b'L' => config.dereference = true,
434 b'm' => {
435 config.format = OutputFormat::Comma;
436 explicit_format = true;
437 }
438 b'n' => {
439 config.long_format = true;
440 config.numeric_ids = true;
441 if !explicit_format {
442 config.format = OutputFormat::Long;
443 }
444 }
445 b'N' => {
446 config.literal = true;
447 config.quoting_style = QuotingStyle::Literal;
448 }
449 b'o' => {
450 config.long_format = true;
451 config.show_group = false;
452 if !explicit_format {
453 config.format = OutputFormat::Long;
454 }
455 }
456 b'p' => config.indicator_style = IndicatorStyle::Slash,
457 b'q' => config.hide_control_chars = true,
458 b'Q' => config.quoting_style = QuotingStyle::C,
459 b'r' => config.reverse = true,
460 b'R' => config.recursive = true,
461 b's' => config.show_size = true,
462 b'S' => config.sort_by = SortBy::Size,
463 b't' => config.sort_by = SortBy::Time,
464 b'u' => config.time_field = TimeField::Atime,
465 b'U' => config.sort_by = SortBy::None,
466 b'v' => config.sort_by = SortBy::Version,
467 b'x' => {
468 config.format = OutputFormat::Across;
469 explicit_format = true;
470 }
471 b'X' => config.sort_by = SortBy::Extension,
472 b'Z' => config.context = true,
473 b'1' => {
474 config.format = OutputFormat::SingleColumn;
475 explicit_format = true;
476 }
477 b'I' => {
478 let val = take_short_value(bytes, i + 1, &mut args, "I", prog);
479 config.ignore_patterns.push(val);
480 break;
481 }
482 b'w' => {
483 let val = take_short_value(bytes, i + 1, &mut args, "w", prog);
484 config.width = val.parse().unwrap_or_else(|_| {
485 eprintln!("{}: invalid line width: '{}'", prog, val);
486 std::process::exit(2);
487 });
488 break;
489 }
490 b'T' => {
491 let val = take_short_value(bytes, i + 1, &mut args, "T", prog);
492 config.tab_size = val.parse().unwrap_or_else(|_| {
493 eprintln!("{}: invalid tab size: '{}'", prog, val);
494 std::process::exit(2);
495 });
496 break;
497 }
498 _ => {
499 eprintln!("{}: invalid option -- '{}'", prog, bytes[i] as char);
500 eprintln!("Try '{} --help' for more information.", prog);
501 std::process::exit(2);
502 }
503 }
504 i += 1;
505 }
506 } else {
507 paths.push(arg.to_string_lossy().into_owned());
508 }
509 }
510
511 (config, paths)
512}
513
514pub fn run_ls(flavor: LsFlavor) {
516 unsafe {
518 libc::setlocale(libc::LC_ALL, c"".as_ptr());
519 }
520 super::detect_c_locale();
521
522 let (config, paths) = parse_ls_args(flavor);
523 let prog = flavor.name();
524
525 let file_args: Vec<String> = if paths.is_empty() {
526 vec![".".to_string()]
527 } else {
528 paths
529 };
530
531 match ls_main(&file_args, &config) {
532 Ok(true) => {}
533 Ok(false) => std::process::exit(2),
534 Err(e) => {
535 if e.kind() == io::ErrorKind::BrokenPipe {
536 std::process::exit(141);
537 }
538 eprintln!("{}: {}", prog, e);
539 std::process::exit(2);
540 }
541 }
542}