1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 19:47:45 +00:00

Merge branch 'main' into fs_extra

This commit is contained in:
Sylvestre Ledru 2023-02-18 17:23:07 +01:00 committed by GitHub
commit f88b4f4109
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 426 additions and 300 deletions

View file

@ -20,6 +20,11 @@ on: [push, pull_request]
permissions: permissions:
contents: read # to fetch code (actions/checkout) contents: read # to fetch code (actions/checkout)
# End the current execution if there is a new changeset in the PR.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs: jobs:
cargo-deny: cargo-deny:
name: Style/cargo-deny name: Style/cargo-deny

View file

@ -14,6 +14,11 @@ on: [push, pull_request]
permissions: permissions:
contents: read contents: read
# End the current execution if there is a new changeset in the PR.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs: jobs:
gnu: gnu:
permissions: permissions:
@ -196,6 +201,9 @@ jobs:
REF_LOG_FILE='${{ steps.vars.outputs.path_reference }}/test-logs/test-suite.log' REF_LOG_FILE='${{ steps.vars.outputs.path_reference }}/test-logs/test-suite.log'
REF_SUMMARY_FILE='${{ steps.vars.outputs.path_reference }}/test-summary/gnu-result.json' REF_SUMMARY_FILE='${{ steps.vars.outputs.path_reference }}/test-summary/gnu-result.json'
REPO_DEFAULT_BRANCH='${{ steps.vars.outputs.repo_default_branch }}' REPO_DEFAULT_BRANCH='${{ steps.vars.outputs.repo_default_branch }}'
# https://github.com/uutils/coreutils/issues/4294
# https://github.com/uutils/coreutils/issues/4295
IGNORE_INTERMITTENT='tests/tail-2/inotify-dir-recreate tests/misc/timeout tests/rm/rm1'
mkdir -p ${{ steps.vars.outputs.path_reference }} mkdir -p ${{ steps.vars.outputs.path_reference }}
@ -227,10 +235,18 @@ jobs:
do do
if ! grep -Fxq ${LINE}<<<"${REF_FAILING}" if ! grep -Fxq ${LINE}<<<"${REF_FAILING}"
then then
MSG="GNU test failed: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?" if ! grep ${LINE} ${IGNORE_INTERMITTENT}
echo "::error ::$MSG" then
echo $MSG >> ${COMMENT_LOG} MSG="GNU test failed: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?"
have_new_failures="true" echo "::error ::$MSG"
echo $MSG >> ${COMMENT_LOG}
have_new_failures="true"
else
MSG="Skip an intermittent issue ${LINE}"
echo "::warning ::$MSG"
echo $MSG >> ${COMMENT_LOG}
echo ""
fi
fi fi
done done
for LINE in ${REF_ERROR} for LINE in ${REF_ERROR}

9
Cargo.lock generated
View file

