Merge branch 'master' of https://github.com/uutils/coreutils into uutils-master-2

This commit is contained in:
Tyler 2021-08-03 17:33:43 -07:00
commit 601c9fc620
33 changed files with 1762 additions and 521 deletions

View file

@ -13,8 +13,8 @@ env:
PROJECT_NAME: coreutils PROJECT_NAME: coreutils
PROJECT_DESC: "Core universal (cross-platform) utilities" PROJECT_DESC: "Core universal (cross-platform) utilities"
PROJECT_AUTH: "uutils" PROJECT_AUTH: "uutils"
RUST_MIN_SRV: "1.43.1" ## v1.43.0 RUST_MIN_SRV: "1.47.0" ## MSRV v1.47.0
RUST_COV_SRV: "2020-08-01" ## (~v1.47.0) supported rust version for code coverage; (date required/used by 'coverage') ## !maint: refactor when code coverage support is included in the stable channel RUST_COV_SRV: "2021-05-06" ## (~v1.52.0) supported rust version for code coverage; (date required/used by 'coverage') ## !maint: refactor when code coverage support is included in the stable channel
on: [push, pull_request] on: [push, pull_request]
@ -249,6 +249,8 @@ jobs:
# { os, target, cargo-options, features, use-cross, toolchain } # { os, target, cargo-options, features, use-cross, toolchain }
- { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf , features: feat_os_unix_gnueabihf , use-cross: use-cross } - { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf , features: feat_os_unix_gnueabihf , use-cross: use-cross }
- { os: ubuntu-latest , target: aarch64-unknown-linux-gnu , features: feat_os_unix_gnueabihf , use-cross: use-cross } - { os: ubuntu-latest , target: aarch64-unknown-linux-gnu , features: feat_os_unix_gnueabihf , use-cross: use-cross }
- { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross }
# - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_selinux , use-cross: use-cross }
# - { os: ubuntu-18.04 , target: i586-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } ## note: older windows platform; not required, dev-FYI only # - { os: ubuntu-18.04 , target: i586-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } ## note: older windows platform; not required, dev-FYI only
# - { os: ubuntu-18.04 , target: i586-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } ## note: older windows platform; not required, dev-FYI only # - { os: ubuntu-18.04 , target: i586-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } ## note: older windows platform; not required, dev-FYI only
- { os: ubuntu-18.04 , target: i686-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } - { os: ubuntu-18.04 , target: i686-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross }

View file

@ -151,6 +151,9 @@ Sylvestre Ledru
T Jameson Little T Jameson Little
Jameson Jameson
Little Little
Thomas Queiroz
Thomas
Queiroz
Tobias Bohumir Schottdorf Tobias Bohumir Schottdorf
Tobias Tobias
Bohumir Bohumir

215
Cargo.lock generated
View file

