1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37: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: permissions:
contents: read # to fetch code (actions/checkout) contents: read # to fetch code (actions/checkout)
# End the current execution if there is a new changeset in the PR.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs: jobs:
cargo-deny: cargo-deny:
name: Style/cargo-deny name: Style/cargo-deny

View file

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

171
Cargo.lock generated
View file

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

View file

@ -263,7 +263,6 @@ feat_os_windows_legacy = [
test = [ "uu_test" ] test = [ "uu_test" ]
[workspace.dependencies] [workspace.dependencies]
atty = "0.2"
bigdecimal = "0.3" bigdecimal = "0.3"
binary-heap-plus = "0.5.0" binary-heap-plus = "0.5.0"
bstr = "1.0" bstr = "1.0"
@ -287,6 +286,7 @@ gcd = "2.2"
glob = "0.3.0" glob = "0.3.0"
half = "2.1" half = "2.1"
indicatif = "0.17" indicatif = "0.17"
is-terminal = "0.4.3"
itertools = "0.10.0" itertools = "0.10.0"
libc = "0.2.139" libc = "0.2.139"
lscolors = { version = "0.13.0", default-features=false, features = ["nu-ansi-term"] } 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" unindent = "0.1"
uucore = { workspace=true, features=["entries", "process", "signals"] } uucore = { workspace=true, features=["entries", "process", "signals"] }
walkdir = { workspace=true } walkdir = { workspace=true }
atty = { workspace=true } is-terminal = { workspace=true }
hex-literal = "0.3.1" hex-literal = "0.3.1"
rstest = "0.16.0" rstest = "0.16.0"
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies] [target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies]
procfs = { version = "0.14", default-features = false } procfs = { version = "0.14", default-features = false }
rlimit = "0.8.3" rlimit = "0.9.1"
[target.'cfg(unix)'.dev-dependencies] [target.'cfg(unix)'.dev-dependencies]
nix = { workspace=true, features=["process", "signal", "user"] } 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 # For each duplicate dependency, indicate the name of the dependency which
# introduces it. # introduces it.
# spell-checker: disable # 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 # spell-checker: enable
# This section is considered when running `cargo deny check sources`. # This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here: # More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources] [sources]
unknown-registry = "warn" unknown-registry = "warn"
unknown-git = "warn" unknown-git = "warn"

View file