@ -878,6 +878,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "fundu"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925250bc259498d4008ee072bf16586083ab2c491aa4b06b3c4d0a6556cebd74"
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.25" version = "0.3.25"
@ -2807,7 +2813,6 @@ version = "0.0.17"
dependencies = [ dependencies = [
"clap", "clap",
"libc", "libc",
"num_cpus",
"uucore", "uucore",
] ]
@ -3094,9 +3099,9 @@ version = "0.0.17"
dependencies = [ dependencies = [
"atty", "atty",
"clap", "clap",
"fundu",
"libc", "libc",
"memchr", "memchr",
"nix",
"notify", "notify",
"same-file", "same-file",
"uucore", "uucore",

View file

@ -1,7 +1,7 @@
# coreutils (uutils) # coreutils (uutils)
# * see the repository LICENSE, README, and CONTRIBUTING files for more information # * see the repository LICENSE, README, and CONTRIBUTING files for more information
# spell-checker:ignore (libs) libselinux gethostid procfs bigdecimal kqueue # spell-checker:ignore (libs) libselinux gethostid procfs bigdecimal kqueue fundu
[package] [package]
name = "coreutils" name = "coreutils"
@ -282,6 +282,7 @@ filetime = "0.2"
fnv = "1.0.7" fnv = "1.0.7"
fs_extra = "1.1.0" fs_extra = "1.1.0"
fts-sys = "0.2" fts-sys = "0.2"
fundu = "0.3.0"
gcd = "2.2" gcd = "2.2"
glob = "0.3.0" glob = "0.3.0"
half = "2.1" half = "2.1"

View file

@ -43,13 +43,23 @@ pacman -S uutils-coreutils
### Debian ### Debian
[![Debian Unstable package](https://repology.org/badge/version-for-repo/debian_unstable/uutils-coreutils.svg)](https://packages.debian.org/sid/source/rust-coreutils) [![Debian package](https://repology.org/badge/version-for-repo/debian_unstable/uutils-coreutils.svg)](https://packages.debian.org/sid/source/rust-coreutils)
```bash ```bash
apt install rust-coreutils apt install rust-coreutils
# To use it:
export PATH=/usr/lib/cargo/bin/coreutils:$PATH
``` ```
> **Note**: Requires the `unstable` repository. > **Note**: Only available from Bookworm (Debian 12)
### Gentoo
[![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/uutils-coreutils.svg)](https://packages.gentoo.org/packages/sys-apps/uutils)
```bash
emerge -pv sys-apps/uutils
```
### Manjaro ### Manjaro
![Manjaro Stable package](https://repology.org/badge/version-for-repo/manjaro_stable/uutils-coreutils.svg) ![Manjaro Stable package](https://repology.org/badge/version-for-repo/manjaro_stable/uutils-coreutils.svg)
@ -69,6 +79,18 @@ pamac install uutils-coreutils
nix-env -iA nixos.uutils-coreutils nix-env -iA nixos.uutils-coreutils
``` ```
### Ubuntu
[![Ubuntu package](https://repology.org/badge/version-for-repo/ubuntu_23_04/uutils-coreutils.svg)](https://packages.ubuntu.com/source/lunar/rust-coreutils)
```bash
apt install rust-coreutils
# To use it:
export PATH=/usr/lib/cargo/bin/coreutils:$PATH
```
> **Note**: Only available from Kinetic (Ubuntu 22.10)
## MacOS ## MacOS
### Homebrew ### Homebrew

11
src/uu/cat/cat.md Normal file
View file

@ -0,0 +1,11 @@
# cat
## Usage
```
cat [OPTION]... [FILE]...
```
## About
Concatenate FILE(s), or standard input, to standard output
With no FILE, or when FILE is -, read standard input.

View file

@ -33,11 +33,10 @@ use std::net::Shutdown;
use std::os::unix::fs::FileTypeExt; use std::os::unix::fs::FileTypeExt;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use uucore::format_usage; use uucore::{format_usage, help_section, help_usage};
static USAGE: &str = "{} [OPTION]... [FILE]..."; const USAGE: &str = help_usage!("cat.md");
static ABOUT: &str = "Concatenate FILE(s), or standard input, to standard output const ABOUT: &str = help_section!("about", "cat.md");
With no FILE, or when FILE is -, read standard input.";
#[derive(Error, Debug)] #[derive(Error, Debug)]
enum CatError { enum CatError {

View file

@ -12,15 +12,21 @@ use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::Path; use std::path::Path;
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{ExitCode, UResult, USimpleError, UUsageError}; use uucore::error::{set_exit_code, ExitCode, UResult, USimpleError, UUsageError};
use uucore::fs::display_permissions_unix; use uucore::fs::display_permissions_unix;
use uucore::libc::mode_t; use uucore::libc::mode_t;
#[cfg(not(windows))] #[cfg(not(windows))]
use uucore::mode; use uucore::mode;
use uucore::{format_usage, show_error}; use uucore::{format_usage, show, show_error};
static ABOUT: &str = "Change the mode of each FILE to MODE. const ABOUT: &str = "Change the mode of each FILE to MODE.\n\
With --reference, change the mode of each FILE to that of RFILE."; With --reference, change the mode of each FILE to that of RFILE.";
const USAGE: &str = "\
{} [OPTION]... MODE[,MODE]... FILE...
{} [OPTION]... OCTAL-MODE FILE...
{} [OPTION]... --reference=RFILE FILE...";
const LONG_USAGE: &str =
"Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'.";
mod options { mod options {
pub const CHANGES: &str = "changes"; pub const CHANGES: &str = "changes";
@ -34,15 +40,6 @@ mod options {
pub const FILE: &str = "FILE"; pub const FILE: &str = "FILE";
} }
const USAGE: &str = "\
{} [OPTION]... MODE[,MODE]... FILE...
{} [OPTION]... OCTAL-MODE FILE...
{} [OPTION]... --reference=RFILE FILE...";
fn get_long_usage() -> &'static str {
"Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'."
}
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let mut args = args.collect_lossy(); let mut args = args.collect_lossy();
@ -51,9 +48,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
// a possible MODE prefix '-' needs to be removed (e.g. "chmod -x FILE"). // a possible MODE prefix '-' needs to be removed (e.g. "chmod -x FILE").
let mode_had_minus_prefix = mode::strip_minus_from_mode(&mut args); let mode_had_minus_prefix = mode::strip_minus_from_mode(&mut args);
let after_help = get_long_usage(); let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
let matches = uu_app().after_help(after_help).try_get_matches_from(args)?;
let changes = matches.get_flag(options::CHANGES); let changes = matches.get_flag(options::CHANGES);
let quiet = matches.get_flag(options::QUIET); let quiet = matches.get_flag(options::QUIET);
@ -200,21 +195,24 @@ impl Chmoder {
filename.quote() filename.quote()
); );
if !self.quiet { if !self.quiet {
return Err(USimpleError::new( show!(USimpleError::new(
1, 1,
format!("cannot operate on dangling symlink {}", filename.quote()), format!("cannot operate on dangling symlink {}", filename.quote()),
)); ));
} }
} else if !self.quiet { } else if !self.quiet {
return Err(USimpleError::new( show!(USimpleError::new(
1, 1,
format!( format!(
"cannot access {}: No such file or directory", "cannot access {}: No such file or directory",
filename.quote() filename.quote()
), )
)); ));
} }
return Err(ExitCode::new(1)); // GNU exits with exit code 1 even if -q or --quiet are passed
// So we set the exit code, because it hasn't been set yet if `self.quiet` is true.
set_exit_code(1);
continue;
} }
if self.recursive && self.preserve_root && filename == "/" { if self.recursive && self.preserve_root && filename == "/" {
return Err(USimpleError::new( return Err(USimpleError::new(

View file

@ -32,21 +32,12 @@ mod options {
pub const TOTAL: &str = "total"; pub const TOTAL: &str = "total";
} }
fn mkdelim(col: usize, opts: &ArgMatches) -> String { fn column_width(col: &str, opts: &ArgMatches) -> usize {
let mut s = String::new(); if opts.get_flag(col) {
let delim = match opts.get_one::<String>(options::DELIMITER).unwrap().as_str() { 0
"" => "\0", } else {
delim => delim, 1
};
if col > 1 && !opts.get_flag(options::COLUMN_1) {
s.push_str(delim.as_ref());
} }
if col > 2 && !opts.get_flag(options::COLUMN_2) {
s.push_str(delim.as_ref());
}
s
} }
fn ensure_nl(line: &mut String) { fn ensure_nl(line: &mut String) {
@ -70,7 +61,16 @@ impl LineReader {
} }
fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) { fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) {
let delim: Vec<String> = (0..4).map(|col| mkdelim(col, opts)).collect(); let delim = match opts.get_one::<String>(options::DELIMITER).unwrap().as_str() {
"" => "\0",
delim => delim,
};
let width_col_1 = column_width(options::COLUMN_1, opts);
let width_col_2 = column_width(options::COLUMN_2, opts);
let delim_col_2 = delim.repeat(width_col_1);
let delim_col_3 = delim.repeat(width_col_1 + width_col_2);
let ra = &mut String::new(); let ra = &mut String::new();
let mut na = a.read_line(ra); let mut na = a.read_line(ra);
@ -98,7 +98,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) {
Ordering::Less => { Ordering::Less => {
if !opts.get_flag(options::COLUMN_1) { if !opts.get_flag(options::COLUMN_1) {
ensure_nl(ra); ensure_nl(ra);
print!("{}{}", delim[1], ra); print!("{ra}");
} }
ra.clear(); ra.clear();
na = a.read_line(ra); na = a.read_line(ra);
@ -107,7 +107,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) {
Ordering::Greater => { Ordering::Greater => {
if !opts.get_flag(options::COLUMN_2) { if !opts.get_flag(options::COLUMN_2) {
ensure_nl(rb); ensure_nl(rb);
print!("{}{}", delim[2], rb); print!("{delim_col_2}{rb}");
} }
rb.clear(); rb.clear();
nb = b.read_line(rb); nb = b.read_line(rb);
@ -116,7 +116,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) {
Ordering::Equal => { Ordering::Equal => {
if !opts.get_flag(options::COLUMN_3) { if !opts.get_flag(options::COLUMN_3) {
ensure_nl(ra); ensure_nl(ra);
print!("{}{}", delim[3], ra); print!("{delim_col_3}{ra}");
} }
ra.clear(); ra.clear();
rb.clear(); rb.clear();
@ -128,7 +128,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) {
} }
if opts.get_flag(options::TOTAL) { if opts.get_flag(options::TOTAL) {
println!("{total_col_1}\t{total_col_2}\t{total_col_3}\ttotal"); println!("{total_col_1}{delim}{total_col_2}{delim}{total_col_3}{delim}total");
} }
} }

12
src/uu/cp/cp.md Normal file
View file

@ -0,0 +1,12 @@
# cp
## Usage
```
cp [OPTION]... [-T] SOURCE DEST
cp [OPTION]... SOURCE... DIRECTORY
cp [OPTION]... -t DIRECTORY SOURCE...
```
## About
Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.

View file

@ -40,7 +40,7 @@ use uucore::error::{set_exit_code, UClapError, UError, UResult, UUsageError};
use uucore::fs::{ use uucore::fs::{
canonicalize, paths_refer_to_same_file, FileInformation, MissingHandling, ResolveMode, canonicalize, paths_refer_to_same_file, FileInformation, MissingHandling, ResolveMode,
}; };
use uucore::{crash, format_usage, prompt_yes, show_error, show_warning}; use uucore::{crash, format_usage, help_section, help_usage, prompt_yes, show_error, show_warning};
use crate::copydir::copy_directory; use crate::copydir::copy_directory;
@ -228,13 +228,10 @@ pub struct Options {
progress_bar: bool, progress_bar: bool,
} }
static ABOUT: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY."; const ABOUT: &str = help_section!("about", "cp.md");
static EXIT_ERR: i32 = 1; static EXIT_ERR: i32 = 1;
const USAGE: &str = "\ const USAGE: &str = help_usage!("cp.md");
{} [OPTION]... [-T] SOURCE DEST
{} [OPTION]... SOURCE... DIRECTORY
{} [OPTION]... -t DIRECTORY SOURCE...";
// Argument constants // Argument constants
mod options { mod options {

View file

@ -11,26 +11,22 @@ use uucore::display::print_verbatim;
use uucore::error::{UResult, UUsageError}; use uucore::error::{UResult, UUsageError};
use uucore::format_usage; use uucore::format_usage;
static ABOUT: &str = "Strip last component from file name"; const ABOUT: &str = "Strip last component from file name";
const USAGE: &str = "{} [OPTION] NAME..."; const USAGE: &str = "{} [OPTION] NAME...";
const LONG_USAGE: &str = "\
Output each NAME with its last non-slash component and trailing slashes \n\
removed; if NAME contains no /'s, output '.' (meaning the current directory).";
mod options { mod options {
pub const ZERO: &str = "zero"; pub const ZERO: &str = "zero";
pub const DIR: &str = "dir"; pub const DIR: &str = "dir";
} }
fn get_long_usage() -> &'static str {
"Output each NAME with its last non-slash component and trailing slashes \n\
removed; if NAME contains no /'s, output '.' (meaning the current directory)."
}
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args = args.collect_lossy(); let args = args.collect_lossy();
let matches = uu_app() let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let separator = if matches.get_flag(options::ZERO) { let separator = if matches.get_flag(options::ZERO) {
"\0" "\0"

View file

@ -22,8 +22,10 @@ use uucore::{format_usage, show, show_if_err};
static DEFAULT_PERM: u32 = 0o755; static DEFAULT_PERM: u32 = 0o755;
static ABOUT: &str = "Create the given DIRECTORY(ies) if they do not exist"; const ABOUT: &str = "Create the given DIRECTORY(ies) if they do not exist";
const USAGE: &str = "{} [OPTION]... [USER]"; const USAGE: &str = "{} [OPTION]... [USER]";
const LONG_USAGE: &str =
"Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'.";
mod options { mod options {
pub const MODE: &str = "mode"; pub const MODE: &str = "mode";
@ -32,10 +34,6 @@ mod options {
pub const DIRS: &str = "dirs"; pub const DIRS: &str = "dirs";
} }
fn get_long_usage() -> &'static str {
"Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'."
}
#[cfg(windows)] #[cfg(windows)]
fn get_mode(_matches: &ArgMatches, _mode_had_minus_prefix: bool) -> Result<u32, String> { fn get_mode(_matches: &ArgMatches, _mode_had_minus_prefix: bool) -> Result<u32, String> {
Ok(DEFAULT_PERM) Ok(DEFAULT_PERM)
@ -92,9 +90,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
// Linux-specific options, not implemented // Linux-specific options, not implemented
// opts.optflag("Z", "context", "set SELinux security context" + // opts.optflag("Z", "context", "set SELinux security context" +
// " of each created directory to CTX"), // " of each created directory to CTX"),
let matches = uu_app() let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let dirs = matches let dirs = matches
.get_many::<OsString>(options::DIRS) .get_many::<OsString>(options::DIRS)

View file

@ -16,7 +16,6 @@ path = "src/nproc.rs"
[dependencies] [dependencies]
libc = { workspace=true } libc = { workspace=true }
num_cpus = { workspace=true }
clap = { workspace=true } clap = { workspace=true }
uucore = { workspace=true, features=["fs"] } uucore = { workspace=true, features=["fs"] }

View file

@ -8,7 +8,7 @@
// spell-checker:ignore (ToDO) NPROCESSORS nprocs numstr threadstr sysconf // spell-checker:ignore (ToDO) NPROCESSORS nprocs numstr threadstr sysconf
use clap::{crate_version, Arg, ArgAction, Command}; use clap::{crate_version, Arg, ArgAction, Command};
use std::env; use std::{env, thread};
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{UResult, USimpleError}; use uucore::error::{UResult, USimpleError};
use uucore::format_usage; use uucore::format_usage;
@ -73,16 +73,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
// If OMP_NUM_THREADS=0, rejects the value // If OMP_NUM_THREADS=0, rejects the value
let thread: Vec<&str> = threadstr.split_terminator(',').collect(); let thread: Vec<&str> = threadstr.split_terminator(',').collect();
match &thread[..] { match &thread[..] {
[] => num_cpus::get(), [] => available_parallelism(),
[s, ..] => match s.parse() { [s, ..] => match s.parse() {
Ok(0) | Err(_) => num_cpus::get(), Ok(0) | Err(_) => available_parallelism(),
Ok(n) => n, Ok(n) => n,
}, },
} }
} }
// the variable 'OMP_NUM_THREADS' doesn't exist // the variable 'OMP_NUM_THREADS' doesn't exist
// fallback to the regular CPU detection // fallback to the regular CPU detection
Err(_) => num_cpus::get(), Err(_) => available_parallelism(),
} }
}; };
@ -127,7 +127,7 @@ fn num_cpus_all() -> usize {
if nprocs == 1 { if nprocs == 1 {
// In some situation, /proc and /sys are not mounted, and sysconf returns 1. // In some situation, /proc and /sys are not mounted, and sysconf returns 1.
// However, we want to guarantee that `nproc --all` >= `nproc`. // However, we want to guarantee that `nproc --all` >= `nproc`.
num_cpus::get() available_parallelism()
} else if nprocs > 0 { } else if nprocs > 0 {
nprocs as usize nprocs as usize
} else { } else {
@ -135,7 +135,7 @@ fn num_cpus_all() -> usize {
} }
} }
// Other platforms (e.g., windows), num_cpus::get() directly. // Other platforms (e.g., windows), available_parallelism() directly.
#[cfg(not(any( #[cfg(not(any(
target_os = "linux", target_os = "linux",
target_vendor = "apple", target_vendor = "apple",
@ -143,5 +143,14 @@ fn num_cpus_all() -> usize {
target_os = "netbsd" target_os = "netbsd"
)))] )))]
fn num_cpus_all() -> usize { fn num_cpus_all() -> usize {
num_cpus::get() available_parallelism()
}
// In some cases, thread::available_parallelism() may return an Err
// In this case, we will return 1 (like GNU)
fn available_parallelism() -> usize {
match thread::available_parallelism() {
Ok(n) => n.get(),
Err(_) => 1,
}
} }

View file

@ -37,8 +37,22 @@ struct Options {
verbose: bool, verbose: bool,
} }
static ABOUT: &str = "Remove (unlink) the FILE(s)"; const ABOUT: &str = "Remove (unlink) the FILE(s)";
const USAGE: &str = "{} [OPTION]... FILE..."; const USAGE: &str = "{} [OPTION]... FILE...";
const LONG_USAGE: &str = "\
By default, rm does not remove directories. Use the --recursive (-r or -R)
option to remove each listed directory, too, along with all of its contents
To remove a file whose name starts with a '-', for example '-foo',
use one of these commands:
rm -- -foo
rm ./-foo
Note that if you use rm to remove a file, it might be possible to recover
some of its contents, given sufficient expertise and/or time. For greater
assurance that the contents are truly unrecoverable, consider using shred.";
static OPT_DIR: &str = "dir"; static OPT_DIR: &str = "dir";
static OPT_INTERACTIVE: &str = "interactive"; static OPT_INTERACTIVE: &str = "interactive";
static OPT_FORCE: &str = "force"; static OPT_FORCE: &str = "force";
@ -53,28 +67,9 @@ static PRESUME_INPUT_TTY: &str = "-presume-input-tty";
static ARG_FILES: &str = "files"; static ARG_FILES: &str = "files";
fn get_long_usage() -> String {
String::from(
"By default, rm does not remove directories. Use the --recursive (-r or -R)
option to remove each listed directory, too, along with all of its contents
To remove a file whose name starts with a '-', for example '-foo',
use one of these commands:
rm -- -foo
rm ./-foo
Note that if you use rm to remove a file, it might be possible to recover
some of its contents, given sufficient expertise and/or time. For greater
assurance that the contents are truly unrecoverable, consider using shred.",
)
}
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app() let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let files: Vec<String> = matches let files: Vec<String> = matches
.get_many::<String>(ARG_FILES) .get_many::<String>(ARG_FILES)

View file

@ -13,7 +13,7 @@ use uucore::fsext::{
pretty_filetype, pretty_fstype, pretty_time, read_fs_list, statfs, BirthTime, FsMeta, pretty_filetype, pretty_fstype, pretty_time, read_fs_list, statfs, BirthTime, FsMeta,
}; };
use uucore::libc::mode_t; use uucore::libc::mode_t;
use uucore::{entries, format_usage, show_error, show_warning}; use uucore::{entries, format_usage, help_section, help_usage, show_error, show_warning};
use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
use std::borrow::Cow; use std::borrow::Cow;
@ -24,8 +24,9 @@ use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::os::unix::prelude::OsStrExt; use std::os::unix::prelude::OsStrExt;
use std::path::Path; use std::path::Path;
const ABOUT: &str = "Display file or file system status."; const ABOUT: &str = help_section!("about", "stat.md");
const USAGE: &str = "{} [OPTION]... FILE..."; const USAGE: &str = help_usage!("stat.md");
const LONG_USAGE: &str = help_section!("long usage", "stat.md");
mod options { mod options {
pub const DEREFERENCE: &str = "dereference"; pub const DEREFERENCE: &str = "dereference";
@ -751,67 +752,9 @@ impl Stater {
} }
} }
fn get_long_usage() -> &'static str {
"
The valid format sequences for files (without --file-system):
%a access rights in octal (note '#' and '0' printf flags)
%A access rights in human readable form
%b number of blocks allocated (see %B)
%B the size in bytes of each block reported by %b
%C SELinux security context string
%d device number in decimal
%D device number in hex
%f raw mode in hex
%F file type
%g group ID of owner
%G group name of owner
%h number of hard links
%i inode number
%m mount point
%n file name
%N quoted file name with dereference if symbolic link
%o optimal I/O transfer size hint
%s total size, in bytes
%t major device type in hex, for character/block device special files
%T minor device type in hex, for character/block device special files
%u user ID of owner
%U user name of owner
%w time of file birth, human-readable; - if unknown
%W time of file birth, seconds since Epoch; 0 if unknown
%x time of last access, human-readable
%X time of last access, seconds since Epoch
%y time of last data modification, human-readable
%Y time of last data modification, seconds since Epoch
%z time of last status change, human-readable
%Z time of last status change, seconds since Epoch
Valid format sequences for file systems:
%a free blocks available to non-superuser
%b total data blocks in file system
%c total file nodes in file system
%d free file nodes in file system
%f free blocks in file system
%i file system ID in hex
%l maximum length of filenames
%n file name
%s block size (for faster transfers)
%S fundamental block size (for block counts)
%t file system type in hex
%T file system type in human readable form
NOTE: your shell may have its own version of stat, which usually supersedes
the version described here. Please refer to your shell's documentation
for details about the options it supports.
"
}
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app() let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let stater = Stater::new(&matches)?; let stater = Stater::new(&matches)?;
let exit_status = stater.exec(); let exit_status = stater.exec();