@ -73,6 +73,29 @@ dependencies = [
"compare", "compare",
] ]
[[package]]
name = "bindgen"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453c49e5950bb0eb63bb3df640e31618846c89d5b7faa54040d76e98e0134375"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"clap",
"env_logger 0.8.4",
"lazy_static",
"lazycell",
"log",
"peeking_take_while",
"proc-macro2",
"quote 1.0.9",
"regex",
"rustc-hash",
"shlex",
"which",
]
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.5.2" version = "0.5.2"
@ -94,6 +117,18 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "bitvec"
version = "0.19.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]] [[package]]
name = "blake2b_simd" name = "blake2b_simd"
version = "0.5.11" version = "0.5.11"
@ -149,9 +184,18 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.68" version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
[[package]]
name = "cexpr"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db507a7679252d2276ed0dd8113c6875ec56d3089f9225b2b42c30cc1f8e5c89"
dependencies = [
"nom",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -178,6 +222,17 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "clang-sys"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "853eda514c284c2287f4bf20ae614f8781f40a81d32ecda6e91449304dfe077c"
dependencies = [
"glob 0.3.0",
"libc",
"libloading",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "2.33.3" version = "2.33.3"
@ -241,6 +296,7 @@ dependencies = [
"rand 0.7.3", "rand 0.7.3",
"regex", "regex",
"rlimit", "rlimit",
"selinux",
"sha1", "sha1",
"tempfile", "tempfile",
"textwrap", "textwrap",
@ -464,9 +520,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"crossbeam-epoch", "crossbeam-epoch",
@ -592,6 +648,19 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "env_logger"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
dependencies = [
"atty",
"humantime",
"log",
"regex",
"termcolor",
]
[[package]] [[package]]
name = "fake-simd" name = "fake-simd"
version = "0.1.2" version = "0.1.2"
@ -634,6 +703,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "funty"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]] [[package]]
name = "gcd" name = "gcd"
version = "2.0.1" version = "2.0.1"
@ -747,6 +822,12 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "if_rust_version" name = "if_rust_version"
version = "1.0.0" version = "1.0.0"
@ -802,12 +883,28 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.85" version = "0.2.85"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3"
[[package]]
name = "libloading"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a"
dependencies = [
"cfg-if 1.0.0",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "locale" name = "locale"
version = "0.2.2" version = "0.2.2"
@ -939,6 +1036,18 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "6.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
dependencies = [
"bitvec",
"funty",
"memchr 2.4.0",
"version_check",
]
[[package]] [[package]]
name = "ntapi" name = "ntapi"
version = "0.3.6" version = "0.3.6"
@ -1104,6 +1213,12 @@ dependencies = [
"proc-macro-hack", "proc-macro-hack",
] ]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.19" version = "0.3.19"
@ -1170,9 +1285,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.27" version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612"
dependencies = [ dependencies = [
"unicode-xid 0.2.2", "unicode-xid 0.2.2",
] ]
@ -1195,7 +1310,7 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f"
dependencies = [ dependencies = [
"env_logger", "env_logger 0.7.1",
"log", "log",
"rand 0.7.3", "rand 0.7.3",
"rand_core 0.5.1", "rand_core 0.5.1",
@ -1216,6 +1331,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "radium"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.5.6" version = "0.5.6"
@ -1384,6 +1505,12 @@ dependencies = [
"redox_syscall 0.2.9", "redox_syscall 0.2.9",
] ]
[[package]]
name = "reference-counted-singleton"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef445213a92fdddc4bc69d9111156d20ffd50704a86ad82b372aab701a0d3a9a"
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.5.4" version = "1.5.4"
@ -1438,6 +1565,12 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@ -1453,6 +1586,32 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "selinux"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd525eeb189eb26c8471463186bba87644e3d8a9c7ae392adaf9ec45ede574bc"
dependencies = [
"bitflags",
"libc",
"once_cell",
"reference-counted-singleton",
"selinux-sys",
"thiserror",
]
[[package]]
name = "selinux-sys"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d842d177120716580c4c6cb56dfe3c5f3a3e3dcec635091f1b2034b6c0be4c6"
dependencies = [
"bindgen",
"cc",
"dunce",
"walkdir",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.6.0" version = "0.6.0"
@ -1484,6 +1643,12 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "shlex"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a568c8f2cd051a4d283bd6eb0343ac214c1b0f1ac19f93e1175b2dee38c73d"
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.9" version = "0.3.9"
@ -1572,15 +1737,21 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.73" version = "1.0.74"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.9", "quote 1.0.9",
"unicode-xid 0.2.2", "unicode-xid 0.2.2",
] ]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.2.0" version = "3.2.0"
@ -1614,6 +1785,15 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "termion" name = "termion"
version = "1.5.6" version = "1.5.6"
@ -2111,6 +2291,7 @@ name = "uu_id"
version = "0.0.7" version = "0.0.7"
dependencies = [ dependencies = [
"clap", "clap",
"selinux",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
] ]
@ -2603,6 +2784,7 @@ version = "0.0.7"
dependencies = [ dependencies = [
"clap", "clap",
"libc", "libc",
"nix 0.20.0",
"redox_syscall 0.1.57", "redox_syscall 0.1.57",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
@ -2872,6 +3054,15 @@ version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "which"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "wild" name = "wild"
version = "2.0.4" version = "2.0.4"
@ -2924,6 +3115,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "wyz"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
[[package]] [[package]]
name = "xattr" name = "xattr"
version = "0.2.2" version = "0.2.2"

View file

@ -1,6 +1,8 @@
# coreutils (uutils) # coreutils (uutils)
# * see the repository LICENSE, README, and CONTRIBUTING files for more information # * see the repository LICENSE, README, and CONTRIBUTING files for more information
# spell-checker:ignore (libs) libselinux
[package] [package]
name = "coreutils" name = "coreutils"
version = "0.0.7" version = "0.0.7"
@ -139,6 +141,11 @@ feat_os_unix_musl = [
# #
"feat_require_unix", "feat_require_unix",
] ]
# "feat_selinux" == set of utilities providing support for SELinux Security Context if enabled with `--features feat_selinux`.
# NOTE:
# The selinux(-sys) crate requires `libselinux` headers and shared library to be accessible in the C toolchain at compile time.
# Running a uutils compiled with `feat_selinux` requires an SELinux enabled Kernel at run time.
feat_selinux = ["id/selinux", "selinux"]
## feature sets with requirements (restricting cross-platform availability) ## feature sets with requirements (restricting cross-platform availability)
# #
# ** NOTE: these `feat_require_...` sets should be minimized as much as possible to encourage cross-platform availability of utilities # ** NOTE: these `feat_require_...` sets should be minimized as much as possible to encourage cross-platform availability of utilities
@ -230,6 +237,7 @@ clap = { version = "2.33", features = ["wrap_help"] }
lazy_static = { version="1.3" } lazy_static = { version="1.3" }
textwrap = { version="=0.11.0", features=["term_size"] } # !maint: [2020-05-10; rivy] unstable crate using undocumented features; pinned currently, will review textwrap = { version="=0.11.0", features=["term_size"] } # !maint: [2020-05-10; rivy] unstable crate using undocumented features; pinned currently, will review
uucore = { version=">=0.0.9", package="uucore", path="src/uucore" } uucore = { version=">=0.0.9", package="uucore", path="src/uucore" }
selinux = { version="0.1.3", optional = true }
# * uutils # * uutils
uu_test = { optional=true, version="0.0.7", package="uu_test", path="src/uu/test" } uu_test = { optional=true, version="0.0.7", package="uu_test", path="src/uu/test" }
# #

View file

@ -39,7 +39,7 @@ to compile anywhere, and this is as good a way as any to try and learn it.
### Rust Version ### Rust Version
uutils follows Rust's release channels and is tested against stable, beta and nightly. uutils follows Rust's release channels and is tested against stable, beta and nightly.
The current oldest supported version of the Rust compiler is `1.43.1`. The current oldest supported version of the Rust compiler is `1.47`.
On both Windows and Redox, only the nightly version is tested currently. On both Windows and Redox, only the nightly version is tested currently.

View file

@ -28,7 +28,7 @@ pub fn main() {
if val == "1" && key.starts_with(env_feature_prefix) { if val == "1" && key.starts_with(env_feature_prefix) {
let krate = key[env_feature_prefix.len()..].to_lowercase(); let krate = key[env_feature_prefix.len()..].to_lowercase();
match krate.as_ref() { match krate.as_ref() {
"default" | "macos" | "unix" | "windows" => continue, // common/standard feature names "default" | "macos" | "unix" | "windows" | "selinux" => continue, // common/standard feature names
"nightly" | "test_unimplemented" => continue, // crate-local custom features "nightly" | "test_unimplemented" => continue, // crate-local custom features
"test" => continue, // over-ridden with 'uu_test' to avoid collision with rust core crate 'test' "test" => continue, // over-ridden with 'uu_test' to avoid collision with rust core crate 'test'
s if s.starts_with(feature_prefix) => continue, // crate feature sets s if s.starts_with(feature_prefix) => continue, // crate feature sets

View file

@ -1 +1 @@
msrv = "1.43.1" msrv = "1.47.0"

View file

@ -8,6 +8,8 @@
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use uucore::error::UCustomError;
use uucore::error::UResult;
#[cfg(unix)] #[cfg(unix)]
use uucore::fsext::statfs_fn; use uucore::fsext::statfs_fn;
use uucore::fsext::{read_fs_list, FsUsage, MountInfo}; use uucore::fsext::{read_fs_list, FsUsage, MountInfo};
@ -19,8 +21,10 @@ use std::cell::Cell;
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::HashSet; use std::collections::HashSet;
use std::error::Error;
#[cfg(unix)] #[cfg(unix)]
use std::ffi::CString; use std::ffi::CString;
use std::fmt::Display;
#[cfg(unix)] #[cfg(unix)]
use std::mem; use std::mem;
@ -33,9 +37,6 @@ use std::path::Path;
static ABOUT: &str = "Show information about the file system on which each FILE resides,\n\ static ABOUT: &str = "Show information about the file system on which each FILE resides,\n\
or all file systems by default."; or all file systems by default.";
static EXIT_OK: i32 = 0;
static EXIT_ERR: i32 = 1;
static OPT_ALL: &str = "all"; static OPT_ALL: &str = "all";
static OPT_BLOCKSIZE: &str = "blocksize"; static OPT_BLOCKSIZE: &str = "blocksize";
static OPT_DIRECT: &str = "direct"; static OPT_DIRECT: &str = "direct";
@ -226,8 +227,8 @@ fn filter_mount_list(vmi: Vec<MountInfo>, paths: &[String], opt: &Options) -> Ve
/// Convert `value` to a human readable string based on `base`. /// Convert `value` to a human readable string based on `base`.
/// e.g. It returns 1G when value is 1 * 1024 * 1024 * 1024 and base is 1024. /// e.g. It returns 1G when value is 1 * 1024 * 1024 * 1024 and base is 1024.
/// Note: It returns `value` if `base` isn't positive. /// Note: It returns `value` if `base` isn't positive.
fn human_readable(value: u64, base: i64) -> String { fn human_readable(value: u64, base: i64) -> UResult<String> {
match base { let base_str = match base {
d if d < 0 => value.to_string(), d if d < 0 => value.to_string(),
// ref: [Binary prefix](https://en.wikipedia.org/wiki/Binary_prefix) @@ <https://archive.is/cnwmF> // ref: [Binary prefix](https://en.wikipedia.org/wiki/Binary_prefix) @@ <https://archive.is/cnwmF>
@ -242,8 +243,10 @@ fn human_readable(value: u64, base: i64) -> String {
NumberPrefix::Prefixed(prefix, bytes) => format!("{:.1}{}", bytes, prefix.symbol()), NumberPrefix::Prefixed(prefix, bytes) => format!("{:.1}{}", bytes, prefix.symbol()),
}, },
_ => crash!(EXIT_ERR, "Internal error: Unknown base value {}", base), _ => return Err(DfError::InvalidBaseValue(base.to_string()).into()),
} };
Ok(base_str)
} }
fn use_size(free_size: u64, total_size: u64) -> String { fn use_size(free_size: u64, total_size: u64) -> String {
@ -256,7 +259,31 @@ fn use_size(free_size: u64, total_size: u64) -> String {
); );
} }
pub fn uumain(args: impl uucore::Args) -> i32 { #[derive(Debug)]
enum DfError {
InvalidBaseValue(String),
}
impl Display for DfError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DfError::InvalidBaseValue(s) => write!(f, "Internal error: Unknown base value {}", s),
}
}
}
impl Error for DfError {}
impl UCustomError for DfError {
fn code(&self) -> i32 {
match self {
DfError::InvalidBaseValue(_) => 1,
}
}
}
#[uucore_procs::gen_uumain]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let usage = get_usage(); let usage = get_usage();
let matches = uu_app().usage(&usage[..]).get_matches_from(args); let matches = uu_app().usage(&usage[..]).get_matches_from(args);
@ -269,7 +296,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
{ {
if matches.is_present(OPT_INODES) { if matches.is_present(OPT_INODES) {
println!("{}: doesn't support -i option", executable!()); println!("{}: doesn't support -i option", executable!());
return EXIT_OK; return Ok(());
} }
} }
@ -353,15 +380,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
if opt.show_inode_instead { if opt.show_inode_instead {
print!( print!(
"{0: >12} ", "{0: >12} ",
human_readable(fs.usage.files, opt.human_readable_base) human_readable(fs.usage.files, opt.human_readable_base)?
); );
print!( print!(
"{0: >12} ", "{0: >12} ",
human_readable(fs.usage.files - fs.usage.ffree, opt.human_readable_base) human_readable(fs.usage.files - fs.usage.ffree, opt.human_readable_base)?
); );
print!( print!(
"{0: >12} ", "{0: >12} ",
human_readable(fs.usage.ffree, opt.human_readable_base) human_readable(fs.usage.ffree, opt.human_readable_base)?
); );
print!( print!(
"{0: >5} ", "{0: >5} ",
@ -375,15 +402,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let free_size = fs.usage.blocksize * fs.usage.bfree; let free_size = fs.usage.blocksize * fs.usage.bfree;
print!( print!(
"{0: >12} ", "{0: >12} ",
human_readable(total_size, opt.human_readable_base) human_readable(total_size, opt.human_readable_base)?
); );
print!( print!(
"{0: >12} ", "{0: >12} ",
human_readable(total_size - free_size, opt.human_readable_base) human_readable(total_size - free_size, opt.human_readable_base)?
); );
print!( print!(
"{0: >12} ", "{0: >12} ",
human_readable(free_size, opt.human_readable_base) human_readable(free_size, opt.human_readable_base)?
); );
if cfg!(target_os = "macos") { if cfg!(target_os = "macos") {
let used = fs.usage.blocks - fs.usage.bfree; let used = fs.usage.blocks - fs.usage.bfree;
@ -396,7 +423,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
println!(); println!();
} }
EXIT_OK Ok(())
} }
pub fn uu_app() -> App<'static, 'static> { pub fn uu_app() -> App<'static, 'static> {

View file

@ -36,28 +36,86 @@ fn get_usage() -> String {
format!("{0} [OPTION]... [FILE]...", executable!()) format!("{0} [OPTION]... [FILE]...", executable!())
} }
fn tabstops_parse(s: String) -> Vec<usize> { /// The mode to use when replacing tabs beyond the last one specified in
let words = s.split(','); /// the `--tabs` argument.
enum RemainingMode {
None,
Slash,
Plus,
}
let nums = words /// Decide whether the character is either a space or a comma.
.map(|sn| { ///
sn.parse::<usize>() /// # Examples
.unwrap_or_else(|_| crash!(1, "{}\n", "tab size contains invalid character(s)")) ///
}) /// ```rust,ignore
.collect::<Vec<usize>>(); /// assert!(is_space_or_comma(' '))
/// assert!(is_space_or_comma(','))
/// assert!(!is_space_or_comma('a'))
/// ```
fn is_space_or_comma(c: char) -> bool {
c == ' ' || c == ','
}
if nums.iter().any(|&n| n == 0) { /// Parse a list of tabstops from a `--tabs` argument.
///
/// This function returns both the vector of numbers appearing in the
/// comma- or space-separated list, and also an optional mode, specified
/// by either a "/" or a "+" character appearing before the final number
/// in the list. This mode defines the strategy to use for computing the
/// number of spaces to use for columns beyond the end of the tab stop
/// list specified here.
fn tabstops_parse(s: String) -> (RemainingMode, Vec<usize>) {
// Leading commas and spaces are ignored.
let s = s.trim_start_matches(is_space_or_comma);
// If there were only commas and spaces in the string, just use the
// default tabstops.
if s.is_empty() {
return (RemainingMode::None, vec![DEFAULT_TABSTOP]);
}
let mut nums = vec![];
let mut remaining_mode = RemainingMode::None;
for word in s.split(is_space_or_comma) {
let bytes = word.as_bytes();
for i in 0..bytes.len() {
match bytes[i] {
b'+' => {
remaining_mode = RemainingMode::Plus;
}
b'/' => {
remaining_mode = RemainingMode::Slash;
}
_ => {
// Parse a number from the byte sequence.
let num = from_utf8(&bytes[i..]).unwrap().parse::<usize>().unwrap();
// Tab size must be positive.
if num == 0 {
crash!(1, "{}\n", "tab size cannot be 0"); crash!(1, "{}\n", "tab size cannot be 0");
} }
if let (false, _) = nums // Tab sizes must be ascending.
.iter() if let Some(last_stop) = nums.last() {
.fold((true, 0), |(acc, last), &n| (acc && last <= n, n)) if *last_stop >= num {
{ crash!(1, "tab sizes must be ascending");
crash!(1, "{}\n", "tab sizes must be ascending"); }
} }
nums // Append this tab stop to the list of all tabstops.
nums.push(num);
break;
}
}
}
}
// If no numbers could be parsed (for example, if `s` were "+,+,+"),
// then just use the default tabstops.
if nums.is_empty() {
nums = vec![DEFAULT_TABSTOP];
}
(remaining_mode, nums)
} }
struct Options { struct Options {
@ -66,13 +124,17 @@ struct Options {
tspaces: String, tspaces: String,
iflag: bool, iflag: bool,
uflag: bool, uflag: bool,
/// Strategy for expanding tabs for columns beyond those specified
/// in `tabstops`.
remaining_mode: RemainingMode,
} }
impl Options { impl Options {
fn new(matches: &ArgMatches) -> Options { fn new(matches: &ArgMatches) -> Options {
let tabstops = match matches.value_of(options::TABS) { let (remaining_mode, tabstops) = match matches.value_of(options::TABS) {
Some(s) => tabstops_parse(s.to_string()), Some(s) => tabstops_parse(s.to_string()),
None => vec![DEFAULT_TABSTOP], None => (RemainingMode::None, vec![DEFAULT_TABSTOP]),
}; };
let iflag = matches.is_present(options::INITIAL); let iflag = matches.is_present(options::INITIAL);
@ -102,6 +164,7 @@ impl Options {
tspaces, tspaces,
iflag, iflag,
uflag, uflag,
remaining_mode,
} }
} }
} }
@ -159,8 +222,34 @@ fn open(path: String) -> BufReader<Box<dyn Read + 'static>> {
} }
} }
fn next_tabstop(tabstops: &[usize], col: usize) -> usize { /// Compute the number of spaces to the next tabstop.
if tabstops.len() == 1 { ///
/// `tabstops` is the sequence of tabstop locations.
///
/// `col` is the index of the current cursor in the line being written.
///
/// If `remaining_mode` is [`RemainingMode::Plus`], then the last entry
/// in the `tabstops` slice is interpreted as a relative number of
/// spaces, which this function will return for every input value of
/// `col` beyond the end of the second-to-last element of `tabstops`.
///
/// If `remaining_mode` is [`RemainingMode::Plus`], then the last entry
/// in the `tabstops` slice is interpreted as a relative number of
/// spaces, which this function will return for every input value of
/// `col` beyond the end of the second-to-last element of `tabstops`.
fn next_tabstop(tabstops: &[usize], col: usize, remaining_mode: &RemainingMode) -> usize {
let num_tabstops = tabstops.len();
match remaining_mode {
RemainingMode::Plus => match tabstops[0..num_tabstops - 1].iter().find(|&&t| t > col) {
Some(t) => t - col,
None => tabstops[num_tabstops - 1] - 1,
},
RemainingMode::Slash => match tabstops[0..num_tabstops - 1].iter().find(|&&t| t > col) {
Some(t) => t - col,
None => tabstops[num_tabstops - 1] - col % tabstops[num_tabstops - 1],
},
RemainingMode::None => {
if num_tabstops == 1 {
tabstops[0] - col % tabstops[0] tabstops[0] - col % tabstops[0]
} else { } else {
match tabstops.iter().find(|&&t| t > col) { match tabstops.iter().find(|&&t| t > col) {
@ -168,6 +257,8 @@ fn next_tabstop(tabstops: &[usize], col: usize) -> usize {
None => 1, None => 1,
} }
} }
}
}
} }
#[derive(PartialEq, Eq, Debug)] #[derive(PartialEq, Eq, Debug)]
@ -232,12 +323,16 @@ fn expand(options: Options) {
match ctype { match ctype {
Tab => { Tab => {
// figure out how many spaces to the next tabstop // figure out how many spaces to the next tabstop
let nts = next_tabstop(ts, col); let nts = next_tabstop(ts, col, &options.remaining_mode);
col += nts; col += nts;
// now dump out either spaces if we're expanding, or a literal tab if we're not // now dump out either spaces if we're expanding, or a literal tab if we're not
if init || !options.iflag { if init || !options.iflag {
if nts <= options.tspaces.len() {
safe_unwrap!(output.write_all(options.tspaces[..nts].as_bytes())); safe_unwrap!(output.write_all(options.tspaces[..nts].as_bytes()));
} else {
safe_unwrap!(output.write_all(" ".repeat(nts).as_bytes()));
};
} else { } else {
safe_unwrap!(output.write_all(&buf[byte..byte + nbytes])); safe_unwrap!(output.write_all(&buf[byte..byte + nbytes]));
} }
@ -269,3 +364,30 @@ fn expand(options: Options) {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::next_tabstop;
use super::RemainingMode;
#[test]
fn test_next_tabstop_remaining_mode_none() {
assert_eq!(next_tabstop(&[1, 5], 0, &RemainingMode::None), 1);
assert_eq!(next_tabstop(&[1, 5], 3, &RemainingMode::None), 2);
assert_eq!(next_tabstop(&[1, 5], 6, &RemainingMode::None), 1);
}
#[test]
fn test_next_tabstop_remaining_mode_plus() {
assert_eq!(next_tabstop(&[1, 5], 0, &RemainingMode::Plus), 1);
assert_eq!(next_tabstop(&[1, 5], 3, &RemainingMode::Plus), 4);
assert_eq!(next_tabstop(&[1, 5], 6, &RemainingMode::Plus), 4);
}
#[test]
fn test_next_tabstop_remaining_mode_slash() {
assert_eq!(next_tabstop(&[1, 5], 0, &RemainingMode::Slash), 1);
assert_eq!(next_tabstop(&[1, 5], 3, &RemainingMode::Slash), 2);
assert_eq!(next_tabstop(&[1, 5], 6, &RemainingMode::Slash), 4);
}
}

View file

@ -18,7 +18,11 @@ path = "src/id.rs"
clap = { version = "2.33", features = ["wrap_help"] } clap = { version = "2.33", features = ["wrap_help"] }
uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["entries", "process"] } uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["entries", "process"] }
uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" }
selinux = { version="0.1.3", optional = true }
[[bin]] [[bin]]
name = "id" name = "id"
path = "src/main.rs" path = "src/main.rs"
[features]
feat_selinux = ["selinux"]

View file

@ -5,7 +5,10 @@
// //
// For the full copyright and license information, please view the LICENSE // For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code. // file that was distributed with this source code.
//
// spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag gsflag zflag cflag
// README:
// This was originally based on BSD's `id` // This was originally based on BSD's `id`
// (noticeable in functionality, usage text, options text, etc.) // (noticeable in functionality, usage text, options text, etc.)
// and synced with: // and synced with:
@ -25,8 +28,10 @@
// //
// * Help text based on BSD's `id` manpage and GNU's `id` manpage. // * Help text based on BSD's `id` manpage and GNU's `id` manpage.
// //
// * This passes GNU's coreutils Test suite (8.32) for "tests/id/context.sh" if compiled with
// spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag gsflag zflag // `--features feat_selinux`. It should also pass "tests/id/no-context.sh", but that depends on
// `uu_ls -Z` being implemented and therefore fails at the moment
//
#![allow(non_camel_case_types)] #![allow(non_camel_case_types)]
#![allow(dead_code)] #![allow(dead_code)]
@ -35,6 +40,8 @@
extern crate uucore; extern crate uucore;
use clap::{crate_version, App, Arg}; use clap::{crate_version, App, Arg};
#[cfg(all(target_os = "linux", feature = "selinux"))]
use selinux;
use std::ffi::CStr; use std::ffi::CStr;
use uucore::entries::{self, Group, Locate, Passwd}; use uucore::entries::{self, Group, Locate, Passwd};
pub use uucore::libc; pub use uucore::libc;
@ -50,6 +57,11 @@ macro_rules! cstr2cow {
static ABOUT: &str = "Print user and group information for each specified USER, static ABOUT: &str = "Print user and group information for each specified USER,
or (when USER omitted) for the current user."; or (when USER omitted) for the current user.";
#[cfg(not(feature = "selinux"))]
static CONTEXT_HELP_TEXT: &str = "print only the security context of the process (not enabled)";
#[cfg(feature = "selinux")]
static CONTEXT_HELP_TEXT: &str = "print only the security context of the process";
mod options { mod options {
pub const OPT_AUDIT: &str = "audit"; // GNU's id does not have this pub const OPT_AUDIT: &str = "audit"; // GNU's id does not have this
pub const OPT_CONTEXT: &str = "context"; pub const OPT_CONTEXT: &str = "context";
@ -93,6 +105,8 @@ struct State {
gsflag: bool, // --groups gsflag: bool, // --groups
rflag: bool, // --real rflag: bool, // --real
zflag: bool, // --zero zflag: bool, // --zero
cflag: bool, // --context
selinux_supported: bool,
ids: Option<Ids>, ids: Option<Ids>,
// The behavior for calling GNU's `id` and calling GNU's `id $USER` is similar but different. // The behavior for calling GNU's `id` and calling GNU's `id $USER` is similar but different.
// * The SELinux context is only displayed without a specified user. // * The SELinux context is only displayed without a specified user.
@ -109,6 +123,7 @@ struct State {
// 1000 10 968 975 // 1000 10 968 975
// +++ exited with 0 +++ // +++ exited with 0 +++
user_specified: bool, user_specified: bool,
exit_code: i32,
} }
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
@ -132,8 +147,21 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
gsflag: matches.is_present(options::OPT_GROUPS), gsflag: matches.is_present(options::OPT_GROUPS),
rflag: matches.is_present(options::OPT_REAL_ID), rflag: matches.is_present(options::OPT_REAL_ID),
zflag: matches.is_present(options::OPT_ZERO), zflag: matches.is_present(options::OPT_ZERO),
cflag: matches.is_present(options::OPT_CONTEXT),
selinux_supported: {
#[cfg(feature = "selinux")]
{
selinux::kernel_support() != selinux::KernelSupport::Unsupported
}
#[cfg(not(feature = "selinux"))]
{
false
}
},
user_specified: !users.is_empty(), user_specified: !users.is_empty(),
ids: None, ids: None,
exit_code: 0,
}; };
let default_format = { let default_format = {
@ -141,13 +169,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
!(state.uflag || state.gflag || state.gsflag) !(state.uflag || state.gflag || state.gsflag)
}; };
if (state.nflag || state.rflag) && default_format { if (state.nflag || state.rflag) && default_format && !state.cflag {
crash!(1, "cannot print only names or real IDs in default format"); crash!(1, "cannot print only names or real IDs in default format");
} }
if (state.zflag) && default_format { if state.zflag && default_format && !state.cflag {
// NOTE: GNU test suite "id/zero.sh" needs this stderr output: // NOTE: GNU test suite "id/zero.sh" needs this stderr output:
crash!(1, "option --zero not permitted in default format"); crash!(1, "option --zero not permitted in default format");
} }
if state.user_specified && state.cflag {
crash!(1, "cannot print security context when user specified");
}
let delimiter = { let delimiter = {
if state.zflag { if state.zflag {
@ -163,7 +194,23 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
'\n' '\n'
} }
}; };
let mut exit_code = 0;
if state.cflag {
if state.selinux_supported {
// print SElinux context and exit
#[cfg(all(target_os = "linux", feature = "selinux"))]
if let Ok(context) = selinux::SecurityContext::current(false) {
let bytes = context.as_bytes();
print!("{}{}", String::from_utf8_lossy(bytes), line_ending);
} else {
// print error because `cflag` was explicitly requested
crash!(1, "can't get process context");
}
return state.exit_code;
} else {
crash!(1, "--context (-Z) works only on an SELinux-enabled kernel");
}
}
for i in 0..=users.len() { for i in 0..=users.len() {
let possible_pw = if !state.user_specified { let possible_pw = if !state.user_specified {
@ -173,7 +220,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
Ok(p) => Some(p), Ok(p) => Some(p),
Err(_) => { Err(_) => {
show_error!("'{}': no such user", users[i]); show_error!("'{}': no such user", users[i]);
exit_code = 1; state.exit_code = 1;
if i + 1 >= users.len() { if i + 1 >= users.len() {
break; break;
} else { } else {
@ -187,17 +234,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
if matches.is_present(options::OPT_PASSWORD) { if matches.is_present(options::OPT_PASSWORD) {
// BSD's `id` ignores all but the first specified user // BSD's `id` ignores all but the first specified user
pline(possible_pw.map(|v| v.uid())); pline(possible_pw.map(|v| v.uid()));
return exit_code; return state.exit_code;
}; };
if matches.is_present(options::OPT_HUMAN_READABLE) { if matches.is_present(options::OPT_HUMAN_READABLE) {
// BSD's `id` ignores all but the first specified user // BSD's `id` ignores all but the first specified user
pretty(possible_pw); pretty(possible_pw);
return exit_code; return state.exit_code;
} }
if matches.is_present(options::OPT_AUDIT) { if matches.is_present(options::OPT_AUDIT) {
// BSD's `id` ignores specified users // BSD's `id` ignores specified users
auditid(); auditid();
return exit_code; return state.exit_code;
} }
let (uid, gid) = possible_pw.map(|p| (p.uid(), p.gid())).unwrap_or(( let (uid, gid) = possible_pw.map(|p| (p.uid(), p.gid())).unwrap_or((
@ -217,7 +264,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
if state.nflag { if state.nflag {
entries::gid2grp(gid).unwrap_or_else(|_| { entries::gid2grp(gid).unwrap_or_else(|_| {
show_error!("cannot find name for group ID {}", gid); show_error!("cannot find name for group ID {}", gid);
exit_code = 1; state.exit_code = 1;
gid.to_string() gid.to_string()
}) })
} else { } else {
@ -232,7 +279,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
if state.nflag { if state.nflag {
entries::uid2usr(uid).unwrap_or_else(|_| { entries::uid2usr(uid).unwrap_or_else(|_| {
show_error!("cannot find name for user ID {}", uid); show_error!("cannot find name for user ID {}", uid);
exit_code = 1; state.exit_code = 1;
uid.to_string() uid.to_string()
}) })
} else { } else {
@ -257,7 +304,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
if state.nflag { if state.nflag {
entries::gid2grp(id).unwrap_or_else(|_| { entries::gid2grp(id).unwrap_or_else(|_| {
show_error!("cannot find name for group ID {}", id); show_error!("cannot find name for group ID {}", id);
exit_code = 1; state.exit_code = 1;
id.to_string() id.to_string()
}) })
} else { } else {
@ -276,7 +323,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
} }
if default_format { if default_format {
id_print(&state, groups); id_print(&mut state, groups);
} }
print!("{}", line_ending); print!("{}", line_ending);
@ -285,7 +332,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
} }
} }
exit_code state.exit_code
} }
pub fn uu_app() -> App<'static, 'static> { pub fn uu_app() -> App<'static, 'static> {
@ -319,6 +366,7 @@ pub fn uu_app() -> App<'static, 'static> {
Arg::with_name(options::OPT_GROUP) Arg::with_name(options::OPT_GROUP)
.short("g") .short("g")
.long(options::OPT_GROUP) .long(options::OPT_GROUP)
.conflicts_with(options::OPT_EFFECTIVE_USER)
.help("Display only the effective group ID as a number"), .help("Display only the effective group ID as a number"),
) )
.arg( .arg(
@ -328,6 +376,7 @@ pub fn uu_app() -> App<'static, 'static> {
.conflicts_with_all(&[ .conflicts_with_all(&[
options::OPT_GROUP, options::OPT_GROUP,
options::OPT_EFFECTIVE_USER, options::OPT_EFFECTIVE_USER,
options::OPT_CONTEXT,
options::OPT_HUMAN_READABLE, options::OPT_HUMAN_READABLE,
options::OPT_PASSWORD, options::OPT_PASSWORD,
options::OPT_AUDIT, options::OPT_AUDIT,
@ -379,7 +428,8 @@ pub fn uu_app() -> App<'static, 'static> {
Arg::with_name(options::OPT_CONTEXT) Arg::with_name(options::OPT_CONTEXT)
.short("Z") .short("Z")
.long(options::OPT_CONTEXT) .long(options::OPT_CONTEXT)
.help("NotImplemented: print only the security context of the process"), .conflicts_with_all(&[options::OPT_GROUP, options::OPT_EFFECTIVE_USER])
.help(CONTEXT_HELP_TEXT),
) )
.arg( .arg(
Arg::with_name(options::ARG_USERS) Arg::with_name(options::ARG_USERS)
@ -499,34 +549,80 @@ fn auditid() {
println!("asid={}", auditinfo.ai_asid); println!("asid={}", auditinfo.ai_asid);
} }
fn id_print(state: &State, groups: Vec<u32>) { fn id_print(state: &mut State, groups: Vec<u32>) {
let uid = state.ids.as_ref().unwrap().uid; let uid = state.ids.as_ref().unwrap().uid;
let gid = state.ids.as_ref().unwrap().gid; let gid = state.ids.as_ref().unwrap().gid;
let euid = state.ids.as_ref().unwrap().euid; let euid = state.ids.as_ref().unwrap().euid;
let egid = state.ids.as_ref().unwrap().egid; let egid = state.ids.as_ref().unwrap().egid;
print!("uid={}({})", uid, entries::uid2usr(uid).unwrap()); print!(
print!(" gid={}({})", gid, entries::gid2grp(gid).unwrap()); "uid={}({})",
uid,
entries::uid2usr(uid).unwrap_or_else(|_| {
show_error!("cannot find name for user ID {}", uid);
state.exit_code = 1;
uid.to_string()
})
);
print!(
" gid={}({})",
gid,
entries::gid2grp(gid).unwrap_or_else(|_| {
show_error!("cannot find name for group ID {}", gid);
state.exit_code = 1;
gid.to_string()
})
);
if !state.user_specified && (euid != uid) { if !state.user_specified && (euid != uid) {
print!(" euid={}({})", euid, entries::uid2usr(euid).unwrap()); print!(
" euid={}({})",
euid,
entries::uid2usr(euid).unwrap_or_else(|_| {
show_error!("cannot find name for user ID {}", euid);
state.exit_code = 1;
euid.to_string()
})
);
} }
if !state.user_specified && (egid != gid) { if !state.user_specified && (egid != gid) {
print!(" egid={}({})", euid, entries::gid2grp(egid).unwrap()); print!(
" egid={}({})",
euid,
entries::gid2grp(egid).unwrap_or_else(|_| {
show_error!("cannot find name for group ID {}", egid);
state.exit_code = 1;
egid.to_string()
})
);
} }
print!( print!(
" groups={}", " groups={}",
groups groups
.iter() .iter()
.map(|&gr| format!("{}({})", gr, entries::gid2grp(gr).unwrap())) .map(|&gr| format!(
"{}({})",
gr,
entries::gid2grp(gr).unwrap_or_else(|_| {
show_error!("cannot find name for group ID {}", gr);
state.exit_code = 1;
gr.to_string()
})
))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(",") .join(",")
); );
// NOTE: (SELinux NotImplemented) placeholder: #[cfg(all(target_os = "linux", feature = "selinux"))]
// if !state.user_specified { if state.selinux_supported
// // print SElinux context (does not depend on "-Z") && !state.user_specified
// print!(" context={}", get_selinux_contexts().join(":")); && std::env::var_os("POSIXLY_CORRECT").is_none()
// } {
// print SElinux context (does not depend on "-Z")
if let Ok(context) = selinux::SecurityContext::current(false) {
let bytes = context.as_bytes();
print!(" context={}", String::from_utf8_lossy(bytes));
}
}
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]

View file

@ -11,9 +11,12 @@
extern crate uucore; extern crate uucore;
use clap::{crate_version, App, Arg}; use clap::{crate_version, App, Arg};
use uucore::error::{UCustomError, UResult};
use std::borrow::Cow; use std::borrow::Cow;
use std::error::Error;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fmt::Display;
use std::fs; use std::fs;
use std::io::{stdin, Result}; use std::io::{stdin, Result};
@ -44,6 +47,51 @@ pub enum OverwriteMode {
Force, Force,
} }
#[derive(Debug)]
enum LnError {
TargetIsDirectory(String),
SomeLinksFailed,
FailedToLink(String),
MissingDestination(String),
ExtraOperand(String),
InvalidBackupMode(String),
}
impl Display for LnError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TargetIsDirectory(s) => write!(f, "target '{}' is not a directory", s),
Self::FailedToLink(s) => write!(f, "failed to link '{}'", s),
Self::SomeLinksFailed => write!(f, "some links failed to create"),
Self::MissingDestination(s) => {
write!(f, "missing destination file operand after '{}'", s)
}
Self::ExtraOperand(s) => write!(
f,
"extra operand '{}'\nTry '{} --help' for more information.",
s,
executable!()
),
Self::InvalidBackupMode(s) => write!(f, "{}", s),
}
}
}
impl Error for LnError {}
impl UCustomError for LnError {
fn code(&self) -> i32 {
match self {
Self::TargetIsDirectory(_) => 1,
Self::SomeLinksFailed => 1,
Self::FailedToLink(_) => 1,
Self::MissingDestination(_) => 1,
Self::ExtraOperand(_) => 1,
Self::InvalidBackupMode(_) => 1,
}
}
}
fn get_usage() -> String { fn get_usage() -> String {
format!( format!(
"{0} [OPTION]... [-T] TARGET LINK_NAME (1st form) "{0} [OPTION]... [-T] TARGET LINK_NAME (1st form)
@ -86,7 +134,8 @@ mod options {
static ARG_FILES: &str = "files"; static ARG_FILES: &str = "files";
pub fn uumain(args: impl uucore::Args) -> i32 { #[uucore_procs::gen_uumain]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let usage = get_usage(); let usage = get_usage();
let long_usage = get_long_usage(); let long_usage = get_long_usage();
@ -122,8 +171,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
); );
let backup_mode = match backup_mode { let backup_mode = match backup_mode {
Err(err) => { Err(err) => {
show_usage_error!("{}", err); return Err(LnError::InvalidBackupMode(err).into());
return 1;
} }
Ok(mode) => mode, Ok(mode) => mode,
}; };
@ -246,7 +294,7 @@ pub fn uu_app() -> App<'static, 'static> {
) )
} }
fn exec(files: &[PathBuf], settings: &Settings) -> i32 { fn exec(files: &[PathBuf], settings: &Settings) -> UResult<()> {
// Handle cases where we create links in a directory first. // Handle cases where we create links in a directory first.
if let Some(ref name) = settings.target_dir { if let Some(ref name) = settings.target_dir {
// 4th form: a directory is specified by -t. // 4th form: a directory is specified by -t.
@ -267,35 +315,22 @@ fn exec(files: &[PathBuf], settings: &Settings) -> i32 {
// 1st form. Now there should be only two operands, but if -T is // 1st form. Now there should be only two operands, but if -T is
// specified we may have a wrong number of operands. // specified we may have a wrong number of operands.
if files.len() == 1 { if files.len() == 1 {
show_error!( return Err(LnError::MissingDestination(files[0].to_string_lossy().into()).into());
"missing destination file operand after '{}'",
files[0].to_string_lossy()
);
return 1;
} }
if files.len() > 2 { if files.len() > 2 {
show_error!( return Err(LnError::ExtraOperand(files[2].display().to_string()).into());
"extra operand '{}'\nTry '{} --help' for more information.",
files[2].display(),
executable!()
);
return 1;
} }
assert!(!files.is_empty()); assert!(!files.is_empty());
match link(&files[0], &files[1], settings) { match link(&files[0], &files[1], settings) {
Ok(_) => 0, Ok(_) => Ok(()),
Err(e) => { Err(e) => Err(LnError::FailedToLink(e.to_string()).into()),
show_error!("{}", e);
1
}
} }
} }
fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) -> i32 { fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) -> UResult<()> {
if !target_dir.is_dir() { if !target_dir.is_dir() {
show_error!("target '{}' is not a directory", target_dir.display()); return Err(LnError::TargetIsDirectory(target_dir.display().to_string()).into());
return 1;
} }
let mut all_successful = true; let mut all_successful = true;
@ -354,9 +389,9 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings)
} }
} }
if all_successful { if all_successful {
0 Ok(())
} else { } else {
1 Err(LnError::SomeLinksFailed.into())
} }
} }

View file

@ -9,24 +9,33 @@
use crate::{ use crate::{
chunks::{self, Chunk, RecycledChunk}, chunks::{self, Chunk, RecycledChunk},
compare_by, open, GlobalSettings, compare_by, open, GlobalSettings, SortError,
}; };
use itertools::Itertools; use itertools::Itertools;
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
ffi::OsStr,
io::Read, io::Read,
iter, iter,
sync::mpsc::{sync_channel, Receiver, SyncSender}, sync::mpsc::{sync_channel, Receiver, SyncSender},
thread, thread,
}; };
use uucore::error::UResult;
/// Check if the file at `path` is ordered. /// Check if the file at `path` is ordered.
/// ///
/// # Returns /// # Returns
/// ///
/// The code we should exit with. /// The code we should exit with.
pub fn check(path: &str, settings: &GlobalSettings) -> i32 { pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> {
let file = open(path); let max_allowed_cmp = if settings.unique {
// If `unique` is enabled, the previous line must compare _less_ to the next one.
Ordering::Less
} else {
// Otherwise, the line previous line must compare _less or equal_ to the next one.
Ordering::Equal
};
let file = open(path)?;
let (recycled_sender, recycled_receiver) = sync_channel(2); let (recycled_sender, recycled_receiver) = sync_channel(2);
let (loaded_sender, loaded_receiver) = sync_channel(2); let (loaded_sender, loaded_receiver) = sync_channel(2);
thread::spawn({ thread::spawn({
@ -34,7 +43,13 @@ pub fn check(path: &str, settings: &GlobalSettings) -> i32 {
move || reader(file, recycled_receiver, loaded_sender, &settings) move || reader(file, recycled_receiver, loaded_sender, &settings)
}); });
for _ in 0..2 { for _ in 0..2 {
let _ = recycled_sender.send(RecycledChunk::new(100 * 1024)); let _ = recycled_sender.send(RecycledChunk::new(if settings.buffer_size < 100 * 1024 {
// when the buffer size is smaller than 100KiB we choose it instead of the default.
// this improves testability.
settings.buffer_size
} else {
100 * 1024
}));
} }
let mut prev_chunk: Option<Chunk> = None; let mut prev_chunk: Option<Chunk> = None;
@ -53,30 +68,35 @@ pub fn check(path: &str, settings: &GlobalSettings) -> i32 {
settings, settings,
prev_chunk.line_data(), prev_chunk.line_data(),
chunk.line_data(), chunk.line_data(),
) == Ordering::Greater ) > max_allowed_cmp
{ {
if !settings.check_silent { return Err(SortError::Disorder {
println!("sort: {}:{}: disorder: {}", path, line_idx, new_first.line); file: path.to_owned(),
line_number: line_idx,
line: new_first.line.to_owned(),
silent: settings.check_silent,
} }
return 1; .into());
} }
let _ = recycled_sender.send(prev_chunk.recycle()); let _ = recycled_sender.send(prev_chunk.recycle());
} }
for (a, b) in chunk.lines().iter().tuple_windows() { for (a, b) in chunk.lines().iter().tuple_windows() {
line_idx += 1; line_idx += 1;
if compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) == Ordering::Greater if compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) > max_allowed_cmp {
{ return Err(SortError::Disorder {
if !settings.check_silent { file: path.to_owned(),
println!("sort: {}:{}: disorder: {}", path, line_idx, b.line); line_number: line_idx,
line: b.line.to_owned(),
silent: settings.check_silent,
} }
return 1; .into());
} }
} }
prev_chunk = Some(chunk); prev_chunk = Some(chunk);
} }
0 Ok(())
} }
/// The function running on the reader thread. /// The function running on the reader thread.
@ -85,7 +105,7 @@ fn reader(
receiver: Receiver<RecycledChunk>, receiver: Receiver<RecycledChunk>,
sender: SyncSender<Chunk>, sender: SyncSender<Chunk>,
settings: &GlobalSettings, settings: &GlobalSettings,
) { ) -> UResult<()> {
let mut carry_over = vec![]; let mut carry_over = vec![];
for recycled_chunk in receiver.iter() { for recycled_chunk in receiver.iter() {
let should_continue = chunks::read( let should_continue = chunks::read(
@ -101,9 +121,10 @@ fn reader(
b'\n' b'\n'
}, },
settings, settings,
); )?;
if !should_continue { if !should_continue {
break; break;
} }
} }
Ok(())
} }

View file

@ -14,8 +14,9 @@ use std::{
use memchr::memchr_iter; use memchr::memchr_iter;
use ouroboros::self_referencing; use ouroboros::self_referencing;
use uucore::error::{UResult, USimpleError};
use crate::{numeric_str_cmp::NumInfo, GeneralF64ParseResult, GlobalSettings, Line}; use crate::{numeric_str_cmp::NumInfo, GeneralF64ParseResult, GlobalSettings, Line, SortError};
/// The chunk that is passed around between threads. /// The chunk that is passed around between threads.
/// `lines` consist of slices into `buffer`. /// `lines` consist of slices into `buffer`.
@ -137,10 +138,10 @@ pub fn read<T: Read>(
max_buffer_size: Option<usize>, max_buffer_size: Option<usize>,
carry_over: &mut Vec<u8>, carry_over: &mut Vec<u8>,
file: &mut T, file: &mut T,
next_files: &mut impl Iterator<Item = T>, next_files: &mut impl Iterator<Item = UResult<T>>,
separator: u8, separator: u8,
settings: &GlobalSettings, settings: &GlobalSettings,
) -> bool { ) -> UResult<bool> {
let RecycledChunk { let RecycledChunk {
lines, lines,
selections, selections,
@ -159,12 +160,12 @@ pub fn read<T: Read>(
max_buffer_size, max_buffer_size,
carry_over.len(), carry_over.len(),
separator, separator,
); )?;
carry_over.clear(); carry_over.clear();
carry_over.extend_from_slice(&buffer[read..]); carry_over.extend_from_slice(&buffer[read..]);
if read != 0 { if read != 0 {
let payload = Chunk::new(buffer, |buffer| { let payload: UResult<Chunk> = Chunk::try_new(buffer, |buffer| {
let selections = unsafe { let selections = unsafe {
// SAFETY: It is safe to transmute to an empty vector of selections with shorter lifetime. // SAFETY: It is safe to transmute to an empty vector of selections with shorter lifetime.
// It was only temporarily transmuted to a Vec<Line<'static>> to make recycling possible. // It was only temporarily transmuted to a Vec<Line<'static>> to make recycling possible.
@ -175,18 +176,19 @@ pub fn read<T: Read>(
// because it was only temporarily transmuted to a Vec<Line<'static>> to make recycling possible. // because it was only temporarily transmuted to a Vec<Line<'static>> to make recycling possible.
std::mem::transmute::<Vec<Line<'static>>, Vec<Line<'_>>>(lines) std::mem::transmute::<Vec<Line<'static>>, Vec<Line<'_>>>(lines)
}; };
let read = crash_if_err!(1, std::str::from_utf8(&buffer[..read])); let read = std::str::from_utf8(&buffer[..read])
.map_err(|error| SortError::Uft8Error { error })?;
let mut line_data = LineData { let mut line_data = LineData {
selections, selections,
num_infos, num_infos,
parsed_floats, parsed_floats,
}; };
parse_lines(read, &mut lines, &mut line_data, separator, settings); parse_lines(read, &mut lines, &mut line_data, separator, settings);
ChunkContents { lines, line_data } Ok(ChunkContents { lines, line_data })
}); });
sender.send(payload).unwrap(); sender.send(payload?).unwrap();
} }
should_continue Ok(should_continue)
} }
/// Split `read` into `Line`s, and add them to `lines`. /// Split `read` into `Line`s, and add them to `lines`.
@ -242,12 +244,12 @@ fn parse_lines<'a>(
/// * Whether this function should be called again. /// * Whether this function should be called again.
fn read_to_buffer<T: Read>( fn read_to_buffer<T: Read>(
file: &mut T, file: &mut T,
next_files: &mut impl Iterator<Item = T>, next_files: &mut impl Iterator<Item = UResult<T>>,
buffer: &mut Vec<u8>, buffer: &mut Vec<u8>,
max_buffer_size: Option<usize>, max_buffer_size: Option<usize>,
start_offset: usize, start_offset: usize,
separator: u8, separator: u8,
) -> (usize, bool) { ) -> UResult<(usize, bool)> {
let mut read_target = &mut buffer[start_offset..]; let mut read_target = &mut buffer[start_offset..];
let mut last_file_target_size = read_target.len(); let mut last_file_target_size = read_target.len();
loop { loop {
@ -274,7 +276,7 @@ fn read_to_buffer<T: Read>(
// We read enough lines. // We read enough lines.
let end = last_line_end.unwrap(); let end = last_line_end.unwrap();
// We want to include the separator here, because it shouldn't be carried over. // We want to include the separator here, because it shouldn't be carried over.
return (end + 1, true); return Ok((end + 1, true));
} else { } else {
// We need to read more lines // We need to read more lines
let len = buffer.len(); let len = buffer.len();
@ -299,11 +301,11 @@ fn read_to_buffer<T: Read>(
if let Some(next_file) = next_files.next() { if let Some(next_file) = next_files.next() {
// There is another file. // There is another file.
last_file_target_size = leftover_len; last_file_target_size = leftover_len;
*file = next_file; *file = next_file?;
} else { } else {
// This was the last file. // This was the last file.
let read_len = buffer.len() - leftover_len; let read_len = buffer.len() - leftover_len;
return (read_len, false); return Ok((read_len, false));
} }
} }
} }
@ -313,7 +315,7 @@ fn read_to_buffer<T: Read>(
Err(e) if e.kind() == ErrorKind::Interrupted => { Err(e) if e.kind() == ErrorKind::Interrupted => {
// retry // retry
} }
Err(e) => crash!(1, "{}", e), Err(e) => return Err(USimpleError::new(2, e.to_string())),
} }
} }
} }

View file

@ -22,12 +22,15 @@ use std::{
}; };
use itertools::Itertools; use itertools::Itertools;
use uucore::error::UResult;
use crate::chunks::RecycledChunk; use crate::chunks::RecycledChunk;
use crate::merge::ClosedTmpFile; use crate::merge::ClosedTmpFile;
use crate::merge::WriteableCompressedTmpFile; use crate::merge::WriteableCompressedTmpFile;
use crate::merge::WriteablePlainTmpFile; use crate::merge::WriteablePlainTmpFile;
use crate::merge::WriteableTmpFile; use crate::merge::WriteableTmpFile;
use crate::Output;
use crate::SortError;
use crate::{ use crate::{
chunks::{self, Chunk}, chunks::{self, Chunk},
compare_by, merge, sort_by, GlobalSettings, compare_by, merge, sort_by, GlobalSettings,
@ -38,7 +41,11 @@ use tempfile::TempDir;
const START_BUFFER_SIZE: usize = 8_000; const START_BUFFER_SIZE: usize = 8_000;
/// Sort files by using auxiliary files for storing intermediate chunks (if needed), and output the result. /// Sort files by using auxiliary files for storing intermediate chunks (if needed), and output the result.
pub fn ext_sort(files: &mut impl Iterator<Item = Box<dyn Read + Send>>, settings: &GlobalSettings) { pub fn ext_sort(
files: &mut impl Iterator<Item = UResult<Box<dyn Read + Send>>>,
settings: &GlobalSettings,
output: Output,
) -> UResult<()> {
let (sorted_sender, sorted_receiver) = std::sync::mpsc::sync_channel(1); let (sorted_sender, sorted_receiver) = std::sync::mpsc::sync_channel(1);
let (recycled_sender, recycled_receiver) = std::sync::mpsc::sync_channel(1); let (recycled_sender, recycled_receiver) = std::sync::mpsc::sync_channel(1);
thread::spawn({ thread::spawn({
@ -51,23 +58,29 @@ pub fn ext_sort(files: &mut impl Iterator<Item = Box<dyn Read + Send>>, settings
settings, settings,
sorted_receiver, sorted_receiver,
recycled_sender, recycled_sender,
); output,
)
} else { } else {
reader_writer::<_, WriteablePlainTmpFile>( reader_writer::<_, WriteablePlainTmpFile>(
files, files,
settings, settings,
sorted_receiver, sorted_receiver,
recycled_sender, recycled_sender,
); output,
)
} }
} }
fn reader_writer<F: Iterator<Item = Box<dyn Read + Send>>, Tmp: WriteableTmpFile + 'static>( fn reader_writer<
F: Iterator<Item = UResult<Box<dyn Read + Send>>>,
Tmp: WriteableTmpFile + 'static,
>(
files: F, files: F,
settings: &GlobalSettings, settings: &GlobalSettings,
receiver: Receiver<Chunk>, receiver: Receiver<Chunk>,
sender: SyncSender<Chunk>, sender: SyncSender<Chunk>,
) { output: Output,
) -> UResult<()> {
let separator = if settings.zero_terminated { let separator = if settings.zero_terminated {
b'\0' b'\0'
} else { } else {
@ -81,22 +94,20 @@ fn reader_writer<F: Iterator<Item = Box<dyn Read + Send>>, Tmp: WriteableTmpFile
files, files,
&settings.tmp_dir, &settings.tmp_dir,
separator, separator,
// Heuristically chosen: Dividing by 10 seems to keep our memory usage roughly
// around settings.buffer_size as a whole.
buffer_size, buffer_size,
settings, settings,
receiver, receiver,
sender, sender,
); )?;
match read_result { match read_result {
ReadResult::WroteChunksToFile { tmp_files, tmp_dir } => { ReadResult::WroteChunksToFile { tmp_files, tmp_dir } => {
let tmp_dir_size = tmp_files.len(); let tmp_dir_size = tmp_files.len();
let mut merger = merge::merge_with_file_limit::<_, _, Tmp>( let merger = merge::merge_with_file_limit::<_, _, Tmp>(
tmp_files.into_iter().map(|c| c.reopen()), tmp_files.into_iter().map(|c| c.reopen()),
settings, settings,
Some((tmp_dir, tmp_dir_size)), Some((tmp_dir, tmp_dir_size)),
); )?;
merger.write_all(settings); merger.write_all(settings, output)?;
} }
ReadResult::SortedSingleChunk(chunk) => { ReadResult::SortedSingleChunk(chunk) => {
if settings.unique { if settings.unique {
@ -106,9 +117,10 @@ fn reader_writer<F: Iterator<Item = Box<dyn Read + Send>>, Tmp: WriteableTmpFile
== Ordering::Equal == Ordering::Equal
}), }),
settings, settings,
output,
); );
} else { } else {
print_sorted(chunk.lines().iter(), settings); print_sorted(chunk.lines().iter(), settings, output);
} }
} }
ReadResult::SortedTwoChunks([a, b]) => { ReadResult::SortedTwoChunks([a, b]) => {
@ -128,15 +140,17 @@ fn reader_writer<F: Iterator<Item = Box<dyn Read + Send>>, Tmp: WriteableTmpFile
}) })
.map(|(line, _)| line), .map(|(line, _)| line),
settings, settings,
output,
); );
} else { } else {
print_sorted(merged_iter.map(|(line, _)| line), settings); print_sorted(merged_iter.map(|(line, _)| line), settings, output);
} }
} }
ReadResult::EmptyInput => { ReadResult::EmptyInput => {
// don't output anything // don't output anything
} }
} }
Ok(())
} }
/// The function that is executed on the sorter thread. /// The function that is executed on the sorter thread.
@ -145,7 +159,11 @@ fn sorter(receiver: Receiver<Chunk>, sender: SyncSender<Chunk>, settings: Global
payload.with_contents_mut(|contents| { payload.with_contents_mut(|contents| {
sort_by(&mut contents.lines, &settings, &contents.line_data) sort_by(&mut contents.lines, &settings, &contents.line_data)
}); });
sender.send(payload).unwrap(); if sender.send(payload).is_err() {
// The receiver has gone away, likely because the other thread hit an error.
// We stop silently because the actual error is printed by the other thread.
return;
}
} }
} }
@ -165,15 +183,15 @@ enum ReadResult<I: WriteableTmpFile> {
} }
/// The function that is executed on the reader/writer thread. /// The function that is executed on the reader/writer thread.
fn read_write_loop<I: WriteableTmpFile>( fn read_write_loop<I: WriteableTmpFile>(
mut files: impl Iterator<Item = Box<dyn Read + Send>>, mut files: impl Iterator<Item = UResult<Box<dyn Read + Send>>>,
tmp_dir_parent: &Path, tmp_dir_parent: &Path,
separator: u8, separator: u8,
buffer_size: usize, buffer_size: usize,
settings: &GlobalSettings, settings: &GlobalSettings,
receiver: Receiver<Chunk>, receiver: Receiver<Chunk>,
sender: SyncSender<Chunk>, sender: SyncSender<Chunk>,
) -> ReadResult<I> { ) -> UResult<ReadResult<I>> {
let mut file = files.next().unwrap(); let mut file = files.next().unwrap()?;
let mut carry_over = vec![]; let mut carry_over = vec![];
// kick things off with two reads // kick things off with two reads
@ -191,14 +209,14 @@ fn read_write_loop<I: WriteableTmpFile>(
&mut files, &mut files,
separator, separator,
settings, settings,
); )?;
if !should_continue { if !should_continue {
drop(sender); drop(sender);
// We have already read the whole input. Since we are in our first two reads, // We have already read the whole input. Since we are in our first two reads,
// this means that we can fit the whole input into memory. Bypass writing below and // this means that we can fit the whole input into memory. Bypass writing below and
// handle this case in a more straightforward way. // handle this case in a more straightforward way.
return if let Ok(first_chunk) = receiver.recv() { return Ok(if let Ok(first_chunk) = receiver.recv() {
if let Ok(second_chunk) = receiver.recv() { if let Ok(second_chunk) = receiver.recv() {
ReadResult::SortedTwoChunks([first_chunk, second_chunk]) ReadResult::SortedTwoChunks([first_chunk, second_chunk])
} else { } else {
@ -206,16 +224,14 @@ fn read_write_loop<I: WriteableTmpFile>(
} }
} else { } else {
ReadResult::EmptyInput ReadResult::EmptyInput
}; });
} }
} }
let tmp_dir = crash_if_err!( let tmp_dir = tempfile::Builder::new()
1,
tempfile::Builder::new()
.prefix("uutils_sort") .prefix("uutils_sort")
.tempdir_in(tmp_dir_parent) .tempdir_in(tmp_dir_parent)
); .map_err(|_| SortError::TmpDirCreationFailed)?;
let mut sender_option = Some(sender); let mut sender_option = Some(sender);
let mut file_number = 0; let mut file_number = 0;
@ -224,7 +240,7 @@ fn read_write_loop<I: WriteableTmpFile>(
let mut chunk = match receiver.recv() { let mut chunk = match receiver.recv() {
Ok(it) => it, Ok(it) => it,
_ => { _ => {
return ReadResult::WroteChunksToFile { tmp_files, tmp_dir }; return Ok(ReadResult::WroteChunksToFile { tmp_files, tmp_dir });
} }
}; };
@ -233,7 +249,7 @@ fn read_write_loop<I: WriteableTmpFile>(
tmp_dir.path().join(file_number.to_string()), tmp_dir.path().join(file_number.to_string()),
settings.compress_prog.as_deref(), settings.compress_prog.as_deref(),
separator, separator,
); )?;
tmp_files.push(tmp_file); tmp_files.push(tmp_file);
file_number += 1; file_number += 1;
@ -250,7 +266,7 @@ fn read_write_loop<I: WriteableTmpFile>(
&mut files, &mut files,
separator, separator,
settings, settings,
); )?;
if !should_continue { if !should_continue {
sender_option = None; sender_option = None;
} }
@ -265,8 +281,8 @@ fn write<I: WriteableTmpFile>(
file: PathBuf, file: PathBuf,
compress_prog: Option<&str>, compress_prog: Option<&str>,
separator: u8, separator: u8,
) -> I::Closed { ) -> UResult<I::Closed> {
let mut tmp_file = I::create(file, compress_prog); let mut tmp_file = I::create(file, compress_prog)?;
write_lines(chunk.lines(), tmp_file.as_write(), separator); write_lines(chunk.lines(), tmp_file.as_write(), separator);
tmp_file.finished_writing() tmp_file.finished_writing()
} }

