Skip to main content

coreutils_rs/ls/
cli.rs

1//! Shared CLI argument parsing for ls / dir / vdir.
2
3use std::io;
4
5use super::{
6    ClassifyMode, ColorMode, HyperlinkMode, IndicatorStyle, LsConfig, OutputFormat, QuotingStyle,
7    SortBy, TimeField, TimeStyle, atty_stdout, ls_main,
8};
9
10/// Which variant of ls we are running.
11#[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
153/// Parse command-line arguments for ls / dir / vdir.
154pub 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
514/// Run ls / dir / vdir with the given flavor.
515pub fn run_ls(flavor: LsFlavor) {
516    // Initialize locale for proper collation ordering (shared by ls, dir, vdir)
517    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}