1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +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:
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:
cargo-deny:
name: Style/cargo-deny

View file

@ -14,6 +14,11 @@ on: [push, pull_request]
permissions:
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:
gnu:
permissions:
@ -196,6 +201,9 @@ jobs:
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'
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 }}
@ -227,10 +235,18 @@ jobs:
do
if ! grep -Fxq ${LINE}<<<"${REF_FAILING}"
then
MSG="GNU test failed: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?"
echo "::error ::$MSG"
echo $MSG >> ${COMMENT_LOG}
have_new_failures="true"
if ! grep ${LINE} ${IGNORE_INTERMITTENT}
then
MSG="GNU test failed: ${LINE}. ${LINE} is passing on '${{ steps.vars.outputs.repo_default_branch }}'. Maybe you have to rebase?"
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
done
for LINE in ${REF_ERROR}

9
Cargo.lock generated
View file

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

View file

@ -1,7 +1,7 @@
# coreutils (uutils)
# * 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]
name = "coreutils"
@ -282,6 +282,7 @@ filetime = "0.2"
fnv = "1.0.7"
fs_extra = "1.1.0"
fts-sys = "0.2"
fundu = "0.3.0"
gcd = "2.2"
glob = "0.3.0"
half = "2.1"

View file

@ -43,13 +43,23 @@ pacman -S uutils-coreutils
### 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
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 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
```
### 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
### 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;
#[cfg(unix)]
use std::os::unix::net::UnixStream;
use uucore::format_usage;
use uucore::{format_usage, help_section, help_usage};
static USAGE: &str = "{} [OPTION]... [FILE]...";
static ABOUT: &str = "Concatenate FILE(s), or standard input, to standard output
With no FILE, or when FILE is -, read standard input.";
const USAGE: &str = help_usage!("cat.md");
const ABOUT: &str = help_section!("about", "cat.md");
#[derive(Error, Debug)]
enum CatError {

View file

@ -12,15 +12,21 @@ use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::Path;
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::libc::mode_t;
#[cfg(not(windows))]
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.
With --reference, change the mode of each FILE to that of RFILE.";
const ABOUT: &str = "Change the mode of each FILE to MODE.\n\
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 {
pub const CHANGES: &str = "changes";
@ -34,15 +40,6 @@ mod options {
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]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
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").
let mode_had_minus_prefix = mode::strip_minus_from_mode(&mut args);
let after_help = get_long_usage();
let matches = uu_app().after_help(after_help).try_get_matches_from(args)?;
let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
let changes = matches.get_flag(options::CHANGES);
let quiet = matches.get_flag(options::QUIET);
@ -200,21 +195,24 @@ impl Chmoder {
filename.quote()
);
if !self.quiet {
return Err(USimpleError::new(
show!(USimpleError::new(
1,
format!("cannot operate on dangling symlink {}", filename.quote()),
));
}
} else if !self.quiet {
return Err(USimpleError::new(
show!(USimpleError::new(
1,
format!(
"cannot access {}: No such file or directory",
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 == "/" {
return Err(USimpleError::new(

View file

@ -32,21 +32,12 @@ mod options {
pub const TOTAL: &str = "total";
}
fn mkdelim(col: usize, opts: &ArgMatches) -> String {
let mut s = String::new();
let delim = match opts.get_one::<String>(options::DELIMITER).unwrap().as_str() {
"" => "\0",
delim => delim,
};
if col > 1 && !opts.get_flag(options::COLUMN_1) {
s.push_str(delim.as_ref());
fn column_width(col: &str, opts: &ArgMatches) -> usize {
if opts.get_flag(col) {
0
} else {
1
}
if col > 2 && !opts.get_flag(options::COLUMN_2) {
s.push_str(delim.as_ref());
}
s
}
fn ensure_nl(line: &mut String) {
@ -70,7 +61,16 @@ impl LineReader {
}
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 mut na = a.read_line(ra);
@ -98,7 +98,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) {
Ordering::Less => {
if !opts.get_flag(options::COLUMN_1) {
ensure_nl(ra);
print!("{}{}", delim[1], ra);
print!("{ra}");
}
ra.clear();
na = a.read_line(ra);
@ -107,7 +107,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) {
Ordering::Greater => {
if !opts.get_flag(options::COLUMN_2) {
ensure_nl(rb);
print!("{}{}", delim[2], rb);
print!("{delim_col_2}{rb}");
}
rb.clear();
nb = b.read_line(rb);
@ -116,7 +116,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) {
Ordering::Equal => {
if !opts.get_flag(options::COLUMN_3) {
ensure_nl(ra);
print!("{}{}", delim[3], ra);
print!("{delim_col_3}{ra}");
}
ra.clear();
rb.clear();
@ -128,7 +128,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) {
}
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::{
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;
@ -228,13 +228,10 @@ pub struct Options {
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;
const USAGE: &str = "\
{} [OPTION]... [-T] SOURCE DEST
{} [OPTION]... SOURCE... DIRECTORY
{} [OPTION]... -t DIRECTORY SOURCE...";
const USAGE: &str = help_usage!("cp.md");
// Argument constants
mod options {

View file

@ -11,26 +11,22 @@ use uucore::display::print_verbatim;
use uucore::error::{UResult, UUsageError};
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 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 {
pub const ZERO: &str = "zero";
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]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args = args.collect_lossy();
let matches = uu_app()
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
let separator = if matches.get_flag(options::ZERO) {
"\0"

View file

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

View file

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

View file

@ -8,7 +8,7 @@
// spell-checker:ignore (ToDO) NPROCESSORS nprocs numstr threadstr sysconf
use clap::{crate_version, Arg, ArgAction, Command};
use std::env;
use std::{env, thread};
use uucore::display::Quotable;
use uucore::error::{UResult, USimpleError};
use uucore::format_usage;
@ -73,16 +73,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
// If OMP_NUM_THREADS=0, rejects the value
let thread: Vec<&str> = threadstr.split_terminator(',').collect();
match &thread[..] {
[] => num_cpus::get(),
[] => available_parallelism(),
[s, ..] => match s.parse() {
Ok(0) | Err(_) => num_cpus::get(),
Ok(0) | Err(_) => available_parallelism(),
Ok(n) => n,
},
}
}
// the variable 'OMP_NUM_THREADS' doesn't exist
// 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 {
// In some situation, /proc and /sys are not mounted, and sysconf returns 1.
// However, we want to guarantee that `nproc --all` >= `nproc`.
num_cpus::get()
available_parallelism()
} else if nprocs > 0 {
nprocs as usize
} 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(
target_os = "linux",
target_vendor = "apple",
@ -143,5 +143,14 @@ fn num_cpus_all() -> usize {
target_os = "netbsd"
)))]
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,
}
static ABOUT: &str = "Remove (unlink) the FILE(s)";
const ABOUT: &str = "Remove (unlink) the FILE(s)";
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_INTERACTIVE: &str = "interactive";
static OPT_FORCE: &str = "force";
@ -53,28 +67,9 @@ static PRESUME_INPUT_TTY: &str = "-presume-input-tty";
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]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app()
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
let files: Vec<String> = matches
.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,
};
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 std::borrow::Cow;
@ -24,8 +24,9 @@ use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::os::unix::prelude::OsStrExt;
use std::path::Path;
const ABOUT: &str = "Display file or file system status.";
const USAGE: &str = "{} [OPTION]... FILE...";
const ABOUT: &str = help_section!("about", "stat.md");
const USAGE: &str = help_usage!("stat.md");
const LONG_USAGE: &str = help_section!("long usage", "stat.md");
mod options {
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]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app()
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
let stater = Stater::new(&matches)?;
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]
name = "uu_tail"
version = "0.0.17"
@ -19,17 +20,15 @@ clap = { workspace=true }
libc = { workspace=true }
memchr = { workspace=true }
notify = { workspace=true }
uucore = { workspace=true, features=["ringbuffer", "lines"] }
uucore = { workspace=true }
same-file = { workspace=true }
atty = { workspace=true }
fundu = { workspace=true }
[target.'cfg(windows)'.dependencies]
windows-sys = { workspace=true, features = ["Win32_System_Threading", "Win32_Foundation"] }
winapi-util = { workspace=true }
[target.'cfg(unix)'.dependencies]
nix = { workspace=true, features = ["fs"] }
[[bin]]
name = "tail"
path = "src/main.rs"

View file

@ -3,13 +3,14 @@
// * For the full copyright and license information, please view the LICENSE
// * 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::{parse, platform, Quotable};
use atty::Stream;
use clap::crate_version;
use clap::{parser::ValueSource, Arg, ArgAction, ArgMatches, Command};
use fundu::DurationParser;
use same_file::Handle;
use std::collections::VecDeque;
use std::ffi::OsString;
@ -148,16 +149,20 @@ impl Settings {
settings.retry =
matches.get_flag(options::RETRY) || matches.get_flag(options::FOLLOW_RETRY);
if let Some(s) = matches.get_one::<String>(options::SLEEP_INT) {
settings.sleep_sec = match s.parse::<f32>() {
Ok(s) => Duration::from_secs_f32(s),
Err(_) => {
return Err(UUsageError::new(
1,
format!("invalid number of seconds: {}", s.quote()),
))
}
}
if let Some(source) = matches.get_one::<String>(options::SLEEP_INT) {
// Advantage of `fundu` over `Duration::(try_)from_secs_f64(source.parse().unwrap())`:
// * doesn't panic on errors like `Duration::from_secs_f64` would.
// * no precision loss, rounding errors or other floating point problems.
// * evaluates to `Duration::MAX` if the parsed number would have exceeded
// `DURATION::MAX` or `infinity` was given
// * 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);

View file

@ -37,21 +37,21 @@ pub enum BadSequence {
impl Display for BadSequence {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingCharClassName => writeln!(f, "missing character class name '[::]'"),
Self::MissingCharClassName => write!(f, "missing character class name '[::]'"),
Self::MissingEquivalentClassChar => {
writeln!(f, "missing equivalence class character '[==]'")
write!(f, "missing equivalence class character '[==]'")
}
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 => {
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) => {
writeln!(f, "invalid repeat count '{count}' in [c*n] construct")
write!(f, "invalid repeat count '{count}' in [c*n] construct")
}
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::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 LONG_USAGE: &str = "\
Translate, squeeze, and/or delete characters from standard input, \
writing to standard output.";
mod options {
pub const COMPLEMENT: &str = "complement";
@ -30,19 +33,11 @@ mod options {
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]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args = args.collect_lossy();
let matches = uu_app()
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
let delete_flag = matches.get_flag(options::DELETE);
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 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 static IO_BLOCKS: &str = "io-blocks";
@ -84,32 +101,10 @@ pub mod options {
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]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app()
.after_help(get_long_usage())
.after_help(LONG_USAGE)
.try_get_matches_from(args)
.map_err(|e| {
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::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 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 static ALL_REPEATED: &str = "all-repeated";
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]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app()
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?;
let files: Vec<String> = matches
.get_many::<String>(ARG_FILES)

View file

@ -574,3 +574,71 @@ fn test_mode_after_dash_dash() {
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");
}
#[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]
fn output_delimiter() {
new_ucmd!()

View file

@ -68,10 +68,10 @@ fn test_date_utc() {
fn test_date_format_y() {
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);
re = Regex::new(r"^\d{2}$").unwrap();
re = Regex::new(r"^\d{2}\n$").unwrap();
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();
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);
}
@ -96,7 +96,7 @@ fn test_date_format_day() {
re = Regex::new(r"\S+").unwrap();
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);
}
@ -117,7 +117,7 @@ fn test_date_issue_3780() {
#[test]
fn test_date_nano_seconds() {
// %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);
}

View file

@ -49,5 +49,5 @@ fn test_long_output() {
.ucmd()
.arg("-l")
.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-2")
.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]

View file

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

View file

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

View file

@ -777,10 +777,7 @@ fn check_against_gnu_tr_tests_range_a_a() {
.stdout_is("zbc");
}
// 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)]
fn check_against_gnu_tr_tests_null() {
// ['null', qw(a ''), {IN=>''}, {OUT=>''}, {EXIT=>1},
// {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");
}
// 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)]
fn check_against_gnu_tr_tests_o_rep_1() {
// # Another couple octal repeat count tests.
// ['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("");
}
// 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]
fn check_against_gnu_tr_tests_empty_eq() {
// # Ensure that these fail.
@ -1049,10 +1039,6 @@ fn check_against_gnu_tr_tests_empty_eq() {
.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]
fn check_against_gnu_tr_tests_empty_cc() {
// ['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");
}
#[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]
fn check_against_gnu_tr_tests_repeat_bs_9() {
// # Weird repeat counts.

View file

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

View file

@ -36,11 +36,6 @@ use std::{env, hint, thread};
use tempfile::{Builder, TempDir};
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 FIXTURES_DIR: &str = "fixtures";
@ -603,7 +598,7 @@ impl CmdResult {
/// asserts that
/// 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
#[track_caller]
pub fn stderr_only<T: AsRef<str>>(&self, msg: T) -> &Self {
@ -628,7 +623,7 @@ impl CmdResult {
/// asserts that
/// 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."`
/// 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!`
@ -686,7 +681,7 @@ impl CmdResult {
#[track_caller]
pub fn stdout_matches(&self, regex: &regex::Regex) -> &Self {
assert!(
regex.is_match(self.stdout_str().trim()),
regex.is_match(self.stdout_str()),
"Stdout does not match regex:\n{}",
self.stdout_str()
);
@ -696,7 +691,7 @@ impl CmdResult {
#[track_caller]
pub fn stdout_does_not_match(&self, regex: &regex::Regex) -> &Self {
assert!(
!regex.is_match(self.stdout_str().trim()),
!regex.is_match(self.stdout_str()),
"Stdout matches regex:\n{}",
self.stdout_str()
);
@ -1101,13 +1096,7 @@ impl TestScenario {
pub fn new(util_name: &str) -> Self {
let tmpd = Rc::new(TempDir::new().unwrap());
let ts = Self {
bin_path: {
// 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())
},
bin_path: PathBuf::from(env!("CARGO_BIN_EXE_coreutils")),
util_name: String::from(util_name),
fixtures: AtPath::new(tmpd.as_ref().path()),
tmpd,

View file

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