64
src/uu/stat/stat.md Normal file
View file

@ -0,0 +1,64 @@
# stat
## About
Display file or file system status.
## Usage
```
stat [OPTION]... FILE...
```
## Long Usage
The valid format sequences for files (without `--file-system`):
%a access rights in octal (note '#' and '0' printf flags)
%A access rights in human readable form
%b number of blocks allocated (see %B)
%B the size in bytes of each block reported by %b
%C SELinux security context string
%d device number in decimal
%D device number in hex
%f raw mode in hex
%F file type
%g group ID of owner
%G group name of owner
%h number of hard links
%i inode number
%m mount point
%n file name
%N quoted file name with dereference if symbolic link
%o optimal I/O transfer size hint
%s total size, in bytes
%t major device type in hex, for character/block device special files
%T minor device type in hex, for character/block device special files
%u user ID of owner
%U user name of owner
%w time of file birth, human-readable; - if unknown
%W time of file birth, seconds since Epoch; 0 if unknown
%x time of last access, human-readable
%X time of last access, seconds since Epoch
%y time of last data modification, human-readable
%Y time of last data modification, seconds since Epoch
%z time of last status change, human-readable
%Z time of last status change, seconds since Epoch
Valid format sequences for file systems:
%a free blocks available to non-superuser
%b total data blocks in file system
%c total file nodes in file system
%d free file nodes in file system
%f free blocks in file system
%i file system ID in hex
%l maximum length of filenames
%n file name
%s block size (for faster transfers)
%S fundamental block size (for block counts)
%t file system type in hex
%T file system type in human readable form
NOTE: your shell may have its own version of stat, which usually supersedes
the version described here. Please refer to your shell's documentation
for details about the options it supports.

