diff --git a/Cargo.lock b/Cargo.lock index cb169c6f0..d7d78420a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2085,9 +2085,9 @@ dependencies = [ [[package]] name = "users" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" dependencies = [ "libc", "log", diff --git a/Cargo.toml b/Cargo.toml index c288ad0b9..4ba105014 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -403,7 +403,7 @@ rlimit = "0.8.3" [target.'cfg(unix)'.dev-dependencies] nix = { version = "0.24.1", default-features = false, features = ["process", "signal", "user"] } -rust-users = { version="0.10", package="users" } +rust-users = { version="0.11", package="users" } unix_socket = "0.5.0" [build-dependencies] diff --git a/src/uu/df/src/table.rs b/src/uu/df/src/table.rs index 22d3a4437..76aa28eac 100644 --- a/src/uu/df/src/table.rs +++ b/src/uu/df/src/table.rs @@ -204,12 +204,18 @@ pub(crate) struct RowFormatter<'a> { // numbers. We could split the options up into those groups to // reduce the coupling between this `table.rs` module and the main // `df.rs` module. + /// Whether to use the special rules for displaying the total row. + is_total_row: bool, } impl<'a> RowFormatter<'a> { /// Instantiate this struct. - pub(crate) fn new(row: &'a Row, options: &'a Options) -> Self { - Self { row, options } + pub(crate) fn new(row: &'a Row, options: &'a Options, is_total_row: bool) -> Self { + Self { + row, + options, + is_total_row, + } } /// Get a string giving the scaled version of the input number. @@ -251,13 +257,25 @@ impl<'a> RowFormatter<'a> { for column in &self.options.columns { let string = match column { - Column::Source => self.row.fs_device.to_string(), + Column::Source => { + if self.is_total_row { + "total".to_string() + } else { + self.row.fs_device.to_string() + } + } Column::Size => self.scaled_bytes(self.row.bytes), Column::Used => self.scaled_bytes(self.row.bytes_used), Column::Avail => self.scaled_bytes(self.row.bytes_avail), Column::Pcent => Self::percentage(self.row.bytes_usage), - Column::Target => self.row.fs_mount.to_string(), + Column::Target => { + if self.is_total_row && !self.options.columns.contains(&Column::Source) { + "total".to_string() + } else { + self.row.fs_mount.to_string() + } + } Column::Itotal => self.scaled_inodes(self.row.inodes), Column::Iused => self.scaled_inodes(self.row.inodes_used), Column::Iavail => self.scaled_inodes(self.row.inodes_free), @@ -371,7 +389,7 @@ impl Table { // the output table. if options.show_all_fs || filesystem.usage.blocks > 0 { let row = Row::from(filesystem); - let fmt = RowFormatter::new(&row, options); + let fmt = RowFormatter::new(&row, options, false); let values = fmt.get_values(); total += row; @@ -386,7 +404,7 @@ impl Table { } if options.show_total { - let total_row = RowFormatter::new(&total, options); + let total_row = RowFormatter::new(&total, options, true); rows.push(total_row.get_values()); } @@ -625,7 +643,7 @@ mod tests { ..Default::default() }; - let fmt = RowFormatter::new(&row, &options); + let fmt = RowFormatter::new(&row, &options, false); assert_eq!( fmt.get_values(), vec!("my_device", "100", "25", "75", "25%", "my_mount") @@ -651,7 +669,7 @@ mod tests { ..Default::default() }; - let fmt = RowFormatter::new(&row, &options); + let fmt = RowFormatter::new(&row, &options, false); assert_eq!( fmt.get_values(), vec!("my_device", "my_type", "100", "25", "75", "25%", "my_mount") @@ -676,7 +694,7 @@ mod tests { ..Default::default() }; - let fmt = RowFormatter::new(&row, &options); + let fmt = RowFormatter::new(&row, &options, false); assert_eq!( fmt.get_values(), vec!("my_device", "10", "2", "8", "20%", "my_mount") @@ -695,7 +713,7 @@ mod tests { inodes: 10, ..Default::default() }; - let fmt = RowFormatter::new(&row, &options); + let fmt = RowFormatter::new(&row, &options, false); assert_eq!(fmt.get_values(), vec!("1", "10")); } @@ -718,7 +736,7 @@ mod tests { ..Default::default() }; - let fmt = RowFormatter::new(&row, &options); + let fmt = RowFormatter::new(&row, &options, false); assert_eq!( fmt.get_values(), vec!("my_device", "my_type", "4k", "1k", "3k", "25%", "my_mount") @@ -744,7 +762,7 @@ mod tests { ..Default::default() }; - let fmt = RowFormatter::new(&row, &options); + let fmt = RowFormatter::new(&row, &options, false); assert_eq!( fmt.get_values(), vec!("my_device", "my_type", "4K", "1K", "3K", "25%", "my_mount") @@ -761,7 +779,7 @@ mod tests { bytes_usage: Some(0.251), ..Default::default() }; - let fmt = RowFormatter::new(&row, &options); + let fmt = RowFormatter::new(&row, &options, false); assert_eq!(fmt.get_values(), vec!("26%")); } @@ -780,7 +798,7 @@ mod tests { bytes_avail, ..Default::default() }; - RowFormatter::new(&row, &options).get_values() + RowFormatter::new(&row, &options, false).get_values() } assert_eq!(get_formatted_values(100, 100, 0), vec!("1", "1", "0")); diff --git a/src/uu/dircolors/README.md b/src/uu/dircolors/README.md index 915520d25..7a02c0c4b 100644 --- a/src/uu/dircolors/README.md +++ b/src/uu/dircolors/README.md @@ -4,6 +4,7 @@ Create the test fixtures by writing the output of the GNU dircolors commands to ``` $ dircolors --print-database > /PATH_TO_COREUTILS/tests/fixtures/dircolors/internal.expected +$ dircolors --print-ls-colors > /PATH_TO_COREUTILS/tests/fixtures/dircolors/ls_colors.expected $ dircolors -b > /PATH_TO_COREUTILS/tests/fixtures/dircolors/bash_def.expected $ dircolors -c > /PATH_TO_COREUTILS/tests/fixtures/dircolors/csh_def.expected ``` diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index 2c31d727e..6bc96adbf 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -21,6 +21,7 @@ mod options { pub const BOURNE_SHELL: &str = "bourne-shell"; pub const C_SHELL: &str = "c-shell"; pub const PRINT_DATABASE: &str = "print-database"; + pub const PRINT_LS_COLORS: &str = "print-ls-colors"; pub const FILE: &str = "FILE"; } @@ -39,6 +40,7 @@ use self::colors::INTERNAL_DB; pub enum OutputFmt { Shell, CShell, + Display, Unknown, } @@ -76,7 +78,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // clap provides .conflicts_with / .conflicts_with_all, but we want to // manually handle conflicts so we can match the output of GNU coreutils if (matches.is_present(options::C_SHELL) || matches.is_present(options::BOURNE_SHELL)) - && matches.is_present(options::PRINT_DATABASE) + && (matches.is_present(options::PRINT_DATABASE) + || matches.is_present(options::PRINT_LS_COLORS)) { return Err(UUsageError::new( 1, @@ -85,6 +88,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { )); } + if matches.is_present(options::PRINT_DATABASE) && matches.is_present(options::PRINT_LS_COLORS) { + return Err(UUsageError::new( + 1, + "options --print-database and --print-ls-colors are mutually exclusive", + )); + } + if matches.is_present(options::PRINT_DATABASE) { if !files.is_empty() { return Err(UUsageError::new( @@ -100,12 +110,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return Ok(()); } - let mut out_format = OutputFmt::Unknown; - if matches.is_present(options::C_SHELL) { - out_format = OutputFmt::CShell; + let mut out_format = if matches.is_present(options::C_SHELL) { + OutputFmt::CShell } else if matches.is_present(options::BOURNE_SHELL) { - out_format = OutputFmt::Shell; - } + OutputFmt::Shell + } else if matches.is_present(options::PRINT_LS_COLORS) { + OutputFmt::Display + } else { + OutputFmt::Unknown + }; if out_format == OutputFmt::Unknown { match guess_syntax() { @@ -186,6 +199,12 @@ pub fn uu_app<'a>() -> Command<'a> { .help("print the byte counts") .display_order(3), ) + .arg( + Arg::new(options::PRINT_LS_COLORS) + .long("print-ls-colors") + .help("output fully escaped colors for display") + .display_order(4), + ) .arg( Arg::new(options::FILE) .hide(true) @@ -254,6 +273,7 @@ enum ParseState { Continue, Pass, } + use std::collections::HashMap; use uucore::{format_usage, InvalidEncodingHandling}; @@ -262,11 +282,12 @@ where T: IntoIterator, T::Item: Borrow, { - // 1440 > $(dircolors | wc -m) - let mut result = String::with_capacity(1440); + // 1790 > $(dircolors | wc -m) + let mut result = String::with_capacity(1790); match fmt { OutputFmt::Shell => result.push_str("LS_COLORS='"), OutputFmt::CShell => result.push_str("setenv LS_COLORS '"), + OutputFmt::Display => (), _ => unreachable!(), } @@ -345,13 +366,25 @@ where } if state != ParseState::Pass { if key.starts_with('.') { - result.push_str(format!("*{}={}:", key, val).as_str()); + if *fmt == OutputFmt::Display { + result.push_str(format!("\x1b[{1}m*{0}\t{1}\x1b[0m\n", key, val).as_str()); + } else { + result.push_str(format!("*{}={}:", key, val).as_str()); + } } else if key.starts_with('*') { - result.push_str(format!("{}={}:", key, val).as_str()); + if *fmt == OutputFmt::Display { + result.push_str(format!("\x1b[{1}m{0}\t{1}\x1b[0m\n", key, val).as_str()); + } else { + result.push_str(format!("{}={}:", key, val).as_str()); + } } else if lower == "options" || lower == "color" || lower == "eightbit" { // Slackware only. Ignore } else if let Some(s) = table.get(lower.as_str()) { - result.push_str(format!("{}={}:", s, val).as_str()); + if *fmt == OutputFmt::Display { + result.push_str(format!("\x1b[{1}m{0}\t{1}\x1b[0m\n", s, val).as_str()); + } else { + result.push_str(format!("{}={}:", s, val).as_str()); + } } else { return Err(format!( "{}:{}: unrecognized keyword {}", @@ -367,6 +400,10 @@ where match fmt { OutputFmt::Shell => result.push_str("';\nexport LS_COLORS"), OutputFmt::CShell => result.push('\''), + OutputFmt::Display => { + // remove latest "\n" + result.pop(); + } _ => unreachable!(), } diff --git a/src/uu/expand/src/expand.rs b/src/uu/expand/src/expand.rs index 117615162..1700920d3 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -93,7 +93,7 @@ fn tabstops_parse(s: &str) -> (RemainingMode, Vec) { // Tab size must be positive. if num == 0 { - crash!(1, "{}\n", "tab size cannot be 0"); + crash!(1, "tab size cannot be 0"); } // Tab sizes must be ascending. @@ -132,8 +132,8 @@ struct Options { impl Options { fn new(matches: &ArgMatches) -> Self { - let (remaining_mode, tabstops) = match matches.value_of(options::TABS) { - Some(s) => tabstops_parse(s), + let (remaining_mode, tabstops) = match matches.values_of(options::TABS) { + Some(s) => tabstops_parse(&s.collect::>().join(",")), None => (RemainingMode::None, vec![DEFAULT_TABSTOP]), }; @@ -195,6 +195,7 @@ pub fn uu_app<'a>() -> Command<'a> { .short('t') .value_name("N, LIST") .takes_value(true) + .multiple_occurrences(true) .help("have tabs N characters apart, not 8 or use comma separated list of explicit tab positions"), ) .arg( diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index fdbb32311..5d7639d61 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -36,6 +36,7 @@ pub struct Settings { suffix: String, symbolic: bool, relative: bool, + logical: bool, target_dir: Option, no_target_dir: bool, no_dereference: bool, @@ -121,9 +122,12 @@ const USAGE: &str = "\ mod options { pub const FORCE: &str = "force"; + //pub const DIRECTORY: &str = "directory"; pub const INTERACTIVE: &str = "interactive"; pub const NO_DEREFERENCE: &str = "no-dereference"; pub const SYMBOLIC: &str = "symbolic"; + pub const LOGICAL: &str = "logical"; + pub const PHYSICAL: &str = "physical"; pub const TARGET_DIRECTORY: &str = "target-directory"; pub const NO_TARGET_DIRECTORY: &str = "no-target-directory"; pub const RELATIVE: &str = "relative"; @@ -152,6 +156,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map(PathBuf::from) .collect(); + let symbolic = matches.is_present(options::SYMBOLIC); + let overwrite_mode = if matches.is_present(options::FORCE) { OverwriteMode::Force } else if matches.is_present(options::INTERACTIVE) { @@ -163,11 +169,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let backup_mode = backup_control::determine_backup_mode(&matches)?; let backup_suffix = backup_control::determine_backup_suffix(&matches); + // When we have "-L" or "-L -P", false otherwise + let logical = matches.is_present(options::LOGICAL); + let settings = Settings { overwrite: overwrite_mode, backup: backup_mode, suffix: backup_suffix, - symbolic: matches.is_present(options::SYMBOLIC), + symbolic, + logical, relative: matches.is_present(options::RELATIVE), target_dir: matches .value_of(options::TARGET_DIRECTORY) @@ -188,9 +198,12 @@ pub fn uu_app<'a>() -> Command<'a> { .infer_long_args(true) .arg(backup_control::arguments::backup()) .arg(backup_control::arguments::backup_no_args()) - // TODO: opts.arg( - // Arg::new(("d", "directory", "allow users with appropriate privileges to attempt \ - // to make hard links to directories"); + /*.arg( + Arg::new(options::DIRECTORY) + .short('d') + .long(options::DIRECTORY) + .help("allow users with appropriate privileges to attempt to make hard links to directories") + )*/ .arg( Arg::new(options::FORCE) .short('f') @@ -212,15 +225,24 @@ pub fn uu_app<'a>() -> Command<'a> { symbolic link to a directory", ), ) - // TODO: opts.arg( - // Arg::new(("L", "logical", "dereference TARGETs that are symbolic links"); - // - // TODO: opts.arg( - // Arg::new(("P", "physical", "make hard links directly to symbolic links"); + .arg( + Arg::new(options::LOGICAL) + .short('L') + .long(options::LOGICAL) + .help("dereference TARGETs that are symbolic links") + .overrides_with(options::PHYSICAL), + ) + .arg( + // Not implemented yet + Arg::new(options::PHYSICAL) + .short('P') + .long(options::PHYSICAL) + .help("make hard links directly to symbolic links"), + ) .arg( Arg::new(options::SYMBOLIC) .short('s') - .long("symbolic") + .long(options::SYMBOLIC) .help("make symbolic links instead of hard links") // override added for https://github.com/uutils/coreutils/issues/2359 .overrides_with(options::SYMBOLIC), @@ -446,7 +468,15 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> { if settings.symbolic { symlink(&source, dst)?; } else { - fs::hard_link(&source, dst)?; + let p = if settings.logical && is_symlink(&source) { + // if we want to have an hard link, + // source is a symlink and -L is passed + // we want to resolve the symlink to create the hardlink + std::fs::canonicalize(&source)? + } else { + source.to_path_buf() + }; + fs::hard_link(&p, dst)?; } if settings.verbose { diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index e145a3933..dd1f57b8b 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -8,7 +8,7 @@ // spell-checker:ignore (paths) GPGHome -use clap::{crate_version, Arg, Command}; +use clap::{crate_version, Arg, ArgMatches, Command}; use uucore::display::{println_verbatim, Quotable}; use uucore::error::{FromIo, UError, UResult}; use uucore::format_usage; @@ -17,7 +17,7 @@ use std::env; use std::error::Error; use std::fmt::Display; use std::iter; -use std::path::{is_separator, Path, PathBuf, MAIN_SEPARATOR}; +use std::path::{Path, PathBuf, MAIN_SEPARATOR}; #[cfg(unix)] use std::fs; @@ -89,86 +89,229 @@ impl Display for MkTempError { } } -#[uucore::main] -pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().get_matches_from(args); +/// Options parsed from the command-line. +/// +/// This provides a layer of indirection between the application logic +/// and the argument parsing library `clap`, allowing each to vary +/// independently. +struct Options { + /// Whether to create a temporary directory instead of a file. + directory: bool, - let template = matches.value_of(ARG_TEMPLATE).unwrap(); - let tmpdir = matches.value_of(OPT_TMPDIR).unwrap_or_default(); + /// Whether to just print the name of a file that would have been created. + dry_run: bool, - // Treat the template string as a path to get the directory - // containing the last component. - let path = PathBuf::from(template); + /// Whether to suppress file creation error messages. + quiet: bool, - let (template, tmpdir) = if matches.is_present(OPT_TMPDIR) - && !PathBuf::from(tmpdir).is_dir() // if a temp dir is provided, it must be an actual path - && tmpdir.contains("XXX") - // If this is a template, it has to contain at least 3 X - && template == DEFAULT_TEMPLATE - // That means that clap does not think we provided a template - { - // Special case to workaround a limitation of clap when doing - // mktemp --tmpdir apt-key-gpghome.XXX - // The behavior should be - // mktemp --tmpdir $TMPDIR apt-key-gpghome.XX - // As --tmpdir is empty + /// The directory in which to create the temporary file. + /// + /// If `None`, the file will be created in the current directory. + tmpdir: Option, + + /// The suffix to append to the temporary file, if any. + suffix: Option, + + /// Whether to treat the template argument as a single file path component. + treat_as_template: bool, + + /// The template to use for the name of the temporary file. + template: String, +} + +/// Decide whether the argument to `--tmpdir` should actually be the template. +/// +/// This function is required to work around a limitation of `clap`, +/// the command-line argument parsing library. In case the command +/// line is +/// +/// ```sh +/// mktemp --tmpdir XXX +/// ``` +/// +/// the program should behave like +/// +/// ```sh +/// mktemp --tmpdir=${TMPDIR:-/tmp} XXX +/// ``` +/// +/// However, `clap` thinks that `XXX` is the value of the `--tmpdir` +/// option. This function returns `true` in this case and `false` +/// in all other cases. +fn is_tmpdir_argument_actually_the_template(matches: &ArgMatches) -> bool { + if !matches.is_present(ARG_TEMPLATE) { + if let Some(tmpdir) = matches.value_of(OPT_TMPDIR) { + if !Path::new(tmpdir).is_dir() && tmpdir.contains("XXX") { + return true; + } + } + } + false +} + +impl Options { + fn from(matches: &ArgMatches) -> Self { + // Special case to work around a limitation of `clap`; see + // `is_tmpdir_argument_actually_the_template()` for more + // information. // // Fixed in clap 3 // See https://github.com/clap-rs/clap/pull/1587 - let tmp = env::temp_dir(); - (tmpdir, tmp) - } else if !matches.is_present(OPT_TMPDIR) { - // In this case, the command line was `mktemp -t XXX`, so we - // treat the argument `XXX` as though it were a filename - // regardless of whether it has path separators in it. - if matches.is_present(OPT_T) { - let tmp = env::temp_dir(); - (template, tmp) - // In this case, the command line was `mktemp XXX`, so we need - // to parse out the parent directory and the filename from the - // argument `XXX`, since it may be include path separators. + let (tmpdir, template) = if is_tmpdir_argument_actually_the_template(matches) { + let tmpdir = Some(env::temp_dir().display().to_string()); + let template = matches.value_of(OPT_TMPDIR).unwrap().to_string(); + (tmpdir, template) } else { - let tmp = match path.parent() { - None => PathBuf::from("."), - Some(d) => PathBuf::from(d), - }; - let filename = path.file_name(); - let template = filename.unwrap().to_str().unwrap(); - // If the command line was `mktemp aXXX/b`, then we will - // find that `tmp`, which is the result of getting the - // parent when treating the argument as a path, contains - // at least three consecutive Xs. This means that there - // was a path separator in the suffix, which is not - // allowed. - if tmp.display().to_string().contains("XXX") { - return Err(MkTempError::SuffixContainsDirSeparator(format!( - "{}{}", - MAIN_SEPARATOR, template - )) - .into()); - } - (template, tmp) + let tmpdir = matches.value_of(OPT_TMPDIR).map(String::from); + let template = matches + .value_of(ARG_TEMPLATE) + .unwrap_or(DEFAULT_TEMPLATE) + .to_string(); + (tmpdir, template) + }; + Self { + directory: matches.is_present(OPT_DIRECTORY), + dry_run: matches.is_present(OPT_DRY_RUN), + quiet: matches.is_present(OPT_QUIET), + tmpdir, + suffix: matches.value_of(OPT_SUFFIX).map(String::from), + treat_as_template: matches.is_present(OPT_T), + template, } - } else { - (template, PathBuf::from(tmpdir)) - }; - - let make_dir = matches.is_present(OPT_DIRECTORY); - let dry_run = matches.is_present(OPT_DRY_RUN); - let suppress_file_err = matches.is_present(OPT_QUIET); - - // If `--tmpdir` is given, the template cannot be an absolute - // path. For example, `mktemp --tmpdir=a /XXX` is not allowed. - if matches.is_present(OPT_TMPDIR) && PathBuf::from(template).is_absolute() { - return Err(MkTempError::InvalidTemplate(template.into()).into()); } +} - let (prefix, rand, suffix) = parse_template(template, matches.value_of(OPT_SUFFIX))?; +/// Parameters that control the path to and name of the temporary file. +/// +/// The temporary file will be created at +/// +/// ```text +/// {directory}/{prefix}{XXX}{suffix} +/// ``` +/// +/// where `{XXX}` is a sequence of random characters whose length is +/// `num_rand_chars`. +struct Params { + /// The directory that will contain the temporary file. + directory: String, + /// The (non-random) prefix of the temporary file. + prefix: String, + + /// The number of random characters in the name of the temporary file. + num_rand_chars: usize, + + /// The (non-random) suffix of the temporary file. + suffix: String, +} + +impl Params { + fn from(options: Options) -> Result { + // Get the start and end indices of the randomized part of the template. + // + // For example, if the template is "abcXXXXyz", then `i` is 3 and `j` is 7. + let i = match options.template.find("XXX") { + None => { + let s = match options.suffix { + None => options.template, + Some(s) => format!("{}{}", options.template, s), + }; + return Err(MkTempError::TooFewXs(s)); + } + Some(i) => i, + }; + let j = options.template.rfind("XXX").unwrap() + 3; + + // Combine the directory given as an option and the prefix of the template. + // + // For example, if `tmpdir` is "a/b" and the template is "c/dXXX", + // then `prefix` is "a/b/c/d". + let tmpdir = options.tmpdir; + let prefix_from_option = tmpdir.clone().unwrap_or_else(|| "".to_string()); + let prefix_from_template = &options.template[..i]; + let prefix = Path::new(&prefix_from_option) + .join(prefix_from_template) + .display() + .to_string(); + if options.treat_as_template && prefix.contains(MAIN_SEPARATOR) { + return Err(MkTempError::PrefixContainsDirSeparator(options.template)); + } + if tmpdir.is_some() && Path::new(prefix_from_template).is_absolute() { + return Err(MkTempError::InvalidTemplate(options.template)); + } + + // Split the parent directory from the file part of the prefix. + // + // For example, if `prefix` is "a/b/c/d", then `directory` is + // "a/b/c" is `prefix` gets reassigned to "d". + let (directory, prefix) = if prefix.ends_with(MAIN_SEPARATOR) { + (prefix, "".to_string()) + } else { + let path = Path::new(&prefix); + let directory = match path.parent() { + None => String::new(), + Some(d) => d.display().to_string(), + }; + let prefix = match path.file_name() { + None => String::new(), + Some(f) => f.to_str().unwrap().to_string(), + }; + (directory, prefix) + }; + + // Combine the suffix from the template with the suffix given as an option. + // + // For example, if the suffix command-line argument is ".txt" and + // the template is "XXXabc", then `suffix` is "abc.txt". + let suffix_from_option = options.suffix.unwrap_or_else(|| "".to_string()); + let suffix_from_template = &options.template[j..]; + let suffix = format!("{}{}", suffix_from_template, suffix_from_option); + if suffix.contains(MAIN_SEPARATOR) { + return Err(MkTempError::SuffixContainsDirSeparator(suffix)); + } + if !suffix_from_template.is_empty() && !suffix_from_option.is_empty() { + return Err(MkTempError::MustEndInX(options.template)); + } + + // The number of random characters in the template. + // + // For example, if the template is "abcXXXXyz", then the number of + // random characters is four. + let num_rand_chars = j - i; + + Ok(Self { + directory, + prefix, + num_rand_chars, + suffix, + }) + } +} + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().try_get_matches_from(args)?; + + // Parse command-line options into a format suitable for the + // application logic. + let options = Options::from(&matches); + let dry_run = options.dry_run; + let suppress_file_err = options.quiet; + let make_dir = options.directory; + + // Parse file path parameters from the command-line options. + let Params { + directory: tmpdir, + prefix, + num_rand_chars: rand, + suffix, + } = Params::from(options)?; + + // Create the temporary file or directory, or simulate creating it. let res = if dry_run { - dry_exec(tmpdir, prefix, rand, suffix) + dry_exec(&tmpdir, &prefix, rand, &suffix) } else { - exec(&tmpdir, prefix, rand, suffix, make_dir) + exec(&tmpdir, &prefix, rand, &suffix, make_dir) }; if suppress_file_err { @@ -234,73 +377,10 @@ pub fn uu_app<'a>() -> Command<'a> { .multiple_occurrences(false) .takes_value(true) .max_values(1) - .default_value(DEFAULT_TEMPLATE), ) } -/// Parse a template string into prefix, suffix, and random components. -/// -/// `temp` is the template string, with three or more consecutive `X`s -/// representing a placeholder for randomly generated characters (for -/// example, `"abc_XXX.txt"`). If `temp` ends in an `X`, then a suffix -/// can be specified by `suffix` instead. -/// -/// # Errors -/// -/// * If there are fewer than three consecutive `X`s in `temp`. -/// * If `suffix` is a [`Some`] object but `temp` does not end in `X`. -/// * If the suffix (specified either way) contains a path separator. -/// -/// # Examples -/// -/// ```rust,ignore -/// assert_eq!(parse_template("XXX", None).unwrap(), ("", 3, "")); -/// assert_eq!(parse_template("abcXXX", None).unwrap(), ("abc", 3, "")); -/// assert_eq!(parse_template("XXXdef", None).unwrap(), ("", 3, "def")); -/// assert_eq!(parse_template("abcXXXdef", None).unwrap(), ("abc", 3, "def")); -/// ``` -fn parse_template<'a>( - temp: &'a str, - suffix: Option<&'a str>, -) -> Result<(&'a str, usize, &'a str), MkTempError> { - let right = match temp.rfind('X') { - Some(r) => r + 1, - None => return Err(MkTempError::TooFewXs(temp.into())), - }; - let left = temp[..right].rfind(|c| c != 'X').map_or(0, |i| i + 1); - let prefix = &temp[..left]; - let rand = right - left; - - if rand < 3 { - let s = match suffix { - None => temp.into(), - Some(s) => format!("{}{}", temp, s), - }; - return Err(MkTempError::TooFewXs(s)); - } - - let mut suf = &temp[right..]; - - if let Some(s) = suffix { - if suf.is_empty() { - suf = s; - } else { - return Err(MkTempError::MustEndInX(temp.into())); - } - }; - - if prefix.chars().any(is_separator) { - return Err(MkTempError::PrefixContainsDirSeparator(temp.into())); - } - - if suf.chars().any(is_separator) { - return Err(MkTempError::SuffixContainsDirSeparator(suf.into())); - } - - Ok((prefix, rand, suf)) -} - -pub fn dry_exec(mut tmpdir: PathBuf, prefix: &str, rand: usize, suffix: &str) -> UResult<()> { +pub fn dry_exec(tmpdir: &str, prefix: &str, rand: usize, suffix: &str) -> UResult<()> { let len = prefix.len() + suffix.len() + rand; let mut buf = Vec::with_capacity(len); buf.extend(prefix.as_bytes()); @@ -320,11 +400,11 @@ pub fn dry_exec(mut tmpdir: PathBuf, prefix: &str, rand: usize, suffix: &str) -> } // We guarantee utf8. let buf = String::from_utf8(buf).unwrap(); - tmpdir.push(buf); + let tmpdir = Path::new(tmpdir).join(buf); println_verbatim(tmpdir).map_err_context(|| "failed to print directory name".to_owned()) } -fn exec(dir: &Path, prefix: &str, rand: usize, suffix: &str, make_dir: bool) -> UResult<()> { +fn exec(dir: &str, prefix: &str, rand: usize, suffix: &str, make_dir: bool) -> UResult<()> { let context = || { format!( "failed to create file via template '{}{}{}'", @@ -366,42 +446,7 @@ fn exec(dir: &Path, prefix: &str, rand: usize, suffix: &str, make_dir: bool) -> // the absolute path and we need to return a filename that matches // the template given on the command-line which might be a // relative path. - let mut path = dir.to_path_buf(); - path.push(filename); + let path = Path::new(dir).join(filename); println_verbatim(path).map_err_context(|| "failed to print directory name".to_owned()) } - -#[cfg(test)] -mod tests { - use crate::parse_template; - - #[test] - fn test_parse_template_no_suffix() { - assert_eq!(parse_template("XXX", None).unwrap(), ("", 3, "")); - assert_eq!(parse_template("abcXXX", None).unwrap(), ("abc", 3, "")); - assert_eq!(parse_template("XXXdef", None).unwrap(), ("", 3, "def")); - assert_eq!( - parse_template("abcXXXdef", None).unwrap(), - ("abc", 3, "def") - ); - } - - #[test] - fn test_parse_template_suffix() { - assert_eq!(parse_template("XXX", Some("def")).unwrap(), ("", 3, "def")); - assert_eq!( - parse_template("abcXXX", Some("def")).unwrap(), - ("abc", 3, "def") - ); - } - - #[test] - fn test_parse_template_errors() { - assert!(parse_template("a/bXXX", None).is_err()); - assert!(parse_template("XXXa/b", None).is_err()); - assert!(parse_template("XX", None).is_err()); - assert!(parse_template("XXXabc", Some("def")).is_err()); - assert!(parse_template("XXX", Some("a/b")).is_err()); - } -} diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 03905e877..88edc569c 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -5,7 +5,7 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// spell-checker:ignore (ToDO) PREFIXaa nbbbb ncccc +// spell-checker:ignore (ToDO) PREFIXaa PREFIXab nbbbb ncccc mod filenames; mod number; @@ -47,7 +47,7 @@ static ARG_PREFIX: &str = "prefix"; const USAGE: &str = "{} [OPTION]... [INPUT [PREFIX]]"; const AFTER_HELP: &str = "\ - Output fixed-size pieces of INPUT to PREFIXaa, PREFIX ab, ...; default \ + Output fixed-size pieces of INPUT to PREFIXaa, PREFIXab, ...; default \ size is 1000, and default PREFIX is 'x'. With no INPUT, or when INPUT is \ -, read standard input."; @@ -74,6 +74,7 @@ pub fn uu_app<'a>() -> Command<'a> { .short('b') .long(OPT_BYTES) .takes_value(true) + .value_name("SIZE") .help("put SIZE bytes per output file"), ) .arg( @@ -81,6 +82,7 @@ pub fn uu_app<'a>() -> Command<'a> { .short('C') .long(OPT_LINE_BYTES) .takes_value(true) + .value_name("SIZE") .default_value("2") .help("put at most SIZE bytes of lines per output file"), ) @@ -89,6 +91,7 @@ pub fn uu_app<'a>() -> Command<'a> { .short('l') .long(OPT_LINES) .takes_value(true) + .value_name("NUMBER") .default_value("1000") .help("put NUMBER lines/records per output file"), ) @@ -97,6 +100,7 @@ pub fn uu_app<'a>() -> Command<'a> { .short('n') .long(OPT_NUMBER) .takes_value(true) + .value_name("CHUNKS") .help("generate CHUNKS output files; see explanation below"), ) // rest of the arguments @@ -104,8 +108,9 @@ pub fn uu_app<'a>() -> Command<'a> { Arg::new(OPT_ADDITIONAL_SUFFIX) .long(OPT_ADDITIONAL_SUFFIX) .takes_value(true) + .value_name("SUFFIX") .default_value("") - .help("additional suffix to append to output file names"), + .help("additional SUFFIX to append to output file names"), ) .arg( Arg::new(OPT_FILTER) @@ -137,6 +142,7 @@ pub fn uu_app<'a>() -> Command<'a> { .short('a') .long(OPT_SUFFIX_LENGTH) .takes_value(true) + .value_name("N") .default_value(OPT_DEFAULT_SUFFIX_LENGTH) .help("use suffixes of length N (default 2)"), ) diff --git a/tests/by-util/test_df.rs b/tests/by-util/test_df.rs index 6272d1082..4206c4c82 100644 --- a/tests/by-util/test_df.rs +++ b/tests/by-util/test_df.rs @@ -360,6 +360,47 @@ fn test_total() { assert_eq!(computed_total_avail, reported_total_avail); } +/// Test that the "total" label appears in the correct column. +/// +/// The "total" label should appear in the "source" column, or in the +/// "target" column if "source" is not visible. +#[test] +fn test_total_label_in_correct_column() { + let output = new_ucmd!() + .args(&["--output=source", "--total", "."]) + .succeeds() + .stdout_move_str(); + let last_line = output.lines().last().unwrap(); + assert_eq!(last_line.trim(), "total"); + + let output = new_ucmd!() + .args(&["--output=target", "--total", "."]) + .succeeds() + .stdout_move_str(); + let last_line = output.lines().last().unwrap(); + assert_eq!(last_line.trim(), "total"); + + let output = new_ucmd!() + .args(&["--output=source,target", "--total", "."]) + .succeeds() + .stdout_move_str(); + let last_line = output.lines().last().unwrap(); + assert_eq!( + last_line.split_whitespace().collect::>(), + vec!["total", "-"] + ); + + let output = new_ucmd!() + .args(&["--output=target,source", "--total", "."]) + .succeeds() + .stdout_move_str(); + let last_line = output.lines().last().unwrap(); + assert_eq!( + last_line.split_whitespace().collect::>(), + vec!["-", "total"] + ); +} + #[test] fn test_use_percentage() { let output = new_ucmd!() @@ -421,7 +462,7 @@ fn test_default_block_size() { .arg("--output=size") .succeeds() .stdout_move_str(); - let header = output.lines().next().unwrap().to_string(); + let header = output.lines().next().unwrap().trim().to_string(); assert_eq!(header, "1K-blocks"); @@ -430,7 +471,7 @@ fn test_default_block_size() { .env("POSIXLY_CORRECT", "1") .succeeds() .stdout_move_str(); - let header = output.lines().next().unwrap().to_string(); + let header = output.lines().next().unwrap().trim().to_string(); assert_eq!(header, "512B-blocks"); } @@ -445,6 +486,7 @@ fn test_default_block_size_in_posix_portability_mode() { .split_whitespace() .nth(1) .unwrap() + .trim() .to_string() } @@ -466,7 +508,7 @@ fn test_block_size_1024() { .args(&["-B", &format!("{}", block_size), "--output=size"]) .succeeds() .stdout_move_str(); - output.lines().next().unwrap().to_string() + output.lines().next().unwrap().trim().to_string() } assert_eq!(get_header(1024), "1K-blocks"); @@ -490,7 +532,7 @@ fn test_block_size_with_suffix() { .args(&["-B", block_size, "--output=size"]) .succeeds() .stdout_move_str(); - output.lines().next().unwrap().to_string() + output.lines().next().unwrap().trim().to_string() } assert_eq!(get_header("K"), "1K-blocks"); @@ -522,6 +564,7 @@ fn test_block_size_in_posix_portability_mode() { .split_whitespace() .nth(1) .unwrap() + .trim() .to_string() } @@ -540,7 +583,7 @@ fn test_block_size_from_env() { .env(env_var, env_value) .succeeds() .stdout_move_str(); - output.lines().next().unwrap().to_string() + output.lines().next().unwrap().trim().to_string() } assert_eq!(get_header("DF_BLOCK_SIZE", "111"), "111B-blocks"); @@ -559,7 +602,7 @@ fn test_block_size_from_env_precedences() { .env(k2, v2) .succeeds() .stdout_move_str(); - output.lines().next().unwrap().to_string() + output.lines().next().unwrap().trim().to_string() } let df_block_size = ("DF_BLOCK_SIZE", "111"); @@ -578,7 +621,7 @@ fn test_precedence_of_block_size_arg_over_env() { .env("DF_BLOCK_SIZE", "111") .succeeds() .stdout_move_str(); - let header = output.lines().next().unwrap().to_string(); + let header = output.lines().next().unwrap().trim().to_string(); assert_eq!(header, "999B-blocks"); } @@ -592,7 +635,7 @@ fn test_invalid_block_size_from_env() { .env("DF_BLOCK_SIZE", "invalid") .succeeds() .stdout_move_str(); - let header = output.lines().next().unwrap().to_string(); + let header = output.lines().next().unwrap().trim().to_string(); assert_eq!(header, default_block_size_header); @@ -602,7 +645,7 @@ fn test_invalid_block_size_from_env() { .env("BLOCK_SIZE", "222") .succeeds() .stdout_move_str(); - let header = output.lines().next().unwrap().to_string(); + let header = output.lines().next().unwrap().trim().to_string(); assert_eq!(header, default_block_size_header); } @@ -626,6 +669,7 @@ fn test_ignore_block_size_from_env_in_posix_portability_mode() { .split_whitespace() .nth(1) .unwrap() + .trim() .to_string(); assert_eq!(header, default_block_size_header); diff --git a/tests/by-util/test_dircolors.rs b/tests/by-util/test_dircolors.rs index bb64fd1e5..a4ad0df32 100644 --- a/tests/by-util/test_dircolors.rs +++ b/tests/by-util/test_dircolors.rs @@ -62,6 +62,14 @@ fn test_internal_db() { .stdout_is_fixture("internal.expected"); } +#[test] +fn test_ls_colors() { + new_ucmd!() + .arg("--print-ls-colors") + .run() + .stdout_is_fixture("ls_colors.expected"); +} + #[test] fn test_bash_default() { new_ucmd!() @@ -109,6 +117,18 @@ fn test_exclusive_option() { .arg("-cp") .fails() .stderr_contains("mutually exclusive"); + new_ucmd!() + .args(&["-b", "--print-ls-colors"]) + .fails() + .stderr_contains("mutually exclusive"); + new_ucmd!() + .args(&["-c", "--print-ls-colors"]) + .fails() + .stderr_contains("mutually exclusive"); + new_ucmd!() + .args(&["-p", "--print-ls-colors"]) + .fails() + .stderr_contains("mutually exclusive"); } fn test_helper(file_name: &str, term: &str) { diff --git a/tests/by-util/test_expand.rs b/tests/by-util/test_expand.rs index 9c06fe904..4566e4176 100644 --- a/tests/by-util/test_expand.rs +++ b/tests/by-util/test_expand.rs @@ -73,6 +73,15 @@ fn test_tabs_mixed_style_list() { .stdout_is("a b c d e"); } +#[test] +fn test_multiple_tabs_args() { + new_ucmd!() + .args(&["--tabs=3", "--tabs=6", "--tabs=9"]) + .pipe_in("a\tb\tc\td\te") + .succeeds() + .stdout_is("a b c d e"); +} + #[test] fn test_tabs_empty_string() { new_ucmd!() diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index 980225260..042aa1e5d 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -650,3 +650,55 @@ fn test_backup_force() { // we should have the same content as b as we had time to do a backup assert_eq!(at.read("b~"), "b2\n"); } + +#[test] +fn test_hard_logical() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_a = "file1"; + let link = "symlink1"; + let target = "hard-to-a"; + let target2 = "hard-to-a2"; + at.touch(file_a); + at.symlink_file(file_a, link); + + ucmd.args(&["-P", "-L", link, target]); + assert!(!at.is_symlink(target)); + + ucmd.args(&["-P", "-L", "-s", link, target2]); + assert!(!at.is_symlink(target2)); +} + +#[test] +fn test_hard_logical_non_exit_fail() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_a = "/no-such-dir"; + let link = "hard-to-dangle"; + + scene.ucmd().args(&["-s", file_a]); + assert!(!at.is_symlink("no-such-dir")); + + scene + .ucmd() + .args(&["-L", "no-such-dir", link]) + .fails() + .stderr_contains("failed to link 'no-such-dir'"); +} + +#[test] +fn test_hard_logical_dir_fail() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let dir = "d"; + at.mkdir(dir); + let target = "link-to-dir"; + + scene.ucmd().args(&["-s", dir, target]); + + scene + .ucmd() + .args(&["-L", target, "hard-to-dir-link"]) + .fails() + .stderr_contains("failed to link 'link-to-dir'"); +} diff --git a/tests/by-util/test_mktemp.rs b/tests/by-util/test_mktemp.rs index 86c4ba4db..55ca021c1 100644 --- a/tests/by-util/test_mktemp.rs +++ b/tests/by-util/test_mktemp.rs @@ -519,6 +519,7 @@ fn test_directory_permissions() { /// Test that a template with a path separator is invalid. #[test] fn test_template_path_separator() { + #[cfg(not(windows))] new_ucmd!() .args(&["-t", "a/bXXX"]) .fails() @@ -526,6 +527,14 @@ fn test_template_path_separator() { "mktemp: invalid template, {}, contains directory separator\n", "a/bXXX".quote() )); + #[cfg(windows)] + new_ucmd!() + .args(&["-t", r"a\bXXX"]) + .fails() + .stderr_only(format!( + "mktemp: invalid template, {}, contains directory separator\n", + r"a\bXXX".quote() + )); } /// Test that a suffix with a path separator is invalid. @@ -558,3 +567,8 @@ fn test_too_few_xs_suffix_directory() { .fails() .stderr_only("mktemp: too few X's in template 'aXXX'\n"); } + +#[test] +fn test_too_many_arguments() { + new_ucmd!().args(&["-q", "a", "b"]).fails().code_is(1); +} diff --git a/tests/fixtures/dircolors/ls_colors.expected b/tests/fixtures/dircolors/ls_colors.expected new file mode 100644 index 000000000..e68a59621 --- /dev/null +++ b/tests/fixtures/dircolors/ls_colors.expected @@ -0,0 +1,148 @@ +rs 0 +di 01;34 +ln 01;36 +mh 00 +pi 40;33 +so 01;35 +do 01;35 +bd 40;33;01 +cd 40;33;01 +or 40;31;01 +mi 00 +su 37;41 +sg 30;43 +ca 00 +tw 30;42 +ow 34;42 +st 37;44 +ex 01;32 +*.tar 01;31 +*.tgz 01;31 +*.arc 01;31 +*.arj 01;31 +*.taz 01;31 +*.lha 01;31 +*.lz4 01;31 +*.lzh 01;31 +*.lzma 01;31 +*.tlz 01;31 +*.txz 01;31 +*.tzo 01;31 +*.t7z 01;31 +*.zip 01;31 +*.z 01;31 +*.dz 01;31 +*.gz 01;31 +*.lrz 01;31 +*.lz 01;31 +*.lzo 01;31 +*.xz 01;31 +*.zst 01;31 +*.tzst 01;31 +*.bz2 01;31 +*.bz 01;31 +*.tbz 01;31 +*.tbz2 01;31 +*.tz 01;31 +*.deb 01;31 +*.rpm 01;31 +*.jar 01;31 +*.war 01;31 +*.ear 01;31 +*.sar 01;31 +*.rar 01;31 +*.alz 01;31 +*.ace 01;31 +*.zoo 01;31 +*.cpio 01;31 +*.7z 01;31 +*.rz 01;31 +*.cab 01;31 +*.wim 01;31 +*.swm 01;31 +*.dwm 01;31 +*.esd 01;31 +*.avif 01;35 +*.jpg 01;35 +*.jpeg 01;35 +*.mjpg 01;35 +*.mjpeg 01;35 +*.gif 01;35 +*.bmp 01;35 +*.pbm 01;35 +*.pgm 01;35 +*.ppm 01;35 +*.tga 01;35 +*.xbm 01;35 +*.xpm 01;35 +*.tif 01;35 +*.tiff 01;35 +*.png 01;35 +*.svg 01;35 +*.svgz 01;35 +*.mng 01;35 +*.pcx 01;35 +*.mov 01;35 +*.mpg 01;35 +*.mpeg 01;35 +*.m2v 01;35 +*.mkv 01;35 +*.webm 01;35 +*.webp 01;35 +*.ogm 01;35 +*.mp4 01;35 +*.m4v 01;35 +*.mp4v 01;35 +*.vob 01;35 +*.qt 01;35 +*.nuv 01;35 +*.wmv 01;35 +*.asf 01;35 +*.rm 01;35 +*.rmvb 01;35 +*.flc 01;35 +*.avi 01;35 +*.fli 01;35 +*.flv 01;35 +*.gl 01;35 +*.dl 01;35 +*.xcf 01;35 +*.xwd 01;35 +*.yuv 01;35 +*.cgm 01;35 +*.emf 01;35 +*.ogv 01;35 +*.ogx 01;35 +*.aac 00;36 +*.au 00;36 +*.flac 00;36 +*.m4a 00;36 +*.mid 00;36 +*.midi 00;36 +*.mka 00;36 +*.mp3 00;36 +*.mpc 00;36 +*.ogg 00;36 +*.ra 00;36 +*.wav 00;36 +*.oga 00;36 +*.opus 00;36 +*.spx 00;36 +*.xspf 00;36 +*~ 00;90 +*# 00;90 +*.bak 00;90 +*.old 00;90 +*.orig 00;90 +*.part 00;90 +*.rej 00;90 +*.swp 00;90 +*.tmp 00;90 +*.dpkg-dist 00;90 +*.dpkg-old 00;90 +*.ucf-dist 00;90 +*.ucf-new 00;90 +*.ucf-old 00;90 +*.rpmnew 00;90 +*.rpmorig 00;90 +*.rpmsave 00;90 diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 3961a8689..a7dc60614 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -199,3 +199,4 @@ sed -i -e "s/provoked error./provoked error\ncat pat |sort -u > pat/" tests/misc # Update the GNU error message to match ours sed -i -e "s/ln: 'f' and 'f' are the same file/ln: failed to link 'f' to 'f': Same file/g" tests/ln/hard-backup.sh +sed -i -e "s/failed to access 'no-such-dir'\":/failed to link 'no-such-dir'\"/" -e "s/link-to-dir: hard link not allowed for directory/failed to link 'link-to-dir' to/" -e "s|link-to-dir/: hard link not allowed for directory|failed to link 'link-to-dir/' to|" tests/ln/hard-to-sym.sh \ No newline at end of file