Merge branch 'main' into tail_notify

This commit is contained in:
Jan Scheer 2022-06-02 16:31:25 +02:00
commit 8ee806a444
No known key found for this signature in database
GPG key ID: C62AD4C29E2B9828
16 changed files with 651 additions and 225 deletions

4
Cargo.lock generated
View file

@ -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",

View file

@ -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]

View file

@ -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"));

View file

@ -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
```

View file

@ -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<str>,
{
// 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!(),
}

View file

@ -93,7 +93,7 @@ fn tabstops_parse(s: &str) -> (RemainingMode, Vec<usize>) {
// 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::<Vec<&str>>().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(

View file

@ -36,6 +36,7 @@ pub struct Settings {
suffix: String,
symbolic: bool,
relative: bool,
logical: bool,
target_dir: Option<String>,
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 {

View file

@ -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<String>,
/// The suffix to append to the temporary file, if any.
suffix: Option<String>,
/// 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<Self, MkTempError> {
// 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());
}
}

View file

@ -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)"),
)

View file

@ -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<&str>>(),
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<&str>>(),
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);

View file

@ -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) {

View file

@ -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!()

View file

@ -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'");
}

View file

@ -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);
}

View file

@ -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

View file

@ -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