View file

@ -1,3 +1,4 @@
# spell-checker:ignore (libs) kqueue fundu
[package] [package]
name = "uu_tail" name = "uu_tail"
version = "0.0.17" version = "0.0.17"
@ -19,17 +20,15 @@ clap = { workspace=true }
libc = { workspace=true } libc = { workspace=true }
memchr = { workspace=true } memchr = { workspace=true }
notify = { workspace=true } notify = { workspace=true }
uucore = { workspace=true, features=["ringbuffer", "lines"] } uucore = { workspace=true }
same-file = { workspace=true } same-file = { workspace=true }
atty = { workspace=true } atty = { workspace=true }
fundu = { workspace=true }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows-sys = { workspace=true, features = ["Win32_System_Threading", "Win32_Foundation"] } windows-sys = { workspace=true, features = ["Win32_System_Threading", "Win32_Foundation"] }
winapi-util = { workspace=true } winapi-util = { workspace=true }
[target.'cfg(unix)'.dependencies]
nix = { workspace=true, features = ["fs"] }
[[bin]] [[bin]]
name = "tail" name = "tail"
path = "src/main.rs" path = "src/main.rs"

View file

@ -3,13 +3,14 @@
// * For the full copyright and license information, please view the LICENSE // * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code. // * file that was distributed with this source code.
// spell-checker:ignore (ToDO) kqueue Signum // spell-checker:ignore (ToDO) kqueue Signum fundu
use crate::paths::Input; use crate::paths::Input;
use crate::{parse, platform, Quotable}; use crate::{parse, platform, Quotable};
use atty::Stream; use atty::Stream;
use clap::crate_version; use clap::crate_version;
use clap::{parser::ValueSource, Arg, ArgAction, ArgMatches, Command}; use clap::{parser::ValueSource, Arg, ArgAction, ArgMatches, Command};
use fundu::DurationParser;
use same_file::Handle; use same_file::Handle;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::ffi::OsString; use std::ffi::OsString;
@ -148,16 +149,20 @@ impl Settings {
settings.retry = settings.retry =
matches.get_flag(options::RETRY) || matches.get_flag(options::FOLLOW_RETRY); matches.get_flag(options::RETRY) || matches.get_flag(options::FOLLOW_RETRY);
if let Some(s) = matches.get_one::<String>(options::SLEEP_INT) { if let Some(source) = matches.get_one::<String>(options::SLEEP_INT) {
settings.sleep_sec = match s.parse::<f32>() { // Advantage of `fundu` over `Duration::(try_)from_secs_f64(source.parse().unwrap())`:
Ok(s) => Duration::from_secs_f32(s), // * doesn't panic on errors like `Duration::from_secs_f64` would.
Err(_) => { // * no precision loss, rounding errors or other floating point problems.
return Err(UUsageError::new( // * evaluates to `Duration::MAX` if the parsed number would have exceeded
1, // `DURATION::MAX` or `infinity` was given
format!("invalid number of seconds: {}", s.quote()), // * not applied here but it supports customizable time units and provides better error
)) // messages
} settings.sleep_sec =
} DurationParser::without_time_units()
.parse(source)
.map_err(|_| {
UUsageError::new(1, format!("invalid number of seconds: '{source}'"))
})?;
} }
settings.use_polling = matches.get_flag(options::USE_POLLING); settings.use_polling = matches.get_flag(options::USE_POLLING);

View file

@ -37,21 +37,21 @@ pub enum BadSequence {
impl Display for BadSequence { impl Display for BadSequence {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::MissingCharClassName => writeln!(f, "missing character class name '[::]'"), Self::MissingCharClassName => write!(f, "missing character class name '[::]'"),
Self::MissingEquivalentClassChar => { Self::MissingEquivalentClassChar => {
writeln!(f, "missing equivalence class character '[==]'") write!(f, "missing equivalence class character '[==]'")
} }
Self::MultipleCharRepeatInSet2 => { Self::MultipleCharRepeatInSet2 => {
writeln!(f, "only one [c*] repeat construct may appear in string2") write!(f, "only one [c*] repeat construct may appear in string2")
} }
Self::CharRepeatInSet1 => { Self::CharRepeatInSet1 => {
writeln!(f, "the [c*] repeat construct may not appear in string1") write!(f, "the [c*] repeat construct may not appear in string1")
} }
Self::InvalidRepeatCount(count) => { Self::InvalidRepeatCount(count) => {
writeln!(f, "invalid repeat count '{count}' in [c*n] construct") write!(f, "invalid repeat count '{count}' in [c*n] construct")
} }
Self::EmptySet2WhenNotTruncatingSet1 => { Self::EmptySet2WhenNotTruncatingSet1 => {
writeln!(f, "when not truncating set1, string2 must be non-empty") write!(f, "when not truncating set1, string2 must be non-empty")
} }
} }
} }

