1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27:44 +00:00

Merge branch 'main' into hotfix-mktemp

This commit is contained in:
Zaú Júlio 2023-02-22 20:23:36 -03:00 committed by GitHub
commit 59490e4a17
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 1585 additions and 751 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}

171
Cargo.lock generated
View file

@ -67,7 +67,7 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"hermit-abi 0.1.19",
"libc",
"winapi",
]
@ -293,7 +293,7 @@ dependencies = [
"lazy_static",
"libc",
"unicode-width",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -321,7 +321,6 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
name = "coreutils"
version = "0.0.17"
dependencies = [
"atty",
"chrono",
"clap",
"clap_complete",
@ -329,6 +328,7 @@ dependencies = [
"filetime",
"glob",
"hex-literal",
"is-terminal",
"libc",
"nix",
"once_cell",
@ -635,7 +635,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1631ca6e3c59112501a9d87fd86f21591ff77acd31331e8a73f8d80a65bbdd71"
dependencies = [
"nix",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -834,7 +834,7 @@ dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -855,9 +855,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "fs_extra"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "fsevent-sys"
@ -1045,6 +1045,12 @@ dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
name = "hex"
version = "0.4.3"
@ -1139,6 +1145,28 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ce5ef949d49ee85593fc4d3f3f95ad61657076395cbbce23e2121fc5542074"
[[package]]
name = "io-lifetimes"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3"
dependencies = [
"libc",
"windows-sys 0.45.0",
]
[[package]]
name = "is-terminal"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef"
dependencies = [
"hermit-abi 0.3.1",
"io-lifetimes 1.0.5",
"rustix 0.36.8",
"windows-sys 0.45.0",
]
[[package]]
name = "itertools"
version = "0.10.5"
@ -1235,6 +1263,12 @@ version = "0.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d"
[[package]]
name = "linux-raw-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
[[package]]
name = "lock_api"
version = "0.4.9"
@ -1326,7 +1360,7 @@ dependencies = [
"libc",
"log",
"wasi",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -1415,7 +1449,7 @@ version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
dependencies = [
"hermit-abi",
"hermit-abi 0.1.19",
"libc",
]
@ -1545,7 +1579,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -1693,7 +1727,7 @@ dependencies = [
"byteorder",
"hex",
"lazy_static",
"rustix",
"rustix 0.35.13",
]
[[package]]
@ -1833,9 +1867,9 @@ dependencies = [
[[package]]
name = "rlimit"
version = "0.8.3"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7278a1ec8bfd4a4e07515c589f5ff7b309a373f987393aef44813d9dcf87aa3"
checksum = "f8a29d87a652dc4d43c586328706bb5cdff211f3f39a530f240b53f7221dab8e"
dependencies = [
"libc",
]
@ -1899,10 +1933,24 @@ checksum = "727a1a6d65f786ec22df8a81ca3121107f235970dc1705ed681d3e6e8b9cd5f9"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"io-lifetimes 0.7.5",
"libc",
"linux-raw-sys",
"windows-sys",
"linux-raw-sys 0.0.46",
"windows-sys 0.42.0",
]
[[package]]
name = "rustix"
version = "0.36.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644"
dependencies = [
"bitflags",
"errno",
"io-lifetimes 1.0.5",
"libc",
"linux-raw-sys 0.1.4",
"windows-sys 0.45.0",
]
[[package]]
@ -2161,8 +2209,8 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ca90c434fd12083d1a6bdcbe9f92a14f96c8a1ba600ba451734ac334521f7a"
dependencies = [
"rustix",
"windows-sys",
"rustix 0.35.13",
"windows-sys 0.42.0",
]
[[package]]
@ -2334,8 +2382,8 @@ dependencies = [
name = "uu_cat"
version = "0.0.17"
dependencies = [
"atty",
"clap",
"is-terminal",
"nix",
"thiserror",
"uucore",
@ -2432,9 +2480,9 @@ dependencies = [
name = "uu_cut"
version = "0.0.17"
dependencies = [
"atty",
"bstr",
"clap",
"is-terminal",
"memchr",
"uucore",
]
@ -2447,7 +2495,7 @@ dependencies = [
"clap",
"libc",
"uucore",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -2503,7 +2551,7 @@ dependencies = [
"clap",
"glob",
"uucore",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -2634,7 +2682,7 @@ dependencies = [
"clap",
"hostname",
"uucore",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -2705,10 +2753,10 @@ dependencies = [
name = "uu_ls"
version = "0.0.17"
dependencies = [
"atty",
"chrono",
"clap",
"glob",
"is-terminal",
"lscolors",
"number_prefix",
"once_cell",
@ -2759,9 +2807,9 @@ dependencies = [
name = "uu_more"
version = "0.0.17"
dependencies = [
"atty",
"clap",
"crossterm",
"is-terminal",
"nix",
"unicode-segmentation",
"unicode-width",
@ -2801,8 +2849,8 @@ dependencies = [
name = "uu_nohup"
version = "0.0.17"
dependencies = [
"atty",
"clap",
"is-terminal",
"libc",
"uucore",
]
@ -2936,7 +2984,7 @@ dependencies = [
"libc",
"uucore",
"walkdir",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -3079,7 +3127,7 @@ dependencies = [
"libc",
"nix",
"uucore",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -3097,17 +3145,16 @@ dependencies = [
name = "uu_tail"
version = "0.0.17"
dependencies = [
"atty",
"clap",
"fundu",
"is-terminal",
"libc",
"memchr",
"nix",
"notify",
"same-file",
"uucore",
"winapi-util",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -3147,7 +3194,7 @@ dependencies = [
"filetime",
"time",
"uucore",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -3187,8 +3234,8 @@ dependencies = [
name = "uu_tty"
version = "0.0.17"
dependencies = [
"atty",
"clap",
"is-terminal",
"nix",
"uucore",
]
@ -3283,7 +3330,7 @@ dependencies = [
"clap",
"libc",
"uucore",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -3317,7 +3364,7 @@ dependencies = [
"walkdir",
"wild",
"winapi-util",
"windows-sys",
"windows-sys 0.42.0",
"z85",
]
@ -3479,46 +3526,70 @@ dependencies = [
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "xattr"

View file

@ -263,7 +263,6 @@ feat_os_windows_legacy = [
test = [ "uu_test" ]
[workspace.dependencies]
atty = "0.2"
bigdecimal = "0.3"
binary-heap-plus = "0.5.0"
bstr = "1.0"
@ -287,6 +286,7 @@ gcd = "2.2"
glob = "0.3.0"
half = "2.1"
indicatif = "0.17"
is-terminal = "0.4.3"
itertools = "0.10.0"
libc = "0.2.139"
lscolors = { version = "0.13.0", default-features=false, features = ["nu-ansi-term"] }
@ -476,13 +476,13 @@ time = { workspace=true, features=["local-offset"] }
unindent = "0.1"
uucore = { workspace=true, features=["entries", "process", "signals"] }
walkdir = { workspace=true }
atty = { workspace=true }
is-terminal = { workspace=true }
hex-literal = "0.3.1"
rstest = "0.16.0"
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies]
procfs = { version = "0.14", default-features = false }
rlimit = "0.8.3"
rlimit = "0.9.1"
[target.'cfg(unix)'.dev-dependencies]
nix = { workspace=true, features=["process", "signal", "user"] }

View file

@ -58,12 +58,24 @@ highlight = "all"
# For each duplicate dependency, indicate the name of the dependency which
# introduces it.
# spell-checker: disable
skip = []
skip = [
# is-terminal
{ name = "hermit-abi", version = "0.3.1" },
# is-terminal
{ name = "rustix", version = "0.36.8" },
# is-terminal (via rustix)
{ name = "io-lifetimes", version = "1.0.5" },
# is-terminal
{ name = "linux-raw-sys", version = "0.1.4" },
# is-terminal
{ name = "windows-sys", version = "0.45.0" },
]
# spell-checker: enable
# This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources]
unknown-registry = "warn"
unknown-git = "warn"

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
@ -85,6 +107,13 @@ brew install uutils-coreutils
port install coreutils-uutils
```
## FreeBSD
[![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/uutils-coreutils.svg)](https://repology.org/project/uutils-coreutils/versions)
```sh
pkg install uutils
```
## Windows
### Scoop

14
src/uu/arch/arch.md Normal file
View file

@ -0,0 +1,14 @@
# arch
```
arch
```
Display machine architecture
## After Help
Determine architecture name for current machine.

View file

@ -10,9 +10,10 @@ use platform_info::*;
use clap::{crate_version, Command};
use uucore::error::{FromIo, UResult};
use uucore::{help_about, help_section};
static ABOUT: &str = "Display machine architecture";
static SUMMARY: &str = "Determine architecture name for current machine.";
static ABOUT: &str = help_about!("arch.md");
static SUMMARY: &str = help_section!("after help", "arch.md");
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {

View file

@ -1,12 +1,9 @@
# base32
## Usage
```
base32 [OPTION]... [FILE]
```
## About
encode/decode data and print to standard output
With no FILE, or when FILE is -, read standard input.

View file

@ -8,11 +8,11 @@
use std::io::{stdin, Read};
use clap::Command;
use uucore::{encoding::Format, error::UResult, help_section, help_usage};
use uucore::{encoding::Format, error::UResult, help_about, help_usage};
pub mod base_common;
const ABOUT: &str = help_section!("about", "base32.md");
const ABOUT: &str = help_about!("base32.md");
const USAGE: &str = help_usage!("base32.md");
#[uucore::main]

View file

@ -1,12 +1,9 @@
# base64
## Usage
```
base64 [OPTION]... [FILE]
```
## About
encode/decode data and print to standard output
With no FILE, or when FILE is -, read standard input.

View file

@ -9,11 +9,11 @@
use uu_base32::base_common;
pub use uu_base32::uu_app;
use uucore::{encoding::Format, error::UResult, help_section, help_usage};
use uucore::{encoding::Format, error::UResult, help_about, help_usage};
use std::io::{stdin, Read};
const ABOUT: &str = help_section!("about", "base64.md");
const ABOUT: &str = help_about!("base64.md");
const USAGE: &str = help_usage!("base64.md");
#[uucore::main]

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

@ -0,0 +1,12 @@
# basenc
```
basenc [OPTION]... [FILE]"
```
Encode/decode data and print to standard output
With no FILE, or when FILE is -, read standard input.
When decoding, the input may contain newlines in addition to the bytes of
the formal alphabet. Use --ignore-garbage to attempt to recover
from any other non-alphabet bytes in the encoded stream.

View file

@ -19,14 +19,10 @@ use uucore::{
use std::io::{stdin, Read};
use uucore::error::UClapError;
static ABOUT: &str = "\
Encode/decode data and print to standard output
With no FILE, or when FILE is -, read standard input.
use uucore::{help_about, help_usage};
When decoding, the input may contain newlines in addition to the bytes of
the formal alphabet. Use --ignore-garbage to attempt to recover
from any other non-alphabet bytes in the encoded stream.
";
const ABOUT: &str = help_about!("basenc.md");
const USAGE: &str = help_usage!("basenc.md");
const ENCODINGS: &[(&str, Format)] = &[
("base64", Format::Base64),
@ -39,8 +35,6 @@ const ENCODINGS: &[(&str, Format)] = &[
("z85", Format::Z85),
];
const USAGE: &str = "{} [OPTION]... [FILE]";
pub fn uu_app() -> Command {
let mut command = base_common::base_app(ABOUT, USAGE);
for encoding in ENCODINGS {

View file

@ -17,7 +17,7 @@ path = "src/cat.rs"
[dependencies]
clap = { workspace=true }
thiserror = { workspace = true }
atty = { workspace=true }
is-terminal = { workspace = true }
uucore = { workspace=true, features=["fs", "pipes"] }
[target.'cfg(unix)'.dependencies]

View file

@ -1,11 +1,8 @@
# 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

@ -12,6 +12,7 @@
// last synced with: cat (GNU coreutils) 8.13
use clap::{crate_version, Arg, ArgAction, Command};
use is_terminal::IsTerminal;
use std::fs::{metadata, File};
use std::io::{self, Read, Write};
use thiserror::Error;
@ -33,10 +34,10 @@ use std::net::Shutdown;
use std::os::unix::fs::FileTypeExt;
#[cfg(unix)]
use std::os::unix::net::UnixStream;
use uucore::{format_usage, help_section, help_usage};
use uucore::{format_usage, help_about, help_usage};
const USAGE: &str = help_usage!("cat.md");
const ABOUT: &str = help_section!("about", "cat.md");
const ABOUT: &str = help_about!("cat.md");
#[derive(Error, Debug)]
enum CatError {
@ -332,7 +333,7 @@ fn cat_path(
let stdin = io::stdin();
let mut handle = InputHandle {
reader: stdin,
is_interactive: atty::is(atty::Stream::Stdin),
is_interactive: std::io::stdin().is_terminal(),
};
cat_handle(&mut handle, options, state)
}

View file

@ -12,12 +12,12 @@ 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};
const ABOUT: &str = "Change the mode of each FILE to MODE.\n\
With --reference, change the mode of each FILE to that of RFILE.";
@ -190,26 +190,31 @@ impl Chmoder {
let file = Path::new(filename);
if !file.exists() {
if file.is_symlink() {
println!(
"failed to change mode of {} from 0000 (---------) to 0000 (---------)",
filename.quote()
);
if !self.quiet {
return Err(USimpleError::new(
show!(USimpleError::new(
1,
format!("cannot operate on dangling symlink {}", filename.quote()),
));
}
if self.verbose {
println!(
"failed to change mode of {} from 0000 (---------) to 1500 (r-x-----T)",
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

@ -1,12 +1,9 @@
# 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, help_section, help_usage, prompt_yes, show_error, show_warning};
use uucore::{crash, format_usage, help_about, help_usage, prompt_yes, show_error, show_warning};
use crate::copydir::copy_directory;
@ -228,11 +228,11 @@ pub struct Options {
progress_bar: bool,
}
const ABOUT: &str = help_section!("about", "cp.md");
static EXIT_ERR: i32 = 1;
const ABOUT: &str = help_about!("cp.md");
const USAGE: &str = help_usage!("cp.md");
static EXIT_ERR: i32 = 1;
// Argument constants
mod options {
pub const ARCHIVE: &str = "archive";

View file

@ -19,7 +19,7 @@ clap = { workspace=true }
uucore = { workspace=true }
memchr = { workspace=true }
bstr = { workspace=true }
atty = { workspace=true }
is-terminal = { workspace=true }
[[bin]]
name = "cut"

View file

@ -9,6 +9,7 @@
use bstr::io::BufReadExt;
use clap::{crate_version, Arg, ArgAction, Command};
use is_terminal::IsTerminal;
use std::fs::File;
use std::io::{stdin, stdout, BufReader, BufWriter, Read, Write};
use std::path::Path;
@ -136,7 +137,7 @@ enum Mode {
}
fn stdout_writer() -> Box<dyn Write> {
if atty::is(atty::Stream::Stdout) {
if std::io::stdout().is_terminal() {
Box::new(stdout())
} else {
Box::new(BufWriter::new(stdout())) as Box<dyn Write>

View file

@ -1,7 +1,11 @@
<!-- spell-checker:ignore convs iseek oseek -->
# dd
## About
```
dd [OPERAND]...
dd OPTION
```
Copy, and optionally convert, a file system resource
## After Help

View file

@ -27,7 +27,9 @@ use std::cmp;
use std::env;
use std::ffi::OsString;
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Stdout, Write};
use std::io::{self, Read, Seek, SeekFrom, Stdin, Stdout, Write};
#[cfg(unix)]
use std::os::unix::fs::FileTypeExt;
#[cfg(any(target_os = "linux", target_os = "android"))]
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
@ -39,11 +41,11 @@ use clap::{crate_version, Arg, Command};
use gcd::Gcd;
use uucore::display::Quotable;
use uucore::error::{FromIo, UResult};
use uucore::help_section;
use uucore::show_error;
use uucore::{format_usage, help_about, help_section, help_usage, show_error};
const ABOUT: &str = help_section!("about", "dd.md");
const ABOUT: &str = help_about!("dd.md");
const AFTER_HELP: &str = help_section!("after help", "dd.md");
const USAGE: &str = help_usage!("dd.md");
const BUF_INIT_BYTE: u8 = 0xDD;
/// Final settings after parsing
@ -90,30 +92,106 @@ impl Num {
}
}
struct Input<'a, R: Read> {
src: R,
/// Data sources.
enum Source {
/// Input from stdin.
Stdin(Stdin),
/// Input from a file.
File(File),
/// Input from a named pipe, also known as a FIFO.
#[cfg(unix)]
Fifo(File),
}
impl Source {
fn skip(&mut self, n: u64) -> io::Result<u64> {
match self {
Self::Stdin(stdin) => match io::copy(&mut stdin.take(n), &mut io::sink()) {
Ok(m) if m < n => {
show_error!("'standard input': cannot skip to specified offset");
Ok(m)
}
Ok(m) => Ok(m),
Err(e) => Err(e),
},
Self::File(f) => f.seek(io::SeekFrom::Start(n)),
#[cfg(unix)]
Self::Fifo(f) => io::copy(&mut f.take(n), &mut io::sink()),
}
}
}
impl Read for Source {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::Stdin(stdin) => stdin.read(buf),
Self::File(f) => f.read(buf),
#[cfg(unix)]
Self::Fifo(f) => f.read(buf),
}
}
}
/// The source of the data, configured with the given settings.
///
/// Use the [`Input::new_stdin`] or [`Input::new_file`] functions to
/// construct a new instance of this struct. Then pass the instance to
/// the [`Output::dd_out`] function to execute the main copy operation
/// for `dd`.
struct Input<'a> {
/// The source from which bytes will be read.
src: Source,
/// Configuration settings for how to read the data.
settings: &'a Settings,
}
impl<'a> Input<'a, io::Stdin> {
fn new(settings: &'a Settings) -> UResult<Self> {
let mut input = Self {
src: io::stdin(),
settings,
impl<'a> Input<'a> {
/// Instantiate this struct with stdin as a source.
fn new_stdin(settings: &'a Settings) -> UResult<Self> {
let mut src = Source::Stdin(io::stdin());
if settings.skip > 0 {
src.skip(settings.skip)?;
}
Ok(Self { src, settings })
}
/// Instantiate this struct with the named file as a source.
fn new_file(filename: &Path, settings: &'a Settings) -> UResult<Self> {
let src = {
let mut opts = OpenOptions::new();
opts.read(true);
#[cfg(any(target_os = "linux", target_os = "android"))]
if let Some(libc_flags) = make_linux_iflags(&settings.iflags) {
opts.custom_flags(libc_flags);
}
opts.open(filename)
.map_err_context(|| format!("failed to open {}", filename.quote()))?
};
let mut src = Source::File(src);
if settings.skip > 0 {
if let Err(e) = input.read_skip(settings.skip) {
if let io::ErrorKind::UnexpectedEof = e.kind() {
show_error!("'standard input': cannot skip to specified offset");
} else {
return io::Result::Err(e)
.map_err_context(|| "I/O error while skipping".to_string());
}
}
src.skip(settings.skip)?;
}
Ok(Self { src, settings })
}
Ok(input)
/// Instantiate this struct with the named pipe as a source.
#[cfg(unix)]
fn new_fifo(filename: &Path, settings: &'a Settings) -> UResult<Self> {
let mut opts = OpenOptions::new();
opts.read(true);
#[cfg(any(target_os = "linux", target_os = "android"))]
opts.custom_flags(make_linux_iflags(&settings.iflags).unwrap_or(0));
let mut src = Source::Fifo(opts.open(filename)?);
if settings.skip > 0 {
src.skip(settings.skip)?;
}
Ok(Self { src, settings })
}
}
@ -153,31 +231,7 @@ fn make_linux_iflags(iflags: &IFlags) -> Option<libc::c_int> {
}
}
impl<'a> Input<'a, File> {
fn new(filename: &Path, settings: &'a Settings) -> UResult<Self> {
let mut src = {
let mut opts = OpenOptions::new();
opts.read(true);
#[cfg(any(target_os = "linux", target_os = "android"))]
if let Some(libc_flags) = make_linux_iflags(&settings.iflags) {
opts.custom_flags(libc_flags);
}
opts.open(filename)
.map_err_context(|| format!("failed to open {}", filename.quote()))?
};
if settings.skip > 0 {
src.seek(io::SeekFrom::Start(settings.skip))
.map_err_context(|| "failed to seek in input file".to_string())?;
}
Ok(Self { src, settings })
}
}
impl<'a, R: Read> Read for Input<'a, R> {
impl<'a> Read for Input<'a> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let mut base_idx = 0;
let target_len = buf.len();
@ -200,7 +254,7 @@ impl<'a, R: Read> Read for Input<'a, R> {
}
}
impl<'a, R: Read> Input<'a, R> {
impl<'a> Input<'a> {
/// Fills a given buffer.
/// Reads in increments of 'self.ibs'.
/// The start of each ibs-sized read follows the previous one.
@ -266,20 +320,6 @@ impl<'a, R: Read> Input<'a, R> {
records_truncated: 0,
})
}
/// Skips amount_to_read bytes from the Input by copying into a sink
fn read_skip(&mut self, amount_to_read: u64) -> std::io::Result<()> {
let copy_result = io::copy(&mut self.src.by_ref().take(amount_to_read), &mut io::sink());
if let Ok(n) = copy_result {
if n != amount_to_read {
io::Result::Err(io::Error::new(io::ErrorKind::UnexpectedEof, ""))
} else {
Ok(())
}
} else {
io::Result::Err(copy_result.unwrap_err())
}
}
}
enum Density {
@ -297,6 +337,14 @@ enum Dest {
/// The [`Density`] component indicates whether to attempt to
/// write a sparse file when all-zero blocks are encountered.
File(File, Density),
/// Output to a named pipe, also known as a FIFO.
#[cfg(unix)]
Fifo(File),
/// Output to nothing, dropping each byte written to the output.
#[cfg(unix)]
Sink,
}
impl Dest {
@ -307,6 +355,13 @@ impl Dest {
f.flush()?;
f.sync_all()
}
#[cfg(unix)]
Self::Fifo(f) => {
f.flush()?;
f.sync_all()
}
#[cfg(unix)]
Self::Sink => Ok(()),
}
}
@ -317,6 +372,13 @@ impl Dest {
f.flush()?;
f.sync_data()
}
#[cfg(unix)]
Self::Fifo(f) => {
f.flush()?;
f.sync_data()
}
#[cfg(unix)]
Self::Sink => Ok(()),
}
}
@ -324,17 +386,24 @@ impl Dest {
match self {
Self::Stdout(stdout) => io::copy(&mut io::repeat(0).take(n), stdout),
Self::File(f, _) => f.seek(io::SeekFrom::Start(n)),
#[cfg(unix)]
Self::Fifo(f) => {
// Seeking in a named pipe means *reading* from the pipe.
io::copy(&mut f.take(n), &mut io::sink())
}
#[cfg(unix)]
Self::Sink => Ok(0),
}
}
/// Truncate the underlying file to the current stream position, if possible.
fn truncate(&mut self) -> io::Result<()> {
match self {
Self::Stdout(_) => Ok(()),
Self::File(f, _) => {
let pos = f.stream_position()?;
f.set_len(pos)
}
_ => Ok(()),
}
}
}
@ -357,6 +426,10 @@ impl Write for Dest {
}
Self::File(f, _) => f.write(buf),
Self::Stdout(stdout) => stdout.write(buf),
#[cfg(unix)]
Self::Fifo(f) => f.write(buf),
#[cfg(unix)]
Self::Sink => Ok(buf.len()),
}
}
@ -364,6 +437,10 @@ impl Write for Dest {
match self {
Self::Stdout(stdout) => stdout.flush(),
Self::File(f, _) => f.flush(),
#[cfg(unix)]
Self::Fifo(f) => f.flush(),
#[cfg(unix)]
Self::Sink => Ok(()),
}
}
}
@ -433,6 +510,35 @@ impl<'a> Output<'a> {
Ok(Self { dst, settings })
}
/// Instantiate this struct with the given named pipe as a destination.
#[cfg(unix)]
fn new_fifo(filename: &Path, settings: &'a Settings) -> UResult<Self> {
// We simulate seeking in a FIFO by *reading*, so we open the
// file for reading. But then we need to close the file and
// re-open it for writing.
if settings.seek > 0 {
Dest::Fifo(File::open(filename)?).seek(settings.seek)?;
}
// If `count=0`, then we don't bother opening the file for
// writing because that would cause this process to block
// indefinitely.
if let Some(Num::Blocks(0) | Num::Bytes(0)) = settings.count {
let dst = Dest::Sink;
return Ok(Self { dst, settings });
}
// At this point, we know there is at least one block to write
// to the output, so we open the file for writing.
let mut opts = OpenOptions::new();
opts.write(true)
.create(!settings.oconv.nocreat)
.create_new(settings.oconv.excl)
.append(settings.oflags.append);
#[cfg(any(target_os = "linux", target_os = "android"))]
opts.custom_flags(make_linux_oflags(&settings.oflags).unwrap_or(0));
let dst = Dest::Fifo(opts.open(filename)?);
Ok(Self { dst, settings })
}
/// Write the given bytes one block at a time.
///
/// This may write partial blocks (for example, if the underlying
@ -485,7 +591,7 @@ impl<'a> Output<'a> {
///
/// If there is a problem reading from the input or writing to
/// this output.
fn dd_out<R: Read>(mut self, mut i: Input<R>) -> std::io::Result<()> {
fn dd_out(mut self, mut i: Input) -> std::io::Result<()> {
// The read and write statistics.
//
// These objects are counters, initialized to zero. After each
@ -645,12 +751,13 @@ fn make_linux_oflags(oflags: &OFlags) -> Option<libc::c_int> {
}
}
/// Read helper performs read operations common to all dd reads, and dispatches the buffer to relevant helper functions as dictated by the operations requested by the user.
fn read_helper<R: Read>(
i: &mut Input<R>,
buf: &mut Vec<u8>,
bsize: usize,
) -> std::io::Result<ReadStat> {
/// Read from an input (that is, a source of bytes) into the given buffer.
///
/// This function also performs any conversions as specified by
/// `conv=swab` or `conv=block` command-line arguments. This function
/// mutates the `buf` argument in-place. The returned [`ReadStat`]
/// indicates how many blocks were read.
fn read_helper(i: &mut Input, buf: &mut Vec<u8>, bsize: usize) -> std::io::Result<ReadStat> {
// Local Helper Fns -------------------------------------------------
fn perform_swab(buf: &mut [u8]) {
for base in (1..buf.len()).step_by(2) {
@ -778,6 +885,17 @@ fn is_stdout_redirected_to_seekable_file() -> bool {
}
}
/// Decide whether the named file is a named pipe, also known as a FIFO.
#[cfg(unix)]
fn is_fifo(filename: &str) -> bool {
if let Ok(metadata) = std::fs::metadata(filename) {
if metadata.file_type().is_fifo() {
return true;
}
}
false
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args = args.collect_ignore();
@ -792,46 +910,29 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
.collect::<Vec<_>>()[..],
)?;
match (&settings.infile, &settings.outfile) {
(Some(infile), Some(outfile)) => {
let i = Input::<File>::new(Path::new(&infile), &settings)?;
let o = Output::new_file(Path::new(&outfile), &settings)?;
o.dd_out(i).map_err_context(|| "IO error".to_string())
let i = match settings.infile {
#[cfg(unix)]
Some(ref infile) if is_fifo(infile) => Input::new_fifo(Path::new(&infile), &settings)?,
Some(ref infile) => Input::new_file(Path::new(&infile), &settings)?,
None => Input::new_stdin(&settings)?,
};
let o = match settings.outfile {
#[cfg(unix)]
Some(ref outfile) if is_fifo(outfile) => Output::new_fifo(Path::new(&outfile), &settings)?,
Some(ref outfile) => Output::new_file(Path::new(&outfile), &settings)?,
None if is_stdout_redirected_to_seekable_file() => {
Output::new_file(Path::new(&stdout_canonicalized()), &settings)?
}
(None, Some(outfile)) => {
let i = Input::<io::Stdin>::new(&settings)?;
let o = Output::new_file(Path::new(&outfile), &settings)?;
o.dd_out(i).map_err_context(|| "IO error".to_string())
}
(Some(infile), None) => {
let i = Input::<File>::new(Path::new(&infile), &settings)?;
if is_stdout_redirected_to_seekable_file() {
let filename = stdout_canonicalized();
let o = Output::new_file(Path::new(&filename), &settings)?;
o.dd_out(i).map_err_context(|| "IO error".to_string())
} else {
let o = Output::new_stdout(&settings)?;
o.dd_out(i).map_err_context(|| "IO error".to_string())
}
}
(None, None) => {
let i = Input::<io::Stdin>::new(&settings)?;
if is_stdout_redirected_to_seekable_file() {
let filename = stdout_canonicalized();
let o = Output::new_file(Path::new(&filename), &settings)?;
o.dd_out(i).map_err_context(|| "IO error".to_string())
} else {
let o = Output::new_stdout(&settings)?;
o.dd_out(i).map_err_context(|| "IO error".to_string())
}
}
}
None => Output::new_stdout(&settings)?,
};
o.dd_out(i).map_err_context(|| "IO error".to_string())
}
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.after_help(AFTER_HELP)
.infer_long_args(true)
.arg(Arg::new(options::OPERANDS).num_args(1..))

24
src/uu/du/du.md Normal file
View file

@ -0,0 +1,24 @@
# du
```
du [OPTION]... [FILE]...
du [OPTION]... --files0-from=F
```
Estimate file space usage
## After Help
Display values are in units of the first available SIZE from --block-size,
and the DU_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables.
Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set).
SIZE is an integer and optional unit (example: 10M is 10*1024*1024).
Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers
of 1000).
PATTERN allows some advanced exclusions. For example, the following syntaxes
are supported:
? will match only one character
* will match zero or more characters
{a,b} will match a or b

View file

@ -36,7 +36,9 @@ use uucore::error::FromIo;
use uucore::error::{UError, UResult};
use uucore::parse_glob;
use uucore::parse_size::{parse_size, ParseSizeError};
use uucore::{crash, format_usage, show, show_error, show_warning};
use uucore::{
crash, format_usage, help_about, help_section, help_usage, show, show_error, show_warning,
};
#[cfg(windows)]
use windows_sys::Win32::Foundation::HANDLE;
#[cfg(windows)]
@ -73,25 +75,9 @@ mod options {
pub const FILE: &str = "FILE";
}
const ABOUT: &str = "Estimate file space usage";
const LONG_HELP: &str = "
Display values are in units of the first available SIZE from --block-size,
and the DU_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables.
Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set).
SIZE is an integer and optional unit (example: 10M is 10*1024*1024).
Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers
of 1000).
PATTERN allows some advanced exclusions. For example, the following syntaxes
are supported:
? will match only one character
* will match zero or more characters
{a,b} will match a or b
";
const USAGE: &str = "\
{} [OPTION]... [FILE]...
{} [OPTION]... --files0-from=F";
const ABOUT: &str = help_about!("du.md");
const AFTER_HELP: &str = help_section!("after help", "du.md");
const USAGE: &str = help_usage!("du.md");
// TODO: Support Z & Y (currently limited by size of u64)
const UNITS: [(char, u32); 6] = [('E', 6), ('P', 5), ('T', 4), ('G', 3), ('M', 2), ('K', 1)];
@ -705,7 +691,7 @@ pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.after_help(LONG_HELP)
.after_help(AFTER_HELP)
.override_usage(format_usage(USAGE))
.infer_long_args(true)
.disable_help_flag(true)

View file

@ -1,15 +1,12 @@
# expr
## About
Print the value of `EXPRESSION` to standard output
## Usage
```
expr [EXPRESSION]
expr [OPTIONS]
```
Print the value of `EXPRESSION` to standard output
## After help
Print the value of `EXPRESSION` to standard output. A blank line below
@ -58,4 +55,4 @@ Environment variables:
- `EXPR_DEBUG_TOKENS=1`: dump expression's tokens
- `EXPR_DEBUG_RPN=1`: dump expression represented in reverse polish notation
- `EXPR_DEBUG_SYA_STEP=1`: dump each parser step
- `EXPR_DEBUG_AST=1`: dump expression represented abstract syntax tree
- `EXPR_DEBUG_AST=1`: dump expression represented abstract syntax tree

View file

@ -8,7 +8,7 @@
use clap::{crate_version, Arg, ArgAction, Command};
use uucore::{
error::{UResult, USimpleError},
format_usage, help_section, help_usage,
format_usage, help_about, help_section, help_usage,
};
mod syntax_tree;
@ -23,7 +23,7 @@ mod options {
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(help_section!("about", "expr.md"))
.about(help_about!("expr.md"))
.override_usage(format_usage(help_usage!("expr.md")))
.after_help(help_section!("after help", "expr.md"))
.infer_long_args(true)

View file

@ -41,8 +41,8 @@ pub struct Behavior {
specified_mode: Option<u32>,
backup_mode: BackupMode,
suffix: String,
owner: String,
group: String,
owner_id: Option<u32>,
group_id: Option<u32>,
verbose: bool,
preserve_timestamps: bool,
compare: bool,
@ -58,14 +58,15 @@ enum InstallError {
DirNeedsArg(),
CreateDirFailed(PathBuf, std::io::Error),
ChmodFailed(PathBuf),
ChownFailed(PathBuf, String),
InvalidTarget(PathBuf),
TargetDirIsntDir(PathBuf),
BackupFailed(PathBuf, PathBuf, std::io::Error),
InstallFailed(PathBuf, PathBuf, std::io::Error),
StripProgramFailed(String),
MetadataFailed(std::io::Error),
NoSuchUser(String),
NoSuchGroup(String),
InvalidUser(String),
InvalidGroup(String),
OmittingDirectory(PathBuf),
}
@ -99,6 +100,7 @@ impl Display for InstallError {
Display::fmt(&uio_error!(e, "failed to create {}", dir.quote()), f)
}
Self::ChmodFailed(file) => write!(f, "failed to chmod {}", file.quote()),
Self::ChownFailed(file, msg) => write!(f, "failed to chown {}: {}", file.quote(), msg),
Self::InvalidTarget(target) => write!(
f,
"invalid target {}: No such file or directory",
@ -117,8 +119,8 @@ impl Display for InstallError {
),
Self::StripProgramFailed(msg) => write!(f, "strip program failed: {msg}"),
Self::MetadataFailed(e) => Display::fmt(&uio_error!(e, ""), f),
Self::NoSuchUser(user) => write!(f, "no such user: {}", user.maybe_quote()),
Self::NoSuchGroup(group) => write!(f, "no such group: {}", group.maybe_quote()),
Self::InvalidUser(user) => write!(f, "invalid user: {}", user.quote()),
Self::InvalidGroup(group) => write!(f, "invalid group: {}", group.quote()),
Self::OmittingDirectory(dir) => write!(f, "omitting directory {}", dir.quote()),
}
}
@ -391,21 +393,44 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
show_error!("Options --compare and --strip are mutually exclusive");
return Err(1.into());
}
let owner = matches
.get_one::<String>(OPT_OWNER)
.map(|s| s.as_str())
.unwrap_or("")
.to_string();
let owner_id = if !owner.is_empty() {
match usr2uid(&owner) {
Ok(u) => Some(u),
Err(_) => return Err(InstallError::InvalidUser(owner.clone()).into()),
}
} else {
None
};
let group = matches
.get_one::<String>(OPT_GROUP)
.map(|s| s.as_str())
.unwrap_or("")
.to_string();
let group_id = if !group.is_empty() {
match grp2gid(&group) {
Ok(g) => Some(g),
Err(_) => return Err(InstallError::InvalidGroup(group.clone()).into()),
}
} else {
None
};
Ok(Behavior {
main_function,
specified_mode,
backup_mode,
suffix: backup_control::determine_backup_suffix(matches),
owner: matches
.get_one::<String>(OPT_OWNER)
.map(|s| s.as_str())
.unwrap_or("")
.to_string(),
group: matches
.get_one::<String>(OPT_GROUP)
.map(|s| s.as_str())
.unwrap_or("")
.to_string(),
owner_id,
group_id,
verbose: matches.get_flag(OPT_VERBOSE),
preserve_timestamps,
compare,
@ -466,6 +491,8 @@ fn directory(paths: &[String], b: &Behavior) -> UResult<()> {
uucore::error::set_exit_code(1);
continue;
}
show_if_err!(chown_optional_user_group(path, b));
}
// If the exit code was set, or show! has been called at least once
// (which sets the exit code as well), function execution will end after
@ -558,12 +585,6 @@ fn standard(mut paths: Vec<String>, b: &Behavior) -> UResult<()> {
if let Err(e) = fs::create_dir_all(to_create) {
return Err(InstallError::CreateDirFailed(to_create.to_path_buf(), e).into());
}
// Silent the warning as we want to the error message
#[allow(clippy::question_mark)]
if mode::chmod(to_create, b.mode()).is_err() {
return Err(InstallError::ChmodFailed(to_create.to_path_buf()).into());
}
}
}
}
@ -626,6 +647,42 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR
Ok(())
}
/// Handle incomplete user/group parings for chown.
///
/// Returns a Result type with the Err variant containing the error message.
///
/// # Parameters
///
/// _path_ must exist.
///
/// # Errors
///
/// If the owner or group are invalid or copy system call fails, we print a verbose error and
/// return an empty error value.
///
fn chown_optional_user_group(path: &Path, b: &Behavior) -> UResult<()> {
if b.owner_id.is_some() || b.group_id.is_some() {
let meta = match fs::metadata(path) {
Ok(meta) => meta,
Err(e) => return Err(InstallError::MetadataFailed(e).into()),
};
// GNU coreutils doesn't print chown operations during install with verbose flag.
let verbosity = Verbosity {
groups_only: b.owner_id.is_none(),
level: VerbosityLevel::Normal,
};
match wrap_chown(path, &meta, b.owner_id, b.group_id, false, verbosity) {
Ok(msg) if b.verbose && !msg.is_empty() => println!("chown: {msg}"),
Ok(_) => {}
Err(e) => return Err(InstallError::ChownFailed(path.to_path_buf(), e).into()),
}
}
Ok(())
}
/// Copy one file to a new location, changing metadata.
///
/// Returns a Result type with the Err variant containing the error message.
@ -708,66 +765,7 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> {
return Err(InstallError::ChmodFailed(to.to_path_buf()).into());
}
if !b.owner.is_empty() {
let meta = match fs::metadata(to) {
Ok(meta) => meta,
Err(e) => return Err(InstallError::MetadataFailed(e).into()),
};
let owner_id = match usr2uid(&b.owner) {
Ok(g) => g,
_ => return Err(InstallError::NoSuchUser(b.owner.clone()).into()),
};
let gid = meta.gid();
match wrap_chown(
to,
&meta,
Some(owner_id),
Some(gid),
false,
Verbosity {
groups_only: false,
level: VerbosityLevel::Normal,
},
) {
Ok(n) => {
if !n.is_empty() {
show_error!("{}", n);
}
}
Err(e) => show_error!("{}", e),
}
}
if !b.group.is_empty() {
let meta = match fs::metadata(to) {
Ok(meta) => meta,
Err(e) => return Err(InstallError::MetadataFailed(e).into()),
};
let group_id = match grp2gid(&b.group) {
Ok(g) => g,
_ => return Err(InstallError::NoSuchGroup(b.group.clone()).into()),
};
match wrap_chown(
to,
&meta,
Some(group_id),
None,
false,
Verbosity {
groups_only: true,
level: VerbosityLevel::Normal,
},
) {
Ok(n) => {
if !n.is_empty() {
show_error!("{}", n);
}
}
Err(e) => show_error!("{}", e),
}
}
chown_optional_user_group(to, b)?;
if b.preserve_timestamps {
let meta = match fs::metadata(from) {
@ -847,19 +845,11 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> UResult<bool> {
// TODO: if -P (#1809) and from/to contexts mismatch, return true.
if !b.owner.is_empty() {
let owner_id = match usr2uid(&b.owner) {
Ok(id) => id,
_ => return Err(InstallError::NoSuchUser(b.owner.clone()).into()),
};
if let Some(owner_id) = b.owner_id {
if owner_id != to_meta.uid() {
return Ok(true);
}
} else if !b.group.is_empty() {
let group_id = match grp2gid(&b.group) {
Ok(id) => id,
_ => return Err(InstallError::NoSuchGroup(b.group.clone()).into()),
};
} else if let Some(group_id) = b.group_id {
if group_id != to_meta.gid() {
return Ok(true);
}

7
src/uu/kill/kill.md Normal file
View file

@ -0,0 +1,7 @@
# kill
```
kill [OPTIONS]... PID...
```
Send signal to processes or list information about signals.

View file

@ -14,10 +14,10 @@ use std::io::Error;
use uucore::display::Quotable;
use uucore::error::{FromIo, UError, UResult, USimpleError};
use uucore::signals::{signal_by_name_or_value, ALL_SIGNALS};
use uucore::{format_usage, show};
use uucore::{format_usage, help_about, help_usage, show};
static ABOUT: &str = "Send signal to processes or list information about signals.";
const USAGE: &str = "{} [OPTIONS]... PID...";
static ABOUT: &str = help_about!("kill.md");
const USAGE: &str = help_usage!("kill.md");
pub mod options {
pub static PIDS_OR_SIGNALS: &str = "pids_or_signals";

View file

@ -25,7 +25,7 @@ glob = { workspace=true }
lscolors = { workspace=true }
uucore = { workspace=true, features = ["entries", "fs"] }
once_cell = { workspace=true }
atty = { workspace=true }
is-terminal = { workspace=true }
selinux = { workspace=true, optional = true }
[[bin]]

View file

@ -12,6 +12,7 @@ use clap::{
crate_version, Arg, ArgAction, Command,
};
use glob::{MatchOptions, Pattern};
use is_terminal::IsTerminal;
use lscolors::LsColors;
use number_prefix::NumberPrefix;
use once_cell::unsync::OnceCell;
@ -451,7 +452,7 @@ impl Config {
(Format::Commas, Some(options::format::COMMAS))
} else if options.get_flag(options::format::COLUMNS) {
(Format::Columns, Some(options::format::COLUMNS))
} else if atty::is(atty::Stream::Stdout) {
} else if std::io::stdout().is_terminal() {
(Format::Columns, None)
} else {
(Format::OneLine, None)
@ -557,7 +558,7 @@ impl Config {
None => options.contains_id(options::COLOR),
Some(val) => match val.as_str() {
"" | "always" | "yes" | "force" => true,
"auto" | "tty" | "if-tty" => atty::is(atty::Stream::Stdout),
"auto" | "tty" | "if-tty" => std::io::stdout().is_terminal(),
/* "never" | "no" | "none" | */ _ => false,
},
};
@ -678,7 +679,7 @@ impl Config {
} else if options.get_flag(options::SHOW_CONTROL_CHARS) {
true
} else {
!atty::is(atty::Stream::Stdout)
!std::io::stdout().is_terminal()
};
let opt_quoting_style = options
@ -750,7 +751,7 @@ impl Config {
"never" | "no" | "none" => IndicatorStyle::None,
"always" | "yes" | "force" => IndicatorStyle::Classify,
"auto" | "tty" | "if-tty" => {
if atty::is(atty::Stream::Stdout) {
if std::io::stdout().is_terminal() {
IndicatorStyle::Classify
} else {
IndicatorStyle::None

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

@ -0,0 +1,12 @@
<!-- spell-checker:ignore ugoa -->
# mkdir
```
mkdir [OPTION]... [USER]
```
Create the given DIRECTORY(ies) if they do not exist
## After Help
Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'.

View file

@ -18,14 +18,13 @@ use uucore::error::{UResult, USimpleError};
#[cfg(not(windows))]
use uucore::mode;
use uucore::{display::Quotable, fs::dir_strip_dot_for_creation};
use uucore::{format_usage, show, show_if_err};
use uucore::{format_usage, help_about, help_section, help_usage, show, show_if_err};
static DEFAULT_PERM: u32 = 0o755;
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]+'.";
const ABOUT: &str = help_about!("mkdir.md");
const USAGE: &str = help_usage!("mkdir.md");
const AFTER_HELP: &str = help_section!("after help", "mkdir.md");
mod options {
pub const MODE: &str = "mode";
@ -90,7 +89,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(LONG_USAGE).try_get_matches_from(args)?;
let matches = uu_app().after_help(AFTER_HELP).try_get_matches_from(args)?;
let dirs = matches
.get_many::<OsString>(options::DIRS)

View file

@ -18,7 +18,7 @@ path = "src/more.rs"
clap = { workspace=true }
uucore = { workspace=true }
crossterm = { workspace=true }
atty = { workspace=true }
is-terminal = { workspace=true }
unicode-width = { workspace=true }
unicode-segmentation = { workspace=true }

View file

@ -23,6 +23,7 @@ use crossterm::{
terminal,
};
use is_terminal::IsTerminal;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use uucore::display::Quotable;
@ -83,7 +84,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
buff.clear();
}
reset_term(&mut stdout);
} else if atty::isnt(atty::Stream::Stdin) {
} else if !std::io::stdin().is_terminal() {
stdin().read_to_string(&mut buff).unwrap();
let mut stdout = setup_term();
more(&buff, &mut stdout, None, silent)?;

View file

@ -17,7 +17,7 @@ path = "src/nohup.rs"
[dependencies]
clap = { workspace=true }
libc = { workspace=true }
atty = { workspace=true }
is-terminal = { workspace=true }
uucore = { workspace=true, features=["fs"] }
[[bin]]

15
src/uu/nohup/nohup.md Normal file
View file

@ -0,0 +1,15 @@
# nohup
```
nohup COMMAND [ARG]...
nohup FLAG
```
Run COMMAND ignoring hangup signals.
## After Help
If standard input is terminal, it'll be replaced with /dev/null.
If standard output is terminal, it'll be appended to nohup.out instead,
or $HOME/nohup.out, if nohup.out open failed.
If standard error is terminal, it'll be redirected to stdout.

View file

@ -8,6 +8,7 @@
// spell-checker:ignore (ToDO) execvp SIGHUP cproc vprocmgr cstrs homeout
use clap::{crate_version, Arg, ArgAction, Command};
use is_terminal::IsTerminal;
use libc::{c_char, dup2, execvp, signal};
use libc::{SIGHUP, SIG_IGN};
use std::env;
@ -19,18 +20,11 @@ use std::os::unix::prelude::*;
use std::path::{Path, PathBuf};
use uucore::display::Quotable;
use uucore::error::{set_exit_code, UClapError, UError, UResult};
use uucore::{format_usage, show_error};
use uucore::{format_usage, help_about, help_section, help_usage, show_error};
static ABOUT: &str = "Run COMMAND ignoring hangup signals.";
static LONG_HELP: &str = "
If standard input is terminal, it'll be replaced with /dev/null.
If standard output is terminal, it'll be appended to nohup.out instead,
or $HOME/nohup.out, if nohup.out open failed.
If standard error is terminal, it'll be redirected to stdout.
";
const USAGE: &str = "\
{} COMMAND [ARG]...
{} FLAG";
const ABOUT: &str = help_about!("nohup.md");
const AFTER_HELP: &str = help_section!("after help", "nohup.md");
const USAGE: &str = help_usage!("nohup.md");
static NOHUP_OUT: &str = "nohup.out";
// exit codes that match the GNU implementation
static EXIT_CANCELED: i32 = 125;
@ -115,7 +109,7 @@ pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.after_help(LONG_HELP)
.after_help(AFTER_HELP)
.override_usage(format_usage(USAGE))
.arg(
Arg::new(options::CMD)
@ -129,7 +123,7 @@ pub fn uu_app() -> Command {
}
fn replace_fds() -> UResult<()> {
if atty::is(atty::Stream::Stdin) {
if std::io::stdin().is_terminal() {
let new_stdin = File::open(Path::new("/dev/null"))
.map_err(|e| NohupError::CannotReplace("STDIN", e))?;
if unsafe { dup2(new_stdin.as_raw_fd(), 0) } != 0 {
@ -137,7 +131,7 @@ fn replace_fds() -> UResult<()> {
}
}
if atty::is(atty::Stream::Stdout) {
if std::io::stdout().is_terminal() {
let new_stdout = find_stdout()?;
let fd = new_stdout.as_raw_fd();
@ -146,7 +140,7 @@ fn replace_fds() -> UResult<()> {
}
}
if atty::is(atty::Stream::Stderr) && unsafe { dup2(1, 2) } != 2 {
if std::io::stderr().is_terminal() && unsafe { dup2(1, 2) } != 2 {
return Err(NohupError::CannotReplace("STDERR", Error::last_os_error()).into());
}
Ok(())

View file

@ -1,13 +1,10 @@
<!-- spell-checker:ignore N'th M'th -->
# numfmt
## Usage
```
numfmt [OPTION]... [NUMBER]...
```
## About
Convert numbers from/to human-readable strings
## After Help

View file

@ -14,16 +14,15 @@ use std::io::{BufRead, Write};
use units::{IEC_BASES, SI_BASES};
use uucore::display::Quotable;
use uucore::error::UResult;
use uucore::format_usage;
use uucore::ranges::Range;
use uucore::{help_section, help_usage};
use uucore::{format_usage, help_about, help_section, help_usage};
pub mod errors;
pub mod format;
pub mod options;
mod units;
const ABOUT: &str = help_section!("about", "numfmt.md");
const ABOUT: &str = help_about!("numfmt.md");
const AFTER_HELP: &str = help_section!("after help", "numfmt.md");
const USAGE: &str = help_usage!("numfmt.md");

49
src/uu/od/od.md Normal file
View file

@ -0,0 +1,49 @@
# od
```
od [OPTION]... [--] [FILENAME]...
od [-abcdDefFhHiIlLoOsxX] [FILENAME] [[+][0x]OFFSET[.][b]]
od --traditional [OPTION]... [FILENAME] [[+][0x]OFFSET[.][b] [[+][0x]LABEL[.][b]]]
```
Dump files in octal and other formats
## After Help
Displays data in various human-readable formats. If multiple formats are
specified, the output will contain all formats in the order they appear on the
command line. Each format will be printed on a new line. Only the line
containing the first format will be prefixed with the offset.
If no filename is specified, or it is "-", stdin will be used. After a "--", no
more options will be recognized. This allows for filenames starting with a "-".
If a filename is a valid number which can be used as an offset in the second
form, you can force it to be recognized as a filename if you include an option
like "-j0", which is only valid in the first form.
RADIX is one of o,d,x,n for octal, decimal, hexadecimal or none.
BYTES is decimal by default, octal if prefixed with a "0", or hexadecimal if
prefixed with "0x". The suffixes b, KB, K, MB, M, GB, G, will multiply the
number with 512, 1000, 1024, 1000^2, 1024^2, 1000^3, 1024^3, 1000^2, 1024^2.
OFFSET and LABEL are octal by default, hexadecimal if prefixed with "0x" or
decimal if a "." suffix is added. The "b" suffix will multiply with 512.
TYPE contains one or more format specifications consisting of:
a for printable 7-bits ASCII
c for utf-8 characters or octal for undefined characters
d[SIZE] for signed decimal
f[SIZE] for floating point
o[SIZE] for octal
u[SIZE] for unsigned decimal
x[SIZE] for hexadecimal
SIZE is the number of bytes which can be the number 1, 2, 4, 8 or 16,
or C, I, S, L for 1, 2, 4, 8 bytes for integer types,
or F, D, L for 4, 8, 16 bytes for floating point.
Any type specification can have a "z" suffix, which will add a ASCII dump at
the end of the line.
If an error occurred, a diagnostic message will be printed to stderr, and the
exit code will be non-zero.

View file

@ -44,57 +44,15 @@ use clap::ArgAction;
use clap::{crate_version, parser::ValueSource, Arg, ArgMatches, Command};
use uucore::display::Quotable;
use uucore::error::{UResult, USimpleError};
use uucore::format_usage;
use uucore::parse_size::ParseSizeError;
use uucore::show_error;
use uucore::show_warning;
use uucore::{format_usage, help_about, help_section, help_usage, show_error, show_warning};
const PEEK_BUFFER_SIZE: usize = 4; // utf-8 can be 4 bytes
static ABOUT: &str = "Dump files in octal and other formats";
const ABOUT: &str = help_about!("od.md");
static USAGE: &str = "\
{} [OPTION]... [--] [FILENAME]...
{} [-abcdDefFhHiIlLoOsxX] [FILENAME] [[+][0x]OFFSET[.][b]]
{} --traditional [OPTION]... [FILENAME] [[+][0x]OFFSET[.][b] [[+][0x]LABEL[.][b]]]";
const USAGE: &str = help_usage!("od.md");
static LONG_HELP: &str = r#"
Displays data in various human-readable formats. If multiple formats are
specified, the output will contain all formats in the order they appear on the
command line. Each format will be printed on a new line. Only the line
containing the first format will be prefixed with the offset.
If no filename is specified, or it is "-", stdin will be used. After a "--", no
more options will be recognized. This allows for filenames starting with a "-".
If a filename is a valid number which can be used as an offset in the second
form, you can force it to be recognized as a filename if you include an option
like "-j0", which is only valid in the first form.
RADIX is one of o,d,x,n for octal, decimal, hexadecimal or none.
BYTES is decimal by default, octal if prefixed with a "0", or hexadecimal if
prefixed with "0x". The suffixes b, KB, K, MB, M, GB, G, will multiply the
number with 512, 1000, 1024, 1000^2, 1024^2, 1000^3, 1024^3, 1000^2, 1024^2.
OFFSET and LABEL are octal by default, hexadecimal if prefixed with "0x" or
decimal if a "." suffix is added. The "b" suffix will multiply with 512.
TYPE contains one or more format specifications consisting of:
a for printable 7-bits ASCII
c for utf-8 characters or octal for undefined characters
d[SIZE] for signed decimal
f[SIZE] for floating point
o[SIZE] for octal
u[SIZE] for unsigned decimal
x[SIZE] for hexadecimal
SIZE is the number of bytes which can be the number 1, 2, 4, 8 or 16,
or C, I, S, L for 1, 2, 4, 8 bytes for integer types,
or F, D, L for 4, 8, 16 bytes for floating point.
Any type specification can have a "z" suffix, which will add a ASCII dump at
the end of the line.
If an error occurred, a diagnostic message will be printed to stderr, and the
exitcode will be non-zero."#;
const AFTER_HELP: &str = help_section!("after help", "od.md");
pub(crate) mod options {
pub const HELP: &str = "help";
@ -295,7 +253,7 @@ pub fn uu_app() -> Command {
.version(crate_version!())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.after_help(LONG_HELP)
.after_help(AFTER_HELP)
.trailing_var_arg(true)
.dont_delimit_trailing_values(true)
.infer_long_args(true)

22
src/uu/rm/rm.md Normal file
View file

@ -0,0 +1,22 @@
# rm
```
rm [OPTION]... FILE...
```
Remove (unlink) the FILE(s)
## After Help
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.

View file

@ -15,7 +15,7 @@ use std::ops::BitOr;
use std::path::{Path, PathBuf};
use uucore::display::Quotable;
use uucore::error::{UResult, USimpleError, UUsageError};
use uucore::{format_usage, prompt_yes, show_error};
use uucore::{format_usage, help_about, help_section, help_usage, prompt_yes, show_error};
use walkdir::{DirEntry, WalkDir};
#[derive(Eq, PartialEq, Clone, Copy)]
@ -37,21 +37,9 @@ struct Options {
verbose: bool,
}
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.";
const ABOUT: &str = help_about!("rm.md");
const USAGE: &str = help_usage!("rm.md");
const AFTER_HELP: &str = help_section!("after help", "rm.md");
static OPT_DIR: &str = "dir";
static OPT_INTERACTIVE: &str = "interactive";
@ -69,7 +57,7 @@ static ARG_FILES: &str = "files";
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
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 files: Vec<String> = matches
.get_many::<String>(ARG_FILES)

7
src/uu/rmdir/rmdir.md Normal file
View file

@ -0,0 +1,7 @@
# rmdir
```
rmdir [OPTION]... DIRECTORY...
```
Remove the DIRECTORY(ies), if they are empty.

View file

@ -16,10 +16,10 @@ use std::path::Path;
use uucore::display::Quotable;
use uucore::error::{set_exit_code, strip_errno, UResult};
use uucore::{format_usage, show_error, util_name};
use uucore::{format_usage, help_about, help_usage, show_error, util_name};
static ABOUT: &str = "Remove the DIRECTORY(ies), if they are empty.";
const USAGE: &str = "{} [OPTION]... DIRECTORY...";
static ABOUT: &str = help_about!("rmdir.md");
const USAGE: &str = help_usage!("rmdir.md");
static OPT_IGNORE_FAIL_NON_EMPTY: &str = "ignore-fail-on-non-empty";
static OPT_PARENTS: &str = "parents";
static OPT_VERBOSE: &str = "verbose";

16
src/uu/sleep/sleep.md Normal file
View file

@ -0,0 +1,16 @@
# sleep
```
sleep NUMBER[SUFFIX]...
sleep OPTION
```
Pause for NUMBER seconds.
## After Help
Pause for NUMBER seconds. SUFFIX may be 's' for seconds (the default),
'm' for minutes, 'h' for hours or 'd' for days. Unlike most implementations
that require NUMBER be an integer, here NUMBER may be an arbitrary floating
point number. Given two or more arguments, pause for the amount of time
specified by the sum of their values.

View file

@ -10,20 +10,14 @@ use std::time::Duration;
use uucore::{
error::{UResult, USimpleError, UUsageError},
format_usage, show,
format_usage, help_about, help_section, help_usage, show,
};
use clap::{crate_version, Arg, ArgAction, Command};
static ABOUT: &str = "Pause for NUMBER seconds.";
const USAGE: &str = "\
{} NUMBER[SUFFIX]...
{} OPTION";
static LONG_HELP: &str = "Pause for NUMBER seconds. SUFFIX may be 's' for seconds (the default),
'm' for minutes, 'h' for hours or 'd' for days. Unlike most implementations
that require NUMBER be an integer, here NUMBER may be an arbitrary floating
point number. Given two or more arguments, pause for the amount of time
specified by the sum of their values.";
static ABOUT: &str = help_about!("sleep.md");
const USAGE: &str = help_usage!("sleep.md");
static AFTER_HELP: &str = help_section!("after help", "sleep.md");
mod options {
pub const NUMBER: &str = "NUMBER";
@ -54,7 +48,7 @@ pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.after_help(LONG_HELP)
.after_help(AFTER_HELP)
.override_usage(format_usage(USAGE))
.infer_long_args(true)
.arg(

View file

@ -13,7 +13,9 @@ 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, help_section, help_usage, show_error, show_warning};
use uucore::{
entries, format_usage, help_about, help_section, help_usage, show_error, show_warning,
};
use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
use std::borrow::Cow;
@ -24,7 +26,7 @@ use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::os::unix::prelude::OsStrExt;
use std::path::Path;
const ABOUT: &str = help_section!("about", "stat.md");
const ABOUT: &str = help_about!("stat.md");
const USAGE: &str = help_usage!("stat.md");
const LONG_USAGE: &str = help_section!("long usage", "stat.md");

View file

@ -1,14 +1,11 @@
# stat
## About
Display file or file system status.
## Usage
```
stat [OPTION]... FILE...
```
Display file or file system status.
## Long Usage
The valid format sequences for files (without `--file-system`):

View file

@ -19,12 +19,12 @@ use std::{
use uucore::display::Quotable;
use uucore::error::UError;
use uucore::error::UResult;
use uucore::{format_usage, show};
use uucore::{format_usage, help_about, help_usage, show};
use crate::error::TacError;
static USAGE: &str = "{} [OPTION]... [FILE]...";
static ABOUT: &str = "Write each file to standard output, last line first.";
static USAGE: &str = help_usage!("tac.md");
static ABOUT: &str = help_about!("tac.md");
mod options {
pub static BEFORE: &str = "before";

7
src/uu/tac/tac.md Normal file
View file

@ -0,0 +1,7 @@
# tac
```
tac [OPTION]... [FILE]...
```
Write each file to standard output, last line first.

View file

@ -20,18 +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 }
is-terminal = { 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

@ -7,10 +7,10 @@
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 is_terminal::IsTerminal;
use same_file::Handle;
use std::collections::VecDeque;
use std::ffi::OsString;
@ -274,7 +274,7 @@ impl Settings {
.map_or(false, |meta| !meta.is_file())
});
if !blocking_stdin && atty::is(Stream::Stdin) {
if !blocking_stdin && std::io::stdin().is_terminal() {
show_warning!("following standard input indefinitely is ineffective");
}
}

View file

@ -17,7 +17,7 @@ path = "src/tty.rs"
[dependencies]
clap = { workspace=true }
nix = { workspace=true, features=["term"] }
atty = { workspace=true }
is-terminal = { workspace=true }
uucore = { workspace=true, features=["fs"] }
[[bin]]

View file

@ -10,6 +10,7 @@
// spell-checker:ignore (ToDO) ttyname filedesc
use clap::{crate_version, Arg, ArgAction, Command};
use is_terminal::IsTerminal;
use std::io::Write;
use std::os::unix::io::AsRawFd;
use uucore::error::{set_exit_code, UResult};
@ -30,7 +31,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
// If silent, we don't need the name, only whether or not stdin is a tty.
if silent {
return if atty::is(atty::Stream::Stdin) {
return if std::io::stdin().is_terminal() {
Ok(())
} else {
Err(1.into())

View file

@ -12,13 +12,13 @@ use std::io::{self, Result, Write};
use clap::{Arg, ArgAction, Command};
use uucore::error::{UResult, USimpleError};
use uucore::format_usage;
use uucore::{format_usage, help_about, help_usage};
#[cfg(any(target_os = "linux", target_os = "android"))]
mod splice;
const ABOUT: &str = "Repeatedly display a line with STRING (or 'y')";
const USAGE: &str = "{} [STRING]...";
const ABOUT: &str = help_about!("yes.md");
const USAGE: &str = help_usage!("yes.md");
// it's possible that using a smaller or larger buffer might provide better performance on some
// systems, but honestly this is good enough

7
src/uu/yes/yes.md Normal file
View file

@ -0,0 +1,7 @@
# yes
```
yes [STRING]...
```
Repeatedly display a line with STRING (or 'y')

View file

@ -192,13 +192,9 @@ impl Utmpx {
}
/// A.K.A. ut.ut_tv
pub fn login_time(&self) -> time::OffsetDateTime {
#[cfg(all(not(target_os = "freebsd"), not(target_vendor = "apple")))]
let ts_nanos: i128 = (self.inner.ut_tv.tv_sec as i64 * 1_000_000_000_i64
+ self.inner.ut_tv.tv_usec as i64 * 1_000_i64)
.into();
#[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
let ts_nanos: i128 = (self.inner.ut_tv.tv_sec * 1_000_000_000_i64
+ self.inner.ut_tv.tv_usec as i64 * 1_000_i64)
#[allow(clippy::unnecessary_cast)]
let ts_nanos: i128 = (1_000_000_000_i64 * self.inner.ut_tv.tv_sec as i64
+ 1_000_i64 * self.inner.ut_tv.tv_usec as i64)
.into();
let local_offset = time::OffsetDateTime::now_local().unwrap().offset();
time::OffsetDateTime::from_unix_timestamp_nanos(ts_nanos)

View file

@ -51,7 +51,10 @@ pub fn from_str(string: &str) -> Result<Duration, String> {
if len == 0 {
return Err("empty string".to_owned());
}
let slice = &string[..len - 1];
let slice = match string.get(..len - 1) {
Some(s) => s,
None => return Err(format!("invalid time interval {}", string.quote())),
};
let (numstr, times) = match string.chars().next_back().unwrap() {
's' => (slice, 1),
'm' => (slice, 60),
@ -112,6 +115,11 @@ mod tests {
assert!(from_str("123X").is_err());
}
#[test]
fn test_error_multi_bytes_characters() {
assert!(from_str("10€").is_err());
}
#[test]
fn test_error_invalid_magnitude() {
assert!(from_str("12abc3s").is_err());

View file

@ -7,6 +7,8 @@ use std::{fs::File, io::Read, path::PathBuf};
use proc_macro::{Literal, TokenStream, TokenTree};
use quote::quote;
const MARKDOWN_CODE_FENCES: &str = "```";
//## rust proc-macro background info
//* ref: <https://dev.to/naufraghi/procedural-macro-in-rust-101-k3f> @@ <http://archive.is/Vbr5e>
//* ref: [path construction from LitStr](https://oschwald.github.io/maxminddb-rust/syn/struct.LitStr.html) @@ <http://archive.is/8YDua>
@ -51,7 +53,19 @@ fn render_markdown(s: &str) -> String {
s.replace('`', "")
}
/// Get the usage from the "Usage" section in the help file.
/// Get the about text from the help file.
///
/// The about text is assumed to be the text between the first markdown
/// code block and the next header, if any. It may span multiple lines.
#[proc_macro]
pub fn help_about(input: TokenStream) -> TokenStream {
let input: Vec<TokenTree> = input.into_iter().collect();
let filename = get_argument(&input, 0, "filename");
let text: String = parse_about(&read_help(&filename));
TokenTree::Literal(Literal::string(&text)).into()
}
/// Get the usage from the help file.
///
/// The usage is assumed to be surrounded by markdown code fences. It may span
/// multiple lines. The first word of each line is assumed to be the name of
@ -61,7 +75,7 @@ fn render_markdown(s: &str) -> String {
pub fn help_usage(input: TokenStream) -> TokenStream {
let input: Vec<TokenTree> = input.into_iter().collect();
let filename = get_argument(&input, 0, "filename");
let text: String = parse_usage(&parse_help("usage", &filename));
let text: String = parse_usage(&read_help(&filename));
TokenTree::Literal(Literal::string(&text)).into()
}
@ -94,7 +108,7 @@ pub fn help_section(input: TokenStream) -> TokenStream {
let input: Vec<TokenTree> = input.into_iter().collect();
let section = get_argument(&input, 0, "section");
let filename = get_argument(&input, 1, "filename");
let text = parse_help(&section, &filename);
let text = parse_help_section(&section, &read_help(&filename));
let rendered = render_markdown(&text);
TokenTree::Literal(Literal::string(&rendered)).into()
}
@ -121,13 +135,11 @@ fn get_argument(input: &[TokenTree], index: usize, name: &str) -> String {
.to_string()
}
/// Read the help file and extract a section
fn parse_help(section: &str, filename: &str) -> String {
let section = section.to_lowercase();
let section = section.trim_matches('"');
/// Read the help file
fn read_help(filename: &str) -> String {
let mut content = String::new();
let mut path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let mut path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
path.push(filename);
File::open(path)
@ -135,7 +147,7 @@ fn parse_help(section: &str, filename: &str) -> String {
.read_to_string(&mut content)
.unwrap();
parse_help_section(section, &content)
content
}
/// Get a single section from content
@ -147,6 +159,8 @@ fn parse_help_section(section: &str, content: &str) -> String {
.map_or(false, |l| l.trim().to_lowercase() == section)
}
let section = &section.to_lowercase();
// We cannot distinguish between an empty or non-existing section below,
// so we do a quick test to check whether the section exists to provide
// a nice error message.
@ -167,17 +181,17 @@ fn parse_help_section(section: &str, content: &str) -> String {
.to_string()
}
/// Parses a markdown code block into a usage string
/// Parses the first markdown code block into a usage string
///
/// The code fences are removed and the name of the util is replaced
/// with `{}` so that it can be replaced with the appropriate name
/// at runtime.
fn parse_usage(content: &str) -> String {
content
.strip_suffix("```")
.unwrap()
.lines()
.skip(1) // Skip the "```" of markdown syntax
.skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES))
.skip(1)
.take_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES))
.map(|l| {
// Replace the util name (assumed to be the first word) with "{}"
// to be replaced with the runtime value later.
@ -187,12 +201,31 @@ fn parse_usage(content: &str) -> String {
"{}\n".to_string()
}
})
.collect()
.collect::<Vec<_>>()
.join("")
.trim()
.to_string()
}
/// Parses the text between the first markdown code block and the next header, if any,
/// into an about string.
fn parse_about(content: &str) -> String {
content
.lines()
.skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES))
.skip(1)
.skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES))
.skip(1)
.take_while(|l| !l.starts_with('#'))
.collect::<Vec<_>>()
.join("\n")
.trim()
.to_string()
}
#[cfg(test)]
mod tests {
use super::{parse_help_section, parse_usage};
use super::{parse_about, parse_help_section, parse_usage};
#[test]
fn section_parsing() {
@ -209,6 +242,10 @@ mod tests {
parse_help_section("some section", input),
"This is some section"
);
assert_eq!(
parse_help_section("SOME SECTION", input),
"This is some section"
);
assert_eq!(
parse_help_section("another section", input),
"This is the other section\nwith multiple lines"
@ -233,7 +270,6 @@ mod tests {
fn usage_parsing() {
let input = "\
# ls\n\
## Usage\n\
```\n\
ls -l\n\
```\n\
@ -244,17 +280,55 @@ mod tests {
This is the other section\n\
with multiple lines\n";
assert_eq!(parse_usage(&parse_help_section("usage", input)), "{} -l",);
assert_eq!(parse_usage(input), "{} -l");
}
assert_eq!(
parse_usage(
"\
```\n\
util [some] [options]\n\
```\
"
),
"{} [some] [options]"
);
#[test]
fn multi_line_usage_parsing() {
let input = "\
# ls\n\
```\n\
ls -a\n\
ls -b\n\
ls -c\n\
```\n\
## some section\n\
This is some section\n";
assert_eq!(parse_usage(input), "{} -a\n{} -b\n{} -c");
}
#[test]
fn about_parsing() {
let input = "\
# ls\n\
```\n\
ls -l\n\
```\n\
\n\
This is the about section\n\
\n\
## some section\n\
This is some section\n";
assert_eq!(parse_about(input), "This is the about section");
}
#[test]
fn multi_line_about_parsing() {
let input = "\
# ls\n\
```\n\
ls -l\n\
```\n\
\n\
about a\n\
\n\
about b\n\
\n\
## some section\n\
This is some section\n";
assert_eq!(parse_about(input), "about a\n\nabout b");
}
}

View file

@ -103,7 +103,7 @@ fn test_closes_file_descriptors() {
"alpha.txt",
"alpha.txt",
])
.with_limit(Resource::NOFILE, 9, 9)
.limit(Resource::NOFILE, 9, 9)
.succeeds();
}

View file

@ -48,15 +48,12 @@ fn run_single_test(test: &TestCase, at: &AtPath, mut ucmd: UCommand) {
let r = ucmd.run();
if !r.succeeded() {
println!("{}", r.stderr_str());
panic!("{:?}: failed", ucmd.raw);
panic!("{ucmd}: failed");
}
let perms = at.metadata(TEST_FILE).permissions().mode();
if perms != test.after {
panic!(
"{:?}: expected: {:o} got: {:o}",
ucmd.raw, test.after, perms
);
panic!("{}: expected: {:o} got: {:o}", ucmd, test.after, perms);
}
}
@ -414,7 +411,7 @@ fn test_chmod_symlink_non_existing_file() {
let non_existing = "test_chmod_symlink_non_existing_file";
let test_symlink = "test_chmod_symlink_non_existing_file_symlink";
let expected_stdout = &format!(
"failed to change mode of '{test_symlink}' from 0000 (---------) to 0000 (---------)"
"failed to change mode of '{test_symlink}' from 0000 (---------) to 1500 (r-x-----T)"
);
let expected_stderr = &format!("cannot operate on dangling symlink '{test_symlink}'");
@ -442,6 +439,17 @@ fn test_chmod_symlink_non_existing_file() {
.code_is(1)
.no_stderr()
.stdout_contains(expected_stdout);
// this should only include the dangling symlink message
// NOT the failure to change mode
scene
.ucmd()
.arg("755")
.arg(test_symlink)
.run()
.code_is(1)
.no_stdout()
.stderr_contains(expected_stderr);
}
#[test]
@ -574,3 +582,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 1500 (r-x-----T)"
);
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

@ -396,7 +396,7 @@ fn test_chown_only_user_id() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let result = scene.cmd_keepenv("id").arg("-u").run();
let result = scene.cmd("id").keep_env().arg("-u").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return;
}
@ -430,7 +430,7 @@ fn test_chown_fail_id() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let result = scene.cmd_keepenv("id").arg("-u").run();
let result = scene.cmd("id").keep_env().arg("-u").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return;
}
@ -487,7 +487,7 @@ fn test_chown_only_group_id() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let result = scene.cmd_keepenv("id").arg("-g").run();
let result = scene.cmd("id").keep_env().arg("-g").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return;
}
@ -551,14 +551,14 @@ fn test_chown_owner_group_id() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let result = scene.cmd_keepenv("id").arg("-u").run();
let result = scene.cmd("id").keep_env().arg("-u").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return;
}
let user_id = String::from(result.stdout_str().trim());
assert!(!user_id.is_empty());
let result = scene.cmd_keepenv("id").arg("-g").run();
let result = scene.cmd("id").keep_env().arg("-g").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return;
}
@ -612,14 +612,14 @@ fn test_chown_owner_group_mix() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let result = scene.cmd_keepenv("id").arg("-u").run();
let result = scene.cmd("id").keep_env().arg("-u").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return;
}
let user_id = String::from(result.stdout_str().trim());
assert!(!user_id.is_empty());
let result = scene.cmd_keepenv("id").arg("-gn").run();
let result = scene.cmd("id").keep_env().arg("-gn").run();
if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return;
}

View file

@ -1545,7 +1545,7 @@ fn test_closes_file_descriptors() {
.arg("--reflink=auto")
.arg("dir_with_10_files/")
.arg("dir_with_10_files_new/")
.with_limit(Resource::NOFILE, limit_fd, limit_fd)
.limit(Resource::NOFILE, limit_fd, limit_fd)
.succeeds();
}
@ -1692,7 +1692,8 @@ fn test_cp_reflink_always_override() {
.succeeds();
if !scene
.cmd_keepenv("env")
.cmd("env")
.keep_env()
.args(&["mkfs.btrfs", "--rootdir", ROOTDIR, DISK])
.run()
.succeeded()
@ -1704,7 +1705,8 @@ fn test_cp_reflink_always_override() {
scene.fixtures.mkdir(MOUNTPOINT);
let mount = scene
.cmd_keepenv("sudo")
.cmd("sudo")
.keep_env()
.args(&["-E", "--non-interactive", "mount", DISK, MOUNTPOINT])
.run();
@ -1730,7 +1732,8 @@ fn test_cp_reflink_always_override() {
.succeeds();
scene
.cmd_keepenv("sudo")
.cmd("sudo")
.keep_env()
.args(&["-E", "--non-interactive", "umount", MOUNTPOINT])
.succeeds();
}
@ -2524,9 +2527,9 @@ fn test_src_base_dot() {
let at = ts.fixtures.clone();
at.mkdir("x");
at.mkdir("y");
let mut ucmd = UCommand::new(ts.bin_path, &Some(ts.util_name), at.plus("y"), true);
ucmd.args(&["--verbose", "-r", "../x/.", "."])
ts.ucmd()
.current_dir(at.plus("y"))
.args(&["--verbose", "-r", "../x/.", "."])
.succeeds()
.no_stderr()
.no_stdout();

View file

@ -5,6 +5,8 @@ use crate::common::util::*;
use std::fs::{File, OpenOptions};
use std::io::{BufReader, Read, Write};
use std::path::PathBuf;
#[cfg(all(not(windows), not(target_os = "macos")))]
use std::process::{Command, Stdio};
#[cfg(not(windows))]
use std::thread::sleep;
#[cfg(not(windows))]
@ -1442,3 +1444,73 @@ fn test_sparse() {
// number of blocks stored on disk may be zero.
assert_eq!(at.metadata("infile").len(), at.metadata("outfile").len());
}
// TODO These FIFO tests should work on macos, but some issue is
// causing our implementation of dd to wait indefinitely when it
// shouldn't.
/// Test that a seek on an output FIFO results in a read.
#[test]
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "freebsd")))]
fn test_seek_output_fifo() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.mkfifo("fifo");
// TODO When `dd` is a bit more advanced, we could use the uutils
// version of dd here as well.
let child = Command::new("dd")
.current_dir(&at.subdir)
.args([
"count=1",
"if=/dev/zero",
&format!("of={}", at.plus_as_string("fifo")),
"status=noxfer",
])
.stderr(Stdio::piped())
.spawn()
.expect("failed to execute child process");
ts.ucmd()
.args(&["count=0", "seek=1", "of=fifo", "status=noxfer"])
.succeeds()
.stderr_only("0+0 records in\n0+0 records out\n");
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
assert!(output.stdout.is_empty());
assert_eq!(&output.stderr, b"1+0 records in\n1+0 records out\n");
}
/// Test that a skip on an input FIFO results in a read.
#[test]
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "freebsd")))]
fn test_skip_input_fifo() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.mkfifo("fifo");
// TODO When `dd` is a bit more advanced, we could use the uutils
// version of dd here as well.
let child = Command::new("dd")
.current_dir(&at.subdir)
.args([
"count=1",
"if=/dev/zero",
&format!("of={}", at.plus_as_string("fifo")),
"status=noxfer",
])
.stderr(Stdio::piped())
.spawn()
.expect("failed to execute child process");
ts.ucmd()
.args(&["count=0", "skip=1", "if=fifo", "status=noxfer"])
.succeeds()
.stderr_only("0+0 records in\n0+0 records out\n");
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
assert!(output.stdout.is_empty());
assert_eq!(&output.stderr, b"1+0 records in\n1+0 records out\n");
}

View file

@ -156,7 +156,8 @@ fn test_unset_variable() {
// This test depends on the HOME variable being pre-defined by the
// default shell
let out = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.arg("-u")
.arg("HOME")
.succeeds()

View file

@ -124,6 +124,37 @@ fn test_install_ancestors_mode_directories() {
assert_eq!(0o40_200_u32, at.metadata(target_dir).permissions().mode());
}
#[test]
fn test_install_ancestors_mode_directories_with_file() {
let (at, mut ucmd) = at_and_ucmd!();
let ancestor1 = "ancestor1";
let ancestor2 = "ancestor1/ancestor2";
let target_file = "ancestor1/ancestor2/target_file";
let directories_arg = "-D";
let mode_arg = "--mode=200";
let file = "file";
let probe = "probe";
at.mkdir(probe);
let default_perms = at.metadata(probe).permissions().mode();
at.touch(file);
ucmd.args(&[mode_arg, directories_arg, file, target_file])
.succeeds()
.no_stderr();
assert!(at.dir_exists(ancestor1));
assert!(at.dir_exists(ancestor2));
assert!(at.file_exists(target_file));
assert_eq!(default_perms, at.metadata(ancestor1).permissions().mode());
assert_eq!(default_perms, at.metadata(ancestor2).permissions().mode());
// Expected mode only on the target_file.
assert_eq!(0o100_200_u32, at.metadata(target_file).permissions().mode());
}
#[test]
fn test_install_parent_directories() {
let (at, mut ucmd) = at_and_ucmd!();
@ -1369,6 +1400,100 @@ fn test_install_dir_req_verbose() {
.stdout_contains("install: creating directory 'sub5/a'\ninstall: creating directory 'sub5/a/b'\ninstall: creating directory 'sub5/a/b/c'\n'source_file1' -> 'sub5/a/b/c/file'");
}
#[test]
fn test_install_chown_file_invalid() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let file_1 = "source_file1";
at.touch(file_1);
scene
.ucmd()
.arg("-o")
.arg("test_invalid_user")
.arg(file_1)
.arg("target_file1")
.fails()
.stderr_contains("install: invalid user: 'test_invalid_user'");
scene
.ucmd()
.arg("-g")
.arg("test_invalid_group")
.arg(file_1)
.arg("target_file1")
.fails()
.stderr_contains("install: invalid group: 'test_invalid_group'");
scene
.ucmd()
.arg("-o")
.arg("test_invalid_user")
.arg("-g")
.arg("test_invalid_group")
.arg(file_1)
.arg("target_file1")
.fails()
.stderr_contains("install: invalid user: 'test_invalid_user'");
scene
.ucmd()
.arg("-g")
.arg("test_invalid_group")
.arg("-o")
.arg("test_invalid_user")
.arg(file_1)
.arg("target_file1")
.fails()
.stderr_contains("install: invalid user: 'test_invalid_user'");
}
#[test]
fn test_install_chown_directory_invalid() {
let scene = TestScenario::new(util_name!());
scene
.ucmd()
.arg("-o")
.arg("test_invalid_user")
.arg("-d")
.arg("dir1/dir2")
.fails()
.stderr_contains("install: invalid user: 'test_invalid_user'");
scene
.ucmd()
.arg("-g")
.arg("test_invalid_group")
.arg("-d")
.arg("dir1/dir2")
.fails()
.stderr_contains("install: invalid group: 'test_invalid_group'");
scene
.ucmd()
.arg("-o")
.arg("test_invalid_user")
.arg("-g")
.arg("test_invalid_group")
.arg("-d")
.arg("dir1/dir2")
.fails()
.stderr_contains("install: invalid user: 'test_invalid_user'");
scene
.ucmd()
.arg("-g")
.arg("test_invalid_group")
.arg("-o")
.arg("test_invalid_user")
.arg("-d")
.arg("dir1/dir2")
.fails()
.stderr_contains("install: invalid user: 'test_invalid_user'");
}
#[test]
fn test_install_compare_option() {
let scene = TestScenario::new(util_name!());

View file

@ -426,7 +426,8 @@ fn test_mktemp_tmpdir_one_arg() {
let scene = TestScenario::new(util_name!());
let result = scene
.ucmd_keepenv()
.ucmd()
.keep_env()
.arg("--tmpdir")
.arg("apt-key-gpghome.XXXXXXXXXX")
.succeeds();
@ -439,7 +440,8 @@ fn test_mktemp_directory_tmpdir() {
let scene = TestScenario::new(util_name!());
let result = scene
.ucmd_keepenv()
.ucmd()
.keep_env()
.arg("--directory")
.arg("--tmpdir")
.arg("apt-key-gpghome.XXXXXXXXXX")

View file

@ -1,9 +1,10 @@
use crate::common::util::*;
use is_terminal::IsTerminal;
#[test]
fn test_more_no_arg() {
// Reading from stdin is now supported, so this must succeed
if atty::is(atty::Stream::Stdout) {
if std::io::stdout().is_terminal() {
new_ucmd!().succeeds();
} else {
}
@ -14,7 +15,7 @@ fn test_more_dir_arg() {
// Run the test only if there's a valid terminal, else do nothing
// Maybe we could capture the error, i.e. "Device not found" in that case
// but I am leaving this for later
if atty::is(atty::Stream::Stdout) {
if std::io::stdout().is_terminal() {
new_ucmd!()
.arg(".")
.fails()

View file

@ -20,7 +20,8 @@ fn test_nproc_all_omp() {
assert!(nproc > 0);
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "60")
.succeeds();
@ -28,7 +29,8 @@ fn test_nproc_all_omp() {
assert_eq!(nproc_omp, 60);
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "1") // Has no effect
.arg("--all")
.succeeds();
@ -37,7 +39,8 @@ fn test_nproc_all_omp() {
// If the parsing fails, returns the number of CPU
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "incorrectnumber") // returns the number CPU
.succeeds();
let nproc_omp: u8 = result.stdout_str().trim().parse().unwrap();
@ -51,7 +54,8 @@ fn test_nproc_ignore() {
if nproc_total > 1 {
// Ignore all CPU but one
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.arg("--ignore")
.arg((nproc_total - 1).to_string())
.succeeds();
@ -59,7 +63,8 @@ fn test_nproc_ignore() {
assert_eq!(nproc, 1);
// Ignore all CPU but one with a string
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.arg("--ignore= 1")
.succeeds();
let nproc: u8 = result.stdout_str().trim().parse().unwrap();
@ -70,7 +75,8 @@ fn test_nproc_ignore() {
#[test]
fn test_nproc_ignore_all_omp() {
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "42")
.arg("--ignore=40")
.succeeds();
@ -81,7 +87,8 @@ fn test_nproc_ignore_all_omp() {
#[test]
fn test_nproc_omp_limit() {
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "42")
.env("OMP_THREAD_LIMIT", "0")
.succeeds();
@ -89,7 +96,8 @@ fn test_nproc_omp_limit() {
assert_eq!(nproc, 42);
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "42")
.env("OMP_THREAD_LIMIT", "2")
.succeeds();
@ -97,7 +105,8 @@ fn test_nproc_omp_limit() {
assert_eq!(nproc, 2);
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "42")
.env("OMP_THREAD_LIMIT", "2bad")
.succeeds();
@ -109,14 +118,16 @@ fn test_nproc_omp_limit() {
assert!(nproc_system > 0);
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_THREAD_LIMIT", "1")
.succeeds();
let nproc: u8 = result.stdout_str().trim().parse().unwrap();
assert_eq!(nproc, 1);
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "0")
.env("OMP_THREAD_LIMIT", "")
.succeeds();
@ -124,7 +135,8 @@ fn test_nproc_omp_limit() {
assert_eq!(nproc, nproc_system);
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "")
.env("OMP_THREAD_LIMIT", "")
.succeeds();
@ -132,7 +144,8 @@ fn test_nproc_omp_limit() {
assert_eq!(nproc, nproc_system);
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "2,2,1")
.env("OMP_THREAD_LIMIT", "")
.succeeds();
@ -140,7 +153,8 @@ fn test_nproc_omp_limit() {
assert_eq!(2, nproc);
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "2,ignored")
.env("OMP_THREAD_LIMIT", "")
.succeeds();
@ -148,7 +162,8 @@ fn test_nproc_omp_limit() {
assert_eq!(2, nproc);
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "2,2,1")
.env("OMP_THREAD_LIMIT", "0")
.succeeds();
@ -156,7 +171,8 @@ fn test_nproc_omp_limit() {
assert_eq!(2, nproc);
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "2,2,1")
.env("OMP_THREAD_LIMIT", "1bad")
.succeeds();
@ -164,7 +180,8 @@ fn test_nproc_omp_limit() {
assert_eq!(2, nproc);
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.env("OMP_NUM_THREADS", "29,2,1")
.env("OMP_THREAD_LIMIT", "1bad")
.succeeds();

View file

@ -8,7 +8,8 @@ fn test_get_all() {
assert_eq!(env::var(key), Ok("VALUE".to_string()));
TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.succeeds()
.stdout_contains("HOME=")
.stdout_contains("KEY=VALUE");
@ -21,7 +22,8 @@ fn test_get_var() {
assert_eq!(env::var(key), Ok("VALUE".to_string()));
let result = TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.arg("KEY")
.succeeds();

View file

@ -60,7 +60,7 @@ fn symlinked_env() -> Env {
// Note: on Windows this requires admin permissions
at.symlink_dir("subdir", "symdir");
let root = PathBuf::from(at.root_dir_resolved());
ucmd.raw.current_dir(root.join("symdir"));
ucmd.current_dir(root.join("symdir"));
#[cfg(not(windows))]
ucmd.env("PWD", root.join("symdir"));
Env {

View file

@ -31,7 +31,8 @@ fn test_buffer_sizes() {
let buffer_sizes = ["0", "50K", "50k", "1M", "100M"];
for buffer_size in &buffer_sizes {
TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.arg("-n")
.arg("-S")
.arg(buffer_size)
@ -44,7 +45,8 @@ fn test_buffer_sizes() {
let buffer_sizes = ["1000G", "10T"];
for buffer_size in &buffer_sizes {
TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.arg("-n")
.arg("-S")
.arg(buffer_size)
@ -918,7 +920,8 @@ fn test_compress_merge() {
fn test_compress_fail() {
#[cfg(not(windows))]
TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.args(&[
"ext_sort.txt",
"-n",
@ -934,7 +937,8 @@ fn test_compress_fail() {
// So, don't check the output
#[cfg(windows)]
TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.args(&[
"ext_sort.txt",
"-n",
@ -949,7 +953,8 @@ fn test_compress_fail() {
#[test]
fn test_merge_batches() {
TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.timeout(Duration::from_secs(120))
.args(&["ext_sort.txt", "-n", "-S", "150b"])
.succeeds()
@ -959,7 +964,8 @@ fn test_merge_batches() {
#[test]
fn test_merge_batch_size() {
TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.arg("--batch-size=2")
.arg("-m")
.arg("--unique")
@ -1067,7 +1073,8 @@ fn test_output_is_input() {
at.touch("file");
at.append("file", input);
scene
.ucmd_keepenv()
.ucmd()
.keep_env()
.args(&["-m", "-u", "-o", "file", "file", "file", "file"])
.succeeds();
assert_eq!(at.read("file"), input);

View file

@ -300,19 +300,15 @@ fn test_invalid_utf8_integer_compare() {
let source = [0x66, 0x6f, 0x80, 0x6f];
let arg = OsStr::from_bytes(&source[..]);
let mut cmd = new_ucmd!();
cmd.arg("123").arg("-ne");
cmd.raw.arg(arg);
cmd.run()
new_ucmd!()
.args(&[OsStr::new("123"), OsStr::new("-ne"), arg])
.run()
.code_is(2)
.stderr_is("test: invalid integer $'fo\\x80o'\n");
let mut cmd = new_ucmd!();
cmd.raw.arg(arg);
cmd.arg("-eq").arg("456");
cmd.run()
new_ucmd!()
.args(&[arg, OsStr::new("-eq"), OsStr::new("456")])
.run()
.code_is(2)
.stderr_is("test: invalid integer $'fo\\x80o'\n");
}

View file

@ -10,7 +10,8 @@ fn test_invalid_arg() {
#[test]
fn test_uptime() {
TestScenario::new(util_name!())
.ucmd_keepenv()
.ucmd()
.keep_env()
.succeeds()
.stdout_contains("load average:")
.stdout_contains(" up ");

View file

@ -22,7 +22,8 @@ fn test_users_check_name() {
// note: clippy::needless_borrow *false positive*
#[allow(clippy::needless_borrow)]
let expected = TestScenario::new(&util_name)
.cmd_keepenv(util_name)
.cmd(util_name)
.keep_env()
.env("LC_ALL", "C")
.succeeds()
.stdout_move_str();

View file

@ -3,19 +3,20 @@
// * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code.
//spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd
//spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized
#![allow(dead_code)]
use pretty_assertions::assert_eq;
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "android"))]
use rlimit::prlimit;
use rstest::rstest;
#[cfg(unix)]
use std::borrow::Cow;
use std::collections::VecDeque;
#[cfg(not(windows))]
use std::ffi::CString;
use std::ffi::OsStr;
use std::ffi::{OsStr, OsString};
use std::fs::{self, hard_link, remove_file, File, OpenOptions};
use std::io::{self, BufWriter, Read, Result, Write};
#[cfg(unix)]
@ -34,7 +35,6 @@ use std::thread::{sleep, JoinHandle};
use std::time::{Duration, Instant};
use std::{env, hint, thread};
use tempfile::{Builder, TempDir};
use uucore::Args;
static TESTS_DIR: &str = "tests";
static FIXTURES_DIR: &str = "fixtures";
@ -46,6 +46,8 @@ static MULTIPLE_STDIN_MEANINGLESS: &str = "Ucommand is designed around a typical
static NO_STDIN_MEANINGLESS: &str = "Setting this flag has no effect if there is no stdin";
pub const TESTS_BINARY: &str = env!("CARGO_BIN_EXE_coreutils");
/// Test if the program is running under CI
pub fn is_ci() -> bool {
std::env::var("CI")
@ -64,7 +66,7 @@ fn read_scenario_fixture<S: AsRef<OsStr>>(tmpd: &Option<Rc<TempDir>>, file_rel_p
#[derive(Debug, Clone)]
pub struct CmdResult {
/// bin_path provided by `TestScenario` or `UCommand`
bin_path: String,
bin_path: PathBuf,
/// util_name provided by `TestScenario` or `UCommand`
util_name: Option<String>,
//tmpd is used for convenience functions for asserts against fixtures
@ -78,21 +80,23 @@ pub struct CmdResult {
}
impl CmdResult {
pub fn new<T, U>(
bin_path: String,
util_name: Option<String>,
pub fn new<S, T, U, V>(
bin_path: S,
util_name: Option<T>,
tmpd: Option<Rc<TempDir>>,
exit_status: Option<ExitStatus>,
stdout: T,
stderr: U,
stdout: U,
stderr: V,
) -> Self
where
T: Into<Vec<u8>>,
S: Into<PathBuf>,
T: AsRef<str>,
U: Into<Vec<u8>>,
V: Into<Vec<u8>>,
{
Self {
bin_path,
util_name,
bin_path: bin_path.into(),
util_name: util_name.map(|s| s.as_ref().into()),
tmpd,
exit_status,
stdout: stdout.into(),
@ -634,7 +638,7 @@ impl CmdResult {
self.stderr_only(format!(
"{0}: {2}\nTry '{1} {0} --help' for more information.\n",
self.util_name.as_ref().unwrap(), // This shouldn't be called using a normal command
self.bin_path,
self.bin_path.display(),
msg.as_ref()
))
}
@ -1093,18 +1097,21 @@ pub struct TestScenario {
}
impl TestScenario {
pub fn new(util_name: &str) -> Self {
pub fn new<T>(util_name: T) -> Self
where
T: AsRef<str>,
{
let tmpd = Rc::new(TempDir::new().unwrap());
let ts = Self {
bin_path: PathBuf::from(env!("CARGO_BIN_EXE_coreutils")),
util_name: String::from(util_name),
bin_path: PathBuf::from(TESTS_BINARY),
util_name: util_name.as_ref().into(),
fixtures: AtPath::new(tmpd.as_ref().path()),
tmpd,
};
let mut fixture_path_builder = env::current_dir().unwrap();
fixture_path_builder.push(TESTS_DIR);
fixture_path_builder.push(FIXTURES_DIR);
fixture_path_builder.push(util_name);
fixture_path_builder.push(util_name.as_ref());
if let Ok(m) = fs::metadata(&fixture_path_builder) {
if m.is_dir() {
recursive_copy(&fixture_path_builder, &ts.fixtures.subdir).unwrap();
@ -1116,58 +1123,50 @@ impl TestScenario {
/// Returns builder for invoking the target uutils binary. Paths given are
/// treated relative to the environment's unique temporary test directory.
pub fn ucmd(&self) -> UCommand {
self.composite_cmd(&self.bin_path, &self.util_name, true)
}
/// Returns builder for invoking the target uutils binary. Paths given are
/// treated relative to the environment's unique temporary test directory.
pub fn composite_cmd<S: AsRef<OsStr>, T: AsRef<OsStr>>(
&self,
bin: S,
util_name: T,
env_clear: bool,
) -> UCommand {
UCommand::new_from_tmp(bin, &Some(util_name), self.tmpd.clone(), env_clear)
UCommand::from_test_scenario(self)
}
/// Returns builder for invoking any system command. Paths given are treated
/// relative to the environment's unique temporary test directory.
pub fn cmd<S: AsRef<OsStr>>(&self, bin: S) -> UCommand {
UCommand::new_from_tmp::<S, S>(bin, &None, self.tmpd.clone(), true)
pub fn cmd<S: Into<PathBuf>>(&self, bin_path: S) -> UCommand {
let mut command = UCommand::new();
command.bin_path(bin_path);
command.temp_dir(self.tmpd.clone());
command
}
/// Returns builder for invoking any uutils command. Paths given are treated
/// relative to the environment's unique temporary test directory.
pub fn ccmd<S: AsRef<OsStr>>(&self, bin: S) -> UCommand {
self.composite_cmd(&self.bin_path, bin, true)
}
// different names are used rather than an argument
// because the need to keep the environment is exceedingly rare.
pub fn ucmd_keepenv(&self) -> UCommand {
self.composite_cmd(&self.bin_path, &self.util_name, false)
}
/// Returns builder for invoking any system command. Paths given are treated
/// relative to the environment's unique temporary test directory.
/// Differs from the builder returned by `cmd` in that `cmd_keepenv` does not call
/// `Command::env_clear` (Clears the entire environment map for the child process.)
pub fn cmd_keepenv<S: AsRef<OsStr>>(&self, bin: S) -> UCommand {
UCommand::new_from_tmp::<S, S>(bin, &None, self.tmpd.clone(), false)
pub fn ccmd<S: AsRef<str>>(&self, util_name: S) -> UCommand {
UCommand::with_util(util_name, self.tmpd.clone())
}
}
/// A `UCommand` is a wrapper around an individual Command that provides several additional features
/// A `UCommand` is a builder wrapping an individual Command that provides several additional features:
/// 1. it has convenience functions that are more ergonomic to use for piping in stdin, spawning the command
/// and asserting on the results.
/// 2. it tracks arguments provided so that in test cases which may provide variations of an arg in loops
/// the test failure can display the exact call which preceded an assertion failure.
/// 3. it provides convenience construction arguments to set the Command working directory and/or clear its environment.
#[derive(Debug)]
/// 3. it provides convenience construction methods to set the Command uutils utility and temporary directory.
///
/// Per default `UCommand` runs a command given as an argument in a shell, platform independently.
/// It does so with safety in mind, so the working directory is set to an individual temporary
/// directory and the environment variables are cleared per default.
///
/// The default behavior can be changed with builder methods:
/// * [`UCommand::with_util`]: Run `coreutils UTIL_NAME` instead of the shell
/// * [`UCommand::from_test_scenario`]: Run `coreutils UTIL_NAME` instead of the shell in the
/// temporary directory of the [`TestScenario`]
/// * [`UCommand::current_dir`]: Sets the working directory
/// * [`UCommand::keep_env`]: Keep environment variables instead of clearing them
/// * ...
#[derive(Debug, Default)]
pub struct UCommand {
pub raw: Command,
comm_string: String,
bin_path: String,
args: VecDeque<OsString>,
env_vars: Vec<(OsString, OsString)>,
current_dir: Option<PathBuf>,
env_clear: bool,
bin_path: Option<PathBuf>,
util_name: Option<String>,
has_run: bool,
ignore_stdin_write_error: bool,
@ -1183,72 +1182,80 @@ pub struct UCommand {
}
impl UCommand {
pub fn new<T: AsRef<OsStr>, S: AsRef<OsStr>, U: AsRef<OsStr>>(
bin_path: T,
util_name: &Option<S>,
curdir: U,
env_clear: bool,
) -> Self {
let bin_path = bin_path.as_ref();
let util_name = util_name.as_ref().map(std::convert::AsRef::as_ref);
let mut ucmd = Self {
tmpd: None,
has_run: false,
raw: {
let mut cmd = Command::new(bin_path);
cmd.current_dir(curdir.as_ref());
if env_clear {
cmd.env_clear();
if cfg!(windows) {
// spell-checker:ignore (dll) rsaenh
// %SYSTEMROOT% is required on Windows to initialize crypto provider
// ... and crypto provider is required for std::rand
// From `procmon`: RegQueryValue HKLM\SOFTWARE\Microsoft\Cryptography\Defaults\Provider\Microsoft Strong Cryptographic Provider\Image Path
// SUCCESS Type: REG_SZ, Length: 66, Data: %SystemRoot%\system32\rsaenh.dll"
if let Some(systemroot) = env::var_os("SYSTEMROOT") {
cmd.env("SYSTEMROOT", systemroot);
}
} else {
// if someone is setting LD_PRELOAD, there's probably a good reason for it
if let Some(ld_preload) = env::var_os("LD_PRELOAD") {
cmd.env("LD_PRELOAD", ld_preload);
}
}
}
cmd
},
comm_string: String::from(bin_path.to_str().unwrap()),
bin_path: bin_path.to_str().unwrap().to_string(),
util_name: util_name.map(|un| un.to_str().unwrap().to_string()),
ignore_stdin_write_error: false,
bytes_into_stdin: None,
stdin: None,
stdout: None,
stderr: None,
#[cfg(any(target_os = "linux", target_os = "android"))]
limits: vec![],
stderr_to_stdout: false,
timeout: Some(Duration::from_secs(30)),
};
if let Some(un) = util_name {
ucmd.arg(un);
/// Create a new plain [`UCommand`].
///
/// Executes a command that must be given as argument (for example with [`UCommand::arg`] in a
/// shell (`sh -c` on unix platforms or `cmd /C` on windows).
///
/// Per default the environment is cleared and the working directory is set to an individual
/// temporary directory for safety purposes.
pub fn new() -> Self {
Self {
env_clear: true,
..Default::default()
}
}
/// Create a [`UCommand`] for a specific uutils utility.
///
/// Sets the temporary directory to `tmpd` and the execution binary to the path where
/// `coreutils` is found.
pub fn with_util<T>(util_name: T, tmpd: Rc<TempDir>) -> Self
where
T: AsRef<str>,
{
let mut ucmd = Self::new();
ucmd.util_name = Some(util_name.as_ref().into());
ucmd.bin_path(TESTS_BINARY).temp_dir(tmpd);
ucmd
}
pub fn new_from_tmp<T: AsRef<OsStr>, S: AsRef<OsStr>>(
bin_path: T,
util_name: &Option<S>,
tmpd: Rc<TempDir>,
env_clear: bool,
) -> Self {
let tmpd_path_buf = String::from(tmpd.as_ref().path().to_str().unwrap());
let mut ucmd: Self = Self::new(bin_path, util_name, tmpd_path_buf, env_clear);
ucmd.tmpd = Some(tmpd);
ucmd
/// Create a [`UCommand`] from a [`TestScenario`].
///
/// The temporary directory and uutils utility are inherited from the [`TestScenario`] and the
/// execution binary is set to `coreutils`.
pub fn from_test_scenario(scene: &TestScenario) -> Self {
Self::with_util(&scene.util_name, scene.tmpd.clone())
}
/// Set the execution binary.
///
/// Make sure the binary found at this path is executable. It's safest to provide the
/// canonicalized path instead of just the name of the executable, since path resolution is not
/// guaranteed to work on all platforms.
fn bin_path<T>(&mut self, bin_path: T) -> &mut Self
where
T: Into<PathBuf>,
{
self.bin_path = Some(bin_path.into());
self
}
/// Set the temporary directory.
///
/// Per default an individual temporary directory is created for every [`UCommand`]. If not
/// specified otherwise with [`UCommand::current_dir`] the working directory is set to this
/// temporary directory.
fn temp_dir(&mut self, temp_dir: Rc<TempDir>) -> &mut Self {
self.tmpd = Some(temp_dir);
self
}
/// Keep the environment variables instead of clearing them before running the command.
pub fn keep_env(&mut self) -> &mut Self {
self.env_clear = false;
self
}
/// Set the working directory for this [`UCommand`]
///
/// Per default the working directory is set to the [`UCommands`] temporary directory.
pub fn current_dir<T>(&mut self, current_dir: T) -> &mut Self
where
T: Into<PathBuf>,
{
self.current_dir = Some(current_dir.into());
self
}
pub fn set_stdin<T: Into<Stdio>>(&mut self, stdin: T) -> &mut Self {
@ -1274,29 +1281,14 @@ impl UCommand {
/// Add a parameter to the invocation. Path arguments are treated relative
/// to the test environment directory.
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
assert!(!self.has_run, "{}", ALREADY_RUN);
self.comm_string.push(' ');
self.comm_string
.push_str(arg.as_ref().to_str().unwrap_or_default());
self.raw.arg(arg.as_ref());
self.args.push_back(arg.as_ref().into());
self
}
/// Add multiple parameters to the invocation. Path arguments are treated relative
/// to the test environment directory.
pub fn args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
assert!(!self.has_run, "{}", MULTIPLE_STDIN_MEANINGLESS);
let strings = args
.iter()
.map(|s| s.as_ref().to_os_string())
.collect_ignore();
for s in strings {
self.comm_string.push(' ');
self.comm_string.push_str(&s);
}
self.raw.args(args.as_ref());
self.args.extend(args.iter().map(|s| s.as_ref().into()));
self
}
@ -1331,13 +1323,13 @@ impl UCommand {
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
assert!(!self.has_run, "{}", ALREADY_RUN);
self.raw.env(key, val);
self.env_vars
.push((key.as_ref().into(), val.as_ref().into()));
self
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn with_limit(
pub fn limit(
&mut self,
resource: rlimit::Resource,
soft_limit: u64,
@ -1359,26 +1351,113 @@ impl UCommand {
self
}
/// Spawns the command, feeds the stdin if any, and returns the
/// child process immediately.
pub fn run_no_wait(&mut self) -> UChild {
assert!(!self.has_run, "{}", ALREADY_RUN);
self.has_run = true;
log_info("run", &self.comm_string);
/// Build the `std::process::Command` and apply the defaults on fields which were not specified
/// by the user.
///
/// These __defaults__ are:
/// * `bin_path`: Depending on the platform and os, the native shell (unix -> `/bin/sh` etc.).
/// This default also requires to set the first argument to `-c` on unix (`/C` on windows) if
/// this argument wasn't specified explicitly by the user.
/// * `util_name`: `None`. If neither `bin_path` nor `util_name` were given the arguments are
/// run in a shell (See `bin_path` above).
/// * `temp_dir`: If `current_dir` was not set, a new temporary directory will be created in
/// which this command will be run and `current_dir` will be set to this `temp_dir`.
/// * `current_dir`: The temporary directory given by `temp_dir`.
/// * `timeout`: `30 seconds`
/// * `env_clear`: `true`. (Almost) all environment variables will be cleared.
/// * `stdin`: `Stdio::null()`
/// * `ignore_stdin_write_error`: `false`
/// * `stdout`, `stderr`: If not specified the output will be captured with [`CapturedOutput`]
/// * `stderr_to_stdout`: `false`
/// * `bytes_into_stdin`: `None`
/// * `limits`: `None`.
fn build(&mut self) -> (Command, Option<CapturedOutput>, Option<CapturedOutput>) {
if self.bin_path.is_some() {
if let Some(util_name) = &self.util_name {
self.args.push_front(util_name.into());
}
} else if let Some(util_name) = &self.util_name {
self.bin_path = Some(PathBuf::from(TESTS_BINARY));
self.args.push_front(util_name.into());
// neither `bin_path` nor `util_name` was set so we apply the default to run the arguments
// in a platform specific shell
} else if cfg!(unix) {
#[cfg(target_os = "android")]
let bin_path = PathBuf::from("/system/bin/sh");
#[cfg(not(target_os = "android"))]
let bin_path = PathBuf::from("/bin/sh");
self.bin_path = Some(bin_path);
let c_arg = OsString::from("-c");
if !self.args.contains(&c_arg) {
self.args.push_front(c_arg);
}
} else {
self.bin_path = Some(PathBuf::from("cmd"));
let c_arg = OsString::from("/C");
let k_arg = OsString::from("/K");
if !self
.args
.iter()
.any(|s| s.eq_ignore_ascii_case(&c_arg) || s.eq_ignore_ascii_case(&k_arg))
{
self.args.push_front(c_arg);
}
};
// unwrap is safe here because we have set `self.bin_path` before
let mut command = Command::new(self.bin_path.as_ref().unwrap());
command.args(&self.args);
// We use a temporary directory as working directory if not specified otherwise with
// `current_dir()`. If neither `current_dir` nor a temporary directory is available, then we
// create our own.
if let Some(current_dir) = &self.current_dir {
command.current_dir(current_dir);
} else if let Some(temp_dir) = &self.tmpd {
command.current_dir(temp_dir.path());
} else {
let temp_dir = tempfile::tempdir().unwrap();
self.current_dir = Some(temp_dir.path().into());
command.current_dir(temp_dir.path());
self.tmpd = Some(Rc::new(temp_dir));
}
if self.env_clear {
command.env_clear();
if cfg!(windows) {
// spell-checker:ignore (dll) rsaenh
// %SYSTEMROOT% is required on Windows to initialize crypto provider
// ... and crypto provider is required for std::rand
// From `procmon`: RegQueryValue HKLM\SOFTWARE\Microsoft\Cryptography\Defaults\Provider\Microsoft Strong Cryptographic Provider\Image Path
// SUCCESS Type: REG_SZ, Length: 66, Data: %SystemRoot%\system32\rsaenh.dll"
if let Some(systemroot) = env::var_os("SYSTEMROOT") {
command.env("SYSTEMROOT", systemroot);
}
} else {
// if someone is setting LD_PRELOAD, there's probably a good reason for it
if let Some(ld_preload) = env::var_os("LD_PRELOAD") {
command.env("LD_PRELOAD", ld_preload);
}
}
}
command.envs(self.env_vars.iter().cloned());
if self.timeout.is_none() {
self.timeout = Some(Duration::from_secs(30));
}
let mut captured_stdout = None;
let mut captured_stderr = None;
let command = if self.stderr_to_stdout {
if self.stderr_to_stdout {
let mut output = CapturedOutput::default();
let command = self
.raw
command
.stdin(self.stdin.take().unwrap_or_else(Stdio::null))
.stdout(Stdio::from(output.try_clone().unwrap()))
.stderr(Stdio::from(output.try_clone().unwrap()));
captured_stdout = Some(output);
command
} else {
let stdout = if self.stdout.is_some() {
self.stdout.take().unwrap()
@ -1398,15 +1477,27 @@ impl UCommand {
stdio
};
self.raw
command
.stdin(self.stdin.take().unwrap_or_else(Stdio::null))
.stdout(stdout)
.stderr(stderr)
.stderr(stderr);
};
(command, captured_stdout, captured_stderr)
}
/// Spawns the command, feeds the stdin if any, and returns the
/// child process immediately.
pub fn run_no_wait(&mut self) -> UChild {
assert!(!self.has_run, "{}", ALREADY_RUN);
self.has_run = true;
let (mut command, captured_stdout, captured_stderr) = self.build();
log_info("run", self.to_string());
let child = command.spawn().unwrap();
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "android"))]
for &(resource, soft_limit, hard_limit) in &self.limits {
prlimit(
child.id() as i32,
@ -1465,6 +1556,17 @@ impl UCommand {
}
}
impl std::fmt::Display for UCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut comm_string: Vec<String> = vec![self
.bin_path
.as_ref()
.map_or(String::new(), |p| p.display().to_string())];
comm_string.extend(self.args.iter().map(|s| s.to_string_lossy().to_string()));
f.write_str(&comm_string.join(" "))
}
}
/// Stored the captured output in a temporary file. The file is deleted as soon as
/// [`CapturedOutput`] is dropped.
#[derive(Debug)]
@ -1597,14 +1699,14 @@ impl<'a> UChildAssertion<'a> {
self.uchild.stderr_exact_bytes(expected_stderr_size),
),
};
CmdResult {
bin_path: self.uchild.bin_path.clone(),
util_name: self.uchild.util_name.clone(),
tmpd: self.uchild.tmpd.clone(),
CmdResult::new(
self.uchild.bin_path.clone(),
self.uchild.util_name.clone(),
self.uchild.tmpd.clone(),
exit_status,
stdout,
stderr,
}
)
}
// Make assertions of [`CmdResult`] with all output from start of the process until now.
@ -1684,7 +1786,7 @@ impl<'a> UChildAssertion<'a> {
/// Abstraction for a [`std::process::Child`] to handle the child process.
pub struct UChild {
raw: Child,
bin_path: String,
bin_path: PathBuf,
util_name: Option<String>,
captured_stdout: Option<CapturedOutput>,
captured_stderr: Option<CapturedOutput>,
@ -1704,7 +1806,7 @@ impl UChild {
) -> Self {
Self {
raw: child,
bin_path: ucommand.bin_path.clone(),
bin_path: ucommand.bin_path.clone().unwrap(),
util_name: ucommand.util_name.clone(),
captured_stdout,
captured_stderr,
@ -2335,11 +2437,13 @@ fn parse_coreutil_version(version_string: &str) -> f32 {
///```
#[cfg(unix)]
pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result<CmdResult, String> {
println!("{}", check_coreutil_version(&ts.util_name, VERSION_MIN)?);
let util_name = &host_name_for(&ts.util_name);
let util_name = ts.util_name.as_str();
println!("{}", check_coreutil_version(util_name, VERSION_MIN)?);
let util_name = host_name_for(util_name);
let result = ts
.cmd_keepenv(util_name.as_ref())
.cmd(util_name.as_ref())
.keep_env()
.env("LC_ALL", "C")
.args(args)
.run();
@ -2411,7 +2515,8 @@ pub fn run_ucmd_as_root(
// we can run sudo and we're root
// run ucmd as root:
Ok(ts
.cmd_keepenv("sudo")
.cmd("sudo")
.keep_env()
.env("LC_ALL", "C")
.arg("-E")
.arg("--non-interactive")
@ -2439,30 +2544,8 @@ mod tests {
// spell-checker:ignore (tests) asdfsadfa
use super::*;
#[cfg(unix)]
pub fn run_cmd<T: AsRef<OsStr>>(cmd: T) -> CmdResult {
let mut ucmd = UCommand::new_from_tmp::<&str, String>(
"sh",
&None,
Rc::new(tempfile::tempdir().unwrap()),
true,
);
ucmd.arg("-c");
ucmd.arg(cmd);
ucmd.run()
}
#[cfg(windows)]
pub fn run_cmd<T: AsRef<OsStr>>(cmd: T) -> CmdResult {
let mut ucmd = UCommand::new_from_tmp::<&str, String>(
"cmd",
&None,
Rc::new(tempfile::tempdir().unwrap()),
true,
);
ucmd.arg("/C");
ucmd.arg(cmd);
ucmd.run()
UCommand::new().arg(cmd).run()
}
#[test]
@ -3200,4 +3283,52 @@ mod tests {
let ts = TestScenario::new("sleep");
ts.ucmd().timeout(Duration::from_secs(60)).arg("1.0").run();
}
#[cfg(feature = "echo")]
#[test]
fn test_ucommand_when_default() {
let shell_cmd = format!("{TESTS_BINARY} echo -n hello");
let mut command = UCommand::new();
command.arg(&shell_cmd).succeeds().stdout_is("hello");
#[cfg(target_os = "android")]
let (expected_bin, expected_arg) = (PathBuf::from("/system/bin/sh"), OsString::from("-c"));
#[cfg(all(unix, not(target_os = "android")))]
let (expected_bin, expected_arg) = (PathBuf::from("/bin/sh"), OsString::from("-c"));
#[cfg(windows)]
let (expected_bin, expected_arg) = (PathBuf::from("cmd"), OsString::from("/C"));
std::assert_eq!(&expected_bin, command.bin_path.as_ref().unwrap());
assert!(command.util_name.is_none());
std::assert_eq!(command.args, &[expected_arg, OsString::from(&shell_cmd)]);
assert!(command.tmpd.is_some());
}
#[cfg(feature = "echo")]
#[test]
fn test_ucommand_with_util() {
let tmpd = tempfile::tempdir().unwrap();
let mut command = UCommand::with_util("echo", Rc::new(tmpd));
command
.args(&["-n", "hello"])
.succeeds()
.stdout_only("hello");
std::assert_eq!(
&PathBuf::from(TESTS_BINARY),
command.bin_path.as_ref().unwrap()
);
std::assert_eq!("echo", &command.util_name.unwrap());
std::assert_eq!(
&[
OsString::from("echo"),
OsString::from("-n"),
OsString::from("hello")
],
command.args.make_contiguous()
);
assert!(command.tmpd.is_some());
}
}