@ -43,13 +43,23 @@ pacman -S uutils-coreutils
### Debian ### Debian
[![Debian Unstable package](https://repology.org/badge/version-for-repo/debian_unstable/uutils-coreutils.svg)](https://packages.debian.org/sid/source/rust-coreutils) [![Debian package](https://repology.org/badge/version-for-repo/debian_unstable/uutils-coreutils.svg)](https://packages.debian.org/sid/source/rust-coreutils)
```bash ```bash
apt install rust-coreutils apt install rust-coreutils
# To use it:
export PATH=/usr/lib/cargo/bin/coreutils:$PATH
``` ```
> **Note**: Requires the `unstable` repository. > **Note**: Only available from Bookworm (Debian 12)
### Gentoo
[![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/uutils-coreutils.svg)](https://packages.gentoo.org/packages/sys-apps/uutils)
```bash
emerge -pv sys-apps/uutils
```
### Manjaro ### Manjaro
![Manjaro Stable package](https://repology.org/badge/version-for-repo/manjaro_stable/uutils-coreutils.svg) ![Manjaro Stable package](https://repology.org/badge/version-for-repo/manjaro_stable/uutils-coreutils.svg)
@ -69,6 +79,18 @@ pamac install uutils-coreutils
nix-env -iA nixos.uutils-coreutils nix-env -iA nixos.uutils-coreutils
``` ```
### Ubuntu
[![Ubuntu package](https://repology.org/badge/version-for-repo/ubuntu_23_04/uutils-coreutils.svg)](https://packages.ubuntu.com/source/lunar/rust-coreutils)
```bash
apt install rust-coreutils
# To use it:
export PATH=/usr/lib/cargo/bin/coreutils:$PATH
```
> **Note**: Only available from Kinetic (Ubuntu 22.10)
## MacOS ## MacOS
### Homebrew ### Homebrew
@ -85,6 +107,13 @@ brew install uutils-coreutils
port install coreutils-uutils 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 ## Windows
### Scoop ### 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 clap::{crate_version, Command};
use uucore::error::{FromIo, UResult}; use uucore::error::{FromIo, UResult};
use uucore::{help_about, help_section};
static ABOUT: &str = "Display machine architecture"; static ABOUT: &str = help_about!("arch.md");
static SUMMARY: &str = "Determine architecture name for current machine."; static SUMMARY: &str = help_section!("after help", "arch.md");
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {

View file

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

View file

@ -8,11 +8,11 @@
use std::io::{stdin, Read}; use std::io::{stdin, Read};
use clap::Command; 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; 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"); const USAGE: &str = help_usage!("base32.md");
#[uucore::main] #[uucore::main]

View file

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

View file

@ -9,11 +9,11 @@
use uu_base32::base_common; use uu_base32::base_common;
pub use uu_base32::uu_app; 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}; 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"); const USAGE: &str = help_usage!("base64.md");
#[uucore::main] #[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 std::io::{stdin, Read};
use uucore::error::UClapError; use uucore::error::UClapError;
static ABOUT: &str = "\ use uucore::{help_about, help_usage};
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 const ABOUT: &str = help_about!("basenc.md");
the formal alphabet. Use --ignore-garbage to attempt to recover const USAGE: &str = help_usage!("basenc.md");
from any other non-alphabet bytes in the encoded stream.
";
const ENCODINGS: &[(&str, Format)] = &[ const ENCODINGS: &[(&str, Format)] = &[
("base64", Format::Base64), ("base64", Format::Base64),
@ -39,8 +35,6 @@ const ENCODINGS: &[(&str, Format)] = &[
("z85", Format::Z85), ("z85", Format::Z85),
]; ];
const USAGE: &str = "{} [OPTION]... [FILE]";
pub fn uu_app() -> Command { pub fn uu_app() -> Command {
let mut command = base_common::base_app(ABOUT, USAGE); let mut command = base_common::base_app(ABOUT, USAGE);
for encoding in ENCODINGS { for encoding in ENCODINGS {

View file

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

View file

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

View file

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

View file

@ -12,12 +12,12 @@ use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::Path; use std::path::Path;
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{ExitCode, UResult, USimpleError, UUsageError}; use uucore::error::{set_exit_code, ExitCode, UResult, USimpleError, UUsageError};
use uucore::fs::display_permissions_unix; use uucore::fs::display_permissions_unix;
use uucore::libc::mode_t; use uucore::libc::mode_t;
#[cfg(not(windows))] #[cfg(not(windows))]
use uucore::mode; use uucore::mode;
use uucore::{format_usage, show_error}; use uucore::{format_usage, show, show_error};
const ABOUT: &str = "Change the mode of each FILE to MODE.\n\ const ABOUT: &str = "Change the mode of each FILE to MODE.\n\
With --reference, change the mode of each FILE to that of RFILE."; With --reference, change the mode of each FILE to that of RFILE.";
@ -190,26 +190,31 @@ impl Chmoder {
let file = Path::new(filename); let file = Path::new(filename);
if !file.exists() { if !file.exists() {
if file.is_symlink() { if file.is_symlink() {
println!(
"failed to change mode of {} from 0000 (---------) to 0000 (---------)",
filename.quote()
);
if !self.quiet { if !self.quiet {
return Err(USimpleError::new( show!(USimpleError::new(
1, 1,
format!("cannot operate on dangling symlink {}", filename.quote()), format!("cannot operate on dangling symlink {}", filename.quote()),
)); ));
} }
if self.verbose {
println!(
"failed to change mode of {} from 0000 (---------) to 1500 (r-x-----T)",
filename.quote()
);
}
} else if !self.quiet { } else if !self.quiet {
return Err(USimpleError::new( show!(USimpleError::new(
1, 1,
format!( format!(
"cannot access {}: No such file or directory", "cannot access {}: No such file or directory",
filename.quote() filename.quote()
), )
)); ));
} }
return Err(ExitCode::new(1)); // GNU exits with exit code 1 even if -q or --quiet are passed
// So we set the exit code, because it hasn't been set yet if `self.quiet` is true.
set_exit_code(1);
continue;
} }
if self.recursive && self.preserve_root && filename == "/" { if self.recursive && self.preserve_root && filename == "/" {
return Err(USimpleError::new( return Err(USimpleError::new(

View file

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

View file

@ -40,7 +40,7 @@ use uucore::error::{set_exit_code, UClapError, UError, UResult, UUsageError};
use uucore::fs::{ use uucore::fs::{
canonicalize, paths_refer_to_same_file, FileInformation, MissingHandling, ResolveMode, canonicalize, paths_refer_to_same_file, FileInformation, MissingHandling, ResolveMode,
}; };
use uucore::{crash, format_usage, 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; use crate::copydir::copy_directory;
@ -228,11 +228,11 @@ pub struct Options {
progress_bar: bool, progress_bar: bool,
} }
const ABOUT: &str = help_section!("about", "cp.md"); const ABOUT: &str = help_about!("cp.md");
static EXIT_ERR: i32 = 1;
const USAGE: &str = help_usage!("cp.md"); const USAGE: &str = help_usage!("cp.md");
static EXIT_ERR: i32 = 1;
// Argument constants // Argument constants
mod options { mod options {
pub const ARCHIVE: &str = "archive"; pub const ARCHIVE: &str = "archive";

View file

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

View file

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

View file

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

View file

@ -27,7 +27,9 @@ use std::cmp;
use std::env; use std::env;
use std::ffi::OsString; use std::ffi::OsString;
use std::fs::{File, OpenOptions}; 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"))] #[cfg(any(target_os = "linux", target_os = "android"))]
use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::OpenOptionsExt;
use std::path::Path; use std::path::Path;
@ -39,11 +41,11 @@ use clap::{crate_version, Arg, Command};
use gcd::Gcd; use gcd::Gcd;
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{FromIo, UResult}; use uucore::error::{FromIo, UResult};
use uucore::help_section; use uucore::{format_usage, help_about, help_section, help_usage, show_error};
use uucore::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 AFTER_HELP: &str = help_section!("after help", "dd.md");
const USAGE: &str = help_usage!("dd.md");
const BUF_INIT_BYTE: u8 = 0xDD; const BUF_INIT_BYTE: u8 = 0xDD;
/// Final settings after parsing /// Final settings after parsing
@ -90,30 +92,106 @@ impl Num {
} }
} }
struct Input<'a, R: Read> { /// Data sources.
src: R, 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, settings: &'a Settings,
} }
impl<'a> Input<'a, io::Stdin> { impl<'a> Input<'a> {
fn new(settings: &'a Settings) -> UResult<Self> { /// Instantiate this struct with stdin as a source.
let mut input = Self { fn new_stdin(settings: &'a Settings) -> UResult<Self> {
src: io::stdin(), let mut src = Source::Stdin(io::stdin());
settings, 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 settings.skip > 0 {
if let Err(e) = input.read_skip(settings.skip) { src.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());
}
} }
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> { impl<'a> Read for Input<'a> {
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> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let mut base_idx = 0; let mut base_idx = 0;
let target_len = buf.len(); 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. /// Fills a given buffer.
/// Reads in increments of 'self.ibs'. /// Reads in increments of 'self.ibs'.
/// The start of each ibs-sized read follows the previous one. /// 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, 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 { enum Density {
@ -297,6 +337,14 @@ enum Dest {
/// The [`Density`] component indicates whether to attempt to /// The [`Density`] component indicates whether to attempt to
/// write a sparse file when all-zero blocks are encountered. /// write a sparse file when all-zero blocks are encountered.
File(File, Density), 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 { impl Dest {
@ -307,6 +355,13 @@ impl Dest {
f.flush()?; f.flush()?;
f.sync_all() 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.flush()?;
f.sync_data() 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 { match self {
Self::Stdout(stdout) => io::copy(&mut io::repeat(0).take(n), stdout), Self::Stdout(stdout) => io::copy(&mut io::repeat(0).take(n), stdout),
Self::File(f, _) => f.seek(io::SeekFrom::Start(n)), 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. /// Truncate the underlying file to the current stream position, if possible.
fn truncate(&mut self) -> io::Result<()> { fn truncate(&mut self) -> io::Result<()> {
match self { match self {
Self::Stdout(_) => Ok(()),
Self::File(f, _) => { Self::File(f, _) => {
let pos = f.stream_position()?; let pos = f.stream_position()?;
f.set_len(pos) f.set_len(pos)
} }
_ => Ok(()),
} }
} }
} }
@ -357,6 +426,10 @@ impl Write for Dest {
} }
Self::File(f, _) => f.write(buf), Self::File(f, _) => f.write(buf),
Self::Stdout(stdout) => stdout.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 { match self {
Self::Stdout(stdout) => stdout.flush(), Self::Stdout(stdout) => stdout.flush(),
Self::File(f, _) => f.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 }) 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. /// Write the given bytes one block at a time.
/// ///
/// This may write partial blocks (for example, if the underlying /// 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 /// If there is a problem reading from the input or writing to
/// this output. /// 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. // The read and write statistics.
// //
// These objects are counters, initialized to zero. After each // 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. /// Read from an input (that is, a source of bytes) into the given buffer.
fn read_helper<R: Read>( ///
i: &mut Input<R>, /// This function also performs any conversions as specified by
buf: &mut Vec<u8>, /// `conv=swab` or `conv=block` command-line arguments. This function
bsize: usize, /// mutates the `buf` argument in-place. The returned [`ReadStat`]
) -> std::io::Result<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 ------------------------------------------------- // Local Helper Fns -------------------------------------------------
fn perform_swab(buf: &mut [u8]) { fn perform_swab(buf: &mut [u8]) {
for base in (1..buf.len()).step_by(2) { 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] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args = args.collect_ignore(); let args = args.collect_ignore();
@ -792,46 +910,29 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
.collect::<Vec<_>>()[..], .collect::<Vec<_>>()[..],
)?; )?;
match (&settings.infile, &settings.outfile) { let i = match settings.infile {
(Some(infile), Some(outfile)) => { #[cfg(unix)]
let i = Input::<File>::new(Path::new(&infile), &settings)?; Some(ref infile) if is_fifo(infile) => Input::new_fifo(Path::new(&infile), &settings)?,
let o = Output::new_file(Path::new(&outfile), &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 => Output::new_stdout(&settings)?,
};
o.dd_out(i).map_err_context(|| "IO error".to_string()) o.dd_out(i).map_err_context(|| "IO error".to_string())
}
(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())
}
}
}
} }
pub fn uu_app() -> Command { pub fn uu_app() -> Command {
Command::new(uucore::util_name()) Command::new(uucore::util_name())
.version(crate_version!()) .version(crate_version!())
.about(ABOUT) .about(ABOUT)
.override_usage(format_usage(USAGE))
.after_help(AFTER_HELP) .after_help(AFTER_HELP)
.infer_long_args(true) .infer_long_args(true)
.arg(Arg::new(options::OPERANDS).num_args(1..)) .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::error::{UError, UResult};
use uucore::parse_glob; use uucore::parse_glob;
use uucore::parse_size::{parse_size, ParseSizeError}; 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)] #[cfg(windows)]
use windows_sys::Win32::Foundation::HANDLE; use windows_sys::Win32::Foundation::HANDLE;
#[cfg(windows)] #[cfg(windows)]
@ -73,25 +75,9 @@ mod options {
pub const FILE: &str = "FILE"; pub const FILE: &str = "FILE";
} }
const ABOUT: &str = "Estimate file space usage"; const ABOUT: &str = help_about!("du.md");
const LONG_HELP: &str = " const AFTER_HELP: &str = help_section!("after help", "du.md");
Display values are in units of the first available SIZE from --block-size, const USAGE: &str = help_usage!("du.md");
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";
// TODO: Support Z & Y (currently limited by size of u64) // 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)]; 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()) Command::new(uucore::util_name())
.version(crate_version!()) .version(crate_version!())
.about(ABOUT) .about(ABOUT)
.after_help(LONG_HELP) .after_help(AFTER_HELP)
.override_usage(format_usage(USAGE)) .override_usage(format_usage(USAGE))
.infer_long_args(true) .infer_long_args(true)
.disable_help_flag(true) .disable_help_flag(true)

View file

@ -1,15 +1,12 @@
# expr # expr
## About
Print the value of `EXPRESSION` to standard output
## Usage
``` ```
expr [EXPRESSION] expr [EXPRESSION]
expr [OPTIONS] expr [OPTIONS]
``` ```
Print the value of `EXPRESSION` to standard output
## After help ## After help
Print the value of `EXPRESSION` to standard output. A blank line below Print the value of `EXPRESSION` to standard output. A blank line below

View file

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

View file

@ -41,8 +41,8 @@ pub struct Behavior {
specified_mode: Option<u32>, specified_mode: Option<u32>,
backup_mode: BackupMode, backup_mode: BackupMode,
suffix: String, suffix: String,
owner: String, owner_id: Option<u32>,
group: String, group_id: Option<u32>,
verbose: bool, verbose: bool,
preserve_timestamps: bool, preserve_timestamps: bool,
compare: bool, compare: bool,
@ -58,14 +58,15 @@ enum InstallError {
DirNeedsArg(), DirNeedsArg(),
CreateDirFailed(PathBuf, std::io::Error), CreateDirFailed(PathBuf, std::io::Error),
ChmodFailed(PathBuf), ChmodFailed(PathBuf),
ChownFailed(PathBuf, String),
InvalidTarget(PathBuf), InvalidTarget(PathBuf),
TargetDirIsntDir(PathBuf), TargetDirIsntDir(PathBuf),
BackupFailed(PathBuf, PathBuf, std::io::Error), BackupFailed(PathBuf, PathBuf, std::io::Error),
InstallFailed(PathBuf, PathBuf, std::io::Error), InstallFailed(PathBuf, PathBuf, std::io::Error),
StripProgramFailed(String), StripProgramFailed(String),
MetadataFailed(std::io::Error), MetadataFailed(std::io::Error),
NoSuchUser(String), InvalidUser(String),
NoSuchGroup(String), InvalidGroup(String),
OmittingDirectory(PathBuf), OmittingDirectory(PathBuf),
} }
@ -99,6 +100,7 @@ impl Display for InstallError {
Display::fmt(&uio_error!(e, "failed to create {}", dir.quote()), f) Display::fmt(&uio_error!(e, "failed to create {}", dir.quote()), f)
} }
Self::ChmodFailed(file) => write!(f, "failed to chmod {}", file.quote()), 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!( Self::InvalidTarget(target) => write!(
f, f,
"invalid target {}: No such file or directory", "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::StripProgramFailed(msg) => write!(f, "strip program failed: {msg}"),
Self::MetadataFailed(e) => Display::fmt(&uio_error!(e, ""), f), Self::MetadataFailed(e) => Display::fmt(&uio_error!(e, ""), f),
Self::NoSuchUser(user) => write!(f, "no such user: {}", user.maybe_quote()), Self::InvalidUser(user) => write!(f, "invalid user: {}", user.quote()),
Self::NoSuchGroup(group) => write!(f, "no such group: {}", group.maybe_quote()), Self::InvalidGroup(group) => write!(f, "invalid group: {}", group.quote()),
Self::OmittingDirectory(dir) => write!(f, "omitting directory {}", dir.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"); show_error!("Options --compare and --strip are mutually exclusive");
return Err(1.into()); 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 { Ok(Behavior {
main_function, main_function,
specified_mode, specified_mode,
backup_mode, backup_mode,
suffix: backup_control::determine_backup_suffix(matches), suffix: backup_control::determine_backup_suffix(matches),
owner: matches owner_id,
.get_one::<String>(OPT_OWNER) group_id,
.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(),
verbose: matches.get_flag(OPT_VERBOSE), verbose: matches.get_flag(OPT_VERBOSE),
preserve_timestamps, preserve_timestamps,
compare, compare,
@ -466,6 +491,8 @@ fn directory(paths: &[String], b: &Behavior) -> UResult<()> {
uucore::error::set_exit_code(1); uucore::error::set_exit_code(1);
continue; continue;
} }
show_if_err!(chown_optional_user_group(path, b));
} }
// If the exit code was set, or show! has been called at least once // 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 // (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) { if let Err(e) = fs::create_dir_all(to_create) {
return Err(InstallError::CreateDirFailed(to_create.to_path_buf(), e).into()); 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(()) 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. /// Copy one file to a new location, changing metadata.
/// ///
/// Returns a Result type with the Err variant containing the error message. /// 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()); return Err(InstallError::ChmodFailed(to.to_path_buf()).into());
} }
if !b.owner.is_empty() { chown_optional_user_group(to, b)?;
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),
}
}
if b.preserve_timestamps { if b.preserve_timestamps {
let meta = match fs::metadata(from) { 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. // TODO: if -P (#1809) and from/to contexts mismatch, return true.
if !b.owner.is_empty() { if let Some(owner_id) = b.owner_id {
let owner_id = match usr2uid(&b.owner) {
Ok(id) => id,
_ => return Err(InstallError::NoSuchUser(b.owner.clone()).into()),
};
if owner_id != to_meta.uid() { if owner_id != to_meta.uid() {
return Ok(true); return Ok(true);
} }
} else if !b.group.is_empty() { } else if let Some(group_id) = b.group_id {
let group_id = match grp2gid(&b.group) {
Ok(id) => id,
_ => return Err(InstallError::NoSuchGroup(b.group.clone()).into()),
};
if group_id != to_meta.gid() { if group_id != to_meta.gid() {
return Ok(true); 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::display::Quotable;
use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::error::{FromIo, UError, UResult, USimpleError};
use uucore::signals::{signal_by_name_or_value, ALL_SIGNALS}; 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."; static ABOUT: &str = help_about!("kill.md");
const USAGE: &str = "{} [OPTIONS]... PID..."; const USAGE: &str = help_usage!("kill.md");
pub mod options { pub mod options {
pub static PIDS_OR_SIGNALS: &str = "pids_or_signals"; pub static PIDS_OR_SIGNALS: &str = "pids_or_signals";

View file

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

View file

@ -12,6 +12,7 @@ use clap::{
crate_version, Arg, ArgAction, Command, crate_version, Arg, ArgAction, Command,
}; };
use glob::{MatchOptions, Pattern}; use glob::{MatchOptions, Pattern};
use is_terminal::IsTerminal;
use lscolors::LsColors; use lscolors::LsColors;
use number_prefix::NumberPrefix; use number_prefix::NumberPrefix;
use once_cell::unsync::OnceCell; use once_cell::unsync::OnceCell;
@ -451,7 +452,7 @@ impl Config {
(Format::Commas, Some(options::format::COMMAS)) (Format::Commas, Some(options::format::COMMAS))
} else if options.get_flag(options::format::COLUMNS) { } else if options.get_flag(options::format::COLUMNS) {
(Format::Columns, Some(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) (Format::Columns, None)
} else { } else {
(Format::OneLine, None) (Format::OneLine, None)
@ -557,7 +558,7 @@ impl Config {
None => options.contains_id(options::COLOR), None => options.contains_id(options::COLOR),
Some(val) => match val.as_str() { Some(val) => match val.as_str() {
"" | "always" | "yes" | "force" => true, "" | "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, /* "never" | "no" | "none" | */ _ => false,
}, },
}; };
@ -678,7 +679,7 @@ impl Config {
} else if options.get_flag(options::SHOW_CONTROL_CHARS) { } else if options.get_flag(options::SHOW_CONTROL_CHARS) {
true true
} else { } else {
!atty::is(atty::Stream::Stdout) !std::io::stdout().is_terminal()
}; };
let opt_quoting_style = options let opt_quoting_style = options
@ -750,7 +751,7 @@ impl Config {
"never" | "no" | "none" => IndicatorStyle::None, "never" | "no" | "none" => IndicatorStyle::None,
"always" | "yes" | "force" => IndicatorStyle::Classify, "always" | "yes" | "force" => IndicatorStyle::Classify,
"auto" | "tty" | "if-tty" => { "auto" | "tty" | "if-tty" => {
if atty::is(atty::Stream::Stdout) { if std::io::stdout().is_terminal() {
IndicatorStyle::Classify IndicatorStyle::Classify
} else { } else {
IndicatorStyle::None 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))] #[cfg(not(windows))]
use uucore::mode; use uucore::mode;
use uucore::{display::Quotable, fs::dir_strip_dot_for_creation}; 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; static DEFAULT_PERM: u32 = 0o755;
const ABOUT: &str = "Create the given DIRECTORY(ies) if they do not exist"; const ABOUT: &str = help_about!("mkdir.md");
const USAGE: &str = "{} [OPTION]... [USER]"; const USAGE: &str = help_usage!("mkdir.md");
const LONG_USAGE: &str = const AFTER_HELP: &str = help_section!("after help", "mkdir.md");
"Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'.";
mod options { mod options {
pub const MODE: &str = "mode"; pub const MODE: &str = "mode";
@ -90,7 +89,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
// Linux-specific options, not implemented // Linux-specific options, not implemented
// opts.optflag("Z", "context", "set SELinux security context" + // opts.optflag("Z", "context", "set SELinux security context" +
// " of each created directory to CTX"), // " of each created directory to CTX"),
let matches = uu_app().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 let dirs = matches
.get_many::<OsString>(options::DIRS) .get_many::<OsString>(options::DIRS)

View file

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

View file

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

View file

@ -17,7 +17,7 @@ path = "src/nohup.rs"
[dependencies] [dependencies]
clap = { workspace=true } clap = { workspace=true }
libc = { workspace=true } libc = { workspace=true }
atty = { workspace=true } is-terminal = { workspace=true }
uucore = { workspace=true, features=["fs"] } uucore = { workspace=true, features=["fs"] }
[[bin]] [[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 // spell-checker:ignore (ToDO) execvp SIGHUP cproc vprocmgr cstrs homeout
use clap::{crate_version, Arg, ArgAction, Command}; use clap::{crate_version, Arg, ArgAction, Command};
use is_terminal::IsTerminal;
use libc::{c_char, dup2, execvp, signal}; use libc::{c_char, dup2, execvp, signal};
use libc::{SIGHUP, SIG_IGN}; use libc::{SIGHUP, SIG_IGN};
use std::env; use std::env;
@ -19,18 +20,11 @@ use std::os::unix::prelude::*;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{set_exit_code, UClapError, UError, UResult}; 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."; const ABOUT: &str = help_about!("nohup.md");
static LONG_HELP: &str = " const AFTER_HELP: &str = help_section!("after help", "nohup.md");
If standard input is terminal, it'll be replaced with /dev/null. const USAGE: &str = help_usage!("nohup.md");
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";
static NOHUP_OUT: &str = "nohup.out"; static NOHUP_OUT: &str = "nohup.out";
// exit codes that match the GNU implementation // exit codes that match the GNU implementation
static EXIT_CANCELED: i32 = 125; static EXIT_CANCELED: i32 = 125;
@ -115,7 +109,7 @@ pub fn uu_app() -> Command {
Command::new(uucore::util_name()) Command::new(uucore::util_name())
.version(crate_version!()) .version(crate_version!())
.about(ABOUT) .about(ABOUT)
.after_help(LONG_HELP) .after_help(AFTER_HELP)
.override_usage(format_usage(USAGE)) .override_usage(format_usage(USAGE))
.arg( .arg(
Arg::new(options::CMD) Arg::new(options::CMD)
@ -129,7 +123,7 @@ pub fn uu_app() -> Command {
} }
fn replace_fds() -> UResult<()> { 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")) let new_stdin = File::open(Path::new("/dev/null"))
.map_err(|e| NohupError::CannotReplace("STDIN", e))?; .map_err(|e| NohupError::CannotReplace("STDIN", e))?;
if unsafe { dup2(new_stdin.as_raw_fd(), 0) } != 0 { 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 new_stdout = find_stdout()?;
let fd = new_stdout.as_raw_fd(); 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()); return Err(NohupError::CannotReplace("STDERR", Error::last_os_error()).into());
} }
Ok(()) Ok(())

View file

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

View file

@ -14,16 +14,15 @@ use std::io::{BufRead, Write};
use units::{IEC_BASES, SI_BASES}; use units::{IEC_BASES, SI_BASES};
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::UResult; use uucore::error::UResult;
use uucore::format_usage;
use uucore::ranges::Range; 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 errors;
pub mod format; pub mod format;
pub mod options; pub mod options;
mod units; 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 AFTER_HELP: &str = help_section!("after help", "numfmt.md");
const USAGE: &str = help_usage!("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 clap::{crate_version, parser::ValueSource, Arg, ArgMatches, Command};
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{UResult, USimpleError}; use uucore::error::{UResult, USimpleError};
use uucore::format_usage;
use uucore::parse_size::ParseSizeError; use uucore::parse_size::ParseSizeError;
use uucore::show_error; use uucore::{format_usage, help_about, help_section, help_usage, show_error, show_warning};
use uucore::show_warning;
const PEEK_BUFFER_SIZE: usize = 4; // utf-8 can be 4 bytes 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 = "\ const USAGE: &str = help_usage!("od.md");
{} [OPTION]... [--] [FILENAME]...
{} [-abcdDefFhHiIlLoOsxX] [FILENAME] [[+][0x]OFFSET[.][b]]
{} --traditional [OPTION]... [FILENAME] [[+][0x]OFFSET[.][b] [[+][0x]LABEL[.][b]]]";
static LONG_HELP: &str = r#" const AFTER_HELP: &str = help_section!("after help", "od.md");
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."#;
pub(crate) mod options { pub(crate) mod options {
pub const HELP: &str = "help"; pub const HELP: &str = "help";
@ -295,7 +253,7 @@ pub fn uu_app() -> Command {
.version(crate_version!()) .version(crate_version!())
.about(ABOUT) .about(ABOUT)
.override_usage(format_usage(USAGE)) .override_usage(format_usage(USAGE))
.after_help(LONG_HELP) .after_help(AFTER_HELP)
.trailing_var_arg(true) .trailing_var_arg(true)
.dont_delimit_trailing_values(true) .dont_delimit_trailing_values(true)
.infer_long_args(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 std::path::{Path, PathBuf};
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{UResult, USimpleError, UUsageError}; 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}; use walkdir::{DirEntry, WalkDir};
#[derive(Eq, PartialEq, Clone, Copy)] #[derive(Eq, PartialEq, Clone, Copy)]
@ -37,21 +37,9 @@ struct Options {
verbose: bool, verbose: bool,
} }
const ABOUT: &str = "Remove (unlink) the FILE(s)"; const ABOUT: &str = help_about!("rm.md");
const USAGE: &str = "{} [OPTION]... FILE..."; const USAGE: &str = help_usage!("rm.md");
const LONG_USAGE: &str = "\ const AFTER_HELP: &str = help_section!("after help", "rm.md");
By default, rm does not remove directories. Use the --recursive (-r or -R)
option to remove each listed directory, too, along with all of its contents
To remove a file whose name starts with a '-', for example '-foo',
use one of these commands:
rm -- -foo
rm ./-foo
Note that if you use rm to remove a file, it might be possible to recover
some of its contents, given sufficient expertise and/or time. For greater
assurance that the contents are truly unrecoverable, consider using shred.";
static OPT_DIR: &str = "dir"; static OPT_DIR: &str = "dir";
static OPT_INTERACTIVE: &str = "interactive"; static OPT_INTERACTIVE: &str = "interactive";
@ -69,7 +57,7 @@ static ARG_FILES: &str = "files";
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { 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 let files: Vec<String> = matches
.get_many::<String>(ARG_FILES) .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::display::Quotable;
use uucore::error::{set_exit_code, strip_errno, UResult}; 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."; static ABOUT: &str = help_about!("rmdir.md");
const USAGE: &str = "{} [OPTION]... DIRECTORY..."; const USAGE: &str = help_usage!("rmdir.md");
static OPT_IGNORE_FAIL_NON_EMPTY: &str = "ignore-fail-on-non-empty"; static OPT_IGNORE_FAIL_NON_EMPTY: &str = "ignore-fail-on-non-empty";
static OPT_PARENTS: &str = "parents"; static OPT_PARENTS: &str = "parents";
static OPT_VERBOSE: &str = "verbose"; 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::{ use uucore::{
error::{UResult, USimpleError, UUsageError}, error::{UResult, USimpleError, UUsageError},
format_usage, show, format_usage, help_about, help_section, help_usage, show,
}; };
use clap::{crate_version, Arg, ArgAction, Command}; use clap::{crate_version, Arg, ArgAction, Command};
static ABOUT: &str = "Pause for NUMBER seconds."; static ABOUT: &str = help_about!("sleep.md");
const USAGE: &str = "\ const USAGE: &str = help_usage!("sleep.md");
{} NUMBER[SUFFIX]... static AFTER_HELP: &str = help_section!("after help", "sleep.md");
{} 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.";
mod options { mod options {
pub const NUMBER: &str = "NUMBER"; pub const NUMBER: &str = "NUMBER";
@ -54,7 +48,7 @@ pub fn uu_app() -> Command {
Command::new(uucore::util_name()) Command::new(uucore::util_name())
.version(crate_version!()) .version(crate_version!())
.about(ABOUT) .about(ABOUT)
.after_help(LONG_HELP) .after_help(AFTER_HELP)
.override_usage(format_usage(USAGE)) .override_usage(format_usage(USAGE))
.infer_long_args(true) .infer_long_args(true)
.arg( .arg(

View file

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

View file

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

View file

@ -19,12 +19,12 @@ use std::{
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::UError; use uucore::error::UError;
use uucore::error::UResult; use uucore::error::UResult;
use uucore::{format_usage, show}; use uucore::{format_usage, help_about, help_usage, show};
use crate::error::TacError; use crate::error::TacError;
static USAGE: &str = "{} [OPTION]... [FILE]..."; static USAGE: &str = help_usage!("tac.md");
static ABOUT: &str = "Write each file to standard output, last line first."; static ABOUT: &str = help_about!("tac.md");
mod options { mod options {
pub static BEFORE: &str = "before"; 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 } libc = { workspace=true }
memchr = { workspace=true } memchr = { workspace=true }
notify = { workspace=true } notify = { workspace=true }
uucore = { workspace=true, features=["ringbuffer", "lines"] } uucore = { workspace=true }
same-file = { workspace=true } same-file = { workspace=true }
atty = { workspace=true } is-terminal = { workspace=true }
fundu = { workspace=true } fundu = { workspace=true }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows-sys = { workspace=true, features = ["Win32_System_Threading", "Win32_Foundation"] } windows-sys = { workspace=true, features = ["Win32_System_Threading", "Win32_Foundation"] }
winapi-util = { workspace=true } winapi-util = { workspace=true }
[target.'cfg(unix)'.dependencies]
nix = { workspace=true, features = ["fs"] }
[[bin]] [[bin]]
name = "tail" name = "tail"
path = "src/main.rs" path = "src/main.rs"

View file

@ -7,10 +7,10 @@
use crate::paths::Input; use crate::paths::Input;
use crate::{parse, platform, Quotable}; use crate::{parse, platform, Quotable};
use atty::Stream;
use clap::crate_version; use clap::crate_version;
use clap::{parser::ValueSource, Arg, ArgAction, ArgMatches, Command}; use clap::{parser::ValueSource, Arg, ArgAction, ArgMatches, Command};
use fundu::DurationParser; use fundu::DurationParser;
use is_terminal::IsTerminal;
use same_file::Handle; use same_file::Handle;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::ffi::OsString; use std::ffi::OsString;
@ -274,7 +274,7 @@ impl Settings {
.map_or(false, |meta| !meta.is_file()) .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"); show_warning!("following standard input indefinitely is ineffective");
} }
} }

View file

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

View file

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

View file

@ -12,13 +12,13 @@ use std::io::{self, Result, Write};
use clap::{Arg, ArgAction, Command}; use clap::{Arg, ArgAction, Command};
use uucore::error::{UResult, USimpleError}; 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"))] #[cfg(any(target_os = "linux", target_os = "android"))]
mod splice; mod splice;
const ABOUT: &str = "Repeatedly display a line with STRING (or 'y')"; const ABOUT: &str = help_about!("yes.md");
const USAGE: &str = "{} [STRING]..."; const USAGE: &str = help_usage!("yes.md");
// it's possible that using a smaller or larger buffer might provide better performance on some // it's possible that using a smaller or larger buffer might provide better performance on some
// systems, but honestly this is good enough // 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 /// A.K.A. ut.ut_tv
pub fn login_time(&self) -> time::OffsetDateTime { pub fn login_time(&self) -> time::OffsetDateTime {
#[cfg(all(not(target_os = "freebsd"), not(target_vendor = "apple")))] #[allow(clippy::unnecessary_cast)]
let ts_nanos: i128 = (self.inner.ut_tv.tv_sec as i64 * 1_000_000_000_i64 let ts_nanos: i128 = (1_000_000_000_i64 * self.inner.ut_tv.tv_sec as i64
+ self.inner.ut_tv.tv_usec as i64 * 1_000_i64) + 1_000_i64 * self.inner.ut_tv.tv_usec as 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)
.into(); .into();
let local_offset = time::OffsetDateTime::now_local().unwrap().offset(); let local_offset = time::OffsetDateTime::now_local().unwrap().offset();
time::OffsetDateTime::from_unix_timestamp_nanos(ts_nanos) 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 { if len == 0 {
return Err("empty string".to_owned()); 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() { let (numstr, times) = match string.chars().next_back().unwrap() {
's' => (slice, 1), 's' => (slice, 1),
'm' => (slice, 60), 'm' => (slice, 60),
@ -112,6 +115,11 @@ mod tests {
assert!(from_str("123X").is_err()); assert!(from_str("123X").is_err());
} }
#[test]
fn test_error_multi_bytes_characters() {
assert!(from_str("10€").is_err());
}
#[test] #[test]
fn test_error_invalid_magnitude() { fn test_error_invalid_magnitude() {
assert!(from_str("12abc3s").is_err()); 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 proc_macro::{Literal, TokenStream, TokenTree};
use quote::quote; use quote::quote;
const MARKDOWN_CODE_FENCES: &str = "```";
//## rust proc-macro background info //## rust proc-macro background info
//* ref: <https://dev.to/naufraghi/procedural-macro-in-rust-101-k3f> @@ <http://archive.is/Vbr5e> //* 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> //* 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('`', "") 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 /// 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 /// 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 { pub fn help_usage(input: TokenStream) -> TokenStream {
let input: Vec<TokenTree> = input.into_iter().collect(); let input: Vec<TokenTree> = input.into_iter().collect();
let filename = get_argument(&input, 0, "filename"); 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() 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 input: Vec<TokenTree> = input.into_iter().collect();
let section = get_argument(&input, 0, "section"); let section = get_argument(&input, 0, "section");
let filename = get_argument(&input, 1, "filename"); 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); let rendered = render_markdown(&text);
TokenTree::Literal(Literal::string(&rendered)).into() TokenTree::Literal(Literal::string(&rendered)).into()
} }
@ -121,13 +135,11 @@ fn get_argument(input: &[TokenTree], index: usize, name: &str) -> String {
.to_string() .to_string()
} }
/// Read the help file and extract a section /// Read the help file
fn parse_help(section: &str, filename: &str) -> String { fn read_help(filename: &str) -> String {
let section = section.to_lowercase();
let section = section.trim_matches('"');
let mut content = String::new(); 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); path.push(filename);
File::open(path) File::open(path)
@ -135,7 +147,7 @@ fn parse_help(section: &str, filename: &str) -> String {
.read_to_string(&mut content) .read_to_string(&mut content)
.unwrap(); .unwrap();
parse_help_section(section, &content) content
} }
/// Get a single section from 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) .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, // 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 // so we do a quick test to check whether the section exists to provide
// a nice error message. // a nice error message.
@ -167,17 +181,17 @@ fn parse_help_section(section: &str, content: &str) -> String {
.to_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 /// The code fences are removed and the name of the util is replaced
/// with `{}` so that it can be replaced with the appropriate name /// with `{}` so that it can be replaced with the appropriate name
/// at runtime. /// at runtime.
fn parse_usage(content: &str) -> String { fn parse_usage(content: &str) -> String {
content content
.strip_suffix("```")
.unwrap()
.lines() .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| { .map(|l| {
// Replace the util name (assumed to be the first word) with "{}" // Replace the util name (assumed to be the first word) with "{}"
// to be replaced with the runtime value later. // to be replaced with the runtime value later.
@ -187,12 +201,31 @@ fn parse_usage(content: &str) -> String {
"{}\n".to_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)] #[cfg(test)]
mod tests { mod tests {
use super::{parse_help_section, parse_usage}; use super::{parse_about, parse_help_section, parse_usage};
#[test] #[test]
fn section_parsing() { fn section_parsing() {
@ -209,6 +242,10 @@ mod tests {
parse_help_section("some section", input), parse_help_section("some section", input),
"This is some section" "This is some section"
); );
assert_eq!(
parse_help_section("SOME SECTION", input),
"This is some section"
);
assert_eq!( assert_eq!(
parse_help_section("another section", input), parse_help_section("another section", input),
"This is the other section\nwith multiple lines" "This is the other section\nwith multiple lines"
@ -233,7 +270,6 @@ mod tests {
fn usage_parsing() { fn usage_parsing() {
let input = "\ let input = "\
# ls\n\ # ls\n\
## Usage\n\
```\n\ ```\n\
ls -l\n\ ls -l\n\
```\n\ ```\n\
@ -244,17 +280,55 @@ mod tests {
This is the other section\n\ This is the other section\n\
with multiple lines\n"; with multiple lines\n";
assert_eq!(parse_usage(&parse_help_section("usage", input)), "{} -l",); assert_eq!(parse_usage(input), "{} -l");
}
assert_eq!( #[test]
parse_usage( fn multi_line_usage_parsing() {
"\ let input = "\
# ls\n\
```\n\ ```\n\
util [some] [options]\n\ ls -a\n\
```\ ls -b\n\
" ls -c\n\
), ```\n\
"{} [some] [options]" ## 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",
"alpha.txt", "alpha.txt",
]) ])
.with_limit(Resource::NOFILE, 9, 9) .limit(Resource::NOFILE, 9, 9)
.succeeds(); .succeeds();
} }

View file

@ -48,15 +48,12 @@ fn run_single_test(test: &TestCase, at: &AtPath, mut ucmd: UCommand) {
let r = ucmd.run(); let r = ucmd.run();
if !r.succeeded() { if !r.succeeded() {
println!("{}", r.stderr_str()); println!("{}", r.stderr_str());
panic!("{:?}: failed", ucmd.raw); panic!("{ucmd}: failed");
} }
let perms = at.metadata(TEST_FILE).permissions().mode(); let perms = at.metadata(TEST_FILE).permissions().mode();
if perms != test.after { if perms != test.after {
panic!( panic!("{}: expected: {:o} got: {:o}", ucmd, test.after, perms);
"{:?}: expected: {:o} got: {:o}",
ucmd.raw, test.after, perms
);
} }
} }
@ -414,7 +411,7 @@ fn test_chmod_symlink_non_existing_file() {
let non_existing = "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 test_symlink = "test_chmod_symlink_non_existing_file_symlink";
let expected_stdout = &format!( 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}'"); 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) .code_is(1)
.no_stderr() .no_stderr()
.stdout_contains(expected_stdout); .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] #[test]
@ -574,3 +582,71 @@ fn test_mode_after_dash_dash() {
ucmd, ucmd,
); );
} }
#[test]
fn test_chmod_file_after_non_existing_file() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch(TEST_FILE);
at.touch("file2");
set_permissions(at.plus(TEST_FILE), Permissions::from_mode(0o664)).unwrap();
set_permissions(at.plus("file2"), Permissions::from_mode(0o664)).unwrap();
scene
.ucmd()
.arg("u+x")
.arg("does-not-exist")
.arg(TEST_FILE)
.fails()
.stderr_contains("chmod: cannot access 'does-not-exist': No such file or directory")
.code_is(1);
assert_eq!(at.metadata(TEST_FILE).permissions().mode(), 0o100764);
scene
.ucmd()
.arg("u+x")
.arg("--q")
.arg("does-not-exist")
.arg("file2")
.fails()
.no_stderr()
.code_is(1);
assert_eq!(at.metadata("file2").permissions().mode(), 0o100764);
}
#[test]
fn test_chmod_file_symlink_after_non_existing_file() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let existing = "file";
let test_existing_symlink = "file_symlink";
let non_existing = "test_chmod_symlink_non_existing_file";
let test_dangling_symlink = "test_chmod_symlink_non_existing_file_symlink";
let expected_stdout = &format!(
"failed to change mode of '{test_dangling_symlink}' from 0000 (---------) to 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 scene = TestScenario::new(util_name!());
let at = &scene.fixtures; 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") { if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return; return;
} }
@ -430,7 +430,7 @@ fn test_chown_fail_id() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures; 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") { if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return; return;
} }
@ -487,7 +487,7 @@ fn test_chown_only_group_id() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures; 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") { if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return; return;
} }
@ -551,14 +551,14 @@ fn test_chown_owner_group_id() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures; 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") { if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return; return;
} }
let user_id = String::from(result.stdout_str().trim()); let user_id = String::from(result.stdout_str().trim());
assert!(!user_id.is_empty()); 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") { if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return; return;
} }
@ -612,14 +612,14 @@ fn test_chown_owner_group_mix() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures; 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") { if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return; return;
} }
let user_id = String::from(result.stdout_str().trim()); let user_id = String::from(result.stdout_str().trim());
assert!(!user_id.is_empty()); 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") { if skipping_test_is_okay(&result, "id: cannot find name for group ID") {
return; return;
} }

View file

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

View file

@ -5,6 +5,8 @@ use crate::common::util::*;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io::{BufReader, Read, Write}; use std::io::{BufReader, Read, Write};
use std::path::PathBuf; use std::path::PathBuf;
#[cfg(all(not(windows), not(target_os = "macos")))]
use std::process::{Command, Stdio};
#[cfg(not(windows))] #[cfg(not(windows))]
use std::thread::sleep; use std::thread::sleep;
#[cfg(not(windows))] #[cfg(not(windows))]
@ -1442,3 +1444,73 @@ fn test_sparse() {
// number of blocks stored on disk may be zero. // number of blocks stored on disk may be zero.
assert_eq!(at.metadata("infile").len(), at.metadata("outfile").len()); 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 // This test depends on the HOME variable being pre-defined by the
// default shell // default shell
let out = TestScenario::new(util_name!()) let out = TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("-u") .arg("-u")
.arg("HOME") .arg("HOME")
.succeeds() .succeeds()

View file

@ -124,6 +124,37 @@ fn test_install_ancestors_mode_directories() {
assert_eq!(0o40_200_u32, at.metadata(target_dir).permissions().mode()); 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] #[test]
fn test_install_parent_directories() { fn test_install_parent_directories() {
let (at, mut ucmd) = at_and_ucmd!(); 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'"); .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] #[test]
fn test_install_compare_option() { fn test_install_compare_option() {
let scene = TestScenario::new(util_name!()); 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 scene = TestScenario::new(util_name!());
let result = scene let result = scene
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("--tmpdir") .arg("--tmpdir")
.arg("apt-key-gpghome.XXXXXXXXXX") .arg("apt-key-gpghome.XXXXXXXXXX")
.succeeds(); .succeeds();
@ -439,7 +440,8 @@ fn test_mktemp_directory_tmpdir() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let result = scene let result = scene
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("--directory") .arg("--directory")
.arg("--tmpdir") .arg("--tmpdir")
.arg("apt-key-gpghome.XXXXXXXXXX") .arg("apt-key-gpghome.XXXXXXXXXX")

View file

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

View file

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

View file

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

View file

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

View file

@ -31,7 +31,8 @@ fn test_buffer_sizes() {
let buffer_sizes = ["0", "50K", "50k", "1M", "100M"]; let buffer_sizes = ["0", "50K", "50k", "1M", "100M"];
for buffer_size in &buffer_sizes { for buffer_size in &buffer_sizes {
TestScenario::new(util_name!()) TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("-n") .arg("-n")
.arg("-S") .arg("-S")
.arg(buffer_size) .arg(buffer_size)
@ -44,7 +45,8 @@ fn test_buffer_sizes() {
let buffer_sizes = ["1000G", "10T"]; let buffer_sizes = ["1000G", "10T"];
for buffer_size in &buffer_sizes { for buffer_size in &buffer_sizes {
TestScenario::new(util_name!()) TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("-n") .arg("-n")
.arg("-S") .arg("-S")
.arg(buffer_size) .arg(buffer_size)
@ -918,7 +920,8 @@ fn test_compress_merge() {
fn test_compress_fail() { fn test_compress_fail() {
#[cfg(not(windows))] #[cfg(not(windows))]
TestScenario::new(util_name!()) TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.args(&[ .args(&[
"ext_sort.txt", "ext_sort.txt",
"-n", "-n",
@ -934,7 +937,8 @@ fn test_compress_fail() {
// So, don't check the output // So, don't check the output
#[cfg(windows)] #[cfg(windows)]
TestScenario::new(util_name!()) TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.args(&[ .args(&[
"ext_sort.txt", "ext_sort.txt",
"-n", "-n",
@ -949,7 +953,8 @@ fn test_compress_fail() {
#[test] #[test]
fn test_merge_batches() { fn test_merge_batches() {
TestScenario::new(util_name!()) TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.timeout(Duration::from_secs(120)) .timeout(Duration::from_secs(120))
.args(&["ext_sort.txt", "-n", "-S", "150b"]) .args(&["ext_sort.txt", "-n", "-S", "150b"])
.succeeds() .succeeds()
@ -959,7 +964,8 @@ fn test_merge_batches() {
#[test] #[test]
fn test_merge_batch_size() { fn test_merge_batch_size() {
TestScenario::new(util_name!()) TestScenario::new(util_name!())
.ucmd_keepenv() .ucmd()
.keep_env()
.arg("--batch-size=2") .arg("--batch-size=2")
.arg("-m") .arg("-m")
.arg("--unique") .arg("--unique")
@ -1067,7 +1073,8 @@ fn test_output_is_input() {
at.touch("file"); at.touch("file");
at.append("file", input); at.append("file", input);
scene scene
.ucmd_keepenv() .ucmd()
.keep_env()
.args(&["-m", "-u", "-o", "file", "file", "file", "file"]) .args(&["-m", "-u", "-o", "file", "file", "file", "file"])
.succeeds(); .succeeds();
assert_eq!(at.read("file"), input); 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 source = [0x66, 0x6f, 0x80, 0x6f];
let arg = OsStr::from_bytes(&source[..]); let arg = OsStr::from_bytes(&source[..]);
let mut cmd = new_ucmd!(); new_ucmd!()
cmd.arg("123").arg("-ne"); .args(&[OsStr::new("123"), OsStr::new("-ne"), arg])
cmd.raw.arg(arg); .run()
cmd.run()
.code_is(2) .code_is(2)
.stderr_is("test: invalid integer $'fo\\x80o'\n"); .stderr_is("test: invalid integer $'fo\\x80o'\n");
let mut cmd = new_ucmd!(); new_ucmd!()
cmd.raw.arg(arg); .args(&[arg, OsStr::new("-eq"), OsStr::new("456")])
cmd.arg("-eq").arg("456"); .run()
cmd.run()
.code_is(2) .code_is(2)
.stderr_is("test: invalid integer $'fo\\x80o'\n"); .stderr_is("test: invalid integer $'fo\\x80o'\n");
} }

View file

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

View file

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

View file

@ -3,19 +3,20 @@
// * For the full copyright and license information, please view the LICENSE // * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code. // * file that was distributed with this source code.
//spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd //spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized
#![allow(dead_code)] #![allow(dead_code)]
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[cfg(target_os = "linux")] #[cfg(any(target_os = "linux", target_os = "android"))]
use rlimit::prlimit; use rlimit::prlimit;
use rstest::rstest; use rstest::rstest;
#[cfg(unix)] #[cfg(unix)]
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::VecDeque;
#[cfg(not(windows))] #[cfg(not(windows))]
use std::ffi::CString; 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::fs::{self, hard_link, remove_file, File, OpenOptions};
use std::io::{self, BufWriter, Read, Result, Write}; use std::io::{self, BufWriter, Read, Result, Write};
#[cfg(unix)] #[cfg(unix)]
@ -34,7 +35,6 @@ use std::thread::{sleep, JoinHandle};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{env, hint, thread}; use std::{env, hint, thread};
use tempfile::{Builder, TempDir}; use tempfile::{Builder, TempDir};
use uucore::Args;
static TESTS_DIR: &str = "tests"; static TESTS_DIR: &str = "tests";
static FIXTURES_DIR: &str = "fixtures"; 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"; 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 /// Test if the program is running under CI
pub fn is_ci() -> bool { pub fn is_ci() -> bool {
std::env::var("CI") 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)] #[derive(Debug, Clone)]
pub struct CmdResult { pub struct CmdResult {
/// bin_path provided by `TestScenario` or `UCommand` /// bin_path provided by `TestScenario` or `UCommand`
bin_path: String, bin_path: PathBuf,
/// util_name provided by `TestScenario` or `UCommand` /// util_name provided by `TestScenario` or `UCommand`
util_name: Option<String>, util_name: Option<String>,
//tmpd is used for convenience functions for asserts against fixtures //tmpd is used for convenience functions for asserts against fixtures
@ -78,21 +80,23 @@ pub struct CmdResult {
} }
impl CmdResult { impl CmdResult {
pub fn new<T, U>( pub fn new<S, T, U, V>(
bin_path: String, bin_path: S,
util_name: Option<String>, util_name: Option<T>,
tmpd: Option<Rc<TempDir>>, tmpd: Option<Rc<TempDir>>,
exit_status: Option<ExitStatus>, exit_status: Option<ExitStatus>,
stdout: T, stdout: U,
stderr: U, stderr: V,
) -> Self ) -> Self
where where
T: Into<Vec<u8>>, S: Into<PathBuf>,
T: AsRef<str>,
U: Into<Vec<u8>>, U: Into<Vec<u8>>,
V: Into<Vec<u8>>,
{ {
Self { Self {
bin_path, bin_path: bin_path.into(),
util_name, util_name: util_name.map(|s| s.as_ref().into()),
tmpd, tmpd,
exit_status, exit_status,
stdout: stdout.into(), stdout: stdout.into(),
@ -634,7 +638,7 @@ impl CmdResult {
self.stderr_only(format!( self.stderr_only(format!(
"{0}: {2}\nTry '{1} {0} --help' for more information.\n", "{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.util_name.as_ref().unwrap(), // This shouldn't be called using a normal command
self.bin_path, self.bin_path.display(),
msg.as_ref() msg.as_ref()
)) ))
} }
@ -1093,18 +1097,21 @@ pub struct TestScenario {
} }
impl 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 tmpd = Rc::new(TempDir::new().unwrap());
let ts = Self { let ts = Self {
bin_path: PathBuf::from(env!("CARGO_BIN_EXE_coreutils")), bin_path: PathBuf::from(TESTS_BINARY),
util_name: String::from(util_name), util_name: util_name.as_ref().into(),
fixtures: AtPath::new(tmpd.as_ref().path()), fixtures: AtPath::new(tmpd.as_ref().path()),
tmpd, tmpd,
}; };
let mut fixture_path_builder = env::current_dir().unwrap(); let mut fixture_path_builder = env::current_dir().unwrap();
fixture_path_builder.push(TESTS_DIR); fixture_path_builder.push(TESTS_DIR);
fixture_path_builder.push(FIXTURES_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 let Ok(m) = fs::metadata(&fixture_path_builder) {
if m.is_dir() { if m.is_dir() {
recursive_copy(&fixture_path_builder, &ts.fixtures.subdir).unwrap(); 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 /// Returns builder for invoking the target uutils binary. Paths given are
/// treated relative to the environment's unique temporary test directory. /// treated relative to the environment's unique temporary test directory.
pub fn ucmd(&self) -> UCommand { pub fn ucmd(&self) -> UCommand {
self.composite_cmd(&self.bin_path, &self.util_name, true) UCommand::from_test_scenario(self)
}
/// 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)
} }
/// Returns builder for invoking any system command. Paths given are treated /// Returns builder for invoking any system command. Paths given are treated
/// relative to the environment's unique temporary test directory. /// relative to the environment's unique temporary test directory.
pub fn cmd<S: AsRef<OsStr>>(&self, bin: S) -> UCommand { pub fn cmd<S: Into<PathBuf>>(&self, bin_path: S) -> UCommand {
UCommand::new_from_tmp::<S, S>(bin, &None, self.tmpd.clone(), true) 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 /// Returns builder for invoking any uutils command. Paths given are treated
/// relative to the environment's unique temporary test directory. /// relative to the environment's unique temporary test directory.
pub fn ccmd<S: AsRef<OsStr>>(&self, bin: S) -> UCommand { pub fn ccmd<S: AsRef<str>>(&self, util_name: S) -> UCommand {
self.composite_cmd(&self.bin_path, bin, true) UCommand::with_util(util_name, self.tmpd.clone())
}
// 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)
} }
} }
/// 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 /// 1. it has convenience functions that are more ergonomic to use for piping in stdin, spawning the command
/// and asserting on the results. /// and asserting on the results.
/// 2. it tracks arguments provided so that in test cases which may provide variations of an arg in loops /// 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. /// 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. /// 3. it provides convenience construction methods to set the Command uutils utility and temporary directory.
#[derive(Debug)] ///
/// 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 struct UCommand {
pub raw: Command, args: VecDeque<OsString>,
comm_string: String, env_vars: Vec<(OsString, OsString)>,
bin_path: String, current_dir: Option<PathBuf>,
env_clear: bool,
bin_path: Option<PathBuf>,
util_name: Option<String>, util_name: Option<String>,
has_run: bool, has_run: bool,
ignore_stdin_write_error: bool, ignore_stdin_write_error: bool,
@ -1183,72 +1182,80 @@ pub struct UCommand {
} }
impl UCommand { impl UCommand {
pub fn new<T: AsRef<OsStr>, S: AsRef<OsStr>, U: AsRef<OsStr>>( /// Create a new plain [`UCommand`].
bin_path: T, ///
util_name: &Option<S>, /// Executes a command that must be given as argument (for example with [`UCommand::arg`] in a
curdir: U, /// shell (`sh -c` on unix platforms or `cmd /C` on windows).
env_clear: bool, ///
) -> Self { /// Per default the environment is cleared and the working directory is set to an individual
let bin_path = bin_path.as_ref(); /// temporary directory for safety purposes.
let util_name = util_name.as_ref().map(std::convert::AsRef::as_ref); pub fn new() -> Self {
Self {
let mut ucmd = Self { env_clear: true,
tmpd: None, ..Default::default()
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 [`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 ucmd
} }
pub fn new_from_tmp<T: AsRef<OsStr>, S: AsRef<OsStr>>( /// Create a [`UCommand`] from a [`TestScenario`].
bin_path: T, ///
util_name: &Option<S>, /// The temporary directory and uutils utility are inherited from the [`TestScenario`] and the
tmpd: Rc<TempDir>, /// execution binary is set to `coreutils`.
env_clear: bool, pub fn from_test_scenario(scene: &TestScenario) -> Self {
) -> Self { Self::with_util(&scene.util_name, scene.tmpd.clone())
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); /// Set the execution binary.
ucmd ///
/// 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 { 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 /// Add a parameter to the invocation. Path arguments are treated relative
/// to the test environment directory. /// to the test environment directory.
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self { pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
assert!(!self.has_run, "{}", ALREADY_RUN); self.args.push_back(arg.as_ref().into());
self.comm_string.push(' ');
self.comm_string
.push_str(arg.as_ref().to_str().unwrap_or_default());
self.raw.arg(arg.as_ref());
self self
} }
/// Add multiple parameters to the invocation. Path arguments are treated relative /// Add multiple parameters to the invocation. Path arguments are treated relative
/// to the test environment directory. /// to the test environment directory.
pub fn args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self { pub fn args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
assert!(!self.has_run, "{}", MULTIPLE_STDIN_MEANINGLESS); self.args.extend(args.iter().map(|s| s.as_ref().into()));
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 self
} }
@ -1331,13 +1323,13 @@ impl UCommand {
K: AsRef<OsStr>, K: AsRef<OsStr>,
V: AsRef<OsStr>, V: AsRef<OsStr>,
{ {
assert!(!self.has_run, "{}", ALREADY_RUN); self.env_vars
self.raw.env(key, val); .push((key.as_ref().into(), val.as_ref().into()));
self self
} }
#[cfg(any(target_os = "linux", target_os = "android"))] #[cfg(any(target_os = "linux", target_os = "android"))]
pub fn with_limit( pub fn limit(
&mut self, &mut self,
resource: rlimit::Resource, resource: rlimit::Resource,
soft_limit: u64, soft_limit: u64,
@ -1359,26 +1351,113 @@ impl UCommand {
self self
} }
/// Spawns the command, feeds the stdin if any, and returns the /// Build the `std::process::Command` and apply the defaults on fields which were not specified
/// child process immediately. /// by the user.
pub fn run_no_wait(&mut self) -> UChild { ///
assert!(!self.has_run, "{}", ALREADY_RUN); /// These __defaults__ are:
self.has_run = true; /// * `bin_path`: Depending on the platform and os, the native shell (unix -> `/bin/sh` etc.).
log_info("run", &self.comm_string); /// 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_stdout = None;
let mut captured_stderr = None; let mut captured_stderr = None;
let command = if self.stderr_to_stdout { if self.stderr_to_stdout {
let mut output = CapturedOutput::default(); let mut output = CapturedOutput::default();
let command = self command
.raw
.stdin(self.stdin.take().unwrap_or_else(Stdio::null)) .stdin(self.stdin.take().unwrap_or_else(Stdio::null))
.stdout(Stdio::from(output.try_clone().unwrap())) .stdout(Stdio::from(output.try_clone().unwrap()))
.stderr(Stdio::from(output.try_clone().unwrap())); .stderr(Stdio::from(output.try_clone().unwrap()));
captured_stdout = Some(output); captured_stdout = Some(output);
command
} else { } else {
let stdout = if self.stdout.is_some() { let stdout = if self.stdout.is_some() {
self.stdout.take().unwrap() self.stdout.take().unwrap()
@ -1398,15 +1477,27 @@ impl UCommand {
stdio stdio
}; };
self.raw command
.stdin(self.stdin.take().unwrap_or_else(Stdio::null)) .stdin(self.stdin.take().unwrap_or_else(Stdio::null))
.stdout(stdout) .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(); 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 { for &(resource, soft_limit, hard_limit) in &self.limits {
prlimit( prlimit(
child.id() as i32, 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 /// Stored the captured output in a temporary file. The file is deleted as soon as
/// [`CapturedOutput`] is dropped. /// [`CapturedOutput`] is dropped.
#[derive(Debug)] #[derive(Debug)]
@ -1597,14 +1699,14 @@ impl<'a> UChildAssertion<'a> {
self.uchild.stderr_exact_bytes(expected_stderr_size), self.uchild.stderr_exact_bytes(expected_stderr_size),
), ),
}; };
CmdResult { CmdResult::new(
bin_path: self.uchild.bin_path.clone(), self.uchild.bin_path.clone(),
util_name: self.uchild.util_name.clone(), self.uchild.util_name.clone(),
tmpd: self.uchild.tmpd.clone(), self.uchild.tmpd.clone(),
exit_status, exit_status,
stdout, stdout,
stderr, stderr,
} )
} }
// Make assertions of [`CmdResult`] with all output from start of the process until now. // 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. /// Abstraction for a [`std::process::Child`] to handle the child process.
pub struct UChild { pub struct UChild {
raw: Child, raw: Child,
bin_path: String, bin_path: PathBuf,
util_name: Option<String>, util_name: Option<String>,
captured_stdout: Option<CapturedOutput>, captured_stdout: Option<CapturedOutput>,
captured_stderr: Option<CapturedOutput>, captured_stderr: Option<CapturedOutput>,
@ -1704,7 +1806,7 @@ impl UChild {
) -> Self { ) -> Self {
Self { Self {
raw: child, raw: child,
bin_path: ucommand.bin_path.clone(), bin_path: ucommand.bin_path.clone().unwrap(),
util_name: ucommand.util_name.clone(), util_name: ucommand.util_name.clone(),
captured_stdout, captured_stdout,
captured_stderr, captured_stderr,
@ -2335,11 +2437,13 @@ fn parse_coreutil_version(version_string: &str) -> f32 {
///``` ///```
#[cfg(unix)] #[cfg(unix)]
pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result<CmdResult, String> { 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 = ts.util_name.as_str();
let util_name = &host_name_for(&ts.util_name); println!("{}", check_coreutil_version(util_name, VERSION_MIN)?);
let util_name = host_name_for(util_name);
let result = ts let result = ts
.cmd_keepenv(util_name.as_ref()) .cmd(util_name.as_ref())
.keep_env()
.env("LC_ALL", "C") .env("LC_ALL", "C")
.args(args) .args(args)
.run(); .run();
@ -2411,7 +2515,8 @@ pub fn run_ucmd_as_root(
// we can run sudo and we're root // we can run sudo and we're root
// run ucmd as root: // run ucmd as root:
Ok(ts Ok(ts
.cmd_keepenv("sudo") .cmd("sudo")
.keep_env()
.env("LC_ALL", "C") .env("LC_ALL", "C")
.arg("-E") .arg("-E")
.arg("--non-interactive") .arg("--non-interactive")
@ -2439,30 +2544,8 @@ mod tests {
// spell-checker:ignore (tests) asdfsadfa // spell-checker:ignore (tests) asdfsadfa
use super::*; use super::*;
#[cfg(unix)]
pub fn run_cmd<T: AsRef<OsStr>>(cmd: T) -> CmdResult { pub fn run_cmd<T: AsRef<OsStr>>(cmd: T) -> CmdResult {
let mut ucmd = UCommand::new_from_tmp::<&str, String>( UCommand::new().arg(cmd).run()
"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()
} }
#[test] #[test]
@ -3200,4 +3283,52 @@ mod tests {
let ts = TestScenario::new("sleep"); let ts = TestScenario::new("sleep");
ts.ucmd().timeout(Duration::from_secs(60)).arg("1.0").run(); 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());
}
} }