View file

@ -19,8 +19,11 @@ use crate::operation::DeleteOperation;
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{UResult, USimpleError, UUsageError}; use uucore::error::{UResult, USimpleError, UUsageError};
static ABOUT: &str = "Translate or delete characters"; const ABOUT: &str = "Translate or delete characters";
const USAGE: &str = "{} [OPTION]... SET1 [SET2]"; const USAGE: &str = "{} [OPTION]... SET1 [SET2]";
const LONG_USAGE: &str = "\
Translate, squeeze, and/or delete characters from standard input, \
writing to standard output.";
mod options { mod options {
pub const COMPLEMENT: &str = "complement"; pub const COMPLEMENT: &str = "complement";
@ -30,19 +33,11 @@ mod options {
pub const SETS: &str = "sets"; pub const SETS: &str = "sets";
} }
fn get_long_usage() -> String {
"Translate, squeeze, and/or delete characters from standard input, \
writing to standard output."
.to_string()
}
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args = args.collect_lossy(); let args = args.collect_lossy();
let matches = uu_app() let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let delete_flag = matches.get_flag(options::DELETE); let delete_flag = matches.get_flag(options::DELETE);
let complement_flag = matches.get_flag(options::COMPLEMENT); let complement_flag = matches.get_flag(options::COMPLEMENT);

View file

@ -73,8 +73,25 @@ impl TruncateMode {
} }
} }
static ABOUT: &str = "Shrink or extend the size of each file to the specified size."; const ABOUT: &str = "Shrink or extend the size of each file to the specified size.";
const USAGE: &str = "{} [OPTION]... [FILE]..."; const USAGE: &str = "{} [OPTION]... [FILE]...";
const LONG_USAGE: &str = "\
SIZE is an integer with an optional prefix and optional unit.
The available units (K, M, G, T, P, E, Z, and Y) use the following format:
'KB' => 1000 (kilobytes)
'K' => 1024 (kibibytes)
'MB' => 1000*1000 (megabytes)
'M' => 1024*1024 (mebibytes)
'GB' => 1000*1000*1000 (gigabytes)
'G' => 1024*1024*1024 (gibibytes)
SIZE may also be prefixed by one of the following to adjust the size of each
file based on its current size:
'+' => extend by
'-' => reduce by
'<' => at most
'>' => at least
'/' => round down to multiple of
'%' => round up to multiple of";
pub mod options { pub mod options {
pub static IO_BLOCKS: &str = "io-blocks"; pub static IO_BLOCKS: &str = "io-blocks";
@ -84,32 +101,10 @@ pub mod options {
pub static ARG_FILES: &str = "files"; pub static ARG_FILES: &str = "files";
} }
fn get_long_usage() -> String {
String::from(
"
SIZE is an integer with an optional prefix and optional unit.
The available units (K, M, G, T, P, E, Z, and Y) use the following format:
'KB' => 1000 (kilobytes)
'K' => 1024 (kibibytes)
'MB' => 1000*1000 (megabytes)
'M' => 1024*1024 (mebibytes)
'GB' => 1000*1000*1000 (gigabytes)
'G' => 1024*1024*1024 (gibibytes)
SIZE may also be prefixed by one of the following to adjust the size of each
file based on its current size:
'+' => extend by
'-' => reduce by
'<' => at most
'>' => at least
'/' => round down to multiple of
'%' => round up to multiple of",
)
}
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app() let matches = uu_app()
.after_help(get_long_usage()) .after_help(LONG_USAGE)
.try_get_matches_from(args) .try_get_matches_from(args)
.map_err(|e| { .map_err(|e| {
e.print().expect("Error writing clap::Error"); e.print().expect("Error writing clap::Error");

View file

@ -15,8 +15,14 @@ use uucore::display::Quotable;
use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::error::{FromIo, UResult, USimpleError, UUsageError};
use uucore::format_usage; use uucore::format_usage;
static ABOUT: &str = "Report or omit repeated lines."; const ABOUT: &str = "Report or omit repeated lines.";
const USAGE: &str = "{} [OPTION]... [INPUT [OUTPUT]]..."; const USAGE: &str = "{} [OPTION]... [INPUT [OUTPUT]]...";
const LONG_USAGE: &str = "\
Filter adjacent matching lines from INPUT (or standard input),\n\
writing to OUTPUT (or standard output).\n\n\
Note: 'uniq' does not detect repeated lines unless they are adjacent.\n\
You may want to sort the input first, or use 'sort -u' without 'uniq'.";
pub mod options { pub mod options {
pub static ALL_REPEATED: &str = "all-repeated"; pub static ALL_REPEATED: &str = "all-repeated";
pub static CHECK_CHARS: &str = "check-chars"; pub static CHECK_CHARS: &str = "check-chars";
@ -241,20 +247,9 @@ fn opt_parsed<T: FromStr>(opt_name: &str, matches: &ArgMatches) -> UResult<Optio
}) })
} }
fn get_long_usage() -> String {
String::from(
"Filter adjacent matching lines from INPUT (or standard input),\n\
writing to OUTPUT (or standard output).
Note: 'uniq' does not detect repeated lines unless they are adjacent.\n\
You may want to sort the input first, or use 'sort -u' without 'uniq'.\n",
)
}
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app() let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let files: Vec<String> = matches let files: Vec<String> = matches
.get_many::<String>(ARG_FILES) .get_many::<String>(ARG_FILES)

View file

@ -574,3 +574,71 @@ fn test_mode_after_dash_dash() {
ucmd, ucmd,
); );
} }
#[test]
fn test_chmod_file_after_non_existing_file() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch(TEST_FILE);
at.touch("file2");
set_permissions(at.plus(TEST_FILE), Permissions::from_mode(0o664)).unwrap();
set_permissions(at.plus("file2"), Permissions::from_mode(0o664)).unwrap();
scene
.ucmd()
.arg("u+x")
.arg("does-not-exist")
.arg(TEST_FILE)
.fails()
.stderr_contains("chmod: cannot access 'does-not-exist': No such file or directory")
.code_is(1);
assert_eq!(at.metadata(TEST_FILE).permissions().mode(), 0o100764);
scene
.ucmd()
.arg("u+x")
.arg("--q")
.arg("does-not-exist")
.arg("file2")
.fails()
.no_stderr()
.code_is(1);
assert_eq!(at.metadata("file2").permissions().mode(), 0o100764);
}
#[test]
fn test_chmod_file_symlink_after_non_existing_file() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let existing = "file";
let test_existing_symlink = "file_symlink";
let non_existing = "test_chmod_symlink_non_existing_file";
let test_dangling_symlink = "test_chmod_symlink_non_existing_file_symlink";
let expected_stdout = &format!(
"failed to change mode of '{test_dangling_symlink}' from 0000 (---------) to 0000 (---------)"
);
let expected_stderr = &format!("cannot operate on dangling symlink '{test_dangling_symlink}'");
at.touch(existing);
set_permissions(at.plus(existing), Permissions::from_mode(0o664)).unwrap();
at.symlink_file(non_existing, test_dangling_symlink);
at.symlink_file(existing, test_existing_symlink);
// this cannot succeed since the symbolic link dangles
// but the metadata for the existing target should change
scene
.ucmd()
.arg("u+x")
.arg("-v")
.arg(test_dangling_symlink)
.arg(test_existing_symlink)
.fails()
.code_is(1)
.stdout_contains(expected_stdout)
.stderr_contains(expected_stderr);
assert_eq!(
at.metadata(test_existing_symlink).permissions().mode(),
0o100764
);
}