View file

@ -9,44 +9,85 @@
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
ffi::OsString,
fs::{self, File}, fs::{self, File},
io::{BufWriter, Read, Write}, io::{BufWriter, Read, Write},
iter, iter,
path::PathBuf, path::{Path, PathBuf},
process::{Child, ChildStdin, ChildStdout, Command, Stdio}, process::{Child, ChildStdin, ChildStdout, Command, Stdio},
rc::Rc, rc::Rc,
sync::mpsc::{channel, sync_channel, Receiver, Sender, SyncSender}, sync::mpsc::{channel, sync_channel, Receiver, Sender, SyncSender},
thread, thread::{self, JoinHandle},
}; };
use compare::Compare; use compare::Compare;
use itertools::Itertools; use itertools::Itertools;
use tempfile::TempDir; use tempfile::TempDir;
use uucore::error::UResult;
use crate::{ use crate::{
chunks::{self, Chunk, RecycledChunk}, chunks::{self, Chunk, RecycledChunk},
compare_by, GlobalSettings, compare_by, open, GlobalSettings, Output, SortError,
}; };
/// If the output file occurs in the input files as well, copy the contents of the output file
/// and replace its occurrences in the inputs with that copy.
fn replace_output_file_in_input_files(
files: &mut [OsString],
settings: &GlobalSettings,
output: Option<&str>,
) -> UResult<Option<(TempDir, usize)>> {
let mut copy: Option<(TempDir, PathBuf)> = None;
if let Some(Ok(output_path)) = output.map(|path| Path::new(path).canonicalize()) {
for file in files {
if let Ok(file_path) = Path::new(file).canonicalize() {
if file_path == output_path {
if let Some((_dir, copy)) = &copy {
*file = copy.clone().into_os_string();
} else {
let tmp_dir = tempfile::Builder::new()
.prefix("uutils_sort")
.tempdir_in(&settings.tmp_dir)
.map_err(|_| SortError::TmpDirCreationFailed)?;
let copy_path = tmp_dir.path().join("0");
std::fs::copy(file_path, &copy_path)
.map_err(|error| SortError::OpenTmpFileFailed { error })?;
*file = copy_path.clone().into_os_string();
copy = Some((tmp_dir, copy_path))
}
}
}
}
}
// if we created a TempDir its size must be one.
Ok(copy.map(|(dir, _copy)| (dir, 1)))
}
/// Merge pre-sorted `Box<dyn Read>`s. /// Merge pre-sorted `Box<dyn Read>`s.
/// ///
/// If `settings.merge_batch_size` is greater than the length of `files`, intermediate files will be used. /// If `settings.merge_batch_size` is greater than the length of `files`, intermediate files will be used.
/// If `settings.compress_prog` is `Some`, intermediate files will be compressed with it. /// If `settings.compress_prog` is `Some`, intermediate files will be compressed with it.
pub fn merge<Files: ExactSizeIterator<Item = Box<dyn Read + Send>>>( pub fn merge<'a>(
files: Files, files: &mut [OsString],
settings: &GlobalSettings, settings: &'a GlobalSettings,
) -> FileMerger { output: Option<&str>,
) -> UResult<FileMerger<'a>> {
let tmp_dir = replace_output_file_in_input_files(files, settings, output)?;
if settings.compress_prog.is_none() { if settings.compress_prog.is_none() {
merge_with_file_limit::<_, _, WriteablePlainTmpFile>( merge_with_file_limit::<_, _, WriteablePlainTmpFile>(
files.map(|file| PlainMergeInput { inner: file }), files
.iter()
.map(|file| open(file).map(|file| PlainMergeInput { inner: file })),
settings, settings,
None, tmp_dir,
) )
} else { } else {
merge_with_file_limit::<_, _, WriteableCompressedTmpFile>( merge_with_file_limit::<_, _, WriteableCompressedTmpFile>(
files.map(|file| PlainMergeInput { inner: file }), files
.iter()
.map(|file| open(file).map(|file| PlainMergeInput { inner: file })),
settings, settings,
None, tmp_dir,
) )
} }
} }
@ -54,24 +95,25 @@ pub fn merge<Files: ExactSizeIterator<Item = Box<dyn Read + Send>>>(
// Merge already sorted `MergeInput`s. // Merge already sorted `MergeInput`s.
pub fn merge_with_file_limit< pub fn merge_with_file_limit<
M: MergeInput + 'static, M: MergeInput + 'static,
F: ExactSizeIterator<Item = M>, F: ExactSizeIterator<Item = UResult<M>>,
Tmp: WriteableTmpFile + 'static, Tmp: WriteableTmpFile + 'static,
>( >(
files: F, files: F,
settings: &GlobalSettings, settings: &GlobalSettings,
tmp_dir: Option<(TempDir, usize)>, tmp_dir: Option<(TempDir, usize)>,
) -> FileMerger { ) -> UResult<FileMerger> {
if files.len() > settings.merge_batch_size { if files.len() > settings.merge_batch_size {
// If we did not get a tmp_dir, create one. // If we did not get a tmp_dir, create one.
let (tmp_dir, mut tmp_dir_size) = tmp_dir.unwrap_or_else(|| { let (tmp_dir, mut tmp_dir_size) = match tmp_dir {
( Some(x) => x,
None => (
tempfile::Builder::new() tempfile::Builder::new()
.prefix("uutils_sort") .prefix("uutils_sort")
.tempdir_in(&settings.tmp_dir) .tempdir_in(&settings.tmp_dir)
.unwrap(), .map_err(|_| SortError::TmpDirCreationFailed)?,
0, 0,
) ),
}); };
let mut remaining_files = files.len(); let mut remaining_files = files.len();
let batches = files.chunks(settings.merge_batch_size); let batches = files.chunks(settings.merge_batch_size);
let mut batches = batches.into_iter(); let mut batches = batches.into_iter();
@ -79,14 +121,14 @@ pub fn merge_with_file_limit<
while remaining_files != 0 { while remaining_files != 0 {
// Work around the fact that `Chunks` is not an `ExactSizeIterator`. // Work around the fact that `Chunks` is not an `ExactSizeIterator`.
remaining_files = remaining_files.saturating_sub(settings.merge_batch_size); remaining_files = remaining_files.saturating_sub(settings.merge_batch_size);
let mut merger = merge_without_limit(batches.next().unwrap(), settings); let merger = merge_without_limit(batches.next().unwrap(), settings)?;
let mut tmp_file = Tmp::create( let mut tmp_file = Tmp::create(
tmp_dir.path().join(tmp_dir_size.to_string()), tmp_dir.path().join(tmp_dir_size.to_string()),
settings.compress_prog.as_deref(), settings.compress_prog.as_deref(),
); )?;
tmp_dir_size += 1; tmp_dir_size += 1;
merger.write_all_to(settings, tmp_file.as_write()); merger.write_all_to(settings, tmp_file.as_write())?;
temporary_files.push(tmp_file.finished_writing()); temporary_files.push(tmp_file.finished_writing()?);
} }
assert!(batches.next().is_none()); assert!(batches.next().is_none());
merge_with_file_limit::<_, _, Tmp>( merge_with_file_limit::<_, _, Tmp>(
@ -94,7 +136,7 @@ pub fn merge_with_file_limit<
.into_iter() .into_iter()
.map(Box::new(|c: Tmp::Closed| c.reopen()) .map(Box::new(|c: Tmp::Closed| c.reopen())
as Box< as Box<
dyn FnMut(Tmp::Closed) -> <Tmp::Closed as ClosedTmpFile>::Reopened, dyn FnMut(Tmp::Closed) -> UResult<<Tmp::Closed as ClosedTmpFile>::Reopened>,
>), >),
settings, settings,
Some((tmp_dir, tmp_dir_size)), Some((tmp_dir, tmp_dir_size)),
@ -108,10 +150,10 @@ pub fn merge_with_file_limit<
/// ///
/// It is the responsibility of the caller to ensure that `files` yields only /// It is the responsibility of the caller to ensure that `files` yields only
/// as many files as we are allowed to open concurrently. /// as many files as we are allowed to open concurrently.
fn merge_without_limit<M: MergeInput + 'static, F: Iterator<Item = M>>( fn merge_without_limit<M: MergeInput + 'static, F: Iterator<Item = UResult<M>>>(
files: F, files: F,
settings: &GlobalSettings, settings: &GlobalSettings,
) -> FileMerger { ) -> UResult<FileMerger> {
let (request_sender, request_receiver) = channel(); let (request_sender, request_receiver) = channel();
let mut reader_files = Vec::with_capacity(files.size_hint().0); let mut reader_files = Vec::with_capacity(files.size_hint().0);
let mut loaded_receivers = Vec::with_capacity(files.size_hint().0); let mut loaded_receivers = Vec::with_capacity(files.size_hint().0);
@ -119,7 +161,7 @@ fn merge_without_limit<M: MergeInput + 'static, F: Iterator<Item = M>>(
let (sender, receiver) = sync_channel(2); let (sender, receiver) = sync_channel(2);
loaded_receivers.push(receiver); loaded_receivers.push(receiver);
reader_files.push(Some(ReaderFile { reader_files.push(Some(ReaderFile {
file, file: file?,
sender, sender,
carry_over: vec![], carry_over: vec![],
})); }));
@ -136,7 +178,7 @@ fn merge_without_limit<M: MergeInput + 'static, F: Iterator<Item = M>>(
.unwrap(); .unwrap();
} }
thread::spawn({ let reader_join_handle = thread::spawn({
let settings = settings.clone(); let settings = settings.clone();
move || { move || {
reader( reader(
@ -155,22 +197,25 @@ fn merge_without_limit<M: MergeInput + 'static, F: Iterator<Item = M>>(
let mut mergeable_files = vec![]; let mut mergeable_files = vec![];
for (file_number, receiver) in loaded_receivers.into_iter().enumerate() { for (file_number, receiver) in loaded_receivers.into_iter().enumerate() {
if let Ok(chunk) = receiver.recv() {
mergeable_files.push(MergeableFile { mergeable_files.push(MergeableFile {
current_chunk: Rc::new(receiver.recv().unwrap()), current_chunk: Rc::new(chunk),
file_number, file_number,
line_idx: 0, line_idx: 0,
receiver, receiver,
}) })
} }
}
FileMerger { Ok(FileMerger {
heap: binary_heap_plus::BinaryHeap::from_vec_cmp( heap: binary_heap_plus::BinaryHeap::from_vec_cmp(
mergeable_files, mergeable_files,
FileComparator { settings }, FileComparator { settings },
), ),
request_sender, request_sender,
prev: None, prev: None,
} reader_join_handle,
})
} }
/// The struct on the reader thread representing an input file /// The struct on the reader thread representing an input file
struct ReaderFile<M: MergeInput> { struct ReaderFile<M: MergeInput> {
@ -185,7 +230,7 @@ fn reader(
files: &mut [Option<ReaderFile<impl MergeInput>>], files: &mut [Option<ReaderFile<impl MergeInput>>],
settings: &GlobalSettings, settings: &GlobalSettings,
separator: u8, separator: u8,
) { ) -> UResult<()> {
for (file_idx, recycled_chunk) in recycled_receiver.iter() { for (file_idx, recycled_chunk) in recycled_receiver.iter() {
if let Some(ReaderFile { if let Some(ReaderFile {
file, file,
@ -202,15 +247,16 @@ fn reader(
&mut iter::empty(), &mut iter::empty(),
separator, separator,
settings, settings,
); )?;
if !should_continue { if !should_continue {
// Remove the file from the list by replacing it with `None`. // Remove the file from the list by replacing it with `None`.
let ReaderFile { file, .. } = files[file_idx].take().unwrap(); let ReaderFile { file, .. } = files[file_idx].take().unwrap();
// Depending on the kind of the `MergeInput`, this may delete the file: // Depending on the kind of the `MergeInput`, this may delete the file:
file.finished_reading(); file.finished_reading()?;
} }
} }
} }
Ok(())
} }
/// The struct on the main thread representing an input file /// The struct on the main thread representing an input file
pub struct MergeableFile { pub struct MergeableFile {
@ -234,17 +280,20 @@ pub struct FileMerger<'a> {
heap: binary_heap_plus::BinaryHeap<MergeableFile, FileComparator<'a>>, heap: binary_heap_plus::BinaryHeap<MergeableFile, FileComparator<'a>>,
request_sender: Sender<(usize, RecycledChunk)>, request_sender: Sender<(usize, RecycledChunk)>,
prev: Option<PreviousLine>, prev: Option<PreviousLine>,
reader_join_handle: JoinHandle<UResult<()>>,
} }
impl<'a> FileMerger<'a> { impl<'a> FileMerger<'a> {
/// Write the merged contents to the output file. /// Write the merged contents to the output file.
pub fn write_all(&mut self, settings: &GlobalSettings) { pub fn write_all(self, settings: &GlobalSettings, output: Output) -> UResult<()> {
let mut out = settings.out_writer(); let mut out = output.into_write();
self.write_all_to(settings, &mut out); self.write_all_to(settings, &mut out)
} }
pub fn write_all_to(&mut self, settings: &GlobalSettings, out: &mut impl Write) { pub fn write_all_to(mut self, settings: &GlobalSettings, out: &mut impl Write) -> UResult<()> {
while self.write_next(settings, out) {} while self.write_next(settings, out) {}
drop(self.request_sender);
self.reader_join_handle.join().unwrap()
} }
fn write_next(&mut self, settings: &GlobalSettings, out: &mut impl Write) -> bool { fn write_next(&mut self, settings: &GlobalSettings, out: &mut impl Write) -> bool {
@ -328,36 +377,41 @@ impl<'a> Compare<MergeableFile> for FileComparator<'a> {
} }
// Wait for the child to exit and check its exit code. // Wait for the child to exit and check its exit code.
fn assert_child_success(mut child: Child, program: &str) { fn check_child_success(mut child: Child, program: &str) -> UResult<()> {
if !matches!( if !matches!(
child.wait().map(|e| e.code()), child.wait().map(|e| e.code()),
Ok(Some(0)) | Ok(None) | Err(_) Ok(Some(0)) | Ok(None) | Err(_)
) { ) {
crash!(2, "'{}' terminated abnormally", program) Err(SortError::CompressProgTerminatedAbnormally {
prog: program.to_owned(),
}
.into())
} else {
Ok(())
} }
} }
/// A temporary file that can be written to. /// A temporary file that can be written to.
pub trait WriteableTmpFile { pub trait WriteableTmpFile: Sized {
type Closed: ClosedTmpFile; type Closed: ClosedTmpFile;
type InnerWrite: Write; type InnerWrite: Write;
fn create(path: PathBuf, compress_prog: Option<&str>) -> Self; fn create(path: PathBuf, compress_prog: Option<&str>) -> UResult<Self>;
/// Closes the temporary file. /// Closes the temporary file.
fn finished_writing(self) -> Self::Closed; fn finished_writing(self) -> UResult<Self::Closed>;
fn as_write(&mut self) -> &mut Self::InnerWrite; fn as_write(&mut self) -> &mut Self::InnerWrite;
} }
/// A temporary file that is (temporarily) closed, but can be reopened. /// A temporary file that is (temporarily) closed, but can be reopened.
pub trait ClosedTmpFile { pub trait ClosedTmpFile {
type Reopened: MergeInput; type Reopened: MergeInput;
/// Reopens the temporary file. /// Reopens the temporary file.
fn reopen(self) -> Self::Reopened; fn reopen(self) -> UResult<Self::Reopened>;
} }
/// A pre-sorted input for merging. /// A pre-sorted input for merging.
pub trait MergeInput: Send { pub trait MergeInput: Send {
type InnerRead: Read; type InnerRead: Read;
/// Cleans this `MergeInput` up. /// Cleans this `MergeInput` up.
/// Implementations may delete the backing file. /// Implementations may delete the backing file.
fn finished_reading(self); fn finished_reading(self) -> UResult<()>;
fn as_read(&mut self) -> &mut Self::InnerRead; fn as_read(&mut self) -> &mut Self::InnerRead;
} }
@ -376,15 +430,17 @@ impl WriteableTmpFile for WriteablePlainTmpFile {
type Closed = ClosedPlainTmpFile; type Closed = ClosedPlainTmpFile;
type InnerWrite = BufWriter<File>; type InnerWrite = BufWriter<File>;
fn create(path: PathBuf, _: Option<&str>) -> Self { fn create(path: PathBuf, _: Option<&str>) -> UResult<Self> {
WriteablePlainTmpFile { Ok(WriteablePlainTmpFile {
file: BufWriter::new(File::create(&path).unwrap()), file: BufWriter::new(
File::create(&path).map_err(|error| SortError::OpenTmpFileFailed { error })?,
),
path, path,
} })
} }
fn finished_writing(self) -> Self::Closed { fn finished_writing(self) -> UResult<Self::Closed> {
ClosedPlainTmpFile { path: self.path } Ok(ClosedPlainTmpFile { path: self.path })
} }
fn as_write(&mut self) -> &mut Self::InnerWrite { fn as_write(&mut self) -> &mut Self::InnerWrite {
@ -393,18 +449,22 @@ impl WriteableTmpFile for WriteablePlainTmpFile {
} }
impl ClosedTmpFile for ClosedPlainTmpFile { impl ClosedTmpFile for ClosedPlainTmpFile {
type Reopened = PlainTmpMergeInput; type Reopened = PlainTmpMergeInput;
fn reopen(self) -> Self::Reopened { fn reopen(self) -> UResult<Self::Reopened> {
PlainTmpMergeInput { Ok(PlainTmpMergeInput {
file: File::open(&self.path).unwrap(), file: File::open(&self.path).map_err(|error| SortError::OpenTmpFileFailed { error })?,
path: self.path, path: self.path,
} })
} }
} }
impl MergeInput for PlainTmpMergeInput { impl MergeInput for PlainTmpMergeInput {
type InnerRead = File; type InnerRead = File;
fn finished_reading(self) { fn finished_reading(self) -> UResult<()> {
fs::remove_file(self.path).ok(); // we ignore failures to delete the temporary file,
// because there is a race at the end of the execution and the whole
// temporary directory might already be gone.
let _ = fs::remove_file(self.path);
Ok(())
} }
fn as_read(&mut self) -> &mut Self::InnerRead { fn as_read(&mut self) -> &mut Self::InnerRead {
@ -432,35 +492,33 @@ impl WriteableTmpFile for WriteableCompressedTmpFile {
type Closed = ClosedCompressedTmpFile; type Closed = ClosedCompressedTmpFile;
type InnerWrite = BufWriter<ChildStdin>; type InnerWrite = BufWriter<ChildStdin>;
fn create(path: PathBuf, compress_prog: Option<&str>) -> Self { fn create(path: PathBuf, compress_prog: Option<&str>) -> UResult<Self> {
let compress_prog = compress_prog.unwrap(); let compress_prog = compress_prog.unwrap();
let mut command = Command::new(compress_prog); let mut command = Command::new(compress_prog);
command let tmp_file =
.stdin(Stdio::piped()) File::create(&path).map_err(|error| SortError::OpenTmpFileFailed { error })?;
.stdout(File::create(&path).unwrap()); command.stdin(Stdio::piped()).stdout(tmp_file);
let mut child = crash_if_err!( let mut child = command
2, .spawn()
command.spawn().map_err(|err| format!( .map_err(|err| SortError::CompressProgExecutionFailed {
"couldn't execute compress program: errno {}", code: err.raw_os_error().unwrap(),
err.raw_os_error().unwrap() })?;
))
);
let child_stdin = child.stdin.take().unwrap(); let child_stdin = child.stdin.take().unwrap();
WriteableCompressedTmpFile { Ok(WriteableCompressedTmpFile {
path, path,
compress_prog: compress_prog.to_owned(), compress_prog: compress_prog.to_owned(),
child, child,
child_stdin: BufWriter::new(child_stdin), child_stdin: BufWriter::new(child_stdin),
} })
} }
fn finished_writing(self) -> Self::Closed { fn finished_writing(self) -> UResult<Self::Closed> {
drop(self.child_stdin); drop(self.child_stdin);
assert_child_success(self.child, &self.compress_prog); check_child_success(self.child, &self.compress_prog)?;
ClosedCompressedTmpFile { Ok(ClosedCompressedTmpFile {
path: self.path, path: self.path,
compress_prog: self.compress_prog, compress_prog: self.compress_prog,
} })
} }
fn as_write(&mut self) -> &mut Self::InnerWrite { fn as_write(&mut self) -> &mut Self::InnerWrite {
@ -470,33 +528,32 @@ impl WriteableTmpFile for WriteableCompressedTmpFile {
impl ClosedTmpFile for ClosedCompressedTmpFile { impl ClosedTmpFile for ClosedCompressedTmpFile {
type Reopened = CompressedTmpMergeInput; type Reopened = CompressedTmpMergeInput;
fn reopen(self) -> Self::Reopened { fn reopen(self) -> UResult<Self::Reopened> {
let mut command = Command::new(&self.compress_prog); let mut command = Command::new(&self.compress_prog);
let file = File::open(&self.path).unwrap(); let file = File::open(&self.path).unwrap();
command.stdin(file).stdout(Stdio::piped()).arg("-d"); command.stdin(file).stdout(Stdio::piped()).arg("-d");
let mut child = crash_if_err!( let mut child = command
2, .spawn()
command.spawn().map_err(|err| format!( .map_err(|err| SortError::CompressProgExecutionFailed {
"couldn't execute compress program: errno {}", code: err.raw_os_error().unwrap(),
err.raw_os_error().unwrap() })?;
))
);
let child_stdout = child.stdout.take().unwrap(); let child_stdout = child.stdout.take().unwrap();
CompressedTmpMergeInput { Ok(CompressedTmpMergeInput {
path: self.path, path: self.path,
compress_prog: self.compress_prog, compress_prog: self.compress_prog,
child, child,
child_stdout, child_stdout,
} })
} }
} }
impl MergeInput for CompressedTmpMergeInput { impl MergeInput for CompressedTmpMergeInput {
type InnerRead = ChildStdout; type InnerRead = ChildStdout;
fn finished_reading(self) { fn finished_reading(self) -> UResult<()> {
drop(self.child_stdout); drop(self.child_stdout);
assert_child_success(self.child, &self.compress_prog); check_child_success(self.child, &self.compress_prog)?;
fs::remove_file(self.path).ok(); let _ = fs::remove_file(self.path);
Ok(())
} }
fn as_read(&mut self) -> &mut Self::InnerRead { fn as_read(&mut self) -> &mut Self::InnerRead {
@ -509,7 +566,9 @@ pub struct PlainMergeInput<R: Read + Send> {
} }
impl<R: Read + Send> MergeInput for PlainMergeInput<R> { impl<R: Read + Send> MergeInput for PlainMergeInput<R> {
type InnerRead = R; type InnerRead = R;
fn finished_reading(self) {} fn finished_reading(self) -> UResult<()> {
Ok(())
}
fn as_read(&mut self) -> &mut Self::InnerRead { fn as_read(&mut self) -> &mut Self::InnerRead {
&mut self.inner &mut self.inner
} }

View file

@ -29,19 +29,22 @@ use custom_str_cmp::custom_str_cmp;
use ext_sort::ext_sort; use ext_sort::ext_sort;
use fnv::FnvHasher; use fnv::FnvHasher;
use numeric_str_cmp::{human_numeric_str_cmp, numeric_str_cmp, NumInfo, NumInfoParseSettings}; use numeric_str_cmp::{human_numeric_str_cmp, numeric_str_cmp, NumInfo, NumInfoParseSettings};
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use rayon::prelude::*; use rayon::prelude::*;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::env; use std::env;
use std::ffi::OsStr; use std::error::Error;
use std::fs::File; use std::ffi::{OsStr, OsString};
use std::fmt::Display;
use std::fs::{File, OpenOptions};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write};
use std::ops::Range; use std::ops::Range;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::Utf8Error;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use uucore::error::{set_exit_code, UCustomError, UResult, USimpleError, UUsageError};
use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::parse_size::{parse_size, ParseSizeError};
use uucore::version_cmp::version_cmp; use uucore::version_cmp::version_cmp;
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
@ -121,6 +124,111 @@ const POSITIVE: char = '+';
// available memory into consideration, instead of relying on this constant only. // available memory into consideration, instead of relying on this constant only.
const DEFAULT_BUF_SIZE: usize = 1_000_000_000; // 1 GB const DEFAULT_BUF_SIZE: usize = 1_000_000_000; // 1 GB
#[derive(Debug)]
enum SortError {
Disorder {
file: OsString,
line_number: usize,
line: String,
silent: bool,
},
OpenFailed {
path: String,
error: std::io::Error,
},
ReadFailed {
path: String,
error: std::io::Error,
},
ParseKeyError {
key: String,
msg: String,
},
OpenTmpFileFailed {
error: std::io::Error,
},
CompressProgExecutionFailed {
code: i32,
},
CompressProgTerminatedAbnormally {
prog: String,
},
TmpDirCreationFailed,
Uft8Error {
error: Utf8Error,
},
}
impl Error for SortError {}
impl UCustomError for SortError {
fn code(&self) -> i32 {
match self {
SortError::Disorder { .. } => 1,
_ => 2,
}
}
fn usage(&self) -> bool {
false
}
}
impl Display for SortError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SortError::Disorder {
file,
line_number,
line,
silent,
} => {
if !silent {
write!(
f,
"{}:{}: disorder: {}",
file.to_string_lossy(),
line_number,
line
)
} else {
Ok(())
}
}
SortError::OpenFailed { path, error } => write!(
f,
"open failed: {}: {}",
path,
strip_errno(&error.to_string())
),
SortError::ParseKeyError { key, msg } => {
write!(f, "failed to parse key `{}`: {}", key, msg)
}
SortError::ReadFailed { path, error } => write!(
f,
"cannot read: {}: {}",
path,
strip_errno(&error.to_string())
),
SortError::OpenTmpFileFailed { error } => {
write!(
f,
"failed to open temporary file: {}",
strip_errno(&error.to_string())
)
}
SortError::CompressProgExecutionFailed { code } => {
write!(f, "couldn't execute compress program: errno {}", code)
}
SortError::CompressProgTerminatedAbnormally { prog } => {
write!(f, "'{}' terminated abnormally", prog)
}
SortError::TmpDirCreationFailed => write!(f, "could not create temporary directory"),
SortError::Uft8Error { error } => write!(f, "{}", error),
}
}
}
#[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy, Debug)] #[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy, Debug)]
enum SortMode { enum SortMode {
Numeric, Numeric,
@ -146,6 +254,49 @@ impl SortMode {
} }
} }
pub struct Output {
file: Option<(String, File)>,
}
impl Output {
fn new(name: Option<&str>) -> UResult<Self> {
let file = if let Some(name) = name {
// This is different from `File::create()` because we don't truncate the output yet.
// This allows using the output file as an input file.
let file = OpenOptions::new()
.write(true)
.create(true)
.open(name)
.map_err(|e| SortError::OpenFailed {
path: name.to_owned(),
error: e,
})?;
Some((name.to_owned(), file))
} else {
None
};
Ok(Self { file })
}
fn into_write(self) -> BufWriter<Box<dyn Write>> {
BufWriter::new(match self.file {
Some((_name, file)) => {
// truncate the file
let _ = file.set_len(0);
Box::new(file)
}
None => Box::new(stdout()),
})
}
fn as_output_name(&self) -> Option<&str> {
match &self.file {
Some((name, _file)) => Some(name),
None => None,
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct GlobalSettings { pub struct GlobalSettings {
mode: SortMode, mode: SortMode,
@ -156,12 +307,11 @@ pub struct GlobalSettings {
ignore_non_printing: bool, ignore_non_printing: bool,
merge: bool, merge: bool,
reverse: bool, reverse: bool,
output_file: Option<String>,
stable: bool, stable: bool,
unique: bool, unique: bool,
check: bool, check: bool,
check_silent: bool, check_silent: bool,
salt: String, salt: Option<[u8; 16]>,
selectors: Vec<FieldSelector>, selectors: Vec<FieldSelector>,
separator: Option<char>, separator: Option<char>,
threads: String, threads: String,
@ -209,19 +359,6 @@ impl GlobalSettings {
} }
} }
fn out_writer(&self) -> BufWriter<Box<dyn Write>> {
match self.output_file {
Some(ref filename) => match File::create(Path::new(&filename)) {
Ok(f) => BufWriter::new(Box::new(f) as Box<dyn Write>),
Err(e) => {
show_error!("{0}: {1}", filename, e.to_string());
panic!("Could not open output file");
}
},
None => BufWriter::new(Box::new(stdout()) as Box<dyn Write>),
}
}
/// Precompute some data needed for sorting. /// Precompute some data needed for sorting.
/// This function **must** be called before starting to sort, and `GlobalSettings` may not be altered /// This function **must** be called before starting to sort, and `GlobalSettings` may not be altered
/// afterwards. /// afterwards.
@ -253,12 +390,11 @@ impl Default for GlobalSettings {
ignore_non_printing: false, ignore_non_printing: false,
merge: false, merge: false,
reverse: false, reverse: false,
output_file: None,
stable: false, stable: false,
unique: false, unique: false,
check: false, check: false,
check_silent: false, check_silent: false,
salt: String::new(), salt: None,
selectors: vec![], selectors: vec![],
separator: None, separator: None,
threads: String::new(), threads: String::new(),
@ -697,13 +833,12 @@ impl FieldSelector {
} }
} }
fn parse(key: &str, global_settings: &GlobalSettings) -> Self { fn parse(key: &str, global_settings: &GlobalSettings) -> UResult<Self> {
let mut from_to = key.split(','); let mut from_to = key.split(',');
let (from, from_options) = Self::split_key_options(from_to.next().unwrap()); let (from, from_options) = Self::split_key_options(from_to.next().unwrap());
let to = from_to.next().map(|to| Self::split_key_options(to)); let to = from_to.next().map(|to| Self::split_key_options(to));
let options_are_empty = from_options.is_empty() && matches!(to, None | Some((_, ""))); let options_are_empty = from_options.is_empty() && matches!(to, None | Some((_, "")));
crash_if_err!(
2,
if options_are_empty { if options_are_empty {
// Inherit the global settings if there are no options attached to this key. // Inherit the global settings if there are no options attached to this key.
(|| { (|| {
@ -722,8 +857,13 @@ impl FieldSelector {
// Do not inherit from `global_settings`, as there are options attached to this key. // Do not inherit from `global_settings`, as there are options attached to this key.
Self::parse_with_options((from, from_options), to) Self::parse_with_options((from, from_options), to)
} }
.map_err(|e| format!("failed to parse key `{}`: {}", key, e)) .map_err(|msg| {
) SortError::ParseKeyError {
key: key.to_owned(),
msg,
}
.into()
})
} }
fn parse_with_options( fn parse_with_options(
@ -916,9 +1056,7 @@ impl FieldSelector {
fn get_usage() -> String { fn get_usage() -> String {
format!( format!(
"{0} "{0} [OPTION]... [FILE]...
Usage:
{0} [OPTION]... [FILE]...
Write the sorted concatenation of all FILE(s) to standard output. Write the sorted concatenation of all FILE(s) to standard output.
Mandatory arguments for long options are mandatory for short options too. Mandatory arguments for long options are mandatory for short options too.
With no FILE, or when FILE is -, read standard input.", With no FILE, or when FILE is -, read standard input.",
@ -937,41 +1075,57 @@ fn make_sort_mode_arg<'a, 'b>(mode: &'a str, short: &'b str, help: &'b str) -> A
arg arg
} }
pub fn uumain(args: impl uucore::Args) -> i32 { #[uucore_procs::gen_uumain]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args = args let args = args
.collect_str(InvalidEncodingHandling::Ignore) .collect_str(InvalidEncodingHandling::Ignore)
.accept_any(); .accept_any();
let usage = get_usage(); let usage = get_usage();
let mut settings: GlobalSettings = Default::default(); let mut settings: GlobalSettings = Default::default();
let matches = uu_app().usage(&usage[..]).get_matches_from(args); let matches = match uu_app().usage(&usage[..]).get_matches_from_safe(args) {
Ok(t) => t,
Err(e) => {
// not all clap "Errors" are because of a failure to parse arguments.
// "--version" also causes an Error to be returned, but we should not print to stderr
// nor return with a non-zero exit code in this case (we should print to stdout and return 0).
// This logic is similar to the code in clap, but we return 2 as the exit code in case of real failure
// (clap returns 1).
if e.use_stderr() {
eprintln!("{}", e.message);
set_exit_code(2);
} else {
println!("{}", e.message);
}
return Ok(());
}
};
settings.debug = matches.is_present(options::DEBUG); settings.debug = matches.is_present(options::DEBUG);
// check whether user specified a zero terminated list of files for input, otherwise read files from args // check whether user specified a zero terminated list of files for input, otherwise read files from args
let mut files: Vec<String> = if matches.is_present(options::FILES0_FROM) { let mut files: Vec<OsString> = if matches.is_present(options::FILES0_FROM) {
let files0_from: Vec<String> = matches let files0_from: Vec<OsString> = matches
.values_of(options::FILES0_FROM) .values_of_os(options::FILES0_FROM)
.map(|v| v.map(ToString::to_string).collect()) .map(|v| v.map(ToOwned::to_owned).collect())
.unwrap_or_default(); .unwrap_or_default();
let mut files = Vec::new(); let mut files = Vec::new();
for path in &files0_from { for path in &files0_from {
let reader = open(path.as_str()); let reader = open(&path)?;
let buf_reader = BufReader::new(reader); let buf_reader = BufReader::new(reader);
for line in buf_reader.split(b'\0').flatten() { for line in buf_reader.split(b'\0').flatten() {
files.push( files.push(OsString::from(
std::str::from_utf8(&line) std::str::from_utf8(&line)
.expect("Could not parse string from zero terminated input.") .expect("Could not parse string from zero terminated input."),
.to_string(), ));
);
} }
} }
files files
} else { } else {
matches matches
.values_of(options::FILES) .values_of_os(options::FILES)
.map(|v| v.map(ToString::to_string).collect()) .map(|v| v.map(ToOwned::to_owned).collect())
.unwrap_or_default() .unwrap_or_default()
}; };
@ -998,7 +1152,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
} else if matches.is_present(options::modes::RANDOM) } else if matches.is_present(options::modes::RANDOM)
|| matches.value_of(options::modes::SORT) == Some("random") || matches.value_of(options::modes::SORT) == Some("random")
{ {
settings.salt = get_rand_string(); settings.salt = Some(get_rand_string());
SortMode::Random SortMode::Random
} else { } else {
SortMode::Default SortMode::Default
@ -1015,12 +1169,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
env::set_var("RAYON_NUM_THREADS", &settings.threads); env::set_var("RAYON_NUM_THREADS", &settings.threads);
} }
settings.buffer_size = matches settings.buffer_size =
matches
.value_of(options::BUF_SIZE) .value_of(options::BUF_SIZE)
.map_or(DEFAULT_BUF_SIZE, |s| { .map_or(Ok(DEFAULT_BUF_SIZE), |s| {
GlobalSettings::parse_byte_count(s) GlobalSettings::parse_byte_count(s).map_err(|e| {
.unwrap_or_else(|e| crash!(2, "{}", format_error_message(e, s, options::BUF_SIZE))) USimpleError::new(2, format_error_message(e, s, options::BUF_SIZE))
}); })
})?;
settings.tmp_dir = matches settings.tmp_dir = matches
.value_of(options::TMP_DIR) .value_of(options::TMP_DIR)
@ -1030,9 +1186,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
settings.compress_prog = matches.value_of(options::COMPRESS_PROG).map(String::from); settings.compress_prog = matches.value_of(options::COMPRESS_PROG).map(String::from);
if let Some(n_merge) = matches.value_of(options::BATCH_SIZE) { if let Some(n_merge) = matches.value_of(options::BATCH_SIZE) {
settings.merge_batch_size = n_merge settings.merge_batch_size = n_merge.parse().map_err(|_| {
.parse() UUsageError::new(2, format!("invalid --batch-size argument '{}'", n_merge))
.unwrap_or_else(|_| crash!(2, "invalid --batch-size argument '{}'", n_merge)); })?;
} }
settings.zero_terminated = matches.is_present(options::ZERO_TERMINATED); settings.zero_terminated = matches.is_present(options::ZERO_TERMINATED);
@ -1053,32 +1209,45 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
settings.ignore_leading_blanks = matches.is_present(options::IGNORE_LEADING_BLANKS); settings.ignore_leading_blanks = matches.is_present(options::IGNORE_LEADING_BLANKS);
settings.output_file = matches.value_of(options::OUTPUT).map(String::from);
settings.reverse = matches.is_present(options::REVERSE); settings.reverse = matches.is_present(options::REVERSE);
settings.stable = matches.is_present(options::STABLE); settings.stable = matches.is_present(options::STABLE);
settings.unique = matches.is_present(options::UNIQUE); settings.unique = matches.is_present(options::UNIQUE);
if files.is_empty() { if files.is_empty() {
/* if no file, default to stdin */ /* if no file, default to stdin */
files.push("-".to_owned()); files.push("-".to_string().into());
} else if settings.check && files.len() != 1 { } else if settings.check && files.len() != 1 {
crash!(1, "extra operand `{}' not allowed with -c", files[1]) return Err(UUsageError::new(
2,
format!(
"extra operand `{}' not allowed with -c",
files[1].to_string_lossy()
),
));
} }
if let Some(arg) = matches.args.get(options::SEPARATOR) { if let Some(arg) = matches.args.get(options::SEPARATOR) {
let separator = arg.vals[0].to_string_lossy(); let separator = arg.vals[0].to_string_lossy();
let separator = separator; let mut separator = separator.as_ref();
if separator == "\\0" {
separator = "\0";
}
if separator.len() != 1 { if separator.len() != 1 {
crash!(1, "separator must be exactly one character long"); return Err(UUsageError::new(
2,
"separator must be exactly one character long".into(),
));
} }
settings.separator = Some(separator.chars().next().unwrap()) settings.separator = Some(separator.chars().next().unwrap())
} }
if let Some(values) = matches.values_of(options::KEY) { if let Some(values) = matches.values_of(options::KEY) {
for value in values { for value in values {
settings let selector = FieldSelector::parse(value, &settings)?;
.selectors if selector.settings.mode == SortMode::Random && settings.salt.is_none() {
.push(FieldSelector::parse(value, &settings)); settings.salt = Some(get_rand_string());
}
settings.selectors.push(selector);
} }
} }
@ -1099,9 +1268,19 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
); );
} }
// Verify that we can open all input files.
// It is the correct behavior to close all files afterwards,
// and to reopen them at a later point. This is different from how the output file is handled,
// probably to prevent running out of file descriptors.
for file in &files {
open(file)?;
}
let output = Output::new(matches.value_of(options::OUTPUT))?;
settings.init_precomputed(); settings.init_precomputed();
exec(&files, &settings) exec(&mut files, &settings, output)
} }
pub fn uu_app() -> App<'static, 'static> { pub fn uu_app() -> App<'static, 'static> {
@ -1112,73 +1291,57 @@ pub fn uu_app() -> App<'static, 'static> {
Arg::with_name(options::modes::SORT) Arg::with_name(options::modes::SORT)
.long(options::modes::SORT) .long(options::modes::SORT)
.takes_value(true) .takes_value(true)
.possible_values( .possible_values(&[
&[
"general-numeric", "general-numeric",
"human-numeric", "human-numeric",
"month", "month",
"numeric", "numeric",
"version", "version",
"random", "random",
] ])
.conflicts_with_all(&options::modes::ALL_SORT_MODES),
) )
.conflicts_with_all(&options::modes::ALL_SORT_MODES) .arg(make_sort_mode_arg(
)
.arg(
make_sort_mode_arg(
options::modes::HUMAN_NUMERIC, options::modes::HUMAN_NUMERIC,
"h", "h",
"compare according to human readable sizes, eg 1M > 100k" "compare according to human readable sizes, eg 1M > 100k",
), ))
) .arg(make_sort_mode_arg(
.arg(
make_sort_mode_arg(
options::modes::MONTH, options::modes::MONTH,
"M", "M",
"compare according to month name abbreviation" "compare according to month name abbreviation",
), ))
) .arg(make_sort_mode_arg(
.arg(
make_sort_mode_arg(
options::modes::NUMERIC, options::modes::NUMERIC,
"n", "n",
"compare according to string numerical value" "compare according to string numerical value",
), ))
) .arg(make_sort_mode_arg(
.arg(
make_sort_mode_arg(
options::modes::GENERAL_NUMERIC, options::modes::GENERAL_NUMERIC,
"g", "g",
"compare according to string general numerical value" "compare according to string general numerical value",
), ))
) .arg(make_sort_mode_arg(
.arg(
make_sort_mode_arg(
options::modes::VERSION, options::modes::VERSION,
"V", "V",
"Sort by SemVer version number, eg 1.12.2 > 1.1.2", "Sort by SemVer version number, eg 1.12.2 > 1.1.2",
), ))
) .arg(make_sort_mode_arg(
.arg(
make_sort_mode_arg(
options::modes::RANDOM, options::modes::RANDOM,
"R", "R",
"shuffle in random order", "shuffle in random order",
), ))
)
.arg( .arg(
Arg::with_name(options::DICTIONARY_ORDER) Arg::with_name(options::DICTIONARY_ORDER)
.short("d") .short("d")
.long(options::DICTIONARY_ORDER) .long(options::DICTIONARY_ORDER)
.help("consider only blanks and alphanumeric characters") .help("consider only blanks and alphanumeric characters")
.conflicts_with_all( .conflicts_with_all(&[
&[
options::modes::NUMERIC, options::modes::NUMERIC,
options::modes::GENERAL_NUMERIC, options::modes::GENERAL_NUMERIC,
options::modes::HUMAN_NUMERIC, options::modes::HUMAN_NUMERIC,
options::modes::MONTH, options::modes::MONTH,
] ]),
),
) )
.arg( .arg(
Arg::with_name(options::MERGE) Arg::with_name(options::MERGE)
@ -1206,7 +1369,10 @@ pub fn uu_app() -> App<'static, 'static> {
.short("C") .short("C")
.long(options::check::CHECK_SILENT) .long(options::check::CHECK_SILENT)
.conflicts_with(options::OUTPUT) .conflicts_with(options::OUTPUT)
.help("exit successfully if the given file is already sorted, and exit with status 1 otherwise."), .help(
"exit successfully if the given file is already sorted,\
and exit with status 1 otherwise.",
),
) )
.arg( .arg(
Arg::with_name(options::IGNORE_CASE) Arg::with_name(options::IGNORE_CASE)
@ -1219,14 +1385,12 @@ pub fn uu_app() -> App<'static, 'static> {
.short("i") .short("i")
.long(options::IGNORE_NONPRINTING) .long(options::IGNORE_NONPRINTING)
.help("ignore nonprinting characters") .help("ignore nonprinting characters")
.conflicts_with_all( .conflicts_with_all(&[
&[
options::modes::NUMERIC, options::modes::NUMERIC,
options::modes::GENERAL_NUMERIC, options::modes::GENERAL_NUMERIC,
options::modes::HUMAN_NUMERIC, options::modes::HUMAN_NUMERIC,
options::modes::MONTH options::modes::MONTH,
] ]),
),
) )
.arg( .arg(
Arg::with_name(options::IGNORE_LEADING_BLANKS) Arg::with_name(options::IGNORE_LEADING_BLANKS)
@ -1275,7 +1439,8 @@ pub fn uu_app() -> App<'static, 'static> {
.short("t") .short("t")
.long(options::SEPARATOR) .long(options::SEPARATOR)
.help("custom separator for -k") .help("custom separator for -k")
.takes_value(true)) .takes_value(true),
)
.arg( .arg(
Arg::with_name(options::ZERO_TERMINATED) Arg::with_name(options::ZERO_TERMINATED)
.short("z") .short("z")
@ -1310,13 +1475,13 @@ pub fn uu_app() -> App<'static, 'static> {
.long(options::COMPRESS_PROG) .long(options::COMPRESS_PROG)
.help("compress temporary files with PROG, decompress with PROG -d") .help("compress temporary files with PROG, decompress with PROG -d")
.long_help("PROG has to take input from stdin and output to stdout") .long_help("PROG has to take input from stdin and output to stdout")
.value_name("PROG") .value_name("PROG"),
) )
.arg( .arg(
Arg::with_name(options::BATCH_SIZE) Arg::with_name(options::BATCH_SIZE)
.long(options::BATCH_SIZE) .long(options::BATCH_SIZE)
.help("Merge at most N_MERGE inputs at once.") .help("Merge at most N_MERGE inputs at once.")
.value_name("N_MERGE") .value_name("N_MERGE"),
) )
.arg( .arg(
Arg::with_name(options::FILES0_FROM) Arg::with_name(options::FILES0_FROM)
@ -1331,24 +1496,27 @@ pub fn uu_app() -> App<'static, 'static> {
.long(options::DEBUG) .long(options::DEBUG)
.help("underline the parts of the line that are actually used for sorting"), .help("underline the parts of the line that are actually used for sorting"),
) )
.arg(Arg::with_name(options::FILES).multiple(true).takes_value(true)) .arg(
Arg::with_name(options::FILES)
.multiple(true)
.takes_value(true),
)
} }
fn exec(files: &[String], settings: &GlobalSettings) -> i32 { fn exec(files: &mut [OsString], settings: &GlobalSettings, output: Output) -> UResult<()> {
if settings.merge { if settings.merge {
let mut file_merger = merge::merge(files.iter().map(open), settings); let file_merger = merge::merge(files, settings, output.as_output_name())?;
file_merger.write_all(settings); file_merger.write_all(settings, output)
} else if settings.check { } else if settings.check {
if files.len() > 1 { if files.len() > 1 {
crash!(1, "only one file allowed with -c"); Err(UUsageError::new(2, "only one file allowed with -c".into()))
} else {
check::check(files.first().unwrap(), settings)
} }
return check::check(files.first().unwrap(), settings);
} else { } else {
let mut lines = files.iter().map(open); let mut lines = files.iter().map(open);
ext_sort(&mut lines, settings, output)
ext_sort(&mut lines, settings);
} }
0
} }
fn sort_by<'a>(unsorted: &mut Vec<Line<'a>>, settings: &GlobalSettings, line_data: &LineData<'a>) { fn sort_by<'a>(unsorted: &mut Vec<Line<'a>>, settings: &GlobalSettings, line_data: &LineData<'a>) {
@ -1387,7 +1555,22 @@ fn compare_by<'a>(
let settings = &selector.settings; let settings = &selector.settings;
let cmp: Ordering = match settings.mode { let cmp: Ordering = match settings.mode {
SortMode::Random => random_shuffle(a_str, b_str, &global_settings.salt), SortMode::Random => {
// check if the two strings are equal
if custom_str_cmp(
a_str,
b_str,
settings.ignore_non_printing,
settings.dictionary_order,
settings.ignore_case,
) == Ordering::Equal
{
Ordering::Equal
} else {
// Only if they are not equal compare by the hash
random_shuffle(a_str, b_str, &global_settings.salt.unwrap())
}
}
SortMode::Numeric => { SortMode::Numeric => {
let a_num_info = &a_line_data.num_infos let a_num_info = &a_line_data.num_infos
[a.index * global_settings.precomputed.num_infos_per_line + num_info_index]; [a.index * global_settings.precomputed.num_infos_per_line + num_info_index];
@ -1536,12 +1719,8 @@ fn general_numeric_compare(a: &GeneralF64ParseResult, b: &GeneralF64ParseResult)
a.partial_cmp(b).unwrap() a.partial_cmp(b).unwrap()
} }
fn get_rand_string() -> String { fn get_rand_string() -> [u8; 16] {
thread_rng() thread_rng().sample(rand::distributions::Standard)
.sample_iter(&Alphanumeric)
.take(16)
.map(char::from)
.collect::<String>()
} }
fn get_hash<T: Hash>(t: &T) -> u64 { fn get_hash<T: Hash>(t: &T) -> u64 {
@ -1550,10 +1729,9 @@ fn get_hash<T: Hash>(t: &T) -> u64 {
s.finish() s.finish()
} }
fn random_shuffle(a: &str, b: &str, salt: &str) -> Ordering { fn random_shuffle(a: &str, b: &str, salt: &[u8]) -> Ordering {
let da = get_hash(&[a, salt].concat()); let da = get_hash(&(a, salt));
let db = get_hash(&[b, salt].concat()); let db = get_hash(&(b, salt));
da.cmp(&db) da.cmp(&db)
} }
@ -1618,26 +1796,38 @@ fn month_compare(a: &str, b: &str) -> Ordering {
} }
} }
fn print_sorted<'a, T: Iterator<Item = &'a Line<'a>>>(iter: T, settings: &GlobalSettings) { fn print_sorted<'a, T: Iterator<Item = &'a Line<'a>>>(
let mut writer = settings.out_writer(); iter: T,
settings: &GlobalSettings,
output: Output,
) {
let mut writer = output.into_write();
for line in iter { for line in iter {
line.print(&mut writer, settings); line.print(&mut writer, settings);
} }
} }
// from cat.rs /// Strips the trailing " (os error XX)" from io error strings.
fn open(path: impl AsRef<OsStr>) -> Box<dyn Read + Send> { fn strip_errno(err: &str) -> &str {
&err[..err.find(" (os error ").unwrap_or(err.len())]
}
fn open(path: impl AsRef<OsStr>) -> UResult<Box<dyn Read + Send>> {
let path = path.as_ref(); let path = path.as_ref();
if path == "-" { if path == "-" {
let stdin = stdin(); let stdin = stdin();
return Box::new(stdin) as Box<dyn Read + Send>; return Ok(Box::new(stdin) as Box<dyn Read + Send>);
} }
match File::open(Path::new(path)) { let path = Path::new(path);
Ok(f) => Box::new(f) as Box<dyn Read + Send>,
Err(e) => { match File::open(path) {
crash!(2, "cannot read: {0:?}: {1}", path, e); Ok(f) => Ok(Box::new(f) as Box<dyn Read + Send>),
Err(error) => Err(SortError::ReadFailed {
path: path.to_string_lossy().to_string(),
error,
} }
.into()),
} }
} }

View file

@ -436,6 +436,7 @@ impl Stater {
'f' => tokens.push(Token::Char('\x0C')), 'f' => tokens.push(Token::Char('\x0C')),
'n' => tokens.push(Token::Char('\n')), 'n' => tokens.push(Token::Char('\n')),
'r' => tokens.push(Token::Char('\r')), 'r' => tokens.push(Token::Char('\r')),
't' => tokens.push(Token::Char('\t')),
'v' => tokens.push(Token::Char('\x0B')), 'v' => tokens.push(Token::Char('\x0B')),
c => { c => {
show_warning!("unrecognized escape '\\{}'", c); show_warning!("unrecognized escape '\\{}'", c);

View file

@ -35,15 +35,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let before = matches.is_present(options::BEFORE); let before = matches.is_present(options::BEFORE);
let regex = matches.is_present(options::REGEX); let regex = matches.is_present(options::REGEX);
let separator = match matches.value_of(options::SEPARATOR) { let raw_separator = matches.value_of(options::SEPARATOR).unwrap_or("\n");
Some(m) => { let separator = if raw_separator.is_empty() {
if m.is_empty() { "\0"
crash!(1, "separator cannot be empty")
} else { } else {
m.to_owned() raw_separator
}
}
None => "\n".to_owned(),
}; };
let files: Vec<String> = match matches.values_of(options::FILE) { let files: Vec<String> = match matches.values_of(options::FILE) {
@ -51,7 +47,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
None => vec!["-".to_owned()], None => vec!["-".to_owned()],
}; };
tac(files, before, regex, &separator[..]) tac(files, before, regex, separator)
} }
pub fn uu_app() -> App<'static, 'static> { pub fn uu_app() -> App<'static, 'static> {
@ -97,7 +93,7 @@ fn tac(filenames: Vec<String>, before: bool, _: bool, separator: &str) -> i32 {
let path = Path::new(filename); let path = Path::new(filename);
if path.is_dir() || path.metadata().is_err() { if path.is_dir() || path.metadata().is_err() {
if path.is_dir() { if path.is_dir() {
show_error!("dir: read error: Invalid argument"); show_error!("{}: read error: Invalid argument", filename);
} else { } else {
show_error!( show_error!(
"failed to open '{}' for reading: No such file or directory", "failed to open '{}' for reading: No such file or directory",
@ -139,9 +135,16 @@ fn tac(filenames: Vec<String>, before: bool, _: bool, separator: &str) -> i32 {
i += 1; i += 1;
} }
} }
// If the file contains no line separators, then simply write
// the contents of the file directly to stdout.
if offsets.is_empty() {
out.write_all(&data)
.unwrap_or_else(|e| crash!(1, "failed to write to stdout: {}", e));
return exit_code;
}
// if there isn't a separator at the end of the file, fake it // if there isn't a separator at the end of the file, fake it
if offsets.is_empty() || *offsets.last().unwrap() < data.len() - slen { if *offsets.last().unwrap() < data.len() - slen {
offsets.push(data.len()); offsets.push(data.len());
} }

View file

@ -24,6 +24,10 @@ winapi = { version="0.3", features=["fileapi", "handleapi", "processthreadsapi",
[target.'cfg(target_os = "redox")'.dependencies] [target.'cfg(target_os = "redox")'.dependencies]
redox_syscall = "0.1" redox_syscall = "0.1"
[target.'cfg(unix)'.dependencies]
nix = "0.20"
libc = "0.2"
[[bin]] [[bin]]
name = "tail" name = "tail"
path = "src/main.rs" path = "src/main.rs"

View file

@ -11,7 +11,7 @@
### Others ### Others
- [ ] The current implementation does not handle `-` as an alias for stdin. - [ ] The current implementation doesn't follow stdin in non-unix platforms
## Possible optimizations ## Possible optimizations

View file

@ -2,13 +2,14 @@
* This file is part of the uutils coreutils package. * This file is part of the uutils coreutils package.
* *
* (c) Alexander Batischev <eual.jp@gmail.com> * (c) Alexander Batischev <eual.jp@gmail.com>
* (c) Thomas Queiroz <thomasqueirozb@gmail.com>
* *
* 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.
*/ */
#[cfg(unix)] #[cfg(unix)]
pub use self::unix::{supports_pid_checks, Pid, ProcessChecker}; pub use self::unix::{stdin_is_pipe_or_fifo, supports_pid_checks, Pid, ProcessChecker};
#[cfg(windows)] #[cfg(windows)]
pub use self::windows::{supports_pid_checks, Pid, ProcessChecker}; pub use self::windows::{supports_pid_checks, Pid, ProcessChecker};

View file

@ -2,6 +2,7 @@
* This file is part of the uutils coreutils package. * This file is part of the uutils coreutils package.
* *
* (c) Alexander Batischev <eual.jp@gmail.com> * (c) Alexander Batischev <eual.jp@gmail.com>
* (c) Thomas Queiroz <thomasqueirozb@gmail.com>
* *
* 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.
@ -9,7 +10,13 @@
// spell-checker:ignore (ToDO) errno EPERM ENOSYS // spell-checker:ignore (ToDO) errno EPERM ENOSYS
use std::io::Error; use std::io::{stdin, Error};
use std::os::unix::prelude::AsRawFd;
use nix::sys::stat::fstat;
use libc::{S_IFIFO, S_IFSOCK};
pub type Pid = libc::pid_t; pub type Pid = libc::pid_t;
@ -40,3 +47,16 @@ pub fn supports_pid_checks(pid: self::Pid) -> bool {
fn get_errno() -> i32 { fn get_errno() -> i32 {
Error::last_os_error().raw_os_error().unwrap() Error::last_os_error().raw_os_error().unwrap()
} }
pub fn stdin_is_pipe_or_fifo() -> bool {
let fd = stdin().lock().as_raw_fd();
fd >= 0 // GNU tail checks fd >= 0
&& match fstat(fd) {
Ok(stat) => {
let mode = stat.st_mode;
// NOTE: This is probably not the most correct way to check this
(mode & S_IFIFO != 0) || (mode & S_IFSOCK != 0)
}
Err(err) => panic!("{}", err),
}
}

View file

@ -2,6 +2,7 @@
// * // *
// * (c) Morten Olsen Lysgaard <morten@lysgaard.no> // * (c) Morten Olsen Lysgaard <morten@lysgaard.no>
// * (c) Alexander Batischev <eual.jp@gmail.com> // * (c) Alexander Batischev <eual.jp@gmail.com>
// * (c) Thomas Queiroz <thomasqueirozb@gmail.com>
// * // *
// * 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.
@ -29,6 +30,9 @@ use std::time::Duration;
use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::parse_size::{parse_size, ParseSizeError};
use uucore::ringbuffer::RingBuffer; use uucore::ringbuffer::RingBuffer;
#[cfg(unix)]
use crate::platform::stdin_is_pipe_or_fifo;
pub mod options { pub mod options {
pub mod verbosity { pub mod verbosity {
pub static QUIET: &str = "quiet"; pub static QUIET: &str = "quiet";
@ -130,25 +134,56 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let files: Vec<String> = matches let files: Vec<String> = matches
.values_of(options::ARG_FILES) .values_of(options::ARG_FILES)
.map(|v| v.map(ToString::to_string).collect()) .map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default(); .unwrap_or_else(|| vec![String::from("-")]);
if files.is_empty() {
let mut buffer = BufReader::new(stdin());
unbounded_tail(&mut buffer, &settings);
} else {
let multiple = files.len() > 1; let multiple = files.len() > 1;
let mut first_header = true; let mut first_header = true;
let mut readers = Vec::new(); let mut readers: Vec<(Box<dyn BufRead>, &String)> = Vec::new();
#[cfg(unix)]
let stdin_string = String::from("standard input");
for filename in &files { for filename in &files {
let use_stdin = filename.as_str() == "-";
if (multiple || verbose) && !quiet { if (multiple || verbose) && !quiet {
if !first_header { if !first_header {
println!(); println!();
} }
if use_stdin {
println!("==> standard input <==");
} else {
println!("==> {} <==", filename); println!("==> {} <==", filename);
} }
}
first_header = false; first_header = false;
if use_stdin {
let mut reader = BufReader::new(stdin());
unbounded_tail(&mut reader, &settings);
// Don't follow stdin since there are no checks for pipes/FIFOs
//
// FIXME windows has GetFileType which can determine if the file is a pipe/FIFO
// so this check can also be performed
#[cfg(unix)]
{
/*
POSIX specification regarding tail -f
If the input file is a regular file or if the file operand specifies a FIFO, do not
terminate after the last line of the input file has been copied, but read and copy
further bytes from the input file when they become available. If no file operand is
specified and standard input is a pipe or FIFO, the -f option shall be ignored. If
the input file is not a FIFO, pipe, or regular file, it is unspecified whether or
not the -f option shall be ignored.
*/
if settings.follow && !stdin_is_pipe_or_fifo() {
readers.push((Box::new(reader), &stdin_string));
}
}
} else {
let path = Path::new(filename); let path = Path::new(filename);
if path.is_dir() { if path.is_dir() {
continue; continue;
@ -158,20 +193,20 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
bounded_tail(&mut file, &settings); bounded_tail(&mut file, &settings);
if settings.follow { if settings.follow {
let reader = BufReader::new(file); let reader = BufReader::new(file);
readers.push(reader); readers.push((Box::new(reader), filename));
} }
} else { } else {
let mut reader = BufReader::new(file); let mut reader = BufReader::new(file);
unbounded_tail(&mut reader, &settings); unbounded_tail(&mut reader, &settings);
if settings.follow { if settings.follow {
readers.push(reader); readers.push((Box::new(reader), filename));
}
} }
} }
} }
if settings.follow { if settings.follow {
follow(&mut readers[..], &files[..], &settings); follow(&mut readers[..], &settings);
}
} }
0 0
@ -248,8 +283,12 @@ pub fn uu_app() -> App<'static, 'static> {
) )
} }
fn follow<T: Read>(readers: &mut [BufReader<T>], filenames: &[String], settings: &Settings) { fn follow<T: BufRead>(readers: &mut [(T, &String)], settings: &Settings) {
assert!(settings.follow); assert!(settings.follow);
if readers.is_empty() {
return;
}
let mut last = readers.len() - 1; let mut last = readers.len() - 1;
let mut read_some = false; let mut read_some = false;
let mut process = platform::ProcessChecker::new(settings.pid); let mut process = platform::ProcessChecker::new(settings.pid);
@ -260,7 +299,7 @@ fn follow<T: Read>(readers: &mut [BufReader<T>], filenames: &[String], settings:
let pid_is_dead = !read_some && settings.pid != 0 && process.is_dead(); let pid_is_dead = !read_some && settings.pid != 0 && process.is_dead();
read_some = false; read_some = false;
for (i, reader) in readers.iter_mut().enumerate() { for (i, (reader, filename)) in readers.iter_mut().enumerate() {
// Print all new content since the last pass // Print all new content since the last pass
loop { loop {
let mut datum = String::new(); let mut datum = String::new();
@ -269,7 +308,7 @@ fn follow<T: Read>(readers: &mut [BufReader<T>], filenames: &[String], settings:
Ok(_) => { Ok(_) => {
read_some = true; read_some = true;
if i != last { if i != last {
println!("\n==> {} <==", filenames[i]); println!("\n==> {} <==", filename);
last = i; last = i;
} }
print!("{}", datum); print!("{}", datum);

View file

@ -251,7 +251,7 @@ impl Display for UError {
/// ///
/// A crate like [`quick_error`](https://crates.io/crates/quick-error) might /// A crate like [`quick_error`](https://crates.io/crates/quick-error) might
/// also be used, but will still require an `impl` for the `code` method. /// also be used, but will still require an `impl` for the `code` method.
pub trait UCustomError: Error { pub trait UCustomError: Error + Send {
/// Error code of a custom error. /// Error code of a custom error.
/// ///
/// Set a return value for each variant of an enum-type to associate an /// Set a return value for each variant of an enum-type to associate an

View file

@ -1,4 +1,5 @@
use crate::common::util::*; use crate::common::util::*;
// spell-checker:ignore (ToDO) taaaa tbbbb tcccc
#[test] #[test]
fn test_with_tab() { fn test_with_tab() {
@ -53,3 +54,140 @@ fn test_with_multiple_files() {
.stdout_contains(" return") .stdout_contains(" return")
.stdout_contains(" "); .stdout_contains(" ");
} }
#[test]
fn test_tabs_space_separated_list() {
new_ucmd!()
.args(&["--tabs", "3 6 9"])
.pipe_in("a\tb\tc\td\te")
.succeeds()
.stdout_is("a b c d e");
}
#[test]
fn test_tabs_mixed_style_list() {
new_ucmd!()
.args(&["--tabs", ", 3,6 9"])
.pipe_in("a\tb\tc\td\te")
.succeeds()
.stdout_is("a b c d e");
}
#[test]
fn test_tabs_empty_string() {
new_ucmd!()
.args(&["--tabs", ""])
.pipe_in("a\tb\tc")
.succeeds()
.stdout_is("a b c");
}
#[test]
fn test_tabs_comma_only() {
new_ucmd!()
.args(&["--tabs", ","])
.pipe_in("a\tb\tc")
.succeeds()
.stdout_is("a b c");
}
#[test]
fn test_tabs_space_only() {
new_ucmd!()
.args(&["--tabs", " "])
.pipe_in("a\tb\tc")
.succeeds()
.stdout_is("a b c");
}
#[test]
fn test_tabs_slash() {
new_ucmd!()
.args(&["--tabs", "/"])
.pipe_in("a\tb\tc")
.succeeds()
.stdout_is("a b c");
}
#[test]
fn test_tabs_plus() {
new_ucmd!()
.args(&["--tabs", "+"])
.pipe_in("a\tb\tc")
.succeeds()
.stdout_is("a b c");
}
#[test]
fn test_tabs_trailing_slash() {
new_ucmd!()
.arg("--tabs=1,/5")
.pipe_in("\ta\tb\tc")
.succeeds()
// 0 1
// 01234567890
.stdout_is(" a b c");
}
#[test]
fn test_tabs_trailing_slash_long_columns() {
new_ucmd!()
.arg("--tabs=1,/3")
.pipe_in("\taaaa\tbbbb\tcccc")
.succeeds()
// 0 1
// 01234567890123456
.stdout_is(" aaaa bbbb cccc");
}
#[test]
fn test_tabs_trailing_plus() {
new_ucmd!()
.arg("--tabs=1,+5")
.pipe_in("\ta\tb\tc")
.succeeds()
// 0 1
// 012345678901
.stdout_is(" a b c");
}
#[test]
fn test_tabs_trailing_plus_long_columns() {
new_ucmd!()
.arg("--tabs=1,+3")
.pipe_in("\taaaa\tbbbb\tcccc")
.succeeds()
// 0 1
// 012345678901234567
.stdout_is(" aaaa bbbb cccc");
}
#[test]
fn test_tabs_must_be_ascending() {
new_ucmd!()
.arg("--tabs=1,1")
.fails()
.stderr_contains("tab sizes must be ascending");
}
#[test]
fn test_tabs_keep_last_trailing_specifier() {
// If there are multiple trailing specifiers, use only the last one
// before the number.
new_ucmd!()
.arg("--tabs=1,+/+/5")
.pipe_in("\ta\tb\tc")
.succeeds()
// 0 1
// 01234567890
.stdout_is(" a b c");
}
#[test]
fn test_tabs_comma_separated_no_numbers() {
new_ucmd!()
.arg("--tabs=+,/,+,/")
.pipe_in("\ta\tb\tc")
.succeeds()
.stdout_is(" a b c");
}

View file

@ -17,9 +17,9 @@ fn test_id_no_specified_user() {
let exp_result = unwrap_or_return!(expected_result(&ts, &[])); let exp_result = unwrap_or_return!(expected_result(&ts, &[]));
let mut _exp_stdout = exp_result.stdout_str().to_string(); let mut _exp_stdout = exp_result.stdout_str().to_string();
#[cfg(target_os = "linux")] #[cfg(not(feature = "feat_selinux"))]
{ {
// NOTE: (SELinux NotImplemented) strip 'context' part from exp_stdout: // NOTE: strip 'context' part from exp_stdout if selinux not enabled:
// example: // example:
// uid=1001(runner) gid=121(docker) groups=121(docker),4(adm),101(systemd-journal) \ // uid=1001(runner) gid=121(docker) groups=121(docker),4(adm),101(systemd-journal) \
// context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 // context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
@ -363,3 +363,88 @@ fn test_id_zero() {
} }
} }
} }
#[test]
#[cfg(feature = "feat_selinux")]
fn test_id_context() {
use selinux::{self, KernelSupport};
if selinux::kernel_support() == KernelSupport::Unsupported {
println!("test skipped: Kernel has no support for SElinux context",);
return;
}
let ts = TestScenario::new(util_name!());
for c_flag in &["-Z", "--context"] {
ts.ucmd()
.args(&[c_flag])
.succeeds()
.stdout_only(unwrap_or_return!(expected_result(&ts, &[c_flag])).stdout_str());
for &z_flag in &["-z", "--zero"] {
let args = [c_flag, z_flag];
ts.ucmd()
.args(&args)
.succeeds()
.stdout_only(unwrap_or_return!(expected_result(&ts, &args)).stdout_str());
for &opt1 in &["--name", "--real"] {
// id: cannot print only names or real IDs in default format
let args = [opt1, c_flag];
ts.ucmd()
.args(&args)
.succeeds()
.stdout_only(unwrap_or_return!(expected_result(&ts, &args)).stdout_str());
let args = [opt1, c_flag, z_flag];
ts.ucmd()
.args(&args)
.succeeds()
.stdout_only(unwrap_or_return!(expected_result(&ts, &args)).stdout_str());
for &opt2 in &["--user", "--group", "--groups"] {
// u/g/G n/r z Z
// for now, we print clap's standard response for "conflicts_with" instead of:
// id: cannot print "only" of more than one choice
let args = [opt2, c_flag, opt1];
let _result = ts.ucmd().args(&args).fails();
// let exp_result = unwrap_or_return!(expected_result(&args));
// result
// .stdout_is(exp_result.stdout_str())
// .stderr_is(exp_result.stderr_str())
// .code_is(exp_result.code());
}
}
for &opt2 in &["--user", "--group", "--groups"] {
// u/g/G z Z
// for now, we print clap's standard response for "conflicts_with" instead of:
// id: cannot print "only" of more than one choice
let args = [opt2, c_flag];
let _result = ts.ucmd().args(&args).fails();
// let exp_result = unwrap_or_return!(expected_result(&args));
// result
// .stdout_is(exp_result.stdout_str())
// .stderr_is(exp_result.stderr_str())
// .code_is(exp_result.code());
}
}
}
}
#[test]
#[cfg(unix)]
fn test_id_no_specified_user_posixly() {
// gnu/tests/id/no-context.sh
let ts = TestScenario::new(util_name!());
let result = ts.ucmd().env("POSIXLY_CORRECT", "1").run();
assert!(!result.stdout_str().contains("context="));
if !is_ci() {
result.success();
}
#[cfg(all(target_os = "linux", feature = "feat_selinux"))]
{
use selinux::{self, KernelSupport};
if selinux::kernel_support() == KernelSupport::Unsupported {
println!("test skipped: Kernel has no support for SElinux context",);
} else {
let result = ts.ucmd().succeeds();
assert!(result.stdout_str().contains("context="));
}
}
}

View file

@ -181,7 +181,7 @@ fn test_check_zero_terminated_failure() {
.arg("-c") .arg("-c")
.arg("zero-terminated.txt") .arg("zero-terminated.txt")
.fails() .fails()
.stdout_is("sort: zero-terminated.txt:2: disorder: ../../fixtures/du\n"); .stderr_only("sort: zero-terminated.txt:2: disorder: ../../fixtures/du\n");
} }
#[test] #[test]
@ -220,32 +220,29 @@ fn test_random_shuffle_contains_all_lines() {
#[test] #[test]
fn test_random_shuffle_two_runs_not_the_same() { fn test_random_shuffle_two_runs_not_the_same() {
for arg in &["-R", "-k1,1R"] {
// check to verify that two random shuffles are not equal; this has the // check to verify that two random shuffles are not equal; this has the
// potential to fail in the very unlikely event that the random order is the same // potential to fail in the very unlikely event that the random order is the same
// as the starting order, or if both random sorts end up having the same order. // as the starting order, or if both random sorts end up having the same order.
const FILE: &str = "default_unsorted_ints.expected"; const FILE: &str = "default_unsorted_ints.expected";
let (at, _ucmd) = at_and_ucmd!(); let (at, _ucmd) = at_and_ucmd!();
let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str(); let result = new_ucmd!().arg(arg).arg(FILE).run().stdout_move_str();
let expected = at.read(FILE); let expected = at.read(FILE);
let unexpected = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str(); let unexpected = new_ucmd!().arg(arg).arg(FILE).run().stdout_move_str();
assert_ne!(result, expected); assert_ne!(result, expected);
assert_ne!(result, unexpected); assert_ne!(result, unexpected);
}
} }
#[test] #[test]
fn test_random_shuffle_contains_two_runs_not_the_same() { fn test_random_ignore_case() {
// check to verify that two random shuffles are not equal; this has the let input = "ABC\nABc\nAbC\nAbc\naBC\naBc\nabC\nabc\n";
// potential to fail in the unlikely event that random order is the same new_ucmd!()
// as the starting order, or if both random sorts end up having the same order. .args(&["-fR"])
const FILE: &str = "default_unsorted_ints.expected"; .pipe_in(input)
let (at, _ucmd) = at_and_ucmd!(); .succeeds()
let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str(); .stdout_is(input);
let expected = at.read(FILE);
let unexpected = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str();
assert_ne!(result, expected);
assert_ne!(result, unexpected);
} }
#[test] #[test]
@ -774,14 +771,15 @@ fn test_check() {
new_ucmd!() new_ucmd!()
.arg(diagnose_arg) .arg(diagnose_arg)
.arg("check_fail.txt") .arg("check_fail.txt")
.arg("--buffer-size=10b")
.fails() .fails()
.stdout_is("sort: check_fail.txt:6: disorder: 5\n"); .stderr_only("sort: check_fail.txt:6: disorder: 5\n");
new_ucmd!() new_ucmd!()
.arg(diagnose_arg) .arg(diagnose_arg)
.arg("multiple_files.expected") .arg("multiple_files.expected")
.succeeds() .succeeds()
.stdout_is(""); .stderr_is("");
} }
} }
@ -796,6 +794,18 @@ fn test_check_silent() {
} }
} }
#[test]
fn test_check_unique() {
// Due to a clap bug the combination "-cu" does not work. "-c -u" works.
// See https://github.com/clap-rs/clap/issues/2624
new_ucmd!()
.args(&["-c", "-u"])
.pipe_in("A\nA\n")
.fails()
.code_is(1)
.stderr_only("sort: -:2: disorder: A");
}
#[test] #[test]
fn test_dictionary_and_nonprinting_conflicts() { fn test_dictionary_and_nonprinting_conflicts() {
let conflicting_args = ["n", "h", "g", "M"]; let conflicting_args = ["n", "h", "g", "M"];
@ -839,9 +849,9 @@ fn test_nonexistent_file() {
.status_code(2) .status_code(2)
.stderr_only( .stderr_only(
#[cfg(not(windows))] #[cfg(not(windows))]
"sort: cannot read: \"nonexistent.txt\": No such file or directory (os error 2)", "sort: cannot read: nonexistent.txt: No such file or directory",
#[cfg(windows)] #[cfg(windows)]
"sort: cannot read: \"nonexistent.txt\": The system cannot find the file specified. (os error 2)", "sort: cannot read: nonexistent.txt: The system cannot find the file specified.",
); );
} }
@ -883,6 +893,29 @@ fn test_compress() {
.stdout_only_fixture("ext_sort.expected"); .stdout_only_fixture("ext_sort.expected");
} }
#[test]
#[cfg(target_os = "linux")]
fn test_compress_merge() {
new_ucmd!()
.args(&[
"--compress-program",
"gzip",
"-S",
"10",
"--batch-size=2",
"-m",
"--unique",
"merge_ints_interleaved_1.txt",
"merge_ints_interleaved_2.txt",
"merge_ints_interleaved_3.txt",
"merge_ints_interleaved_3.txt",
"merge_ints_interleaved_2.txt",
"merge_ints_interleaved_1.txt",
])
.succeeds()
.stdout_only_fixture("merge_ints_interleaved.expected");
}
#[test] #[test]
fn test_compress_fail() { fn test_compress_fail() {
TestScenario::new(util_name!()) TestScenario::new(util_name!())
@ -959,3 +992,102 @@ fn test_key_takes_one_arg() {
.succeeds() .succeeds()
.stdout_is_fixture("keys_open_ended.expected"); .stdout_is_fixture("keys_open_ended.expected");
} }
#[test]
fn test_verifies_out_file() {
let inputs = ["" /* no input */, "some input"];
for &input in &inputs {
new_ucmd!()
.args(&["-o", "nonexistent_dir/nonexistent_file"])
.pipe_in(input)
.ignore_stdin_write_error()
.fails()
.status_code(2)
.stderr_only(
#[cfg(not(windows))]
"sort: open failed: nonexistent_dir/nonexistent_file: No such file or directory",
#[cfg(windows)]
"sort: open failed: nonexistent_dir/nonexistent_file: The system cannot find the path specified.",
);
}
}
#[test]
fn test_verifies_files_after_keys() {
new_ucmd!()
.args(&[
"-o",
"nonexistent_dir/nonexistent_file",
"-k",
"0",
"nonexistent_dir/input_file",
])
.fails()
.status_code(2)
.stderr_contains("failed to parse key");
}
#[test]
#[cfg(unix)]
fn test_verifies_input_files() {
new_ucmd!()
.args(&["/dev/random", "nonexistent_file"])
.fails()
.status_code(2)
.stderr_is("sort: cannot read: nonexistent_file: No such file or directory");
}
#[test]
fn test_separator_null() {
new_ucmd!()
.args(&["-k1,1", "-k3,3", "-t", "\\0"])
.pipe_in("z\0a\0b\nz\0b\0a\na\0z\0z\n")
.succeeds()
.stdout_only("a\0z\0z\nz\0b\0a\nz\0a\0b\n");
}
#[test]
fn test_output_is_input() {
let input = "a\nb\nc\n";
let (at, mut cmd) = at_and_ucmd!();
at.touch("file");
at.append("file", input);
cmd.args(&["-m", "-u", "-o", "file", "file", "file", "file"])
.succeeds();
assert_eq!(at.read("file"), input);
}
#[test]
#[cfg(unix)]
fn test_output_device() {
new_ucmd!()
.args(&["-o", "/dev/null"])
.pipe_in("input")
.succeeds();
}
#[test]
fn test_merge_empty_input() {
new_ucmd!()
.args(&["-m", "empty.txt"])
.succeeds()
.no_stderr()
.no_stdout();
}
#[test]
fn test_no_error_for_version() {
new_ucmd!()
.arg("--version")
.succeeds()
.stdout_contains("sort");
}
#[test]
fn test_wrong_args_exit_code() {
new_ucmd!()
.arg("--misspelled")
.fails()
.status_code(2)
.stderr_contains("--misspelled");
}

View file

@ -64,7 +64,7 @@ mod test_generate_tokens {
#[test] #[test]
fn printf_format() { fn printf_format() {
let s = "%-# 15a\\r\\\"\\\\\\a\\b\\e\\f\\v%+020.-23w\\x12\\167\\132\\112\\n"; let s = "%-# 15a\\t\\r\\\"\\\\\\a\\b\\e\\f\\v%+020.-23w\\x12\\167\\132\\112\\n";
let expected = vec![ let expected = vec![
Token::Directive { Token::Directive {
flag: F_LEFT | F_ALTER | F_SPACE, flag: F_LEFT | F_ALTER | F_SPACE,
@ -72,6 +72,7 @@ mod test_generate_tokens {
precision: -1, precision: -1,
format: 'a', format: 'a',
}, },
Token::Char('\t'),
Token::Char('\r'), Token::Char('\r'),
Token::Char('"'), Token::Char('"'),
Token::Char('\\'), Token::Char('\\'),

View file

@ -66,5 +66,19 @@ fn test_invalid_input() {
.ucmd() .ucmd()
.arg("a") .arg("a")
.fails() .fails()
.stderr_contains("dir: read error: Invalid argument"); .stderr_contains("a: read error: Invalid argument");
}
#[test]
fn test_no_line_separators() {
new_ucmd!().pipe_in("a").succeeds().stdout_is("a");
}
#[test]
fn test_null_separator() {
new_ucmd!()
.args(&["-s", ""])
.pipe_in("a\0b\0")
.succeeds()
.stdout_is("b\0a\0");
} }

View file

@ -23,6 +23,15 @@ fn test_stdin_default() {
.stdout_is_fixture("foobar_stdin_default.expected"); .stdout_is_fixture("foobar_stdin_default.expected");
} }
#[test]
fn test_stdin_explicit() {
new_ucmd!()
.pipe_in_fixture(FOOBAR_TXT)
.arg("-")
.run()
.stdout_is_fixture("foobar_stdin_default.expected");
}
#[test] #[test]
fn test_single_default() { fn test_single_default() {
new_ucmd!() new_ucmd!()

View file

@ -591,28 +591,40 @@ impl AtPath {
} }
} }
pub fn hard_link(&self, src: &str, dst: &str) { pub fn hard_link(&self, original: &str, link: &str) {
log_info( log_info(
"hard_link", "hard_link",
&format!("{},{}", self.plus_as_string(src), self.plus_as_string(dst)), &format!(
"{},{}",
self.plus_as_string(original),
self.plus_as_string(link)
),
); );
hard_link(&self.plus(src), &self.plus(dst)).unwrap(); hard_link(&self.plus(original), &self.plus(link)).unwrap();
} }
pub fn symlink_file(&self, src: &str, dst: &str) { pub fn symlink_file(&self, original: &str, link: &str) {
log_info( log_info(
"symlink", "symlink",
&format!("{},{}", self.plus_as_string(src), self.plus_as_string(dst)), &format!(
"{},{}",
self.plus_as_string(original),
self.plus_as_string(link)
),
); );
symlink_file(&self.plus(src), &self.plus(dst)).unwrap(); symlink_file(&self.plus(original), &self.plus(link)).unwrap();
} }
pub fn symlink_dir(&self, src: &str, dst: &str) { pub fn symlink_dir(&self, original: &str, link: &str) {
log_info( log_info(
"symlink", "symlink",
&format!("{},{}", self.plus_as_string(src), self.plus_as_string(dst)), &format!(
"{},{}",
self.plus_as_string(original),
self.plus_as_string(link)
),
); );
symlink_dir(&self.plus(src), &self.plus(dst)).unwrap(); symlink_dir(&self.plus(original), &self.plus(link)).unwrap();
} }
pub fn is_symlink(&self, path: &str) -> bool { pub fn is_symlink(&self, path: &str) -> bool {

0
tests/fixtures/sort/empty.txt vendored Normal file
View file