View file

@ -71,6 +71,14 @@ fn total_with_suppressed_regular_output() {
.stdout_is_fixture("ab_total_suppressed_regular_output.expected"); .stdout_is_fixture("ab_total_suppressed_regular_output.expected");
} }
#[test]
fn total_with_output_delimiter() {
new_ucmd!()
.args(&["--total", "--output-delimiter=word", "a", "b"])
.succeeds()
.stdout_is_fixture("ab_total_delimiter_word.expected");
}
#[test] #[test]
fn output_delimiter() { fn output_delimiter() {
new_ucmd!() new_ucmd!()

View file

@ -68,10 +68,10 @@ fn test_date_utc() {
fn test_date_format_y() { fn test_date_format_y() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let mut re = Regex::new(r"^\d{4}$").unwrap(); let mut re = Regex::new(r"^\d{4}\n$").unwrap();
scene.ucmd().arg("+%Y").succeeds().stdout_matches(&re); scene.ucmd().arg("+%Y").succeeds().stdout_matches(&re);
re = Regex::new(r"^\d{2}$").unwrap(); re = Regex::new(r"^\d{2}\n$").unwrap();
scene.ucmd().arg("+%y").succeeds().stdout_matches(&re); scene.ucmd().arg("+%y").succeeds().stdout_matches(&re);
} }
@ -82,7 +82,7 @@ fn test_date_format_m() {
let mut re = Regex::new(r"\S+").unwrap(); let mut re = Regex::new(r"\S+").unwrap();
scene.ucmd().arg("+%b").succeeds().stdout_matches(&re); scene.ucmd().arg("+%b").succeeds().stdout_matches(&re);
re = Regex::new(r"^\d{2}$").unwrap(); re = Regex::new(r"^\d{2}\n$").unwrap();
scene.ucmd().arg("+%m").succeeds().stdout_matches(&re); scene.ucmd().arg("+%m").succeeds().stdout_matches(&re);
} }
@ -96,7 +96,7 @@ fn test_date_format_day() {
re = Regex::new(r"\S+").unwrap(); re = Regex::new(r"\S+").unwrap();
scene.ucmd().arg("+%A").succeeds().stdout_matches(&re); scene.ucmd().arg("+%A").succeeds().stdout_matches(&re);
re = Regex::new(r"^\d{1}$").unwrap(); re = Regex::new(r"^\d{1}\n$").unwrap();
scene.ucmd().arg("+%u").succeeds().stdout_matches(&re); scene.ucmd().arg("+%u").succeeds().stdout_matches(&re);
} }
@ -117,7 +117,7 @@ fn test_date_issue_3780() {
#[test] #[test]
fn test_date_nano_seconds() { fn test_date_nano_seconds() {
// %N nanoseconds (000000000..999999999) // %N nanoseconds (000000000..999999999)
let re = Regex::new(r"^\d{1,9}$").unwrap(); let re = Regex::new(r"^\d{1,9}\n$").unwrap();
new_ucmd!().arg("+%N").succeeds().stdout_matches(&re); new_ucmd!().arg("+%N").succeeds().stdout_matches(&re);
} }

View file

@ -49,5 +49,5 @@ fn test_long_output() {
.ucmd() .ucmd()
.arg("-l") .arg("-l")
.succeeds() .succeeds()
.stdout_matches(&Regex::new("[rwx-]{10}.*some-file1$").unwrap()); .stdout_matches(&Regex::new("[rwx-]{10}.*some-file1\n$").unwrap());
} }

View file

@ -950,7 +950,7 @@ fn test_ls_commas_trailing() {
.arg("./test-commas-trailing-1") .arg("./test-commas-trailing-1")
.arg("./test-commas-trailing-2") .arg("./test-commas-trailing-2")
.succeeds() .succeeds()
.stdout_matches(&Regex::new(r"\S$").unwrap()); // matches if there is no whitespace at the end of stdout. .stdout_matches(&Regex::new(r"\S\n$").unwrap());
} }
#[test] #[test]

View file

@ -300,7 +300,7 @@ fn test_relative_base_not_prefix_of_relative_to() {
.succeeds(); .succeeds();
#[cfg(windows)] #[cfg(windows)]
result.stdout_matches(&Regex::new(r"^.*:\\usr\n.*:\\usr\\local$").unwrap()); result.stdout_matches(&Regex::new(r"^.*:\\usr\n.*:\\usr\\local\n$").unwrap());
#[cfg(not(windows))] #[cfg(not(windows))]
result.stdout_is("/usr\n/usr/local\n"); result.stdout_is("/usr\n/usr/local\n");
@ -344,7 +344,7 @@ fn test_relative() {
#[cfg(not(windows))] #[cfg(not(windows))]
result.stdout_is("/tmp\n.\n"); result.stdout_is("/tmp\n.\n");
#[cfg(windows)] #[cfg(windows)]
result.stdout_matches(&Regex::new(r"^.*:\\tmp\n\.$").unwrap()); result.stdout_matches(&Regex::new(r"^.*:\\tmp\n\.\n$").unwrap());
new_ucmd!() new_ucmd!()
.args(&["-sm", "--relative-base=/", "--relative-to=/", "/", "/usr"]) .args(&["-sm", "--relative-base=/", "--relative-to=/", "/", "/usr"])
@ -357,7 +357,7 @@ fn test_relative() {
#[cfg(not(windows))] #[cfg(not(windows))]
result.stdout_is("/tmp\n.\n"); result.stdout_is("/tmp\n.\n");
#[cfg(windows)] #[cfg(windows)]
result.stdout_matches(&Regex::new(r"^.*:\\tmp\n\.$").unwrap()); result.stdout_matches(&Regex::new(r"^.*:\\tmp\n\.\n$").unwrap());
new_ucmd!() new_ucmd!()
.args(&["-sm", "--relative-base=/", "/", "/usr"]) .args(&["-sm", "--relative-base=/", "/", "/usr"])

View file

@ -13,6 +13,7 @@ use crate::common::random::*;
use crate::common::util::*; use crate::common::util::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use rand::distributions::Alphanumeric; use rand::distributions::Alphanumeric;
use rstest::rstest;
use std::char::from_digit; use std::char::from_digit;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
@ -4453,29 +4454,24 @@ fn test_follow_when_files_are_pointing_to_same_relative_file_and_file_stays_same
.stdout_only(expected_stdout); .stdout_only(expected_stdout);
} }
#[test] #[rstest]
#[cfg(disable_until_fixed)] #[case::exponent_exceed_float_max("1.0e2048")]
fn test_args_sleep_interval_when_illegal_argument_then_usage_error() { #[case::underscore_delimiter("1_000")]
let scene = TestScenario::new(util_name!()); #[case::only_point(".")]
for interval in [ #[case::space_in_primes("' '")]
&format!("{}0", f64::MAX), #[case::space(" ")]
&format!("{}0.0", f64::MAX), #[case::empty("")]
"1_000", #[case::comma_separator("0,0")]
".", #[case::words_nominator_fract("one.zero")]
"' '", #[case::words_fract(".zero")]
"", #[case::words_nominator("one.")]
" ", #[case::two_points("0..0")]
"0,0", #[case::seconds_unit("1.0s")]
"one.zero", #[case::circumflex_exponent("1.0e^1000")]
".zero", fn test_args_sleep_interval_when_illegal_argument_then_usage_error(#[case] sleep_interval: &str) {
"one.", new_ucmd!()
"0..0", .args(&["--sleep-interval", sleep_interval])
] { .run()
scene .usage_error(format!("invalid number of seconds: '{sleep_interval}'"))
.ucmd() .code_is(1);
.args(&["--sleep-interval", interval])
.run()
.usage_error(format!("invalid number of seconds: '{}'", interval))
.code_is(1);
}
} }

View file

@ -777,10 +777,7 @@ fn check_against_gnu_tr_tests_range_a_a() {
.stdout_is("zbc"); .stdout_is("zbc");
} }
// FIXME: Since pr https://github.com/uutils/coreutils/pull/4261:
// stderr ends with 2 newlines but expected is only 1.
#[test] #[test]
#[cfg(disabled_until_fixed)]
fn check_against_gnu_tr_tests_null() { fn check_against_gnu_tr_tests_null() {
// ['null', qw(a ''), {IN=>''}, {OUT=>''}, {EXIT=>1}, // ['null', qw(a ''), {IN=>''}, {OUT=>''}, {EXIT=>1},
// {ERR=>"$prog: when not truncating set1, string2 must be non-empty\n"}], // {ERR=>"$prog: when not truncating set1, string2 must be non-empty\n"}],
@ -855,10 +852,7 @@ fn check_against_gnu_tr_tests_rep_3() {
.stdout_is("1x2"); .stdout_is("1x2");
} }
// FIXME: Since pr https://github.com/uutils/coreutils/pull/4261:
// stderr ends with 2 newlines but expected is only 1.
#[test] #[test]
#[cfg(disabled_until_fixed)]
fn check_against_gnu_tr_tests_o_rep_1() { fn check_against_gnu_tr_tests_o_rep_1() {
// # Another couple octal repeat count tests. // # Another couple octal repeat count tests.
// ['o-rep-1', qw('[b*08]' '[x*]'), {IN=>''}, {OUT=>''}, {EXIT=>1}, // ['o-rep-1', qw('[b*08]' '[x*]'), {IN=>''}, {OUT=>''}, {EXIT=>1},
@ -1032,10 +1026,6 @@ fn check_against_gnu_tr_tests_ross_6() {
.stdout_is(""); .stdout_is("");
} }
// FIXME: Since pr https://github.com/uutils/coreutils/pull/4261:
// stderr ends with 2 newlines but expected is only 1.
#[test]
#[cfg(disabled_until_fixed)]
#[test] #[test]
fn check_against_gnu_tr_tests_empty_eq() { fn check_against_gnu_tr_tests_empty_eq() {
// # Ensure that these fail. // # Ensure that these fail.
@ -1049,10 +1039,6 @@ fn check_against_gnu_tr_tests_empty_eq() {
.stderr_is("tr: missing equivalence class character '[==]'\n"); .stderr_is("tr: missing equivalence class character '[==]'\n");
} }
// FIXME: Since pr https://github.com/uutils/coreutils/pull/4261:
// stderr ends with 2 newlines but expected is only 1.
#[test]
#[cfg(disabled_until_fixed)]
#[test] #[test]
fn check_against_gnu_tr_tests_empty_cc() { fn check_against_gnu_tr_tests_empty_cc() {
// ['empty-cc', qw('[::]' x), {IN=>''}, {OUT=>''}, {EXIT=>1}, // ['empty-cc', qw('[::]' x), {IN=>''}, {OUT=>''}, {EXIT=>1},
@ -1064,6 +1050,24 @@ fn check_against_gnu_tr_tests_empty_cc() {
.stderr_is("tr: missing character class name '[::]'\n"); .stderr_is("tr: missing character class name '[::]'\n");
} }
#[test]
fn check_against_gnu_tr_tests_repeat_set1() {
new_ucmd!()
.args(&["[a*]", "a"])
.pipe_in("")
.fails()
.stderr_is("tr: the [c*] repeat construct may not appear in string1\n");
}
#[test]
fn check_against_gnu_tr_tests_repeat_set2() {
new_ucmd!()
.args(&["a", "[a*][a*]"])
.pipe_in("")
.fails()
.stderr_is("tr: only one [c*] repeat construct may appear in string2\n");
}
#[test] #[test]
fn check_against_gnu_tr_tests_repeat_bs_9() { fn check_against_gnu_tr_tests_repeat_bs_9() {
// # Weird repeat counts. // # Weird repeat counts.

View file

@ -29,7 +29,7 @@ fn test_default_output() {
scene scene
.ucmd() .ucmd()
.succeeds() .succeeds()
.stdout_matches(&Regex::new("[rwx-]{10}.*some-file1$").unwrap()); .stdout_matches(&Regex::new("[rwx-]{10}.*some-file1\n$").unwrap());
} }
#[test] #[test]

View file

@ -36,11 +36,6 @@ use std::{env, hint, thread};
use tempfile::{Builder, TempDir}; use tempfile::{Builder, TempDir};
use uucore::Args; use uucore::Args;
#[cfg(windows)]
static PROGNAME: &str = concat!(env!("CARGO_PKG_NAME"), ".exe");
#[cfg(not(windows))]
static PROGNAME: &str = env!("CARGO_PKG_NAME");
static TESTS_DIR: &str = "tests"; static TESTS_DIR: &str = "tests";
static FIXTURES_DIR: &str = "fixtures"; static FIXTURES_DIR: &str = "fixtures";
@ -603,7 +598,7 @@ impl CmdResult {
/// asserts that /// asserts that
/// 1. the command resulted in stderr stream output that equals the /// 1. the command resulted in stderr stream output that equals the
/// passed in value, when both are trimmed of trailing whitespace /// passed in value
/// 2. the command resulted in empty (zero-length) stdout stream output /// 2. the command resulted in empty (zero-length) stdout stream output
#[track_caller] #[track_caller]
pub fn stderr_only<T: AsRef<str>>(&self, msg: T) -> &Self { pub fn stderr_only<T: AsRef<str>>(&self, msg: T) -> &Self {
@ -628,7 +623,7 @@ impl CmdResult {
/// asserts that /// asserts that
/// 1. the command resulted in stderr stream output that equals the /// 1. the command resulted in stderr stream output that equals the
/// the following format when both are trimmed of trailing whitespace /// the following format
/// `"{util_name}: {msg}\nTry '{bin_path} {util_name} --help' for more information."` /// `"{util_name}: {msg}\nTry '{bin_path} {util_name} --help' for more information."`
/// This the expected format when a `UUsageError` is returned or when `show_error!` is called /// This the expected format when a `UUsageError` is returned or when `show_error!` is called
/// `msg` should be the same as the one provided to `UUsageError::new` or `show_error!` /// `msg` should be the same as the one provided to `UUsageError::new` or `show_error!`
@ -686,7 +681,7 @@ impl CmdResult {
#[track_caller] #[track_caller]
pub fn stdout_matches(&self, regex: &regex::Regex) -> &Self { pub fn stdout_matches(&self, regex: &regex::Regex) -> &Self {
assert!( assert!(
regex.is_match(self.stdout_str().trim()), regex.is_match(self.stdout_str()),
"Stdout does not match regex:\n{}", "Stdout does not match regex:\n{}",
self.stdout_str() self.stdout_str()
); );
@ -696,7 +691,7 @@ impl CmdResult {
#[track_caller] #[track_caller]
pub fn stdout_does_not_match(&self, regex: &regex::Regex) -> &Self { pub fn stdout_does_not_match(&self, regex: &regex::Regex) -> &Self {
assert!( assert!(
!regex.is_match(self.stdout_str().trim()), !regex.is_match(self.stdout_str()),
"Stdout matches regex:\n{}", "Stdout matches regex:\n{}",
self.stdout_str() self.stdout_str()
); );
@ -1101,13 +1096,7 @@ impl TestScenario {
pub fn new(util_name: &str) -> Self { pub fn new(util_name: &str) -> Self {
let tmpd = Rc::new(TempDir::new().unwrap()); let tmpd = Rc::new(TempDir::new().unwrap());
let ts = Self { let ts = Self {
bin_path: { bin_path: PathBuf::from(env!("CARGO_BIN_EXE_coreutils")),
// Instead of hard coding the path relative to the current
// directory, use Cargo's OUT_DIR to find path to executable.
// This allows tests to be run using profiles other than debug.
let target_dir = path_concat!(env!("OUT_DIR"), "..", "..", "..", PROGNAME);
PathBuf::from(AtPath::new(Path::new(&target_dir)).root_dir_resolved())
},
util_name: String::from(util_name), util_name: String::from(util_name),
fixtures: AtPath::new(tmpd.as_ref().path()), fixtures: AtPath::new(tmpd.as_ref().path()),
tmpd, tmpd,

View file

@ -0,0 +1,4 @@
a
wordb
wordwordz
1word1word1wordtotal