1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-30 12:37:49 +00:00

Merge branch 'master' into ls/device_information

This commit is contained in:
Terts Diepraam 2021-05-07 18:56:44 +02:00
commit 6834d0256e
76 changed files with 3502 additions and 1591 deletions

3
.gitignore vendored
View file

@ -12,3 +12,6 @@ target/
Cargo.lock Cargo.lock
lib*.a lib*.a
/docs/_build /docs/_build
*.iml
### macOS ###
.DS_Store

86
Cargo.lock generated
View file

@ -12,11 +12,11 @@ dependencies = [
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.15" version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [ dependencies = [
"memchr 2.3.4", "memchr 2.4.0",
] ]
[[package]] [[package]]
@ -106,12 +106,12 @@ dependencies = [
[[package]] [[package]]
name = "bstr" name = "bstr"
version = "0.2.15" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"memchr 2.3.4", "memchr 2.4.0",
"regex-automata", "regex-automata",
"serde", "serde",
] ]
@ -495,9 +495,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-epoch" name = "crossbeam-epoch"
version = "0.9.3" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12" checksum = "52fb27eab85b17fbb9f6fd667089e07d6a2eb8743d02639ee7f6a7a7729c9c94"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"crossbeam-utils", "crossbeam-utils",
@ -508,9 +508,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.3" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"cfg-if 1.0.0", "cfg-if 1.0.0",
@ -536,7 +536,7 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
dependencies = [ dependencies = [
"memchr 2.3.4", "memchr 2.4.0",
] ]
[[package]] [[package]]
@ -618,7 +618,7 @@ checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"redox_syscall 0.2.7", "redox_syscall 0.2.8",
"winapi 0.3.9", "winapi 0.3.9",
] ]
@ -868,9 +868,9 @@ dependencies = [
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.3.4" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]] [[package]]
name = "memoffset" name = "memoffset"
@ -1089,7 +1089,7 @@ version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
dependencies = [ dependencies = [
"unicode-xid 0.2.1", "unicode-xid 0.2.2",
] ]
[[package]] [[package]]
@ -1259,9 +1259,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.7" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85dd92e586f7355c633911e11f77f3d12f04b1b1bd76a198bd34ae3af8341ef2" checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
@ -1272,17 +1272,17 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
dependencies = [ dependencies = [
"redox_syscall 0.2.7", "redox_syscall 0.2.8",
] ]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.4.6" version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr 2.3.4", "memchr 2.4.0",
"regex-syntax", "regex-syntax",
] ]
@ -1297,9 +1297,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.23" version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]] [[package]]
name = "remove_dir_all" name = "remove_dir_all"
@ -1312,9 +1312,9 @@ dependencies = [
[[package]] [[package]]
name = "retain_mut" name = "retain_mut"
version = "0.1.2" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53552c6c49e1e13f1a203ef0080ab3bbef0beb570a528993e83df057a9d9bba1" checksum = "e9c17925a9027d298a4603d286befe3f9dc0e8ed02523141914eb628798d6e5b"
[[package]] [[package]]
name = "rust-ini" name = "rust-ini"
@ -1372,9 +1372,6 @@ name = "serde"
version = "1.0.125" version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
dependencies = [
"serde_derive",
]
[[package]] [[package]]
name = "serde_cbor" name = "serde_cbor"
@ -1453,9 +1450,6 @@ name = "smallvec"
version = "1.6.1" version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
@ -1483,13 +1477,13 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.71" version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad184cc9470f9117b2ac6817bfe297307418819ba40552f9b3846f05c33d5373" checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.9", "quote 1.0.9",
"unicode-xid 0.2.1", "unicode-xid 0.2.2",
] ]
[[package]] [[package]]
@ -1543,7 +1537,7 @@ checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
dependencies = [ dependencies = [
"libc", "libc",
"numtoa", "numtoa",
"redox_syscall 0.2.7", "redox_syscall 0.2.8",
"redox_termios", "redox_termios",
] ]
@ -1636,9 +1630,9 @@ checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.1" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]] [[package]]
name = "unindent" name = "unindent"
@ -1688,6 +1682,8 @@ dependencies = [
name = "uu_base64" name = "uu_base64"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"clap",
"uu_base32",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
] ]
@ -1696,6 +1692,7 @@ dependencies = [
name = "uu_basename" name = "uu_basename"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"clap",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
] ]
@ -1806,7 +1803,7 @@ version = "0.0.6"
dependencies = [ dependencies = [
"bstr", "bstr",
"clap", "clap",
"memchr 2.3.4", "memchr 2.4.0",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
] ]
@ -2041,6 +2038,7 @@ dependencies = [
name = "uu_kill" name = "uu_kill"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"clap",
"libc", "libc",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
@ -2050,6 +2048,7 @@ dependencies = [
name = "uu_link" name = "uu_link"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"clap",
"libc", "libc",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
@ -2069,6 +2068,7 @@ dependencies = [
name = "uu_logname" name = "uu_logname"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"clap",
"libc", "libc",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
@ -2118,7 +2118,7 @@ dependencies = [
name = "uu_mknod" name = "uu_mknod"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"getopts", "clap",
"libc", "libc",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
@ -2175,7 +2175,7 @@ dependencies = [
"aho-corasick", "aho-corasick",
"clap", "clap",
"libc", "libc",
"memchr 2.3.4", "memchr 2.4.0",
"regex", "regex",
"regex-syntax", "regex-syntax",
"uucore", "uucore",
@ -2247,6 +2247,7 @@ dependencies = [
name = "uu_pinky" name = "uu_pinky"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"clap",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
] ]
@ -2276,7 +2277,7 @@ dependencies = [
"aho-corasick", "aho-corasick",
"clap", "clap",
"libc", "libc",
"memchr 2.3.4", "memchr 2.4.0",
"regex", "regex",
"regex-syntax", "regex-syntax",
"uucore", "uucore",
@ -2391,8 +2392,6 @@ dependencies = [
"rand 0.7.3", "rand 0.7.3",
"rayon", "rayon",
"semver", "semver",
"serde",
"serde_json",
"smallvec 1.6.1", "smallvec 1.6.1",
"tempdir", "tempdir",
"unicode-width", "unicode-width",
@ -2648,6 +2647,7 @@ dependencies = [
name = "uu_who" name = "uu_who"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"clap",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
] ]

View file

@ -429,6 +429,7 @@ This is an auto-generated table showing which binaries compile for each target-t
|windows-msvc|i686|y|y|y|y|y| | | | |y|y|y|y|y|y|y|y|y| |y|y|y| |y|y|y|y| |y|y|y|y| | |y| |y|y|y|y|y| | |y|y|y| |y| |y|y|y|y| | |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| | |y|y|y|y|y|y| |y|y|y|y|y| |y|y|y| |y| |y| |y|y| |windows-msvc|i686|y|y|y|y|y| | | | |y|y|y|y|y|y|y|y|y| |y|y|y| |y|y|y|y| |y|y|y|y| | |y| |y|y|y|y|y| | |y|y|y| |y| |y|y|y|y| | |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| | |y|y|y|y|y|y| |y|y|y|y|y| |y|y|y| |y| |y| |y|y|
|windows-gnu|x86_64|y|y|y|y|y| | | | |y|y|y|y|y|y|y|y|y| |y|y|y| |y|y|y|y| |y|y|y|y| | |y| |y|y|y|y|y| | |y|y|y| |y| |y|y|y|y| | |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| | |y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y| |y| |y| |y|y| |windows-gnu|x86_64|y|y|y|y|y| | | | |y|y|y|y|y|y|y|y|y| |y|y|y| |y|y|y|y| |y|y|y|y| | |y| |y|y|y|y|y| | |y|y|y| |y| |y|y|y|y| | |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| | |y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y| |y| |y| |y|y|
|windows-msvc|x86_64|y|y|y|y|y| | | | |y|y|y|y|y|y|y|y|y| |y|y|y| |y|y|y|y| |y|y|y|y| | |y| |y|y|y|y|y| | |y|y|y| |y| |y|y|y|y| | |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| | |y|y|y|y|y|y| |y|y|y|y|y| |y|y|y| |y| |y| |y|y| |windows-msvc|x86_64|y|y|y|y|y| | | | |y|y|y|y|y|y|y|y|y| |y|y|y| |y|y|y|y| |y|y|y|y| | |y| |y|y|y|y|y| | |y|y|y| |y| |y|y|y|y| | |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| | |y|y|y|y|y|y| |y|y|y|y|y| |y|y|y| |y| |y| |y|y|
|apple MacOS|aarch64|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|
|apple MacOS|x86_64|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| |apple MacOS|x86_64|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|
|freebsd|x86_64|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| |freebsd|x86_64|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|
|netbsd|x86_64|y|y|y|y|y|y|y|y| |y|y|y|y|y|y| |y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y| |y|y| |y|y|y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y| |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y|y| |y|y|y| | |y| |y|y| |netbsd|x86_64|y|y|y|y|y|y|y|y| |y|y|y|y|y|y| |y|y|y|y|y|y| |y|y|y|y|y|y|y|y|y| |y|y| |y|y|y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y| |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| |y|y|y|y|y|y| |y|y|y| | |y| |y|y|

View file

@ -26,6 +26,7 @@ TARGETS = [
"x86_64-pc-windows-gnu", "x86_64-pc-windows-gnu",
"x86_64-pc-windows-msvc", "x86_64-pc-windows-msvc",
# Apple # Apple
"aarch64-apple-darwin",
"x86_64-apple-darwin", "x86_64-apple-darwin",
"aarch64-apple-ios", "aarch64-apple-ios",
"x86_64-apple-ios", "x86_64-apple-ios",

View file

@ -7,19 +7,14 @@
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use std::io::{stdin, Read};
use uucore::encoding::Format; use uucore::encoding::Format;
use uucore::InvalidEncodingHandling;
use std::fs::File; pub mod base_common;
use std::io::{stdin, BufReader};
use std::path::Path;
use clap::{App, Arg}; static ABOUT: &str = "
mod base_common;
static SUMMARY: &str = "Base32 encode or decode FILE, or standard input, to standard output.";
static LONG_HELP: &str = "
With no FILE, or when FILE is -, read standard input. With no FILE, or when FILE is -, read standard input.
The data are encoded as described for the base32 alphabet in RFC The data are encoded as described for the base32 alphabet in RFC
@ -30,126 +25,41 @@ static LONG_HELP: &str = "
"; ";
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static BASE_CMD_PARSE_ERROR: i32 = 1;
fn get_usage() -> String { fn get_usage() -> String {
format!("{0} [OPTION]... [FILE]", executable!()) format!("{0} [OPTION]... [FILE]", executable!())
} }
pub mod options {
pub static DECODE: &str = "decode";
pub static WRAP: &str = "wrap";
pub static IGNORE_GARBAGE: &str = "ignore-garbage";
pub static FILE: &str = "file";
}
struct Config {
decode: bool,
ignore_garbage: bool,
wrap_cols: Option<usize>,
to_read: Option<String>,
}
impl Config {
fn from(options: clap::ArgMatches) -> Config {
let file: Option<String> = match options.values_of(options::FILE) {
Some(mut values) => {
let name = values.next().unwrap();
if values.len() != 0 {
crash!(3, "extra operand {}", name);
}
if name == "-" {
None
} else {
if !Path::exists(Path::new(name)) {
crash!(2, "{}: No such file or directory", name);
}
Some(name.to_owned())
}
}
None => None,
};
let cols = match options.value_of(options::WRAP) {
Some(num) => match num.parse::<usize>() {
Ok(n) => Some(n),
Err(e) => {
crash!(1, "invalid wrap size: {}: {}", num, e);
}
},
None => None,
};
Config {
decode: options.is_present(options::DECODE),
ignore_garbage: options.is_present(options::IGNORE_GARBAGE),
wrap_cols: cols,
to_read: file,
}
}
}
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
let format = Format::Base32; let format = Format::Base32;
let usage = get_usage(); let usage = get_usage();
let app = App::new(executable!()) let name = executable!();
.version(VERSION)
.about(SUMMARY)
.usage(&usage[..])
.about(LONG_HELP)
// Format arguments.
.arg(
Arg::with_name(options::DECODE)
.short("d")
.long(options::DECODE)
.help("decode data"),
)
.arg(
Arg::with_name(options::IGNORE_GARBAGE)
.short("i")
.long(options::IGNORE_GARBAGE)
.help("when decoding, ignore non-alphabetic characters"),
)
.arg(
Arg::with_name(options::WRAP)
.short("w")
.long(options::WRAP)
.takes_value(true)
.help(
"wrap encoded lines after COLS character (default 76, 0 to disable wrapping)",
),
)
// "multiple" arguments are used to check whether there is more than one
// file passed in.
.arg(Arg::with_name(options::FILE).index(1).multiple(true));
let arg_list = args let config_result: Result<base_common::Config, String> =
.collect_str(InvalidEncodingHandling::ConvertLossy) base_common::parse_base_cmd_args(args, name, VERSION, ABOUT, &usage);
.accept_any();
let config: Config = Config::from(app.get_matches_from(arg_list)); if config_result.is_err() {
match config.to_read { match config_result {
// Read from file. Ok(_) => panic!(),
Some(name) => { Err(s) => crash!(BASE_CMD_PARSE_ERROR, "{}", s),
let file_buf = safe_unwrap!(File::open(Path::new(&name)));
let mut input = BufReader::new(file_buf);
base_common::handle_input(
&mut input,
format,
config.wrap_cols,
config.ignore_garbage,
config.decode,
);
} }
// stdin }
None => {
base_common::handle_input( // Create a reference to stdin so we can return a locked stdin from
&mut stdin().lock(), // parse_base_cmd_args
format, let stdin_raw = stdin();
config.wrap_cols, let config = config_result.unwrap();
config.ignore_garbage, let mut input: Box<dyn Read> = base_common::get_input(&config, &stdin_raw);
config.decode,
); base_common::handle_input(
} &mut input,
}; format,
config.wrap_cols,
config.ignore_garbage,
config.decode,
name,
);
0 0
} }

View file

@ -10,6 +10,122 @@
use std::io::{stdout, Read, Write}; use std::io::{stdout, Read, Write};
use uucore::encoding::{wrap_print, Data, Format}; use uucore::encoding::{wrap_print, Data, Format};
use uucore::InvalidEncodingHandling;
use std::fs::File;
use std::io::{BufReader, Stdin};
use std::path::Path;
use clap::{App, Arg};
// Config.
pub struct Config {
pub decode: bool,
pub ignore_garbage: bool,
pub wrap_cols: Option<usize>,
pub to_read: Option<String>,
}
pub mod options {
pub static DECODE: &str = "decode";
pub static WRAP: &str = "wrap";
pub static IGNORE_GARBAGE: &str = "ignore-garbage";
pub static FILE: &str = "file";
}
impl Config {
fn from(options: clap::ArgMatches) -> Result<Config, String> {
let file: Option<String> = match options.values_of(options::FILE) {
Some(mut values) => {
let name = values.next().unwrap();
if values.len() != 0 {
return Err(format!("extra operand {}", name));
}
if name == "-" {
None
} else {
if !Path::exists(Path::new(name)) {
return Err(format!("{}: No such file or directory", name));
}
Some(name.to_owned())
}
}
None => None,
};
let cols = match options.value_of(options::WRAP) {
Some(num) => match num.parse::<usize>() {
Ok(n) => Some(n),
Err(e) => {
return Err(format!("Invalid wrap size: {}: {}", num, e));
}
},
None => None,
};
Ok(Config {
decode: options.is_present(options::DECODE),
ignore_garbage: options.is_present(options::IGNORE_GARBAGE),
wrap_cols: cols,
to_read: file,
})
}
}
pub fn parse_base_cmd_args(
args: impl uucore::Args,
name: &str,
version: &str,
about: &str,
usage: &str,
) -> Result<Config, String> {
let app = App::new(name)
.version(version)
.about(about)
.usage(usage)
// Format arguments.
.arg(
Arg::with_name(options::DECODE)
.short("d")
.long(options::DECODE)
.help("decode data"),
)
.arg(
Arg::with_name(options::IGNORE_GARBAGE)
.short("i")
.long(options::IGNORE_GARBAGE)
.help("when decoding, ignore non-alphabetic characters"),
)
.arg(
Arg::with_name(options::WRAP)
.short("w")
.long(options::WRAP)
.takes_value(true)
.help(
"wrap encoded lines after COLS character (default 76, 0 to disable wrapping)",
),
)
// "multiple" arguments are used to check whether there is more than one
// file passed in.
.arg(Arg::with_name(options::FILE).index(1).multiple(true));
let arg_list = args
.collect_str(InvalidEncodingHandling::ConvertLossy)
.accept_any();
Config::from(app.get_matches_from(arg_list))
}
pub fn get_input<'a>(config: &Config, stdin_ref: &'a Stdin) -> Box<dyn Read + 'a> {
match &config.to_read {
Some(name) => {
let file_buf = safe_unwrap!(File::open(Path::new(name)));
Box::new(BufReader::new(file_buf)) // as Box<dyn Read>
}
None => {
Box::new(stdin_ref.lock()) // as Box<dyn Read>
}
}
}
pub fn handle_input<R: Read>( pub fn handle_input<R: Read>(
input: &mut R, input: &mut R,
@ -17,6 +133,7 @@ pub fn handle_input<R: Read>(
line_wrap: Option<usize>, line_wrap: Option<usize>,
ignore_garbage: bool, ignore_garbage: bool,
decode: bool, decode: bool,
name: &str,
) { ) {
let mut data = Data::new(input, format).ignore_garbage(ignore_garbage); let mut data = Data::new(input, format).ignore_garbage(ignore_garbage);
if let Some(wrap) = line_wrap { if let Some(wrap) = line_wrap {
@ -31,10 +148,14 @@ pub fn handle_input<R: Read>(
Ok(s) => { Ok(s) => {
if stdout().write_all(&s).is_err() { if stdout().write_all(&s).is_err() {
// on windows console, writing invalid utf8 returns an error // on windows console, writing invalid utf8 returns an error
crash!(1, "Cannot write non-utf8 data"); eprintln!("{}: error: Cannot write non-utf8 data", name);
exit!(1)
} }
} }
Err(_) => crash!(1, "invalid input"), Err(_) => {
eprintln!("{}: error: invalid input", name);
exit!(1)
}
} }
} }
} }

View file

@ -15,8 +15,10 @@ edition = "2018"
path = "src/base64.rs" path = "src/base64.rs"
[dependencies] [dependencies]
clap = "2.33"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features = ["encoding"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features = ["encoding"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
uu_base32 = { version=">=0.0.6", package="uu_base32", path="../base32"}
[[bin]] [[bin]]
name = "base64" name = "base64"

View file

@ -8,14 +8,14 @@
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use uu_base32::base_common;
use uucore::encoding::Format; use uucore::encoding::Format;
use uucore::InvalidEncodingHandling;
mod base_common; use std::io::{stdin, Read};
static SYNTAX: &str = "[OPTION]... [FILE]"; static ABOUT: &str = "
static SUMMARY: &str = "Base64 encode or decode FILE, or standard input, to standard output.";
static LONG_HELP: &str = "
With no FILE, or when FILE is -, read standard input. With no FILE, or when FILE is -, read standard input.
The data are encoded as described for the base64 alphabet in RFC The data are encoded as described for the base64 alphabet in RFC
@ -24,14 +24,42 @@ static LONG_HELP: &str = "
to attempt to recover from any other non-alphabet bytes in the to attempt to recover from any other non-alphabet bytes in the
encoded stream. encoded stream.
"; ";
static VERSION: &str = env!("CARGO_PKG_VERSION");
static BASE_CMD_PARSE_ERROR: i32 = 1;
fn get_usage() -> String {
format!("{0} [OPTION]... [FILE]", executable!())
}
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
base_common::execute( let format = Format::Base64;
args.collect_str(InvalidEncodingHandling::Ignore) let usage = get_usage();
.accept_any(), let name = executable!();
SYNTAX, let config_result: Result<base_common::Config, String> =
SUMMARY, base_common::parse_base_cmd_args(args, name, VERSION, ABOUT, &usage);
LONG_HELP,
Format::Base64, if config_result.is_err() {
) match config_result {
Ok(_) => panic!(),
Err(s) => crash!(BASE_CMD_PARSE_ERROR, "{}", s),
}
}
// Create a reference to stdin so we can return a locked stdin from
// parse_base_cmd_args
let stdin_raw = stdin();
let config = config_result.unwrap();
let mut input: Box<dyn Read> = base_common::get_input(&config, &stdin_raw);
base_common::handle_input(
&mut input,
format,
config.wrap_cols,
config.ignore_garbage,
config.decode,
name,
);
0
} }

View file

@ -1,97 +0,0 @@
// This file is part of the uutils coreutils package.
//
// (c) Jordy Dickinson <jordy.dickinson@gmail.com>
// (c) Jian Zeng <anonymousknight96@gmail.com>
// (c) Alex Lyon <arcterus@mail.com>
//
// For the full copyright and license information, please view the LICENSE file
// that was distributed with this source code.
use std::fs::File;
use std::io::{stdin, stdout, BufReader, Read, Write};
use std::path::Path;
use uucore::encoding::{wrap_print, Data, Format};
pub fn execute(
args: Vec<String>,
syntax: &str,
summary: &str,
long_help: &str,
format: Format,
) -> i32 {
let matches = app!(syntax, summary, long_help)
.optflag("d", "decode", "decode data")
.optflag(
"i",
"ignore-garbage",
"when decoding, ignore non-alphabetic characters",
)
.optopt(
"w",
"wrap",
"wrap encoded lines after COLS character (default 76, 0 to disable wrapping)",
"COLS",
)
.parse(args);
let line_wrap = matches.opt_str("wrap").map(|s| match s.parse() {
Ok(n) => n,
Err(e) => {
crash!(1, "invalid wrap size: {}: {}", s, e);
}
});
let ignore_garbage = matches.opt_present("ignore-garbage");
let decode = matches.opt_present("decode");
if matches.free.len() > 1 {
show_usage_error!("extra operand {}", matches.free[0]);
return 1;
}
if matches.free.is_empty() || &matches.free[0][..] == "-" {
let stdin_raw = stdin();
handle_input(
&mut stdin_raw.lock(),
format,
line_wrap,
ignore_garbage,
decode,
);
} else {
let path = Path::new(matches.free[0].as_str());
let file_buf = safe_unwrap!(File::open(&path));
let mut input = BufReader::new(file_buf);
handle_input(&mut input, format, line_wrap, ignore_garbage, decode);
};
0
}
fn handle_input<R: Read>(
input: &mut R,
format: Format,
line_wrap: Option<usize>,
ignore_garbage: bool,
decode: bool,
) {
let mut data = Data::new(input, format).ignore_garbage(ignore_garbage);
if let Some(wrap) = line_wrap {
data = data.line_wrap(wrap);
}
if !decode {
let encoded = data.encode();
wrap_print(&data, encoded);
} else {
match data.decode() {
Ok(s) => {
if stdout().write_all(&s).is_err() {
// on windows console, writing invalid utf8 returns an error
crash!(1, "Cannot write non-utf8 data");
}
}
Err(_) => crash!(1, "invalid input"),
}
}
}

View file

@ -15,6 +15,7 @@ edition = "2018"
path = "src/basename.rs" path = "src/basename.rs"
[dependencies] [dependencies]
clap = "2.33.2"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }

View file

@ -10,83 +10,106 @@
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use clap::{App, Arg};
use std::path::{is_separator, PathBuf}; use std::path::{is_separator, PathBuf};
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
static NAME: &str = "basename"; static VERSION: &str = env!("CARGO_PKG_VERSION");
static SYNTAX: &str = "NAME [SUFFIX]";
static SUMMARY: &str = "Print NAME with any leading directory components removed static SUMMARY: &str = "Print NAME with any leading directory components removed
If specified, also remove a trailing SUFFIX"; If specified, also remove a trailing SUFFIX";
static LONG_HELP: &str = "";
fn get_usage() -> String {
format!(
"{0} NAME [SUFFIX]
{0} OPTION... NAME...",
executable!()
)
}
pub mod options {
pub static MULTIPLE: &str = "multiple";
pub static NAME: &str = "name";
pub static SUFFIX: &str = "suffix";
pub static ZERO: &str = "zero";
}
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
let args = args let args = args
.collect_str(InvalidEncodingHandling::ConvertLossy) .collect_str(InvalidEncodingHandling::ConvertLossy)
.accept_any(); .accept_any();
let usage = get_usage();
// //
// Argument parsing // Argument parsing
// //
let matches = app!(SYNTAX, SUMMARY, LONG_HELP) let matches = App::new(executable!())
.optflag( .version(VERSION)
"a", .about(SUMMARY)
"multiple", .usage(&usage[..])
"Support more than one argument. Treat every argument as a name.", .arg(
Arg::with_name(options::MULTIPLE)
.short("a")
.long(options::MULTIPLE)
.help("support multiple arguments and treat each as a NAME"),
) )
.optopt( .arg(Arg::with_name(options::NAME).multiple(true).hidden(true))
"s", .arg(
"suffix", Arg::with_name(options::SUFFIX)
"Remove a trailing suffix. This option implies the -a option.", .short("s")
"SUFFIX", .long(options::SUFFIX)
.value_name("SUFFIX")
.help("remove a trailing SUFFIX; implies -a"),
) )
.optflag( .arg(
"z", Arg::with_name(options::ZERO)
"zero", .short("z")
"Output a zero byte (ASCII NUL) at the end of each line, rather than a newline.", .long(options::ZERO)
.help("end each output line with NUL, not newline"),
) )
.parse(args); .get_matches_from(args);
// too few arguments // too few arguments
if matches.free.is_empty() { if !matches.is_present(options::NAME) {
crash!( crash!(
1, 1,
"{0}: {1}\nTry '{0} --help' for more information.", "{1}\nTry '{0} --help' for more information.",
NAME, executable!(),
"missing operand" "missing operand"
); );
} }
let opt_s = matches.opt_present("s");
let opt_a = matches.opt_present("a"); let opt_suffix = matches.is_present(options::SUFFIX);
let opt_z = matches.opt_present("z"); let opt_multiple = matches.is_present(options::MULTIPLE);
let multiple_paths = opt_s || opt_a; let opt_zero = matches.is_present(options::ZERO);
let multiple_paths = opt_suffix || opt_multiple;
// too many arguments // too many arguments
if !multiple_paths && matches.free.len() > 2 { if !multiple_paths && matches.occurrences_of(options::NAME) > 2 {
crash!( crash!(
1, 1,
"{0}: extra operand '{1}'\nTry '{0} --help' for more information.", "extra operand '{1}'\nTry '{0} --help' for more information.",
NAME, executable!(),
matches.free[2] matches.values_of(options::NAME).unwrap().nth(2).unwrap()
); );
} }
let suffix = if opt_s { let suffix = if opt_suffix {
matches.opt_str("s").unwrap() matches.value_of(options::SUFFIX).unwrap()
} else if !opt_a && matches.free.len() > 1 { } else if !opt_multiple && matches.occurrences_of(options::NAME) > 1 {
matches.free[1].clone() matches.values_of(options::NAME).unwrap().nth(1).unwrap()
} else { } else {
"".to_owned() ""
}; };
// //
// Main Program Processing // Main Program Processing
// //
let paths = if multiple_paths { let paths: Vec<_> = if multiple_paths {
&matches.free[..] matches.values_of(options::NAME).unwrap().collect()
} else { } else {
&matches.free[0..1] matches.values_of(options::NAME).unwrap().take(1).collect()
}; };
let line_ending = if opt_z { "\0" } else { "\n" }; let line_ending = if opt_zero { "\0" } else { "\n" };
for path in paths { for path in paths {
print!("{}{}", basename(&path, &suffix), line_ending); print!("{}{}", basename(&path, &suffix), line_ending);
} }

View file

@ -81,7 +81,7 @@ fn copy_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<
let mut buf = [0; BUF_SIZE]; let mut buf = [0; BUF_SIZE];
loop { loop {
let read = unistd::read(read_fd, &mut buf[..left])?; let read = unistd::read(read_fd, &mut buf[..left])?;
let written = unistd::write(write_fd, &mut buf[..read])?; let written = unistd::write(write_fd, &buf[..read])?;
left -= written; left -= written;
if left == 0 { if left == 0 {
break; break;

View file

@ -551,6 +551,10 @@ impl FromStr for Attribute {
fn add_all_attributes() -> Vec<Attribute> { fn add_all_attributes() -> Vec<Attribute> {
use Attribute::*; use Attribute::*;
#[cfg(target_os = "windows")]
let attr = vec![Ownership, Timestamps, Context, Xattr, Links];
#[cfg(not(target_os = "windows"))]
let mut attr = vec![Ownership, Timestamps, Context, Xattr, Links]; let mut attr = vec![Ownership, Timestamps, Context, Xattr, Links];
#[cfg(unix)] #[cfg(unix)]

View file

@ -348,7 +348,7 @@ fn set_system_datetime(_date: DateTime<Utc>) -> i32 {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
fn set_system_datetime(_date: DateTime<Utc>) -> i32 { fn set_system_datetime(_date: DateTime<Utc>) -> i32 {
eprintln!("date: setting the date is not supported by macOS"); eprintln!("date: setting the date is not supported by macOS");
return 1; 1
} }
#[cfg(all(unix, not(target_os = "macos")))] #[cfg(all(unix, not(target_os = "macos")))]

View file

@ -916,6 +916,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
"Use%", "Use%",
] ]
}); });
if cfg!(target_os = "macos") && !opt.show_inode_instead {
header.insert(header.len() - 1, "Capacity");
}
header.push("Mounted on"); header.push("Mounted on");
for (idx, title) in header.iter().enumerate() { for (idx, title) in header.iter().enumerate() {
@ -970,6 +973,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
"{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") {
let used = fs.usage.blocks - fs.usage.bfree;
let blocks = used + fs.usage.bavail;
print!("{0: >12} ", use_size(used, blocks));
}
print!("{0: >5} ", use_size(free_size, total_size)); print!("{0: >5} ", use_size(free_size, total_size));
} }
print!("{0: <16}", fs.mountinfo.mount_dir); print!("{0: <16}", fs.mountinfo.mount_dir);

View file

@ -12,37 +12,42 @@ use clap::{App, Arg};
use std::path::Path; use std::path::Path;
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
static NAME: &str = "dirname"; static ABOUT: &str = "strip last component from file name";
static SYNTAX: &str = "[OPTION] NAME...";
static SUMMARY: &str = "strip last component from file name";
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static LONG_HELP: &str = "
Output each NAME with its last non-slash component and trailing slashes
removed; if NAME contains no /'s, output '.' (meaning the current
directory).
";
mod options { mod options {
pub const ZERO: &str = "zero"; pub const ZERO: &str = "zero";
pub const DIR: &str = "dir"; pub const DIR: &str = "dir";
} }
fn get_usage() -> String {
format!("{0} [OPTION] NAME...", executable!())
}
fn get_long_usage() -> String {
String::from(
"Output each NAME with its last non-slash component and trailing slashes
removed; if NAME contains no /'s, output '.' (meaning the current directory).",
)
}
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
let args = args let args = args
.collect_str(InvalidEncodingHandling::ConvertLossy) .collect_str(InvalidEncodingHandling::ConvertLossy)
.accept_any(); .accept_any();
let usage = get_usage();
let after_help = get_long_usage();
let matches = App::new(executable!()) let matches = App::new(executable!())
.name(NAME) .about(ABOUT)
.usage(SYNTAX) .usage(&usage[..])
.about(SUMMARY) .after_help(&after_help[..])
.after_help(LONG_HELP)
.version(VERSION) .version(VERSION)
.arg( .arg(
Arg::with_name(options::ZERO) Arg::with_name(options::ZERO)
.short(options::ZERO) .long(options::ZERO)
.short("z") .short("z")
.takes_value(false)
.help("separate output with NUL rather than newline"), .help("separate output with NUL rather than newline"),
) )
.arg(Arg::with_name(options::DIR).hidden(true).multiple(true)) .arg(Arg::with_name(options::DIR).hidden(true).multiple(true))

View file

@ -125,7 +125,7 @@ fn _factor<A: Arithmetic + miller_rabin::Basis>(num: u64, f: Factors) -> Factors
let n = A::new(num); let n = A::new(num);
let divisor = match miller_rabin::test::<A>(n) { let divisor = match miller_rabin::test::<A>(n) {
Prime => { Prime => {
#[cfg(feature="coz")] #[cfg(feature = "coz")]
coz::progress!("factor found"); coz::progress!("factor found");
let mut r = f; let mut r = f;
r.push(num); r.push(num);
@ -141,7 +141,7 @@ fn _factor<A: Arithmetic + miller_rabin::Basis>(num: u64, f: Factors) -> Factors
} }
pub fn factor(mut n: u64) -> Factors { pub fn factor(mut n: u64) -> Factors {
#[cfg(feature="coz")] #[cfg(feature = "coz")]
coz::begin!("factorization"); coz::begin!("factorization");
let mut factors = Factors::one(); let mut factors = Factors::one();
@ -156,7 +156,7 @@ pub fn factor(mut n: u64) -> Factors {
} }
if n == 1 { if n == 1 {
#[cfg(feature="coz")] #[cfg(feature = "coz")]
coz::end!("factorization"); coz::end!("factorization");
return factors; return factors;
} }
@ -169,10 +169,10 @@ pub fn factor(mut n: u64) -> Factors {
_factor::<Montgomery<u64>>(n, factors) _factor::<Montgomery<u64>>(n, factors)
}; };
#[cfg(feature="coz")] #[cfg(feature = "coz")]
coz::end!("factorization"); coz::end!("factorization");
return r; r
} }
#[cfg(test)] #[cfg(test)]

View file

@ -33,7 +33,7 @@ pub(crate) fn factor(mut num: u64, mut factors: Factors) -> (Factors, u64) {
if x <= ceil { if x <= ceil {
num = x; num = x;
k += 1; k += 1;
#[cfg(feature="coz")] #[cfg(feature = "coz")]
coz::progress!("factor found"); coz::progress!("factor found");
} else { } else {
if k > 0 { if k > 0 {

View file

@ -296,7 +296,7 @@ fn find_kp_breakpoints<'a, T: Iterator<Item = &'a WordInfo<'a>>>(
(0, 0.0) (0, 0.0)
} else { } else {
compute_demerits( compute_demerits(
(args.opts.goal - tlen) as isize, args.opts.goal as isize - tlen as isize,
stretch, stretch,
w.word_nchars as isize, w.word_nchars as isize,
active.prev_rat, active.prev_rat,

View file

@ -15,6 +15,7 @@ edition = "2018"
path = "src/kill.rs" path = "src/kill.rs"
[dependencies] [dependencies]
clap = "2.33"
libc = "0.2.42" libc = "0.2.42"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["signals"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["signals"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }

View file

@ -10,18 +10,26 @@
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use clap::{App, Arg};
use libc::{c_int, pid_t}; use libc::{c_int, pid_t};
use std::io::Error; use std::io::Error;
use uucore::signals::ALL_SIGNALS; use uucore::signals::ALL_SIGNALS;
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
static SYNTAX: &str = "[options] <pid> [...]"; static VERSION: &str = env!("CARGO_PKG_VERSION");
static SUMMARY: &str = ""; static ABOUT: &str = "Send signal to processes or list information about signals.";
static LONG_HELP: &str = "";
static EXIT_OK: i32 = 0; static EXIT_OK: i32 = 0;
static EXIT_ERR: i32 = 1; static EXIT_ERR: i32 = 1;
pub mod options {
pub static PIDS_OR_SIGNALS: &str = "pids_of_signals";
pub static LIST: &str = "list";
pub static TABLE: &str = "table";
pub static TABLE_OLD: &str = "table_old";
pub static SIGNAL: &str = "signal";
}
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum Mode { pub enum Mode {
Kill, Kill,
@ -33,41 +41,70 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let args = args let args = args
.collect_str(InvalidEncodingHandling::Ignore) .collect_str(InvalidEncodingHandling::Ignore)
.accept_any(); .accept_any();
let (args, obs_signal) = handle_obsolete(args); let (args, obs_signal) = handle_obsolete(args);
let matches = app!(SYNTAX, SUMMARY, LONG_HELP)
.optopt("s", "signal", "specify the <signal> to be sent", "SIGNAL")
.optflagopt(
"l",
"list",
"list all signal names, or convert one to a name",
"LIST",
)
.optflag("L", "table", "list all signal names in a nice table")
.parse(args);
let mode = if matches.opt_present("table") { let usage = format!("{} [OPTIONS]... PID...", executable!());
let matches = App::new(executable!())
.version(VERSION)
.about(ABOUT)
.usage(&usage[..])
.arg(
Arg::with_name(options::LIST)
.short("l")
.long(options::LIST)
.help("Lists signals")
.conflicts_with(options::TABLE)
.conflicts_with(options::TABLE_OLD),
)
.arg(
Arg::with_name(options::TABLE)
.short("t")
.long(options::TABLE)
.help("Lists table of signals"),
)
.arg(Arg::with_name(options::TABLE_OLD).short("L").hidden(true))
.arg(
Arg::with_name(options::SIGNAL)
.short("s")
.long(options::SIGNAL)
.help("Sends given signal")
.takes_value(true),
)
.arg(
Arg::with_name(options::PIDS_OR_SIGNALS)
.hidden(true)
.multiple(true),
)
.get_matches_from(args);
let mode = if matches.is_present(options::TABLE) || matches.is_present(options::TABLE_OLD) {
Mode::Table Mode::Table
} else if matches.opt_present("list") { } else if matches.is_present(options::LIST) {
Mode::List Mode::List
} else { } else {
Mode::Kill Mode::Kill
}; };
let pids_or_signals: Vec<String> = matches
.values_of(options::PIDS_OR_SIGNALS)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();
match mode { match mode {
Mode::Kill => { Mode::Kill => {
return kill( let sig = match (obs_signal, matches.value_of(options::SIGNAL)) {
&matches (Some(s), Some(_)) => s, // -s takes precedence
.opt_str("signal") (Some(s), None) => s,
.unwrap_or_else(|| obs_signal.unwrap_or_else(|| "9".to_owned())), (None, Some(s)) => s.to_owned(),
matches.free, (None, None) => "TERM".to_owned(),
) };
return kill(&sig, &pids_or_signals);
} }
Mode::Table => table(), Mode::Table => table(),
Mode::List => list(matches.opt_str("list")), Mode::List => list(pids_or_signals.get(0).cloned()),
} }
0 EXIT_OK
} }
fn handle_obsolete(mut args: Vec<String>) -> (Vec<String>, Option<String>) { fn handle_obsolete(mut args: Vec<String>) -> (Vec<String>, Option<String>) {
@ -148,14 +185,14 @@ fn list(arg: Option<String>) {
}; };
} }
fn kill(signalname: &str, pids: std::vec::Vec<String>) -> i32 { fn kill(signalname: &str, pids: &[String]) -> i32 {
let mut status = 0; let mut status = 0;
let optional_signal_value = uucore::signals::signal_by_name_or_value(signalname); let optional_signal_value = uucore::signals::signal_by_name_or_value(signalname);
let signal_value = match optional_signal_value { let signal_value = match optional_signal_value {
Some(x) => x, Some(x) => x,
None => crash!(EXIT_ERR, "unknown signal name {}", signalname), None => crash!(EXIT_ERR, "unknown signal name {}", signalname),
}; };
for pid in &pids { for pid in pids {
match pid.parse::<usize>() { match pid.parse::<usize>() {
Ok(x) => { Ok(x) => {
if unsafe { libc::kill(x as pid_t, signal_value as c_int) } != 0 { if unsafe { libc::kill(x as pid_t, signal_value as c_int) } != 0 {

View file

@ -18,6 +18,7 @@ path = "src/link.rs"
libc = "0.2.42" libc = "0.2.42"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
clap = "2.33"
[[bin]] [[bin]]
name = "link" name = "link"

View file

@ -8,14 +8,21 @@
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use clap::{App, Arg};
use std::fs::hard_link; use std::fs::hard_link;
use std::io::Error; use std::io::Error;
use std::path::Path; use std::path::Path;
use uucore::InvalidEncodingHandling;
static SYNTAX: &str = "[OPTIONS] FILE1 FILE2"; static VERSION: &str = env!("CARGO_PKG_VERSION");
static SUMMARY: &str = "Create a link named FILE2 to FILE1"; static ABOUT: &str = "Call the link function to create a link named FILE2 to an existing FILE1.";
static LONG_HELP: &str = "";
pub mod options {
pub static FILES: &str = "FILES";
}
fn get_usage() -> String {
format!("{0} FILE1 FILE2", executable!())
}
pub fn normalize_error_message(e: Error) -> String { pub fn normalize_error_message(e: Error) -> String {
match e.raw_os_error() { match e.raw_os_error() {
@ -25,16 +32,27 @@ pub fn normalize_error_message(e: Error) -> String {
} }
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
let matches = app!(SYNTAX, SUMMARY, LONG_HELP).parse( let usage = get_usage();
args.collect_str(InvalidEncodingHandling::Ignore) let matches = App::new(executable!())
.accept_any(), .version(VERSION)
); .about(ABOUT)
if matches.free.len() != 2 { .usage(&usage[..])
crash!(1, "{}", msg_wrong_number_of_arguments!(2)); .arg(
} Arg::with_name(options::FILES)
.hidden(true)
.required(true)
.min_values(2)
.max_values(2)
.takes_value(true),
)
.get_matches_from(args);
let old = Path::new(&matches.free[0]); let files: Vec<_> = matches
let new = Path::new(&matches.free[1]); .values_of_os(options::FILES)
.unwrap_or_default()
.collect();
let old = Path::new(files[0]);
let new = Path::new(files[1]);
match hard_link(old, new) { match hard_link(old, new) {
Ok(_) => 0, Ok(_) => 0,

View file

@ -16,6 +16,7 @@ path = "src/logname.rs"
[dependencies] [dependencies]
libc = "0.2.42" libc = "0.2.42"
clap = "2.33"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore = { version=">=0.0.8", package="uucore", path="../../uucore" }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }

View file

@ -15,6 +15,8 @@ extern crate uucore;
use std::ffi::CStr; use std::ffi::CStr;
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
use clap::App;
extern "C" { extern "C" {
// POSIX requires using getlogin (or equivalent code) // POSIX requires using getlogin (or equivalent code)
pub fn getlogin() -> *const libc::c_char; pub fn getlogin() -> *const libc::c_char;
@ -31,15 +33,24 @@ fn get_userlogin() -> Option<String> {
} }
} }
static SYNTAX: &str = "";
static SUMMARY: &str = "Print user's login name"; static SUMMARY: &str = "Print user's login name";
static LONG_HELP: &str = ""; static VERSION: &str = env!("CARGO_PKG_VERSION");
fn get_usage() -> String {
String::from(executable!())
}
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
app!(SYNTAX, SUMMARY, LONG_HELP).parse( let args = args
args.collect_str(InvalidEncodingHandling::ConvertLossy) .collect_str(InvalidEncodingHandling::Ignore)
.accept_any(), .accept_any();
);
let usage = get_usage();
let _ = App::new(executable!())
.version(VERSION)
.about(SUMMARY)
.usage(&usage[..])
.get_matches_from(args);
match get_userlogin() { match get_userlogin() {
Some(userlogin) => println!("{}", userlogin), Some(userlogin) => println!("{}", userlogin),

View file

@ -36,6 +36,8 @@ args="$@"
hyperfine "ls $args" "target/release/coreutils ls $args" hyperfine "ls $args" "target/release/coreutils ls $args"
``` ```
**Note**: No localization is currently implemented. This means that the comparison above is not really fair. We can fix this by setting `LC_ALL=C`, so GNU `ls` can ignore localization.
## Checking system call count ## Checking system call count
- Another thing to look at would be system calls count using strace (on linux) or equivalent on other operating systems. - Another thing to look at would be system calls count using strace (on linux) or equivalent on other operating systems.

View file

@ -39,8 +39,6 @@ use std::{
time::Duration, time::Duration,
}; };
use chrono;
use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@ -1122,14 +1120,21 @@ impl PathData {
fn new( fn new(
p_buf: PathBuf, p_buf: PathBuf,
file_type: Option<std::io::Result<FileType>>, file_type: Option<std::io::Result<FileType>>,
file_name: Option<String>,
config: &Config, config: &Config,
command_line: bool, command_line: bool,
) -> Self { ) -> Self {
let name = p_buf // We cannot use `Path::ends_with` or `Path::Components`, because they remove occurrences of '.'
.file_name() // For '..', the filename is None
.unwrap_or_else(|| p_buf.iter().next_back().unwrap()) let name = if let Some(name) = file_name {
.to_string_lossy() name
.into_owned(); } else {
p_buf
.file_name()
.unwrap_or_else(|| p_buf.iter().next_back().unwrap())
.to_string_lossy()
.into_owned()
};
let must_dereference = match &config.dereference { let must_dereference = match &config.dereference {
Dereference::All => true, Dereference::All => true,
Dereference::Args => command_line, Dereference::Args => command_line,
@ -1174,31 +1179,32 @@ impl PathData {
} }
fn list(locs: Vec<String>, config: Config) -> i32 { fn list(locs: Vec<String>, config: Config) -> i32 {
let number_of_locs = locs.len();
let mut files = Vec::<PathData>::new(); let mut files = Vec::<PathData>::new();
let mut dirs = Vec::<PathData>::new(); let mut dirs = Vec::<PathData>::new();
let mut has_failed = false; let mut has_failed = false;
let mut out = BufWriter::new(stdout()); let mut out = BufWriter::new(stdout());
for loc in locs { for loc in &locs {
let p = PathBuf::from(&loc); let p = PathBuf::from(&loc);
if !p.exists() { if !p.exists() {
show_error!("'{}': {}", &loc, "No such file or directory"); show_error!("'{}': {}", &loc, "No such file or directory");
// We found an error, the return code of ls should not be 0 /*
// And no need to continue the execution We found an error, the return code of ls should not be 0
And no need to continue the execution
*/
has_failed = true; has_failed = true;
continue; continue;
} }
let path_data = PathData::new(p, None, &config, true); let path_data = PathData::new(p, None, None, &config, true);
let show_dir_contents = if let Some(ft) = path_data.file_type() { let show_dir_contents = match path_data.file_type() {
!config.directory && ft.is_dir() Some(ft) => !config.directory && ft.is_dir(),
} else { None => {
has_failed = true; has_failed = true;
false false
}
}; };
if show_dir_contents { if show_dir_contents {
@ -1212,7 +1218,7 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
sort_entries(&mut dirs, &config); sort_entries(&mut dirs, &config);
for dir in dirs { for dir in dirs {
if number_of_locs > 1 { if locs.len() > 1 {
let _ = writeln!(out, "\n{}:", dir.p_buf.display()); let _ = writeln!(out, "\n{}:", dir.p_buf.display());
} }
enter_directory(&dir, &config, &mut out); enter_directory(&dir, &config, &mut out);
@ -1237,14 +1243,8 @@ fn sort_entries(entries: &mut Vec<PathData>, config: &Config) {
entries.sort_by_key(|k| Reverse(k.md().as_ref().map(|md| md.len()).unwrap_or(0))) entries.sort_by_key(|k| Reverse(k.md().as_ref().map(|md| md.len()).unwrap_or(0)))
} }
// The default sort in GNU ls is case insensitive // The default sort in GNU ls is case insensitive
Sort::Name => entries.sort_by_cached_key(|k| { Sort::Name => entries.sort_by(|a, b| a.file_name.cmp(&b.file_name)),
let has_dot: bool = k.file_name.starts_with('.'); Sort::Version => entries.sort_by(|a, b| version_cmp::version_cmp(&a.p_buf, &b.p_buf)),
let filename_nodot: &str = &k.file_name[if has_dot { 1 } else { 0 }..];
// We want hidden files to appear before regular files of the same
// name, so we need to negate the "has_dot" variable.
(filename_nodot.to_lowercase(), !has_dot)
}),
Sort::Version => entries.sort_by(|k, j| version_cmp::version_cmp(&k.p_buf, &j.p_buf)),
Sort::Extension => entries.sort_by(|a, b| { Sort::Extension => entries.sort_by(|a, b| {
a.p_buf a.p_buf
.extension() .extension()
@ -1283,8 +1283,14 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool {
fn enter_directory(dir: &PathData, config: &Config, out: &mut BufWriter<Stdout>) { fn enter_directory(dir: &PathData, config: &Config, out: &mut BufWriter<Stdout>) {
let mut entries: Vec<_> = if config.files == Files::All { let mut entries: Vec<_> = if config.files == Files::All {
vec![ vec![
PathData::new(dir.p_buf.join("."), None, config, false), PathData::new(
PathData::new(dir.p_buf.join(".."), None, config, false), dir.p_buf.clone(),
Some(Ok(*dir.file_type().unwrap())),
Some(".".into()),
config,
false,
),
PathData::new(dir.p_buf.join(".."), None, Some("..".into()), config, false),
] ]
} else { } else {
vec![] vec![]
@ -1293,7 +1299,7 @@ fn enter_directory(dir: &PathData, config: &Config, out: &mut BufWriter<Stdout>)
let mut temp: Vec<_> = safe_unwrap!(fs::read_dir(&dir.p_buf)) let mut temp: Vec<_> = safe_unwrap!(fs::read_dir(&dir.p_buf))
.map(|res| safe_unwrap!(res)) .map(|res| safe_unwrap!(res))
.filter(|e| should_display(e, config)) .filter(|e| should_display(e, config))
.map(|e| PathData::new(DirEntry::path(&e), Some(e.file_type()), config, false)) .map(|e| PathData::new(DirEntry::path(&e), Some(e.file_type()), None, config, false))
.collect(); .collect();
sort_entries(&mut temp, config); sort_entries(&mut temp, config);
@ -1326,7 +1332,7 @@ fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize) {
if let Some(md) = entry.md() { if let Some(md) = entry.md() {
( (
display_symlink_count(&md).len(), display_symlink_count(&md).len(),
display_file_size(&md, config).len(), display_size(md.len(), config).len(),
) )
} else { } else {
(0, 0) (0, 0)
@ -1339,14 +1345,22 @@ fn pad_left(string: String, count: usize) -> String {
fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout>) { fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout>) {
if config.format == Format::Long { if config.format == Format::Long {
let (mut max_links, mut max_size) = (1, 1); let (mut max_links, mut max_width) = (1, 1);
let mut total_size = 0;
for item in items { for item in items {
let (links, size) = display_dir_entry_size(item, config); let (links, width) = display_dir_entry_size(item, config);
max_links = links.max(max_links); max_links = links.max(max_links);
max_size = size.max(max_size); max_width = width.max(max_width);
total_size += item.md().map_or(0, |md| get_block_size(md, config));
} }
if total_size > 0 {
let _ = writeln!(out, "total {}", display_size(total_size, config));
}
for item in items { for item in items {
display_item_long(item, max_links, max_size, config, out); display_item_long(item, max_links, max_width, config, out);
} }
} else { } else {
let names = items.iter().filter_map(|i| display_file_name(&i, config)); let names = items.iter().filter_map(|i| display_file_name(&i, config));
@ -1391,6 +1405,29 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout
} }
} }
fn get_block_size(md: &Metadata, config: &Config) -> u64 {
/* GNU ls will display sizes in terms of block size
md.len() will differ from this value when the file has some holes
*/
#[cfg(unix)]
{
// hard-coded for now - enabling setting this remains a TODO
let ls_block_size = 1024;
return match config.size_format {
SizeFormat::Binary => md.blocks() * 512,
SizeFormat::Decimal => md.blocks() * 512,
SizeFormat::Bytes => md.blocks() * 512 / ls_block_size,
};
}
#[cfg(not(unix))]
{
let _ = config;
// no way to get block size for windows, fall-back to file size
md.len()
}
}
fn display_grid( fn display_grid(
names: impl Iterator<Item = Cell>, names: impl Iterator<Item = Cell>,
width: u16, width: u16,
@ -1466,7 +1503,7 @@ fn display_item_long(
let _ = writeln!( let _ = writeln!(
out, out,
" {} {} {}", " {} {} {}",
pad_left(display_file_size(&md, config), max_size), pad_left(display_size(md.len(), config), max_size),
display_date(&md, config), display_date(&md, config),
// unwrap is fine because it fails when metadata is not available // unwrap is fine because it fails when metadata is not available
// but we already know that it is because it's checked at the // but we already know that it is because it's checked at the
@ -1559,6 +1596,7 @@ fn get_system_time(md: &Metadata, config: &Config) -> Option<SystemTime> {
match config.time { match config.time {
Time::Modification => md.modified().ok(), Time::Modification => md.modified().ok(),
Time::Access => md.accessed().ok(), Time::Access => md.accessed().ok(),
Time::Birth => md.created().ok(),
_ => None, _ => None,
} }
} }
@ -1620,7 +1658,7 @@ fn format_prefixed(prefixed: NumberPrefix<f64>) -> String {
} }
} }
fn display_file_size(metadata: &Metadata, config: &Config) -> String { fn display_size(metadata: &Metadata, config: &Config) -> String {
#[cfg(unix)] #[cfg(unix)]
{ {
let ft = metadata.file_type(); let ft = metadata.file_type();
@ -1635,9 +1673,9 @@ fn display_file_size(metadata: &Metadata, config: &Config) -> String {
// NOTE: The human-readable behaviour deviates from the GNU ls. // NOTE: The human-readable behaviour deviates from the GNU ls.
// The GNU ls uses binary prefixes by default. // The GNU ls uses binary prefixes by default.
match config.size_format { match config.size_format {
SizeFormat::Binary => format_prefixed(NumberPrefix::binary(metadata.len() as f64)), SizeFormat::Binary => format_prefixed(NumberPrefix::binary(len as f64)),
SizeFormat::Decimal => format_prefixed(NumberPrefix::decimal(metadata.len() as f64)), SizeFormat::Decimal => format_prefixed(NumberPrefix::decimal(len as f64)),
SizeFormat::Bytes => metadata.len().to_string(), SizeFormat::Bytes => len.to_string(),
} }
} }

View file

@ -16,7 +16,7 @@ name = "uu_mknod"
path = "src/mknod.rs" path = "src/mknod.rs"
[dependencies] [dependencies]
getopts = "0.2.18" clap = "2.33"
libc = "^0.2.42" libc = "^0.2.42"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["mode"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["mode"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }

View file

@ -5,21 +5,41 @@
// 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) parsemode makedev sysmacros makenod newmode perror IFBLK IFCHR IFIFO // spell-checker:ignore (ToDO) parsemode makedev sysmacros perror IFBLK IFCHR IFIFO
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use std::ffi::CString;
use clap::{App, Arg, ArgMatches};
use libc::{dev_t, mode_t}; use libc::{dev_t, mode_t};
use libc::{S_IFBLK, S_IFCHR, S_IFIFO, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR}; use libc::{S_IFBLK, S_IFCHR, S_IFIFO, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR};
use getopts::Options;
use std::ffi::CString;
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
static NAME: &str = "mknod"; static NAME: &str = "mknod";
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static ABOUT: &str = "Create the special file NAME of the given TYPE.";
static USAGE: &str = "mknod [OPTION]... NAME TYPE [MAJOR MINOR]";
static LONG_HELP: &str = "Mandatory arguments to long options are mandatory for short options too.
-m, --mode=MODE set file permission bits to MODE, not a=rw - umask
--help display this help and exit
--version output version information and exit
Both MAJOR and MINOR must be specified when TYPE is b, c, or u, and they
must be omitted when TYPE is p. If MAJOR or MINOR begins with 0x or 0X,
it is interpreted as hexadecimal; otherwise, if it begins with 0, as octal;
otherwise, as decimal. TYPE may be:
b create a block (buffered) special file
c, u create a character (unbuffered) special file
p create a FIFO
NOTE: your shell may have its own version of mknod, which usually supersedes
the version described here. Please refer to your shell's documentation
for details about the options it supports.
";
const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
@ -30,13 +50,35 @@ fn makedev(maj: u64, min: u64) -> dev_t {
} }
#[cfg(windows)] #[cfg(windows)]
fn _makenod(path: CString, mode: mode_t, dev: dev_t) -> i32 { fn _makenod(file_name: &str, mode: mode_t, dev: dev_t) -> i32 {
panic!("Unsupported for windows platform") panic!("Unsupported for windows platform")
} }
#[cfg(unix)] #[cfg(unix)]
fn _makenod(path: CString, mode: mode_t, dev: dev_t) -> i32 { fn _makenod(file_name: &str, mode: mode_t, dev: dev_t) -> i32 {
unsafe { libc::mknod(path.as_ptr(), mode, dev) } let c_str = CString::new(file_name).expect("Failed to convert to CString");
// the user supplied a mode
let set_umask = mode & MODE_RW_UGO != MODE_RW_UGO;
unsafe {
// store prev umask
let last_umask = if set_umask { libc::umask(0) } else { 0 };
let errno = libc::mknod(c_str.as_ptr(), mode, dev);
// set umask back to original value
if set_umask {
libc::umask(last_umask);
}
if errno == -1 {
let c_str = CString::new(NAME).expect("Failed to convert to CString");
// shows the error from the mknod syscall
libc::perror(c_str.as_ptr());
}
errno
}
} }
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
@ -44,156 +86,136 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let args = args let args = args
.collect_str(InvalidEncodingHandling::Ignore) .collect_str(InvalidEncodingHandling::Ignore)
.accept_any(); .accept_any();
let mut opts = Options::new();
// Linux-specific options, not implemented // Linux-specific options, not implemented
// opts.optflag("Z", "", "set the SELinux security context to default type"); // opts.optflag("Z", "", "set the SELinux security context to default type");
// opts.optopt("", "context", "like -Z, or if CTX is specified then set the SELinux or SMACK security context to CTX"); // opts.optopt("", "context", "like -Z, or if CTX is specified then set the SELinux or SMACK security context to CTX");
opts.optopt(
"m",
"mode",
"set file permission bits to MODE, not a=rw - umask",
"MODE",
);
opts.optflag("", "help", "display this help and exit"); let matches = App::new(executable!())
opts.optflag("", "version", "output version information and exit"); .version(VERSION)
.usage(USAGE)
.after_help(LONG_HELP)
.about(ABOUT)
.arg(
Arg::with_name("mode")
.short("m")
.long("mode")
.value_name("MODE")
.help("set file permission bits to MODE, not a=rw - umask"),
)
.arg(
Arg::with_name("name")
.value_name("NAME")
.help("name of the new file")
.required(true)
.index(1),
)
.arg(
Arg::with_name("type")
.value_name("TYPE")
.help("type of the new file (b, c, u or p)")
.required(true)
.validator(valid_type)
.index(2),
)
.arg(
Arg::with_name("major")
.value_name("MAJOR")
.help("major file type")
.validator(valid_u64)
.index(3),
)
.arg(
Arg::with_name("minor")
.value_name("MINOR")
.help("minor file type")
.validator(valid_u64)
.index(4),
)
.get_matches_from(args);
let matches = match opts.parse(&args[1..]) { let mode = match get_mode(&matches) {
Ok(m) => m, Ok(mode) => mode,
Err(f) => crash!(1, "{}\nTry '{} --help' for more information.", f, NAME), Err(err) => {
show_info!("{}", err);
return 1;
}
}; };
if matches.opt_present("help") { let file_name = matches.value_of("name").expect("Missing argument 'NAME'");
println!(
"Usage: {0} [OPTION]... NAME TYPE [MAJOR MINOR]
Mandatory arguments to long options are mandatory for short options too. // Only check the first character, to allow mnemonic usage like
-m, --mode=MODE set file permission bits to MODE, not a=rw - umask // 'mknod /dev/rst0 character 18 0'.
--help display this help and exit let ch = matches
--version output version information and exit .value_of("type")
.expect("Missing argument 'TYPE'")
.chars()
.next()
.expect("Failed to get the first char");
Both MAJOR and MINOR must be specified when TYPE is b, c, or u, and they if ch == 'p' {
must be omitted when TYPE is p. If MAJOR or MINOR begins with 0x or 0X, if matches.is_present("major") || matches.is_present("minor") {
it is interpreted as hexadecimal; otherwise, if it begins with 0, as octal; eprintln!("Fifos do not have major and minor device numbers.");
otherwise, as decimal. TYPE may be: eprintln!("Try '{} --help' for more information.", NAME);
1
b create a block (buffered) special file } else {
c, u create a character (unbuffered) special file _makenod(file_name, S_IFIFO | mode, 0)
p create a FIFO
NOTE: your shell may have its own version of mknod, which usually supersedes
the version described here. Please refer to your shell's documentation
for details about the options it supports.",
NAME
);
return 0;
}
if matches.opt_present("version") {
println!("{} {}", NAME, VERSION);
return 0;
}
let mut last_umask: mode_t = 0;
let mut newmode: mode_t = MODE_RW_UGO;
if matches.opt_present("mode") {
match uucore::mode::parse_mode(matches.opt_str("mode")) {
Ok(parsed) => {
if parsed > 0o777 {
show_info!("mode must specify only file permission bits");
return 1;
}
newmode = parsed;
}
Err(e) => {
show_info!("{}", e);
return 1;
}
} }
unsafe { } else {
last_umask = libc::umask(0); match (matches.value_of("major"), matches.value_of("minor")) {
} (None, None) | (_, None) | (None, _) => {
} eprintln!("Special files require major and minor device numbers.");
eprintln!("Try '{} --help' for more information.", NAME);
1
}
(Some(major), Some(minor)) => {
let major = major.parse::<u64>().expect("validated by clap");
let minor = minor.parse::<u64>().expect("validated by clap");
let mut ret = 0i32; let dev = makedev(major, minor);
match matches.free.len() {
0 => show_usage_error!("missing operand"),
1 => show_usage_error!("missing operand after {}", matches.free[0]),
_ => {
let args = &matches.free;
let c_str = CString::new(args[0].as_str()).expect("Failed to convert to CString");
// Only check the first character, to allow mnemonic usage like
// 'mknod /dev/rst0 character 18 0'.
let ch = args[1]
.chars()
.next()
.expect("Failed to get the first char");
if ch == 'p' {
if args.len() > 2 {
show_info!("{}: extra operand {}", NAME, args[2]);
if args.len() == 4 {
eprintln!("Fifos do not have major and minor device numbers.");
}
eprintln!("Try '{} --help' for more information.", NAME);
return 1;
}
ret = _makenod(c_str, S_IFIFO | newmode, 0);
} else {
if args.len() < 4 {
show_info!("missing operand after {}", args[args.len() - 1]);
if args.len() == 2 {
eprintln!("Special files require major and minor device numbers.");
}
eprintln!("Try '{} --help' for more information.", NAME);
return 1;
} else if args.len() > 4 {
show_usage_error!("extra operand {}", args[4]);
return 1;
} else if !"bcu".contains(ch) {
show_usage_error!("invalid device type {}", args[1]);
return 1;
}
let maj = args[2].parse::<u64>();
let min = args[3].parse::<u64>();
if maj.is_err() {
show_info!("invalid major device number {}", args[2]);
return 1;
} else if min.is_err() {
show_info!("invalid minor device number {}", args[3]);
return 1;
}
let (maj, min) = (maj.unwrap(), min.unwrap());
let dev = makedev(maj, min);
if ch == 'b' { if ch == 'b' {
// block special file // block special file
ret = _makenod(c_str, S_IFBLK | newmode, dev); _makenod(file_name, S_IFBLK | mode, dev)
} else { } else if ch == 'c' || ch == 'u' {
// char special file // char special file
ret = _makenod(c_str, S_IFCHR | newmode, dev); _makenod(file_name, S_IFCHR | mode, dev)
} else {
unreachable!("{} was validated to be only b, c or u", ch);
} }
} }
} }
} }
}
if last_umask != 0 {
unsafe { fn get_mode(matches: &ArgMatches) -> Result<mode_t, String> {
libc::umask(last_umask); match matches.value_of("mode") {
} None => Ok(MODE_RW_UGO),
} Some(str_mode) => uucore::mode::parse_mode(str_mode)
if ret == -1 { .map_err(|e| format!("invalid mode ({})", e))
let c_str = CString::new(format!("{}: {}", NAME, matches.free[0]).as_str()) .and_then(|mode| {
.expect("Failed to convert to CString"); if mode > 0o777 {
unsafe { Err("mode must specify only file permission bits".to_string())
libc::perror(c_str.as_ptr()); } else {
} Ok(mode)
} }
}),
ret }
}
fn valid_type(tpe: String) -> Result<(), String> {
// Only check the first character, to allow mnemonic usage like
// 'mknod /dev/rst0 character 18 0'.
tpe.chars()
.next()
.ok_or_else(|| "missing device type".to_string())
.and_then(|first_char| {
if vec!['b', 'c', 'u', 'p'].contains(&first_char) {
Ok(())
} else {
Err(format!("invalid device type {}", tpe))
}
})
}
fn valid_u64(num: String) -> Result<(), String> {
num.parse::<u64>().map(|_| ()).map_err(|_| num)
} }

View file

@ -0,0 +1,54 @@
// spell-checker:ignore (ToDO) fperm
use libc::{mode_t, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR};
use uucore::mode;
pub const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
pub fn parse_mode(mode: &str) -> Result<mode_t, String> {
let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
let result = if mode.contains(arr) {
mode::parse_numeric(MODE_RW_UGO as u32, mode)
} else {
mode::parse_symbolic(MODE_RW_UGO as u32, mode, true)
};
result.map(|mode| mode as mode_t)
}
#[cfg(test)]
mod test {
/// Test if the program is running under WSL
// ref: <https://github.com/microsoft/WSL/issues/4555> @@ <https://archive.is/dP0bz>
// ToDO: test on WSL2 which likely doesn't need special handling; plan change to `is_wsl_1()` if WSL2 is less needy
pub fn is_wsl() -> bool {
#[cfg(target_os = "linux")]
{
if let Ok(b) = std::fs::read("/proc/sys/kernel/osrelease") {
if let Ok(s) = std::str::from_utf8(&b) {
let a = s.to_ascii_lowercase();
return a.contains("microsoft") || a.contains("wsl");
}
}
}
false
}
#[test]
fn symbolic_modes() {
assert_eq!(super::parse_mode("u+x").unwrap(), 0o766);
assert_eq!(
super::parse_mode("+x").unwrap(),
if !is_wsl() { 0o777 } else { 0o776 }
);
assert_eq!(super::parse_mode("a-w").unwrap(), 0o444);
assert_eq!(super::parse_mode("g-r").unwrap(), 0o626);
}
#[test]
fn numeric_modes() {
assert_eq!(super::parse_mode("644").unwrap(), 0o644);
assert_eq!(super::parse_mode("+100").unwrap(), 0o766);
assert_eq!(super::parse_mode("-4").unwrap(), 0o662);
}
}

View file

@ -17,6 +17,7 @@ path = "src/pinky.rs"
[dependencies] [dependencies]
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["utmpx", "entries"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["utmpx", "entries"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
clap = "2.33.3"
[[bin]] [[bin]]
name = "pinky" name = "pinky"

View file

@ -19,67 +19,110 @@ use std::io::BufReader;
use std::fs::File; use std::fs::File;
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use clap::{App, Arg};
use std::path::PathBuf; use std::path::PathBuf;
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
static SYNTAX: &str = "[OPTION]... [USER]...";
static SUMMARY: &str = "A lightweight 'finger' program; print user information.";
const BUFSIZE: usize = 1024; const BUFSIZE: usize = 1024;
static VERSION: &str = env!("CARGO_PKG_VERSION");
static ABOUT: &str = "pinky - lightweight finger";
mod options {
pub const LONG_FORMAT: &str = "long_format";
pub const OMIT_HOME_DIR: &str = "omit_home_dir";
pub const OMIT_PROJECT_FILE: &str = "omit_project_file";
pub const OMIT_PLAN_FILE: &str = "omit_plan_file";
pub const SHORT_FORMAT: &str = "short_format";
pub const OMIT_HEADINGS: &str = "omit_headings";
pub const OMIT_NAME: &str = "omit_name";
pub const OMIT_NAME_HOST: &str = "omit_name_host";
pub const OMIT_NAME_HOST_TIME: &str = "omit_name_host_time";
pub const USER: &str = "user";
}
fn get_usage() -> String {
format!("{0} [OPTION]... [USER]...", executable!())
}
fn get_long_usage() -> String {
format!(
"A lightweight 'finger' program; print user information.\n\
The utmp file will be {}.",
utmpx::DEFAULT_FILE
)
}
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
let args = args let args = args
.collect_str(InvalidEncodingHandling::Ignore) .collect_str(InvalidEncodingHandling::Ignore)
.accept_any(); .accept_any();
let long_help = &format!( let usage = get_usage();
" let after_help = get_long_usage();
-l produce long format output for the specified USERs
-b omit the user's home directory and shell in long format
-h omit the user's project file in long format
-p omit the user's plan file in long format
-s do short format output, this is the default
-f omit the line of column headings in short format
-w omit the user's full name in short format
-i omit the user's full name and remote host in short format
-q omit the user's full name, remote host and idle time
in short format
--help display this help and exit
--version output version information and exit
The utmp file will be {}", let matches = App::new(executable!())
utmpx::DEFAULT_FILE .version(VERSION)
); .about(ABOUT)
let mut opts = app!(SYNTAX, SUMMARY, &long_help); .usage(&usage[..])
opts.optflag( .after_help(&after_help[..])
"l", .arg(
"", Arg::with_name(options::LONG_FORMAT)
"produce long format output for the specified USERs", .short("l")
); .requires(options::USER)
opts.optflag( .help("produce long format output for the specified USERs"),
"b", )
"", .arg(
"omit the user's home directory and shell in long format", Arg::with_name(options::OMIT_HOME_DIR)
); .short("b")
opts.optflag("h", "", "omit the user's project file in long format"); .help("omit the user's home directory and shell in long format"),
opts.optflag("p", "", "omit the user's plan file in long format"); )
opts.optflag("s", "", "do short format output, this is the default"); .arg(
opts.optflag("f", "", "omit the line of column headings in short format"); Arg::with_name(options::OMIT_PROJECT_FILE)
opts.optflag("w", "", "omit the user's full name in short format"); .short("h")
opts.optflag( .help("omit the user's project file in long format"),
"i", )
"", .arg(
"omit the user's full name and remote host in short format", Arg::with_name(options::OMIT_PLAN_FILE)
); .short("p")
opts.optflag( .help("omit the user's plan file in long format"),
"q", )
"", .arg(
"omit the user's full name, remote host and idle time in short format", Arg::with_name(options::SHORT_FORMAT)
); .short("s")
opts.optflag("", "help", "display this help and exit"); .help("do short format output, this is the default"),
opts.optflag("", "version", "output version information and exit"); )
.arg(
Arg::with_name(options::OMIT_HEADINGS)
.short("f")
.help("omit the line of column headings in short format"),
)
.arg(
Arg::with_name(options::OMIT_NAME)
.short("w")
.help("omit the user's full name in short format"),
)
.arg(
Arg::with_name(options::OMIT_NAME_HOST)
.short("i")
.help("omit the user's full name and remote host in short format"),
)
.arg(
Arg::with_name(options::OMIT_NAME_HOST_TIME)
.short("q")
.help("omit the user's full name, remote host and idle time in short format"),
)
.arg(
Arg::with_name(options::USER)
.takes_value(true)
.multiple(true),
)
.get_matches_from(args);
let matches = opts.parse(args); let users: Vec<String> = matches
.values_of(options::USER)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();
// If true, display the hours:minutes since each user has touched // If true, display the hours:minutes since each user has touched
// the keyboard, or blank if within the last minute, or days followed // the keyboard, or blank if within the last minute, or days followed
@ -87,45 +130,40 @@ The utmp file will be {}",
let mut include_idle = true; let mut include_idle = true;
// If true, display a line at the top describing each field. // If true, display a line at the top describing each field.
let include_heading = !matches.opt_present("f"); let include_heading = !matches.is_present(options::OMIT_HEADINGS);
// if true, display the user's full name from pw_gecos. // if true, display the user's full name from pw_gecos.
let mut include_fullname = true; let mut include_fullname = true;
// if true, display the user's ~/.project file when doing long format. // if true, display the user's ~/.project file when doing long format.
let include_project = !matches.opt_present("h"); let include_project = !matches.is_present(options::OMIT_PROJECT_FILE);
// if true, display the user's ~/.plan file when doing long format. // if true, display the user's ~/.plan file when doing long format.
let include_plan = !matches.opt_present("p"); let include_plan = !matches.is_present(options::OMIT_PLAN_FILE);
// if true, display the user's home directory and shell // if true, display the user's home directory and shell
// when doing long format. // when doing long format.
let include_home_and_shell = !matches.opt_present("b"); let include_home_and_shell = !matches.is_present(options::OMIT_HOME_DIR);
// if true, use the "short" output format. // if true, use the "short" output format.
let do_short_format = !matches.opt_present("l"); let do_short_format = !matches.is_present(options::LONG_FORMAT);
/* if true, display the ut_host field. */ /* if true, display the ut_host field. */
let mut include_where = true; let mut include_where = true;
if matches.opt_present("w") { if matches.is_present(options::OMIT_NAME) {
include_fullname = false; include_fullname = false;
} }
if matches.opt_present("i") { if matches.is_present(options::OMIT_NAME_HOST) {
include_fullname = false; include_fullname = false;
include_where = false; include_where = false;
} }
if matches.opt_present("q") { if matches.is_present(options::OMIT_NAME_HOST_TIME) {
include_fullname = false; include_fullname = false;
include_idle = false; include_idle = false;
include_where = false; include_where = false;
} }
if !do_short_format && matches.free.is_empty() {
show_usage_error!("no username specified; at least one must be specified when using -l");
return 1;
}
let pk = Pinky { let pk = Pinky {
include_idle, include_idle,
include_heading, include_heading,
@ -134,7 +172,7 @@ The utmp file will be {}",
include_plan, include_plan,
include_home_and_shell, include_home_and_shell,
include_where, include_where,
names: matches.free, names: users,
}; };
if do_short_format { if do_short_format {

View file

@ -158,7 +158,6 @@ pub fn base_conv_float(src: &[u8], radix_src: u8, _radix_dest: u8) -> f64 {
// to implement this for arbitrary string input. // to implement this for arbitrary string input.
// until then, the below operates as an outline // until then, the below operates as an outline
// of how it would work. // of how it would work.
let result: Vec<u8> = vec![0];
let mut factor: f64 = 1_f64; let mut factor: f64 = 1_f64;
let radix_src_float: f64 = f64::from(radix_src); let radix_src_float: f64 = f64::from(radix_src);
let mut r: f64 = 0_f64; let mut r: f64 = 0_f64;

View file

@ -69,6 +69,14 @@ Run `cargo build --release` before benchmarking after you make a change!
- Benchmark numeric sorting with hyperfine: `hyperfine "target/release/coreutils sort shuffled_numbers_si.txt -h -o output.txt"`. - Benchmark numeric sorting with hyperfine: `hyperfine "target/release/coreutils sort shuffled_numbers_si.txt -h -o output.txt"`.
## External sorting
Try running commands with the `-S` option set to an amount of memory to be used, such as `1M`. Additionally, you could try sorting
huge files (ideally multiple Gigabytes) with `-S`. Creating such a large file can be achieved by running `cat shuffled_wordlist.txt | sort -R >> shuffled_wordlist.txt`
multiple times (this will add the contents of `shuffled_wordlist.txt` to itself).
Example: Run `hyperfine './target/release/coreutils sort shuffled_wordlist.txt -S 1M' 'sort shuffled_wordlist.txt -S 1M'`
`
## Stdout and stdin performance ## Stdout and stdin performance
Try to run the above benchmarks by piping the input through stdin (standard input) and redirect the Try to run the above benchmarks by piping the input through stdin (standard input) and redirect the

View file

@ -15,15 +15,13 @@ edition = "2018"
path = "src/sort.rs" path = "src/sort.rs"
[dependencies] [dependencies]
serde_json = { version = "1.0.64", default-features = false, features = ["alloc"] }
serde = { version = "1.0", features = ["derive"] }
rayon = "1.5" rayon = "1.5"
rand = "0.7" rand = "0.7"
clap = "2.33" clap = "2.33"
fnv = "1.0.7" fnv = "1.0.7"
itertools = "0.10.0" itertools = "0.10.0"
semver = "0.9.0" semver = "0.9.0"
smallvec = { version="1.6.1", features=["serde"] } smallvec = "1.6.1"
unicode-width = "0.1.8" unicode-width = "0.1.8"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }

View file

@ -0,0 +1,64 @@
// * This file is part of the uutils coreutils package.
// *
// * (c) Michael Debertol <michael.debertol..AT..gmail.com>
// *
// * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code.
//! Custom string comparisons.
//!
//! The goal is to compare strings without transforming them first (i.e. not allocating new strings)
use std::cmp::Ordering;
fn filter_char(c: char, ignore_non_printing: bool, ignore_non_dictionary: bool) -> bool {
if ignore_non_dictionary && !(c.is_ascii_alphanumeric() || c.is_ascii_whitespace()) {
return false;
}
if ignore_non_printing && (c.is_ascii_control() || !c.is_ascii()) {
return false;
}
true
}
fn cmp_chars(a: char, b: char, ignore_case: bool) -> Ordering {
if ignore_case {
a.to_ascii_uppercase().cmp(&b.to_ascii_uppercase())
} else {
a.cmp(&b)
}
}
pub fn custom_str_cmp(
a: &str,
b: &str,
ignore_non_printing: bool,
ignore_non_dictionary: bool,
ignore_case: bool,
) -> Ordering {
if !(ignore_case || ignore_non_dictionary || ignore_non_printing) {
// There are no custom settings. Fall back to the default strcmp, which is faster.
return a.cmp(&b);
}
let mut a_chars = a
.chars()
.filter(|&c| filter_char(c, ignore_non_printing, ignore_non_dictionary));
let mut b_chars = b
.chars()
.filter(|&c| filter_char(c, ignore_non_printing, ignore_non_dictionary));
loop {
let a_char = a_chars.next();
let b_char = b_chars.next();
match (a_char, b_char) {
(None, None) => return Ordering::Equal,
(Some(_), None) => return Ordering::Greater,
(None, Some(_)) => return Ordering::Less,
(Some(a_char), Some(b_char)) => {
let ordering = cmp_chars(a_char, b_char, ignore_case);
if ordering != Ordering::Equal {
return ordering;
}
}
}
}
}

View file

@ -1,50 +1,32 @@
use std::clone::Clone; use std::cmp::Ordering;
use std::cmp::Ordering::Less;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::error::Error;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io::SeekFrom::Start; use std::io::SeekFrom;
use std::io::{BufRead, BufReader, BufWriter, Seek, Write}; use std::io::{BufRead, BufReader, BufWriter, Seek, Write};
use std::marker::PhantomData; use std::path::Path;
use std::path::PathBuf;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json;
use tempdir::TempDir; use tempdir::TempDir;
use super::{GlobalSettings, Line}; use super::{GlobalSettings, Line};
/// Trait for types that can be used by
/// [ExternalSorter](struct.ExternalSorter.html). Must be sortable, cloneable,
/// serializeable, and able to report on it's size
pub trait ExternallySortable: Clone + Serialize + DeserializeOwned {
/// Get the size, in bytes, of this object (used to constrain the buffer
/// used in the external sort).
fn get_size(&self) -> u64;
}
/// Iterator that provides sorted `T`s /// Iterator that provides sorted `T`s
pub struct ExtSortedIterator<Line> { pub struct ExtSortedIterator {
buffers: Vec<VecDeque<Line>>, buffers: Vec<VecDeque<Line>>,
chunk_offsets: Vec<u64>, chunk_offsets: Vec<u64>,
max_per_chunk: u64, max_per_chunk: usize,
chunks: u64, chunks: usize,
tmp_dir: TempDir, tmp_dir: TempDir,
settings: GlobalSettings, settings: GlobalSettings,
failed: bool, failed: bool,
} }
impl Iterator for ExtSortedIterator<Line> impl Iterator for ExtSortedIterator {
where type Item = Line;
Line: ExternallySortable,
{
type Item = Result<Line, Box<dyn Error>>;
/// # Errors /// # Errors
/// ///
/// This method can fail due to issues reading intermediate sorted chunks /// This method can fail due to issues reading intermediate sorted chunks
/// from disk, or due to serde deserialization issues /// from disk
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
if self.failed { if self.failed {
return None; return None;
@ -53,29 +35,18 @@ where
let mut empty = true; let mut empty = true;
for chunk_num in 0..self.chunks { for chunk_num in 0..self.chunks {
if self.buffers[chunk_num as usize].is_empty() { if self.buffers[chunk_num as usize].is_empty() {
let mut f = match File::open(self.tmp_dir.path().join(chunk_num.to_string())) { let mut f = crash_if_err!(
Ok(f) => f, 1,
Err(e) => { File::open(self.tmp_dir.path().join(chunk_num.to_string()))
self.failed = true; );
return Some(Err(Box::new(e))); crash_if_err!(1, f.seek(SeekFrom::Start(self.chunk_offsets[chunk_num])));
} let bytes_read = fill_buff(
}; &mut self.buffers[chunk_num as usize],
match f.seek(Start(self.chunk_offsets[chunk_num as usize])) { f,
Ok(_) => (), self.max_per_chunk,
Err(e) => { &self.settings,
self.failed = true; );
return Some(Err(Box::new(e))); self.chunk_offsets[chunk_num as usize] += bytes_read as u64;
}
}
let bytes_read =
match fill_buff(&mut self.buffers[chunk_num as usize], f, self.max_per_chunk) {
Ok(bytes_read) => bytes_read,
Err(e) => {
self.failed = true;
return Some(Err(e));
}
};
self.chunk_offsets[chunk_num as usize] += bytes_read;
if !self.buffers[chunk_num as usize].is_empty() { if !self.buffers[chunk_num as usize].is_empty() {
empty = false; empty = false;
} }
@ -91,205 +62,150 @@ where
// check is_empty() before unwrap()ing // check is_empty() before unwrap()ing
let mut idx = 0; let mut idx = 0;
for chunk_num in 0..self.chunks as usize { for chunk_num in 0..self.chunks as usize {
if !self.buffers[chunk_num].is_empty() { if !self.buffers[chunk_num].is_empty()
if self.buffers[idx].is_empty() && (self.buffers[idx].is_empty()
|| (super::compare_by)( || super::compare_by(
self.buffers[chunk_num].front().unwrap(), self.buffers[chunk_num].front().unwrap(),
self.buffers[idx].front().unwrap(), self.buffers[idx].front().unwrap(),
&self.settings, &self.settings,
) == Less ) == Ordering::Less)
{ {
idx = chunk_num; idx = chunk_num;
}
} }
} }
// unwrap due to checks above // unwrap due to checks above
let r = self.buffers[idx].pop_front().unwrap(); let r = self.buffers[idx].pop_front().unwrap();
Some(Ok(r)) Some(r)
} }
} }
/// Perform an external sort on an unsorted stream of incoming data /// Sort (based on `compare`) the `T`s provided by `unsorted` and return an
pub struct ExternalSorter<Line> /// iterator
where ///
Line: ExternallySortable, /// # Errors
{ ///
tmp_dir: Option<PathBuf>, /// This method can fail due to issues writing intermediate sorted chunks
buffer_bytes: u64, /// to disk.
phantom: PhantomData<Line>, pub fn ext_sort(
settings: GlobalSettings, unsorted: impl Iterator<Item = Line>,
} settings: &GlobalSettings,
) -> ExtSortedIterator {
let tmp_dir = crash_if_err!(1, TempDir::new_in(&settings.tmp_dir, "uutils_sort"));
impl ExternalSorter<Line> let mut iter = ExtSortedIterator {
where buffers: Vec::new(),
Line: ExternallySortable, chunk_offsets: Vec::new(),
{ max_per_chunk: 0,
/// Create a new `ExternalSorter` with a specified memory buffer and chunks: 0,
/// temporary directory tmp_dir,
pub fn new( settings: settings.clone(),
buffer_bytes: u64, failed: false,
tmp_dir: Option<PathBuf>, };
settings: GlobalSettings,
) -> ExternalSorter<Line> { let mut total_read = 0;
ExternalSorter { let mut chunk = Vec::new();
buffer_bytes,
tmp_dir, // make the initial chunks on disk
phantom: PhantomData, for seq in unsorted {
let seq_size = seq.estimate_size();
total_read += seq_size;
chunk.push(seq);
if total_read >= settings.buffer_size {
super::sort_by(&mut chunk, &settings);
write_chunk(
settings,
&iter.tmp_dir.path().join(iter.chunks.to_string()),
&mut chunk,
);
chunk.clear();
total_read = 0;
iter.chunks += 1;
}
}
// write the last chunk
if !chunk.is_empty() {
super::sort_by(&mut chunk, &settings);
write_chunk(
settings, settings,
} &iter.tmp_dir.path().join(iter.chunks.to_string()),
&mut chunk,
);
iter.chunks += 1;
} }
/// Sort (based on `compare`) the `T`s provided by `unsorted` and return an // initialize buffers for each chunk
/// iterator //
/// // Having a right sized buffer for each chunk for smallish values seems silly to me?
/// # Errors //
/// // We will have to have the entire iter in memory sometime right?
/// This method can fail due to issues writing intermediate sorted chunks // Set minimum to the size of the writer buffer, ~8K
/// to disk, or due to serde serialization issues
pub fn sort_by<I>(
&self,
unsorted: I,
settings: GlobalSettings,
) -> Result<ExtSortedIterator<Line>, Box<dyn Error>>
where
I: Iterator<Item = Line>,
{
let tmp_dir = match self.tmp_dir {
Some(ref p) => TempDir::new_in(p, "uutils_sort")?,
None => TempDir::new("uutils_sort")?,
};
// creating the thing we need to return first due to the face that we need to
// borrow tmp_dir and move it out
let mut iter = ExtSortedIterator {
buffers: Vec::new(),
chunk_offsets: Vec::new(),
max_per_chunk: 0,
chunks: 0,
tmp_dir,
settings,
failed: false,
};
{ const MINIMUM_READBACK_BUFFER: usize = 8200;
let mut total_read = 0; let right_sized_buffer = settings
let mut chunk = Vec::new(); .buffer_size
// Initial buffer is specified by user .checked_div(iter.chunks)
let mut adjusted_buffer_size = self.buffer_bytes; .unwrap_or(settings.buffer_size);
let (iter_size, _) = unsorted.size_hint(); iter.max_per_chunk = if right_sized_buffer > MINIMUM_READBACK_BUFFER {
right_sized_buffer
// make the initial chunks on disk } else {
for seq in unsorted { MINIMUM_READBACK_BUFFER
let seq_size = seq.get_size(); };
total_read += seq_size; iter.buffers = vec![VecDeque::new(); iter.chunks];
iter.chunk_offsets = vec![0; iter.chunks];
// GNU minimum is 16 * (sizeof struct + 2), but GNU uses about for chunk_num in 0..iter.chunks {
// 1/10 the memory that we do. And GNU even says in the code it may let offset = fill_buff(
// not work on small buffer sizes. &mut iter.buffers[chunk_num],
// crash_if_err!(
// The following seems to work pretty well, and has about the same max 1,
// RSS as lower minimum values. File::open(iter.tmp_dir.path().join(chunk_num.to_string()))
// ),
let minimum_buffer_size: u64 = iter_size as u64 * seq_size / 8; iter.max_per_chunk,
&settings,
adjusted_buffer_size = );
// Grow buffer size for a struct/Line larger than buffer iter.chunk_offsets[chunk_num] = offset as u64;
if adjusted_buffer_size < seq_size {
seq_size
} else if adjusted_buffer_size < minimum_buffer_size {
minimum_buffer_size
} else {
adjusted_buffer_size
};
chunk.push(seq);
if total_read >= adjusted_buffer_size {
super::sort_by(&mut chunk, &self.settings);
self.write_chunk(
&iter.tmp_dir.path().join(iter.chunks.to_string()),
&mut chunk,
)?;
chunk.clear();
total_read = 0;
iter.chunks += 1;
}
}
// write the last chunk
if chunk.len() > 0 {
super::sort_by(&mut chunk, &self.settings);
self.write_chunk(
&iter.tmp_dir.path().join(iter.chunks.to_string()),
&mut chunk,
)?;
iter.chunks += 1;
}
// initialize buffers for each chunk
//
// Having a right sized buffer for each chunk for smallish values seems silly to me?
//
// We will have to have the entire iter in memory sometime right?
// Set minimum to the size of the writer buffer, ~8K
//
const MINIMUM_READBACK_BUFFER: u64 = 8200;
let right_sized_buffer = adjusted_buffer_size
.checked_div(iter.chunks)
.unwrap_or(adjusted_buffer_size);
iter.max_per_chunk = if right_sized_buffer > MINIMUM_READBACK_BUFFER {
right_sized_buffer
} else {
MINIMUM_READBACK_BUFFER
};
iter.buffers = vec![VecDeque::new(); iter.chunks as usize];
iter.chunk_offsets = vec![0 as u64; iter.chunks as usize];
for chunk_num in 0..iter.chunks {
let offset = fill_buff(
&mut iter.buffers[chunk_num as usize],
File::open(iter.tmp_dir.path().join(chunk_num.to_string()))?,
iter.max_per_chunk,
)?;
iter.chunk_offsets[chunk_num as usize] = offset;
}
}
Ok(iter)
} }
fn write_chunk(&self, file: &PathBuf, chunk: &mut Vec<Line>) -> Result<(), Box<dyn Error>> { iter
let new_file = OpenOptions::new().create(true).append(true).open(file)?;
let mut buf_write = Box::new(BufWriter::new(new_file)) as Box<dyn Write>;
for s in chunk {
let mut serialized = serde_json::to_string(&s).expect("JSON write error: ");
serialized.push_str("\n");
buf_write.write(serialized.as_bytes())?;
}
buf_write.flush()?;
Ok(())
}
} }
fn fill_buff<Line>( fn write_chunk(settings: &GlobalSettings, file: &Path, chunk: &mut Vec<Line>) {
let new_file = crash_if_err!(1, OpenOptions::new().create(true).append(true).open(file));
let mut buf_write = BufWriter::new(new_file);
for s in chunk {
crash_if_err!(1, buf_write.write_all(s.line.as_bytes()));
crash_if_err!(
1,
buf_write.write_all(if settings.zero_terminated { "\0" } else { "\n" }.as_bytes(),)
);
}
crash_if_err!(1, buf_write.flush());
}
fn fill_buff(
vec: &mut VecDeque<Line>, vec: &mut VecDeque<Line>,
file: File, file: File,
max_bytes: u64, max_bytes: usize,
) -> Result<u64, Box<dyn Error>> settings: &GlobalSettings,
where ) -> usize {
Line: ExternallySortable,
{
let mut total_read = 0; let mut total_read = 0;
let mut bytes_read = 0; let mut bytes_read = 0;
for line in BufReader::new(file).lines() { for line in BufReader::new(file).split(if settings.zero_terminated {
let line_s = line?; b'\0'
} else {
b'\n'
}) {
let line_s = String::from_utf8(crash_if_err!(1, line)).unwrap();
bytes_read += line_s.len() + 1; bytes_read += line_s.len() + 1;
// This is where the bad stuff happens usually let deserialized = Line::new(line_s, settings);
let deserialized: Line = serde_json::from_str(&line_s).expect("JSON read error: "); total_read += deserialized.estimate_size();
total_read += deserialized.get_size();
vec.push_back(deserialized); vec.push_back(deserialized);
if total_read > max_bytes { if total_read > max_bytes {
break; break;
} }
} }
Ok(bytes_read as u64) bytes_read
} }

View file

@ -14,21 +14,20 @@
//! More specifically, exponent can be understood so that the original number is in (1..10)*10^exponent. //! More specifically, exponent can be understood so that the original number is in (1..10)*10^exponent.
//! From that follows the constraints of this algorithm: It is able to compare numbers in ±(1*10^[i64::MIN]..10*10^[i64::MAX]). //! From that follows the constraints of this algorithm: It is able to compare numbers in ±(1*10^[i64::MIN]..10*10^[i64::MAX]).
use serde::{Deserialize, Serialize};
use std::{cmp::Ordering, ops::Range}; use std::{cmp::Ordering, ops::Range};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
enum Sign { enum Sign {
Negative, Negative,
Positive, Positive,
} }
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct NumInfo { pub struct NumInfo {
exponent: i64, exponent: i64,
sign: Sign, sign: Sign,
} }
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct NumInfoParseSettings { pub struct NumInfoParseSettings {
pub accept_si_units: bool, pub accept_si_units: bool,
pub thousands_separator: Option<char>, pub thousands_separator: Option<char>,

View file

@ -15,11 +15,13 @@
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
mod custom_str_cmp;
mod external_sort; mod external_sort;
mod numeric_str_cmp; mod numeric_str_cmp;
use clap::{App, Arg}; use clap::{App, Arg};
use external_sort::{ExternalSorter, ExternallySortable}; use external_sort::ext_sort;
use custom_str_cmp::custom_str_cmp;
use fnv::FnvHasher; use fnv::FnvHasher;
use itertools::Itertools; use itertools::Itertools;
use numeric_str_cmp::{numeric_str_cmp, NumInfo, NumInfoParseSettings}; use numeric_str_cmp::{numeric_str_cmp, NumInfo, NumInfoParseSettings};
@ -27,14 +29,13 @@ use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use rayon::prelude::*; use rayon::prelude::*;
use semver::Version; use semver::Version;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::BinaryHeap; use std::collections::BinaryHeap;
use std::env; use std::env;
use std::fs::File; use std::fs::File;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Lines, Read, Write}; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write};
use std::mem::replace; use std::mem::replace;
use std::ops::Range; use std::ops::Range;
use std::path::Path; use std::path::Path;
@ -104,7 +105,7 @@ enum SortMode {
Default, Default,
} }
#[derive(Clone)] #[derive(Clone)]
struct GlobalSettings { pub struct GlobalSettings {
mode: SortMode, mode: SortMode,
debug: bool, debug: bool,
ignore_blanks: bool, ignore_blanks: bool,
@ -204,39 +205,29 @@ impl From<&GlobalSettings> for KeySettings {
} }
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Clone)]
/// Represents the string selected by a FieldSelector. /// Represents the string selected by a FieldSelector.
enum SelectionRange { struct SelectionRange {
/// If we had to transform this selection, we have to store a new string. range: Range<usize>,
String(String),
/// If there was no transformation, we can store an index into the line.
ByIndex(Range<usize>),
} }
impl SelectionRange { impl SelectionRange {
fn new(range: Range<usize>) -> Self {
Self { range }
}
/// Gets the actual string slice represented by this Selection. /// Gets the actual string slice represented by this Selection.
fn get_str<'a>(&'a self, line: &'a str) -> &'a str { fn get_str<'a>(&self, line: &'a str) -> &'a str {
match self { &line[self.range.to_owned()]
SelectionRange::String(string) => string.as_str(),
SelectionRange::ByIndex(range) => &line[range.to_owned()],
}
} }
fn shorten(&mut self, new_range: Range<usize>) { fn shorten(&mut self, new_range: Range<usize>) {
match self { self.range.end = self.range.start + new_range.end;
SelectionRange::String(string) => { self.range.start += new_range.start;
string.drain(new_range.end..);
string.drain(..new_range.start);
}
SelectionRange::ByIndex(range) => {
range.end = range.start + new_range.end;
range.start += new_range.start;
}
}
} }
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Clone)]
enum NumCache { enum NumCache {
AsF64(GeneralF64ParseResult), AsF64(GeneralF64ParseResult),
WithInfo(NumInfo), WithInfo(NumInfo),
@ -257,7 +248,8 @@ impl NumCache {
} }
} }
} }
#[derive(Serialize, Deserialize, Clone)]
#[derive(Clone)]
struct Selection { struct Selection {
range: SelectionRange, range: SelectionRange,
num_cache: NumCache, num_cache: NumCache,
@ -272,22 +264,21 @@ impl Selection {
type Field = Range<usize>; type Field = Range<usize>;
#[derive(Serialize, Deserialize, Clone)] #[derive(Clone)]
struct Line { pub struct Line {
line: String, line: String,
// The common case is not to specify fields. Let's make this fast. // The common case is not to specify fields. Let's make this fast.
selections: SmallVec<[Selection; 1]>, selections: SmallVec<[Selection; 1]>,
} }
impl ExternallySortable for Line {
fn get_size(&self) -> u64 {
// Currently 96 bytes, but that could change, so we get that size here
std::mem::size_of::<Line>() as u64
}
}
impl Line { impl Line {
fn new(line: String, settings: &GlobalSettings) -> Self { pub fn estimate_size(&self) -> usize {
self.line.capacity()
+ self.selections.capacity() * std::mem::size_of::<Selection>()
+ std::mem::size_of::<Self>()
}
pub fn new(line: String, settings: &GlobalSettings) -> Self {
let fields = if settings let fields = if settings
.selectors .selectors
.iter() .iter()
@ -299,18 +290,12 @@ impl Line {
None None
}; };
let selections = settings let selections: SmallVec<[Selection; 1]> = settings
.selectors .selectors
.iter() .iter()
.map(|selector| { .map(|selector| {
let range = selector.get_selection(&line, fields.as_deref()); let mut range =
let mut range = if let Some(transformed) = SelectionRange::new(selector.get_selection(&line, fields.as_deref()));
transform(&line[range.to_owned()], &selector.settings)
{
SelectionRange::String(transformed)
} else {
SelectionRange::ByIndex(range)
};
let num_cache = if selector.settings.mode == SortMode::Numeric let num_cache = if selector.settings.mode == SortMode::Numeric
|| selector.settings.mode == SortMode::HumanNumeric || selector.settings.mode == SortMode::HumanNumeric
{ {
@ -460,34 +445,6 @@ impl Line {
} }
} }
/// Transform this line. Returns None if there's no need to transform.
fn transform(line: &str, settings: &KeySettings) -> Option<String> {
let mut transformed = None;
if settings.ignore_case {
transformed = Some(line.to_uppercase());
}
if settings.ignore_blanks {
transformed = Some(
transformed
.as_deref()
.unwrap_or(line)
.trim_start()
.to_string(),
);
}
if settings.dictionary_order {
transformed = Some(remove_nondictionary_chars(
transformed.as_deref().unwrap_or(line),
));
}
if settings.ignore_non_printing {
transformed = Some(remove_nonprinting_chars(
transformed.as_deref().unwrap_or(line),
));
}
transformed
}
/// Tokenize a line into fields. /// Tokenize a line into fields.
fn tokenize(line: &str, separator: Option<char>) -> Vec<Field> { fn tokenize(line: &str, separator: Option<char>) -> Vec<Field> {
if let Some(separator) = separator { if let Some(separator) = separator {
@ -725,7 +682,7 @@ impl FieldSelector {
} }
struct MergeableFile<'a> { struct MergeableFile<'a> {
lines: Lines<BufReader<Box<dyn Read>>>, lines: Box<dyn Iterator<Item = Line> + 'a>,
current_line: Line, current_line: Line,
settings: &'a GlobalSettings, settings: &'a GlobalSettings,
} }
@ -765,11 +722,11 @@ impl<'a> FileMerger<'a> {
settings, settings,
} }
} }
fn push_file(&mut self, mut lines: Lines<BufReader<Box<dyn Read>>>) { fn push_file(&mut self, mut lines: Box<dyn Iterator<Item = Line> + 'a>) {
if let Some(Ok(next_line)) = lines.next() { if let Some(next_line) = lines.next() {
let mergeable_file = MergeableFile { let mergeable_file = MergeableFile {
lines, lines,
current_line: Line::new(next_line, &self.settings), current_line: next_line,
settings: &self.settings, settings: &self.settings,
}; };
self.heap.push(mergeable_file); self.heap.push(mergeable_file);
@ -783,11 +740,8 @@ impl<'a> Iterator for FileMerger<'a> {
match self.heap.pop() { match self.heap.pop() {
Some(mut current) => { Some(mut current) => {
match current.lines.next() { match current.lines.next() {
Some(Ok(next_line)) => { Some(next_line) => {
let ret = replace( let ret = replace(&mut current.current_line, next_line);
&mut current.current_line,
Line::new(next_line, &self.settings),
);
self.heap.push(current); self.heap.push(current);
Some(ret) Some(ret)
} }
@ -1155,90 +1109,106 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
exec(files, settings) exec(files, settings)
} }
fn file_to_lines_iter<'a>(
file: &str,
settings: &'a GlobalSettings,
) -> Option<impl Iterator<Item = Line> + 'a> {
let (reader, _) = match open(file) {
Some(x) => x,
None => return None,
};
let buf_reader = BufReader::new(reader);
Some(
buf_reader
.split(if settings.zero_terminated {
b'\0'
} else {
b'\n'
})
.map(move |line| {
Line::new(
crash_if_err!(1, String::from_utf8(crash_if_err!(1, line))),
settings,
)
}),
)
}
fn output_sorted_lines(iter: impl Iterator<Item = Line>, settings: &GlobalSettings) {
if settings.unique {
print_sorted(
iter.dedup_by(|a, b| compare_by(a, b, &settings) == Ordering::Equal),
&settings,
);
} else {
print_sorted(iter, &settings);
}
}
fn exec(files: Vec<String>, settings: GlobalSettings) -> i32 { fn exec(files: Vec<String>, settings: GlobalSettings) -> i32 {
let mut lines = Vec::new(); if settings.merge {
let mut file_merger = FileMerger::new(&settings); let mut file_merger = FileMerger::new(&settings);
for lines in files
.iter()
.filter_map(|file| file_to_lines_iter(file, &settings))
{
file_merger.push_file(Box::new(lines));
}
output_sorted_lines(file_merger, &settings);
} else {
let lines = files
.iter()
.filter_map(|file| file_to_lines_iter(file, &settings))
.flatten();
for path in &files { if settings.check {
let (reader, _) = match open(path) { return exec_check_file(lines, &settings);
Some(x) => x, }
None => continue,
};
let buf_reader = BufReader::new(reader); // Only use ext_sorter when we need to.
// Probably faster that we don't create
if settings.merge { // an owned value each run
file_merger.push_file(buf_reader.lines()); if settings.ext_sort {
} else if settings.zero_terminated { let sorted_lines = ext_sort(lines, &settings);
for line in buf_reader.split(b'\0').flatten() { output_sorted_lines(sorted_lines, &settings);
lines.push(Line::new(
std::str::from_utf8(&line)
.expect("Could not parse string from zero terminated input.")
.to_string(),
&settings,
));
}
} else { } else {
for line in buf_reader.lines() { let mut lines = vec![];
if let Ok(n) = line {
lines.push(Line::new(n, &settings)); // This is duplicated from fn file_to_lines_iter, but using that function directly results in a performance regression.
for (file, _) in files.iter().map(|file| open(file)).flatten() {
let buf_reader = BufReader::new(file);
for line in buf_reader.split(if settings.zero_terminated {
b'\0'
} else { } else {
break; b'\n'
}) {
let string = crash_if_err!(1, String::from_utf8(crash_if_err!(1, line)));
lines.push(Line::new(string, &settings));
} }
} }
sort_by(&mut lines, &settings);
output_sorted_lines(lines.into_iter(), &settings);
} }
} }
if settings.check {
return exec_check_file(&lines, &settings);
}
// Only use ext_sorter when we need to.
// Probably faster that we don't create
// an owned value each run
if settings.ext_sort {
lines = ext_sort_by(lines, settings.clone());
} else {
sort_by(&mut lines, &settings);
}
if settings.merge {
if settings.unique {
print_sorted(
file_merger.dedup_by(|a, b| compare_by(a, b, &settings) == Ordering::Equal),
&settings,
)
} else {
print_sorted(file_merger, &settings)
}
} else if settings.unique {
print_sorted(
lines
.into_iter()
.dedup_by(|a, b| compare_by(a, b, &settings) == Ordering::Equal),
&settings,
)
} else {
print_sorted(lines.into_iter(), &settings)
}
0 0
} }
fn exec_check_file(unwrapped_lines: &[Line], settings: &GlobalSettings) -> i32 { fn exec_check_file(unwrapped_lines: impl Iterator<Item = Line>, settings: &GlobalSettings) -> i32 {
// errors yields the line before each disorder, // errors yields the line before each disorder,
// plus the last line (quirk of .coalesce()) // plus the last line (quirk of .coalesce())
let mut errors = let mut errors = unwrapped_lines
unwrapped_lines .enumerate()
.iter() .coalesce(|(last_i, last_line), (i, line)| {
.enumerate() if compare_by(&last_line, &line, &settings) == Ordering::Greater {
.coalesce(|(last_i, last_line), (i, line)| { Err(((last_i, last_line), (i, line)))
if compare_by(&last_line, &line, &settings) == Ordering::Greater { } else {
Err(((last_i, last_line), (i, line))) Ok((i, line))
} else { }
Ok((i, line)) });
}
});
if let Some((first_error_index, _line)) = errors.next() { if let Some((first_error_index, _line)) = errors.next() {
// Check for a second "error", as .coalesce() always returns the last // Check for a second "error", as .coalesce() always returns the last
// line, no matter what our merging function does. // line, no matter what our merging function does.
@ -1257,20 +1227,6 @@ fn exec_check_file(unwrapped_lines: &[Line], settings: &GlobalSettings) -> i32 {
} }
} }
fn ext_sort_by(unsorted: Vec<Line>, settings: GlobalSettings) -> Vec<Line> {
let external_sorter = ExternalSorter::new(
settings.buffer_size as u64,
Some(settings.tmp_dir.clone()),
settings.clone(),
);
let iter = external_sorter
.sort_by(unsorted.into_iter(), settings.clone())
.unwrap()
.map(|x| x.unwrap())
.collect::<Vec<Line>>();
iter
}
fn sort_by(unsorted: &mut Vec<Line>, settings: &GlobalSettings) { fn sort_by(unsorted: &mut Vec<Line>, settings: &GlobalSettings) {
if settings.stable || settings.unique { if settings.stable || settings.unique {
unsorted.par_sort_by(|a, b| compare_by(a, b, &settings)) unsorted.par_sort_by(|a, b| compare_by(a, b, &settings))
@ -1296,12 +1252,18 @@ fn compare_by(a: &Line, b: &Line, global_settings: &GlobalSettings) -> Ordering
(b_str, b_selection.num_cache.as_num_info()), (b_str, b_selection.num_cache.as_num_info()),
), ),
SortMode::GeneralNumeric => general_numeric_compare( SortMode::GeneralNumeric => general_numeric_compare(
general_f64_parse(&a_str[get_leading_gen(a_str)]), a_selection.num_cache.as_f64(),
general_f64_parse(&b_str[get_leading_gen(b_str)]), b_selection.num_cache.as_f64(),
), ),
SortMode::Month => month_compare(a_str, b_str), SortMode::Month => month_compare(a_str, b_str),
SortMode::Version => version_compare(a_str, b_str), SortMode::Version => version_compare(a_str, b_str),
SortMode::Default => default_compare(a_str, b_str), SortMode::Default => custom_str_cmp(
a_str,
b_str,
settings.ignore_non_printing,
settings.dictionary_order,
settings.ignore_case,
),
} }
}; };
if cmp != Ordering::Equal { if cmp != Ordering::Equal {
@ -1313,7 +1275,7 @@ fn compare_by(a: &Line, b: &Line, global_settings: &GlobalSettings) -> Ordering
let cmp = if global_settings.random || global_settings.stable || global_settings.unique { let cmp = if global_settings.random || global_settings.stable || global_settings.unique {
Ordering::Equal Ordering::Equal
} else { } else {
default_compare(&a.line, &b.line) a.line.cmp(&b.line)
}; };
if global_settings.reverse { if global_settings.reverse {
@ -1323,13 +1285,6 @@ fn compare_by(a: &Line, b: &Line, global_settings: &GlobalSettings) -> Ordering
} }
} }
// Test output against BSDs and GNU with their locale
// env var set to lc_ctype=utf-8 to enjoy the exact same output.
#[inline(always)]
fn default_compare(a: &str, b: &str) -> Ordering {
a.cmp(b)
}
// This function cleans up the initial comparison done by leading_num_common for a general numeric compare. // This function cleans up the initial comparison done by leading_num_common for a general numeric compare.
// In contrast to numeric compare, GNU general numeric/FP sort *should* recognize positive signs and // In contrast to numeric compare, GNU general numeric/FP sort *should* recognize positive signs and
// scientific notation, so we strip those lines only after the end of the following numeric string. // scientific notation, so we strip those lines only after the end of the following numeric string.
@ -1375,7 +1330,7 @@ fn get_leading_gen(input: &str) -> Range<usize> {
leading_whitespace_len..input.len() leading_whitespace_len..input.len()
} }
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, PartialOrd)] #[derive(Copy, Clone, PartialEq, PartialOrd)]
enum GeneralF64ParseResult { enum GeneralF64ParseResult {
Invalid, Invalid,
NaN, NaN,
@ -1516,22 +1471,6 @@ fn version_compare(a: &str, b: &str) -> Ordering {
} }
} }
fn remove_nondictionary_chars(s: &str) -> String {
// According to GNU, dictionary chars are those of ASCII
// and a blank is a space or a tab
s.chars()
.filter(|c| c.is_ascii_alphanumeric() || c.is_ascii_whitespace())
.collect::<String>()
}
fn remove_nonprinting_chars(s: &str) -> String {
// However, GNU says nonprinting chars are more permissive.
// All of ASCII except control chars ie, escape, newline
s.chars()
.filter(|c| c.is_ascii() && !c.is_ascii_control())
.collect::<String>()
}
fn print_sorted<T: Iterator<Item = Line>>(iter: T, settings: &GlobalSettings) { fn print_sorted<T: Iterator<Item = Line>>(iter: T, settings: &GlobalSettings) {
let mut file: Box<dyn Write> = match settings.outfile { let mut file: Box<dyn Write> = match settings.outfile {
Some(ref filename) => match File::create(Path::new(&filename)) { Some(ref filename) => match File::create(Path::new(&filename)) {
@ -1598,14 +1537,6 @@ mod tests {
assert_eq!(Ordering::Equal, random_shuffle(a, b, c)); assert_eq!(Ordering::Equal, random_shuffle(a, b, c));
} }
#[test]
fn test_default_compare() {
let a = "your own";
let b = "your place";
assert_eq!(Ordering::Less, default_compare(a, b));
}
#[test] #[test]
fn test_month_compare() { fn test_month_compare() {
let a = "JaN"; let a = "JaN";

292
src/uu/test/src/parser.rs Normal file
View file

@ -0,0 +1,292 @@
// This file is part of the uutils coreutils package.
//
// (c) Daniel Rocco <drocco@gmail.com>
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::ffi::OsString;
use std::iter::Peekable;
/// Represents a parsed token from a test expression
#[derive(Debug, PartialEq)]
pub enum Symbol {
LParen,
Bang,
BoolOp(OsString),
Literal(OsString),
StringOp(OsString),
IntOp(OsString),
FileOp(OsString),
StrlenOp(OsString),
FiletestOp(OsString),
None,
}
impl Symbol {
/// Create a new Symbol from an OsString.
///
/// Returns Symbol::None in place of None
fn new(token: Option<OsString>) -> Symbol {
match token {
Some(s) => match s.to_string_lossy().as_ref() {
"(" => Symbol::LParen,
"!" => Symbol::Bang,
"-a" | "-o" => Symbol::BoolOp(s),
"=" | "!=" => Symbol::StringOp(s),
"-eq" | "-ge" | "-gt" | "-le" | "-lt" | "-ne" => Symbol::IntOp(s),
"-ef" | "-nt" | "-ot" => Symbol::FileOp(s),
"-n" | "-z" => Symbol::StrlenOp(s),
"-b" | "-c" | "-d" | "-e" | "-f" | "-g" | "-G" | "-h" | "-k" | "-L" | "-O"
| "-p" | "-r" | "-s" | "-S" | "-t" | "-u" | "-w" | "-x" => Symbol::FiletestOp(s),
_ => Symbol::Literal(s),
},
None => Symbol::None,
}
}
/// Convert this Symbol into a Symbol::Literal, useful for cases where
/// test treats an operator as a string operand (test has no reserved
/// words).
///
/// # Panics
///
/// Panics if `self` is Symbol::None
fn into_literal(self) -> Symbol {
Symbol::Literal(match self {
Symbol::LParen => OsString::from("("),
Symbol::Bang => OsString::from("!"),
Symbol::BoolOp(s)
| Symbol::Literal(s)
| Symbol::StringOp(s)
| Symbol::IntOp(s)
| Symbol::FileOp(s)
| Symbol::StrlenOp(s)
| Symbol::FiletestOp(s) => s,
Symbol::None => panic!(),
})
}
}
/// Recursive descent parser for test, which converts a list of OsStrings
/// (typically command line arguments) into a stack of Symbols in postfix
/// order.
///
/// Grammar:
///
/// EXPR → TERM | EXPR BOOLOP EXPR
/// TERM → ( EXPR )
/// TERM → ( )
/// TERM → ! EXPR
/// TERM → UOP str
/// UOP → STRLEN | FILETEST
/// TERM → str OP str
/// TERM → str | 𝜖
/// OP → STRINGOP | INTOP | FILEOP
/// STRINGOP → = | !=
/// INTOP → -eq | -ge | -gt | -le | -lt | -ne
/// FILEOP → -ef | -nt | -ot
/// STRLEN → -n | -z
/// FILETEST → -b | -c | -d | -e | -f | -g | -G | -h | -k | -L | -O | -p |
/// -r | -s | -S | -t | -u | -w | -x
/// BOOLOP → -a | -o
///
#[derive(Debug)]
struct Parser {
tokens: Peekable<std::vec::IntoIter<OsString>>,
pub stack: Vec<Symbol>,
}
impl Parser {
/// Construct a new Parser from a `Vec<OsString>` of tokens.
fn new(tokens: Vec<OsString>) -> Parser {
Parser {
tokens: tokens.into_iter().peekable(),
stack: vec![],
}
}
/// Fetch the next token from the input stream as a Symbol.
fn next_token(&mut self) -> Symbol {
Symbol::new(self.tokens.next())
}
/// Peek at the next token from the input stream, returning it as a Symbol.
/// The stream is unchanged and will return the same Symbol on subsequent
/// calls to `next()` or `peek()`.
fn peek(&mut self) -> Symbol {
Symbol::new(self.tokens.peek().map(|s| s.to_os_string()))
}
/// Test if the next token in the stream is a BOOLOP (-a or -o), without
/// removing the token from the stream.
fn peek_is_boolop(&mut self) -> bool {
if let Symbol::BoolOp(_) = self.peek() {
true
} else {
false
}
}
/// Parse an expression.
///
/// EXPR → TERM | EXPR BOOLOP EXPR
fn expr(&mut self) {
if !self.peek_is_boolop() {
self.term();
}
self.maybe_boolop();
}
/// Parse a term token and possible subsequent symbols: "(", "!", UOP,
/// literal, or None.
fn term(&mut self) {
let symbol = self.next_token();
match symbol {
Symbol::LParen => self.lparen(),
Symbol::Bang => self.bang(),
Symbol::StrlenOp(_) => self.uop(symbol),
Symbol::FiletestOp(_) => self.uop(symbol),
Symbol::None => self.stack.push(symbol),
literal => self.literal(literal),
}
}
/// Parse a (possibly) parenthesized expression.
///
/// test has no reserved keywords, so "(" will be interpreted as a literal
/// if it is followed by nothing or a comparison operator OP.
fn lparen(&mut self) {
match self.peek() {
// lparen is a literal when followed by nothing or comparison
Symbol::None | Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) => {
self.literal(Symbol::Literal(OsString::from("(")));
}
// empty parenthetical
Symbol::Literal(s) if s == ")" => {}
_ => {
self.expr();
match self.next_token() {
Symbol::Literal(s) if s == ")" => (),
_ => panic!("expected )"),
}
}
}
}
/// Parse a (possibly) negated expression.
///
/// Example cases:
///
/// * `! =`: negate the result of the implicit string length test of `=`
/// * `! = foo`: compare the literal strings `!` and `foo`
/// * `! <expr>`: negate the result of the expression
///
fn bang(&mut self) {
if let Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) = self.peek() {
// we need to peek ahead one more token to disambiguate the first
// two cases listed above: case 1 — `! <OP as literal>` — and
// case 2: `<! as literal> OP str`.
let peek2 = self.tokens.clone().nth(1);
if peek2.is_none() {
// op is literal
let op = self.next_token().into_literal();
self.stack.push(op);
self.stack.push(Symbol::Bang);
} else {
// bang is literal; parsing continues with op
self.literal(Symbol::Literal(OsString::from("!")));
}
} else {
self.expr();
self.stack.push(Symbol::Bang);
}
}
/// Peek at the next token and parse it as a BOOLOP or string literal,
/// as appropriate.
fn maybe_boolop(&mut self) {
if self.peek_is_boolop() {
let token = self.tokens.next().unwrap(); // safe because we peeked
// BoolOp by itself interpreted as Literal
if let Symbol::None = self.peek() {
self.literal(Symbol::Literal(token))
} else {
self.boolop(Symbol::BoolOp(token))
}
}
}
/// Parse a Boolean expression.
///
/// Logical and (-a) has higher precedence than or (-o), so in an
/// expression like `foo -o '' -a ''`, the and subexpression is evaluated
/// first.
fn boolop(&mut self, op: Symbol) {
if op == Symbol::BoolOp(OsString::from("-a")) {
self.term();
self.stack.push(op);
self.maybe_boolop();
} else {
self.expr();
self.stack.push(op);
}
}
/// Parse a (possible) unary argument test (string length or file
/// attribute check).
///
/// If a UOP is followed by nothing it is interpreted as a literal string.
fn uop(&mut self, op: Symbol) {
match self.next_token() {
Symbol::None => self.stack.push(op.into_literal()),
symbol => {
self.stack.push(symbol.into_literal());
self.stack.push(op);
}
}
}
/// Parse a string literal, optionally followed by a comparison operator
/// and a second string literal.
fn literal(&mut self, token: Symbol) {
self.stack.push(token.into_literal());
// EXPR → str OP str
match self.peek() {
Symbol::StringOp(_) | Symbol::IntOp(_) | Symbol::FileOp(_) => {
let op = self.next_token();
match self.next_token() {
Symbol::None => panic!("missing argument after {:?}", op),
token => self.stack.push(token.into_literal()),
}
self.stack.push(op);
}
_ => {}
}
}
/// Parser entry point: parse the token stream `self.tokens`, storing the
/// resulting `Symbol` stack in `self.stack`.
fn parse(&mut self) -> Result<(), String> {
self.expr();
match self.tokens.next() {
Some(token) => Err(format!("extra argument {}", token.to_string_lossy())),
None => Ok(()),
}
}
}
/// Parse the token stream `args`, returning a `Symbol` stack representing the
/// operations to perform in postfix order.
pub fn parse(args: Vec<OsString>) -> Result<Vec<Symbol>, String> {
let mut p = Parser::new(args);
p.parse()?;
Ok(p.stack)
}

View file

@ -1,144 +1,154 @@
// * This file is part of the uutils coreutils package. // This file is part of the uutils coreutils package.
// * //
// * (c) mahkoh (ju.orth [at] gmail [dot] com) // (c) mahkoh (ju.orth [at] gmail [dot] com)
// * // (c) Daniel Rocco <drocco@gmail.com>
// * For the full copyright and license information, please view the LICENSE //
// * file that was distributed with this source code. // For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) retval paren prec subprec cond // spell-checker:ignore (ToDO) retval paren prec subprec cond
use std::collections::HashMap; mod parser;
use std::str::from_utf8;
static NAME: &str = "test"; use parser::{parse, Symbol};
use std::ffi::{OsStr, OsString};
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
let args: Vec<_> = args.collect(); // TODO: handle being called as `[`
// This is completely disregarding valid windows paths that aren't valid unicode let args: Vec<_> = args.skip(1).collect();
let args = args
.iter()
.map(|a| a.to_str().unwrap().as_bytes())
.collect::<Vec<&[u8]>>();
if args.is_empty() {
return 2;
}
let args = if !args[0].ends_with(NAME.as_bytes()) {
&args[1..]
} else {
&args[..]
};
let args = match args[0] {
b"[" => match args[args.len() - 1] {
b"]" => &args[1..args.len() - 1],
_ => return 2,
},
_ => &args[1..args.len()],
};
let mut error = false;
let retval = 1 - parse_expr(args, &mut error) as i32;
if error {
2
} else {
retval
}
}
fn one(args: &[&[u8]]) -> bool { let result = parse(args).and_then(|mut stack| eval(&mut stack));
!args[0].is_empty()
}
fn two(args: &[&[u8]], error: &mut bool) -> bool { match result {
match args[0] { Ok(result) => {
b"!" => !one(&args[1..]), if result {
b"-b" => path(args[1], PathCondition::BlockSpecial), 0
b"-c" => path(args[1], PathCondition::CharacterSpecial), } else {
b"-d" => path(args[1], PathCondition::Directory), 1
b"-e" => path(args[1], PathCondition::Exists),
b"-f" => path(args[1], PathCondition::Regular),
b"-g" => path(args[1], PathCondition::GroupIdFlag),
b"-h" => path(args[1], PathCondition::SymLink),
b"-L" => path(args[1], PathCondition::SymLink),
b"-n" => one(&args[1..]),
b"-p" => path(args[1], PathCondition::Fifo),
b"-r" => path(args[1], PathCondition::Readable),
b"-S" => path(args[1], PathCondition::Socket),
b"-s" => path(args[1], PathCondition::NonEmpty),
b"-t" => isatty(args[1]),
b"-u" => path(args[1], PathCondition::UserIdFlag),
b"-w" => path(args[1], PathCondition::Writable),
b"-x" => path(args[1], PathCondition::Executable),
b"-z" => !one(&args[1..]),
_ => {
*error = true;
false
}
}
}
fn three(args: &[&[u8]], error: &mut bool) -> bool {
match args[1] {
b"=" => args[0] == args[2],
b"==" => args[0] == args[2],
b"!=" => args[0] != args[2],
b"-eq" => integers(args[0], args[2], IntegerCondition::Equal),
b"-ne" => integers(args[0], args[2], IntegerCondition::Unequal),
b"-gt" => integers(args[0], args[2], IntegerCondition::Greater),
b"-ge" => integers(args[0], args[2], IntegerCondition::GreaterEqual),
b"-lt" => integers(args[0], args[2], IntegerCondition::Less),
b"-le" => integers(args[0], args[2], IntegerCondition::LessEqual),
_ => match args[0] {
b"!" => !two(&args[1..], error),
_ => {
*error = true;
false
} }
}, }
} Err(e) => {
} eprintln!("test: {}", e);
2
fn four(args: &[&[u8]], error: &mut bool) -> bool {
match args[0] {
b"!" => !three(&args[1..], error),
_ => {
*error = true;
false
} }
} }
} }
enum IntegerCondition { /// Evaluate a stack of Symbols, returning the result of the evaluation or
Equal, /// an error message if evaluation failed.
Unequal, fn eval(stack: &mut Vec<Symbol>) -> Result<bool, String> {
Greater, macro_rules! pop_literal {
GreaterEqual, () => {
Less, match stack.pop() {
LessEqual, Some(Symbol::Literal(s)) => s,
} _ => panic!(),
}
};
}
fn integers(a: &[u8], b: &[u8], cond: IntegerCondition) -> bool { let s = stack.pop();
let (a, b): (&str, &str) = match (from_utf8(a), from_utf8(b)) {
(Ok(a), Ok(b)) => (a, b), match s {
_ => return false, Some(Symbol::Bang) => {
}; let result = eval(stack)?;
let (a, b): (i64, i64) = match (a.parse(), b.parse()) {
(Ok(a), Ok(b)) => (a, b), Ok(!result)
_ => return false, }
}; Some(Symbol::StringOp(op)) => {
match cond { let b = stack.pop();
IntegerCondition::Equal => a == b, let a = stack.pop();
IntegerCondition::Unequal => a != b, Ok(if op == "=" { a == b } else { a != b })
IntegerCondition::Greater => a > b, }
IntegerCondition::GreaterEqual => a >= b, Some(Symbol::IntOp(op)) => {
IntegerCondition::Less => a < b, let b = pop_literal!();
IntegerCondition::LessEqual => a <= b, let a = pop_literal!();
Ok(integers(&a, &b, &op)?)
}
Some(Symbol::FileOp(_op)) => unimplemented!(),
Some(Symbol::StrlenOp(op)) => {
let s = match stack.pop() {
Some(Symbol::Literal(s)) => s,
Some(Symbol::None) => OsString::from(""),
None => {
return Ok(true);
}
_ => {
return Err(format!("missing argument after {:?}", op));
}
};
Ok(if op == "-z" {
s.is_empty()
} else {
!s.is_empty()
})
}
Some(Symbol::FiletestOp(op)) => {
let op = op.to_string_lossy();
let f = pop_literal!();
Ok(match op.as_ref() {
"-b" => path(&f, PathCondition::BlockSpecial),
"-c" => path(&f, PathCondition::CharacterSpecial),
"-d" => path(&f, PathCondition::Directory),
"-e" => path(&f, PathCondition::Exists),
"-f" => path(&f, PathCondition::Regular),
"-g" => path(&f, PathCondition::GroupIdFlag),
"-h" => path(&f, PathCondition::SymLink),
"-L" => path(&f, PathCondition::SymLink),
"-p" => path(&f, PathCondition::Fifo),
"-r" => path(&f, PathCondition::Readable),
"-S" => path(&f, PathCondition::Socket),
"-s" => path(&f, PathCondition::NonEmpty),
"-t" => isatty(&f)?,
"-u" => path(&f, PathCondition::UserIdFlag),
"-w" => path(&f, PathCondition::Writable),
"-x" => path(&f, PathCondition::Executable),
_ => panic!(),
})
}
Some(Symbol::Literal(s)) => Ok(!s.is_empty()),
Some(Symbol::None) => Ok(false),
Some(Symbol::BoolOp(op)) => {
let b = eval(stack)?;
let a = eval(stack)?;
Ok(if op == "-a" { a && b } else { a || b })
}
None => Ok(false),
_ => Err("expected value".to_string()),
} }
} }
fn isatty(fd: &[u8]) -> bool { fn integers(a: &OsStr, b: &OsStr, cond: &OsStr) -> Result<bool, String> {
from_utf8(fd) let format_err = |value| format!("invalid integer {}", value);
.ok()
.and_then(|s| s.parse().ok()) let a = a.to_string_lossy();
.map_or(false, |i| { let a: i64 = a.parse().map_err(|_| format_err(a))?;
let b = b.to_string_lossy();
let b: i64 = b.parse().map_err(|_| format_err(b))?;
let cond = cond.to_string_lossy();
Ok(match cond.as_ref() {
"-eq" => a == b,
"-ne" => a != b,
"-gt" => a > b,
"-ge" => a >= b,
"-lt" => a < b,
"-le" => a <= b,
_ => return Err(format!("unknown operator {}", cond)),
})
}
fn isatty(fd: &OsStr) -> Result<bool, String> {
let fd = fd.to_string_lossy();
fd.parse()
.map_err(|_| format!("invalid integer {}", fd))
.map(|i| {
#[cfg(not(target_os = "redox"))] #[cfg(not(target_os = "redox"))]
unsafe { unsafe {
libc::isatty(i) == 1 libc::isatty(i) == 1
@ -148,173 +158,6 @@ fn isatty(fd: &[u8]) -> bool {
}) })
} }
fn dispatch(args: &mut &[&[u8]], error: &mut bool) -> bool {
let (val, idx) = match args.len() {
0 => {
*error = true;
(false, 0)
}
1 => (one(*args), 1),
2 => dispatch_two(args, error),
3 => dispatch_three(args, error),
_ => dispatch_four(args, error),
};
*args = &(*args)[idx..];
val
}
fn dispatch_two(args: &mut &[&[u8]], error: &mut bool) -> (bool, usize) {
let val = two(*args, error);
if *error {
*error = false;
(one(*args), 1)
} else {
(val, 2)
}
}
fn dispatch_three(args: &mut &[&[u8]], error: &mut bool) -> (bool, usize) {
let val = three(*args, error);
if *error {
*error = false;
dispatch_two(args, error)
} else {
(val, 3)
}
}
fn dispatch_four(args: &mut &[&[u8]], error: &mut bool) -> (bool, usize) {
let val = four(*args, error);
if *error {
*error = false;
dispatch_three(args, error)
} else {
(val, 4)
}
}
#[derive(Clone, Copy)]
enum Precedence {
Unknown = 0,
Paren, // FIXME: this is useless (parentheses have not been implemented)
Or,
And,
BUnOp,
BinOp,
UnOp,
}
fn parse_expr(mut args: &[&[u8]], error: &mut bool) -> bool {
if args.is_empty() {
false
} else {
let hashmap = setup_hashmap();
let lhs = dispatch(&mut args, error);
if !args.is_empty() {
parse_expr_helper(&hashmap, &mut args, lhs, Precedence::Unknown, error)
} else {
lhs
}
}
}
fn parse_expr_helper<'a>(
hashmap: &HashMap<&'a [u8], Precedence>,
args: &mut &[&'a [u8]],
mut lhs: bool,
min_prec: Precedence,
error: &mut bool,
) -> bool {
let mut prec = *hashmap.get(&args[0]).unwrap_or_else(|| {
*error = true;
&min_prec
});
while !*error && !args.is_empty() && prec as usize >= min_prec as usize {
let op = args[0];
*args = &(*args)[1..];
let mut rhs = dispatch(args, error);
while !args.is_empty() {
let subprec = *hashmap.get(&args[0]).unwrap_or_else(|| {
*error = true;
&min_prec
});
if subprec as usize <= prec as usize || *error {
break;
}
rhs = parse_expr_helper(hashmap, args, rhs, subprec, error);
}
lhs = match prec {
Precedence::UnOp | Precedence::BUnOp => {
*error = true;
false
}
Precedence::And => lhs && rhs,
Precedence::Or => lhs || rhs,
Precedence::BinOp => three(
&[
if lhs { b" " } else { b"" },
op,
if rhs { b" " } else { b"" },
],
error,
),
Precedence::Paren => unimplemented!(), // TODO: implement parentheses
_ => unreachable!(),
};
if !args.is_empty() {
prec = *hashmap.get(&args[0]).unwrap_or_else(|| {
*error = true;
&min_prec
});
}
}
lhs
}
#[inline]
fn setup_hashmap<'a>() -> HashMap<&'a [u8], Precedence> {
let mut hashmap = HashMap::<&'a [u8], Precedence>::new();
hashmap.insert(b"-b", Precedence::UnOp);
hashmap.insert(b"-c", Precedence::UnOp);
hashmap.insert(b"-d", Precedence::UnOp);
hashmap.insert(b"-e", Precedence::UnOp);
hashmap.insert(b"-f", Precedence::UnOp);
hashmap.insert(b"-g", Precedence::UnOp);
hashmap.insert(b"-h", Precedence::UnOp);
hashmap.insert(b"-L", Precedence::UnOp);
hashmap.insert(b"-n", Precedence::UnOp);
hashmap.insert(b"-p", Precedence::UnOp);
hashmap.insert(b"-r", Precedence::UnOp);
hashmap.insert(b"-S", Precedence::UnOp);
hashmap.insert(b"-s", Precedence::UnOp);
hashmap.insert(b"-t", Precedence::UnOp);
hashmap.insert(b"-u", Precedence::UnOp);
hashmap.insert(b"-w", Precedence::UnOp);
hashmap.insert(b"-x", Precedence::UnOp);
hashmap.insert(b"-z", Precedence::UnOp);
hashmap.insert(b"=", Precedence::BinOp);
hashmap.insert(b"!=", Precedence::BinOp);
hashmap.insert(b"-eq", Precedence::BinOp);
hashmap.insert(b"-ne", Precedence::BinOp);
hashmap.insert(b"-gt", Precedence::BinOp);
hashmap.insert(b"-ge", Precedence::BinOp);
hashmap.insert(b"-lt", Precedence::BinOp);
hashmap.insert(b"-le", Precedence::BinOp);
hashmap.insert(b"!", Precedence::BUnOp);
hashmap.insert(b"-a", Precedence::And);
hashmap.insert(b"-o", Precedence::Or);
hashmap.insert(b"(", Precedence::Paren);
hashmap.insert(b")", Precedence::Paren);
hashmap
}
#[derive(Eq, PartialEq)] #[derive(Eq, PartialEq)]
enum PathCondition { enum PathCondition {
BlockSpecial, BlockSpecial,
@ -334,14 +177,10 @@ enum PathCondition {
} }
#[cfg(not(windows))] #[cfg(not(windows))]
fn path(path: &[u8], cond: PathCondition) -> bool { fn path(path: &OsStr, cond: PathCondition) -> bool {
use std::ffi::OsStr;
use std::fs::{self, Metadata}; use std::fs::{self, Metadata};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::os::unix::fs::{FileTypeExt, MetadataExt};
let path = OsStr::from_bytes(path);
const S_ISUID: u32 = 0o4000; const S_ISUID: u32 = 0o4000;
const S_ISGID: u32 = 0o2000; const S_ISGID: u32 = 0o2000;
@ -403,13 +242,14 @@ fn path(path: &[u8], cond: PathCondition) -> bool {
} }
#[cfg(windows)] #[cfg(windows)]
fn path(path: &[u8], cond: PathCondition) -> bool { fn path(path: &OsStr, cond: PathCondition) -> bool {
use std::fs::metadata; use std::fs::metadata;
let path = from_utf8(path).unwrap();
let stat = match metadata(path) { let stat = match metadata(path) {
Ok(s) => s, Ok(s) => s,
_ => return false, _ => return false,
}; };
match cond { match cond {
PathCondition::BlockSpecial => false, PathCondition::BlockSpecial => false,
PathCondition::CharacterSpecial => false, PathCondition::CharacterSpecial => false,

View file

@ -23,11 +23,9 @@ use std::io::{stdin, stdout, BufRead, BufWriter, Write};
use crate::expand::ExpandSet; use crate::expand::ExpandSet;
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
static NAME: &str = "tr";
static VERSION: &str = env!("CARGO_PKG_VERSION"); static VERSION: &str = env!("CARGO_PKG_VERSION");
static ABOUT: &str = "translate or delete characters"; static ABOUT: &str = "translate or delete characters";
static LONG_HELP: &str = "Translate, squeeze, and/or delete characters from standard input,
writing to standard output.";
const BUFFER_LEN: usize = 1024; const BUFFER_LEN: usize = 1024;
mod options { mod options {
@ -125,10 +123,17 @@ impl SymbolTranslator for DeleteAndSqueezeOperation {
struct TranslateOperation { struct TranslateOperation {
translate_map: FnvHashMap<usize, char>, translate_map: FnvHashMap<usize, char>,
complement: bool,
s2_last: char,
} }
impl TranslateOperation { impl TranslateOperation {
fn new(set1: ExpandSet, set2: &mut ExpandSet, truncate: bool) -> TranslateOperation { fn new(
set1: ExpandSet,
set2: &mut ExpandSet,
truncate: bool,
complement: bool,
) -> TranslateOperation {
let mut map = FnvHashMap::default(); let mut map = FnvHashMap::default();
let mut s2_prev = '_'; let mut s2_prev = '_';
for i in set1 { for i in set1 {
@ -141,13 +146,54 @@ impl TranslateOperation {
map.insert(i as usize, s2_prev); map.insert(i as usize, s2_prev);
} }
} }
TranslateOperation { translate_map: map } TranslateOperation {
translate_map: map,
complement,
s2_last: set2.last().unwrap_or(s2_prev),
}
} }
} }
impl SymbolTranslator for TranslateOperation { impl SymbolTranslator for TranslateOperation {
fn translate(&self, c: char, _prev_c: char) -> Option<char> { fn translate(&self, c: char, _prev_c: char) -> Option<char> {
Some(*self.translate_map.get(&(c as usize)).unwrap_or(&c)) if self.complement {
Some(if self.translate_map.contains_key(&(c as usize)) {
c
} else {
self.s2_last
})
} else {
Some(*self.translate_map.get(&(c as usize)).unwrap_or(&c))
}
}
}
struct TranslateAndSqueezeOperation {
translate: TranslateOperation,
squeeze: SqueezeOperation,
}
impl TranslateAndSqueezeOperation {
fn new(
set1: ExpandSet,
set2: &mut ExpandSet,
set2_: ExpandSet,
truncate: bool,
complement: bool,
) -> TranslateAndSqueezeOperation {
TranslateAndSqueezeOperation {
translate: TranslateOperation::new(set1, set2, truncate, complement),
squeeze: SqueezeOperation::new(set2_, complement),
}
}
}
impl SymbolTranslator for TranslateAndSqueezeOperation {
fn translate(&self, c: char, prev_c: char) -> Option<char> {
// `unwrap()` will never panic because `Translate.translate()`
// always returns `Some`.
self.squeeze
.translate(self.translate.translate(c, 0 as char).unwrap(), prev_c)
} }
} }
@ -168,8 +214,11 @@ fn translate_input<T: SymbolTranslator>(
// isolation to make borrow checker happy // isolation to make borrow checker happy
let filtered = buf.chars().filter_map(|c| { let filtered = buf.chars().filter_map(|c| {
let res = translator.translate(c, prev_c); let res = translator.translate(c, prev_c);
if res.is_some() { // Set `prev_c` to the post-translate character. This
prev_c = c; // allows the squeeze operation to correctly function
// after the translate operation.
if let Some(rc) = res {
prev_c = rc;
} }
res res
}); });
@ -186,24 +235,38 @@ fn get_usage() -> String {
format!("{} [OPTION]... SET1 [SET2]", executable!()) format!("{} [OPTION]... SET1 [SET2]", executable!())
} }
fn get_long_usage() -> String {
String::from(
"Translate, squeeze, and/or delete characters from standard input,
writing to standard output.",
)
}
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
let usage = get_usage();
let args = args let args = args
.collect_str(InvalidEncodingHandling::ConvertLossy) .collect_str(InvalidEncodingHandling::ConvertLossy)
.accept_any(); .accept_any();
let usage = get_usage();
let after_help = get_long_usage();
let matches = App::new(executable!()) let matches = App::new(executable!())
.version(VERSION) .version(VERSION)
.about(ABOUT) .about(ABOUT)
.usage(&usage[..]) .usage(&usage[..])
.after_help(LONG_HELP) .after_help(&after_help[..])
.arg( .arg(
Arg::with_name(options::COMPLEMENT) Arg::with_name(options::COMPLEMENT)
.short("C") // .visible_short_alias('C') // TODO: requires clap "3.0.0-beta.2"
.short("c") .short("c")
.long(options::COMPLEMENT) .long(options::COMPLEMENT)
.help("use the complement of SET1"), .help("use the complement of SET1"),
) )
.arg(
Arg::with_name("C") // work around for `Arg::visible_short_alias`
.short("C")
.help("same as -c"),
)
.arg( .arg(
Arg::with_name(options::DELETE) Arg::with_name(options::DELETE)
.short("d") .short("d")
@ -216,8 +279,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.short("s") .short("s")
.help( .help(
"replace each sequence of a repeated character that is "replace each sequence of a repeated character that is
listed in the last specified SET, with a single occurrence listed in the last specified SET, with a single occurrence
of that character", of that character",
), ),
) )
.arg( .arg(
@ -230,7 +293,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.get_matches_from(args); .get_matches_from(args);
let delete_flag = matches.is_present(options::DELETE); let delete_flag = matches.is_present(options::DELETE);
let complement_flag = matches.is_present(options::COMPLEMENT); let complement_flag = matches.is_present(options::COMPLEMENT) || matches.is_present("C");
let squeeze_flag = matches.is_present(options::SQUEEZE); let squeeze_flag = matches.is_present(options::SQUEEZE);
let truncate_flag = matches.is_present(options::TRUNCATE); let truncate_flag = matches.is_present(options::TRUNCATE);
@ -242,7 +305,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
if sets.is_empty() { if sets.is_empty() {
show_error!( show_error!(
"missing operand\nTry `{} --help` for more information.", "missing operand\nTry `{} --help` for more information.",
NAME executable!()
); );
return 1; return 1;
} }
@ -251,16 +314,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
show_error!( show_error!(
"missing operand after {}\nTry `{} --help` for more information.", "missing operand after {}\nTry `{} --help` for more information.",
sets[0], sets[0],
NAME executable!()
); );
return 1; return 1;
} }
if complement_flag && !delete_flag && !squeeze_flag {
show_error!("-c is only supported with -d or -s");
return 1;
}
let stdin = stdin(); let stdin = stdin();
let mut locked_stdin = stdin.lock(); let mut locked_stdin = stdin.lock();
let stdout = stdout(); let stdout = stdout();
@ -278,12 +336,25 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
translate_input(&mut locked_stdin, &mut buffered_stdout, op); translate_input(&mut locked_stdin, &mut buffered_stdout, op);
} }
} else if squeeze_flag { } else if squeeze_flag {
let op = SqueezeOperation::new(set1, complement_flag); if sets.len() < 2 {
translate_input(&mut locked_stdin, &mut buffered_stdout, op); let op = SqueezeOperation::new(set1, complement_flag);
translate_input(&mut locked_stdin, &mut buffered_stdout, op);
} else {
let mut set2 = ExpandSet::new(sets[1].as_ref());
let set2_ = ExpandSet::new(sets[1].as_ref());
let op = TranslateAndSqueezeOperation::new(
set1,
&mut set2,
set2_,
complement_flag,
truncate_flag,
);
translate_input(&mut locked_stdin, &mut buffered_stdout, op);
}
} else { } else {
let mut set2 = ExpandSet::new(sets[1].as_ref()); let mut set2 = ExpandSet::new(sets[1].as_ref());
let op = TranslateOperation::new(set1, &mut set2, truncate_flag); let op = TranslateOperation::new(set1, &mut set2, truncate_flag, complement_flag);
translate_input(&mut locked_stdin, &mut buffered_stdout, op) translate_input(&mut locked_stdin, &mut buffered_stdout, op);
} }
0 0

View file

@ -0,0 +1,72 @@
//! Traits and implementations for iterating over lines in a file-like object.
//!
//! This module provides a [`WordCountable`] trait and implementations
//! for some common file-like objects. Use the [`WordCountable::lines`]
//! method to get an iterator over lines of a file-like object.
use std::fs::File;
use std::io::{self, BufRead, BufReader, Read, StdinLock};
#[cfg(unix)]
use std::os::unix::io::AsRawFd;
#[cfg(unix)]
pub trait WordCountable: AsRawFd + Read {
type Buffered: BufRead;
fn lines(self) -> Lines<Self::Buffered>;
}
#[cfg(not(unix))]
pub trait WordCountable: Read {
type Buffered: BufRead;
fn lines(self) -> Lines<Self::Buffered>;
}
impl WordCountable for StdinLock<'_> {
type Buffered = Self;
fn lines(self) -> Lines<Self::Buffered>
where
Self: Sized,
{
Lines { buf: self }
}
}
impl WordCountable for File {
type Buffered = BufReader<Self>;
fn lines(self) -> Lines<Self::Buffered>
where
Self: Sized,
{
Lines {
buf: BufReader::new(self),
}
}
}
/// An iterator over the lines of an instance of `BufRead`.
///
/// Similar to [`io::Lines`] but yields each line as a `Vec<u8>` and
/// includes the newline character (`\n`, the `0xA` byte) that
/// terminates the line.
///
/// [`io::Lines`]:: io::Lines
pub struct Lines<B> {
buf: B,
}
impl<B: BufRead> Iterator for Lines<B> {
type Item = io::Result<Vec<u8>>;
fn next(&mut self) -> Option<Self::Item> {
let mut line = Vec::new();
// reading from a TTY seems to raise a condition on, rather than return Some(0) like a file.
// hence the option wrapped in a result here
match self.buf.read_until(b'\n', &mut line) {
Ok(0) => None,
Ok(_n) => Some(Ok(line)),
Err(e) => Some(Err(e)),
}
}
}

View file

@ -11,17 +11,17 @@
extern crate uucore; extern crate uucore;
mod count_bytes; mod count_bytes;
mod countable;
use count_bytes::count_bytes_fast; use count_bytes::count_bytes_fast;
use countable::WordCountable;
use clap::{App, Arg, ArgMatches}; use clap::{App, Arg, ArgMatches};
use thiserror::Error; use thiserror::Error;
use std::cmp::max; use std::cmp::max;
use std::fs::File; use std::fs::File;
use std::io::{self, BufRead, BufReader, Read, StdinLock, Write}; use std::io::{self, Write};
use std::ops::{Add, AddAssign}; use std::ops::{Add, AddAssign};
#[cfg(unix)]
use std::os::unix::io::AsRawFd;
use std::path::Path; use std::path::Path;
use std::str::from_utf8; use std::str::from_utf8;
@ -82,32 +82,6 @@ impl Settings {
} }
} }
#[cfg(unix)]
trait WordCountable: AsRawFd + Read {
type Buffered: BufRead;
fn get_buffered(self) -> Self::Buffered;
}
#[cfg(not(unix))]
trait WordCountable: Read {
type Buffered: BufRead;
fn get_buffered(self) -> Self::Buffered;
}
impl WordCountable for StdinLock<'_> {
type Buffered = Self;
fn get_buffered(self) -> Self::Buffered {
self
}
}
impl WordCountable for File {
type Buffered = BufReader<Self>;
fn get_buffered(self) -> Self::Buffered {
BufReader::new(self)
}
}
#[derive(Debug, Default, Copy, Clone)] #[derive(Debug, Default, Copy, Clone)]
struct WordCount { struct WordCount {
bytes: usize, bytes: usize,
@ -270,25 +244,16 @@ fn word_count_from_reader<T: WordCountable>(
let mut byte_count: usize = 0; let mut byte_count: usize = 0;
let mut char_count: usize = 0; let mut char_count: usize = 0;
let mut longest_line_length: usize = 0; let mut longest_line_length: usize = 0;
let mut raw_line = Vec::new();
let mut ends_lf: bool; let mut ends_lf: bool;
// reading from a TTY seems to raise a condition on, rather than return Some(0) like a file. // reading from a TTY seems to raise a condition on, rather than return Some(0) like a file.
// hence the option wrapped in a result here // hence the option wrapped in a result here
let mut buffered_reader = reader.get_buffered(); for line_result in reader.lines() {
loop { let raw_line = match line_result {
match buffered_reader.read_until(LF, &mut raw_line) { Ok(l) => l,
Ok(n) => { Err(e) => {
if n == 0 { show_warning!("Error while reading {}: {}", path, e);
break; continue;
}
}
Err(ref e) => {
if !raw_line.is_empty() {
show_warning!("Error while reading {}: {}", path, e);
} else {
break;
}
} }
}; };
@ -317,8 +282,6 @@ fn word_count_from_reader<T: WordCountable>(
longest_line_length = current_char_count - (ends_lf as usize); longest_line_length = current_char_count - (ends_lf as usize);
} }
} }
raw_line.truncate(0);
} }
Ok(WordCount { Ok(WordCount {

View file

@ -17,6 +17,7 @@ path = "src/who.rs"
[dependencies] [dependencies]
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["utmpx"] } uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["utmpx"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
clap = "2.33.3"
[[bin]] [[bin]]
name = "who" name = "who"

View file

@ -12,79 +12,169 @@ extern crate uucore;
use uucore::libc::{ttyname, STDIN_FILENO, S_IWGRP}; use uucore::libc::{ttyname, STDIN_FILENO, S_IWGRP};
use uucore::utmpx::{self, time, Utmpx}; use uucore::utmpx::{self, time, Utmpx};
use clap::{App, Arg};
use std::borrow::Cow; use std::borrow::Cow;
use std::ffi::CStr; use std::ffi::CStr;
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::path::PathBuf; use std::path::PathBuf;
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
static SYNTAX: &str = "[OPTION]... [ FILE | ARG1 ARG2 ]"; mod options {
static SUMMARY: &str = "Print information about users who are currently logged in."; pub const ALL: &str = "all";
static LONG_HELP: &str = " pub const BOOT: &str = "boot";
-a, --all same as -b -d --login -p -r -t -T -u pub const DEAD: &str = "dead";
-b, --boot time of last system boot pub const HEADING: &str = "heading";
-d, --dead print dead processes pub const LOGIN: &str = "login";
-H, --heading print line of column headings pub const LOOKUP: &str = "lookup";
-l, --login print system login processes pub const ONLY_HOSTNAME_USER: &str = "only_hostname_user";
--lookup attempt to canonicalize hostnames via DNS pub const PROCESS: &str = "process";
-m only hostname and user associated with stdin pub const COUNT: &str = "count";
-p, --process print active processes spawned by init #[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
-q, --count all login names and number of users logged on pub const RUNLEVEL: &str = "runlevel";
-r, --runlevel print current runlevel (not available on BSDs) pub const SHORT: &str = "short";
-s, --short print only name, line, and time (default) pub const TIME: &str = "time";
-t, --time print last system clock change pub const USERS: &str = "users";
-T, -w, --mesg add user's message status as +, - or ? pub const MESG: &str = "mesg"; // aliases: --message, --writable
-u, --users list users logged in pub const FILE: &str = "FILE"; // if length=1: FILE, if length=2: ARG1 ARG2
--message same as -T }
--writable same as -T
--help display this help and exit
--version output version information and exit
If FILE is not specified, use /var/run/utmp. /var/log/wtmp as FILE is common. static VERSION: &str = env!("CARGO_PKG_VERSION");
If ARG1 ARG2 given, -m presumed: 'am i' or 'mom likes' are usual. static ABOUT: &str = "Print information about users who are currently logged in.";
";
fn get_usage() -> String {
format!("{0} [OPTION]... [ FILE | ARG1 ARG2 ]", executable!())
}
fn get_long_usage() -> String {
String::from(
"If FILE is not specified, use /var/run/utmp. /var/log/wtmp as FILE is common.\n\
If ARG1 ARG2 given, -m presumed: 'am i' or 'mom likes' are usual.",
)
}
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
let args = args let args = args
.collect_str(InvalidEncodingHandling::Ignore) .collect_str(InvalidEncodingHandling::Ignore)
.accept_any(); .accept_any();
let mut opts = app!(SYNTAX, SUMMARY, LONG_HELP); let usage = get_usage();
opts.optflag("a", "all", "same as -b -d --login -p -r -t -T -u"); let after_help = get_long_usage();
opts.optflag("b", "boot", "time of last system boot");
opts.optflag("d", "dead", "print dead processes");
opts.optflag("H", "heading", "print line of column headings");
opts.optflag("l", "login", "print system login processes");
opts.optflag("", "lookup", "attempt to canonicalize hostnames via DNS");
opts.optflag("m", "", "only hostname and user associated with stdin");
opts.optflag("p", "process", "print active processes spawned by init");
opts.optflag(
"q",
"count",
"all login names and number of users logged on",
);
#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
opts.optflag("r", "runlevel", "print current runlevel");
opts.optflag("s", "short", "print only name, line, and time (default)");
opts.optflag("t", "time", "print last system clock change");
opts.optflag("u", "users", "list users logged in");
opts.optflag("w", "mesg", "add user's message status as +, - or ?");
// --message, --writable are the same as --mesg
opts.optflag("T", "message", "");
opts.optflag("T", "writable", "");
opts.optflag("", "help", "display this help and exit"); let matches = App::new(executable!())
opts.optflag("", "version", "output version information and exit"); .version(VERSION)
.about(ABOUT)
.usage(&usage[..])
.after_help(&after_help[..])
.arg(
Arg::with_name(options::ALL)
.long(options::ALL)
.short("a")
.help("same as -b -d --login -p -r -t -T -u"),
)
.arg(
Arg::with_name(options::BOOT)
.long(options::BOOT)
.short("b")
.help("time of last system boot"),
)
.arg(
Arg::with_name(options::DEAD)
.long(options::DEAD)
.short("d")
.help("print dead processes"),
)
.arg(
Arg::with_name(options::HEADING)
.long(options::HEADING)
.short("H")
.help("print line of column headings"),
)
.arg(
Arg::with_name(options::LOGIN)
.long(options::LOGIN)
.short("l")
.help("print system login processes"),
)
.arg(
Arg::with_name(options::LOOKUP)
.long(options::LOOKUP)
.help("attempt to canonicalize hostnames via DNS"),
)
.arg(
Arg::with_name(options::ONLY_HOSTNAME_USER)
.short("m")
.help("only hostname and user associated with stdin"),
)
.arg(
Arg::with_name(options::PROCESS)
.long(options::PROCESS)
.short("p")
.help("print active processes spawned by init"),
)
.arg(
Arg::with_name(options::COUNT)
.long(options::COUNT)
.short("q")
.help("all login names and number of users logged on"),
)
.arg(
#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
Arg::with_name(options::RUNLEVEL)
.long(options::RUNLEVEL)
.short("r")
.help("print current runlevel"),
)
.arg(
Arg::with_name(options::SHORT)
.long(options::SHORT)
.short("s")
.help("print only name, line, and time (default)"),
)
.arg(
Arg::with_name(options::TIME)
.long(options::TIME)
.short("t")
.help("print last system clock change"),
)
.arg(
Arg::with_name(options::USERS)
.long(options::USERS)
.short("u")
.help("list users logged in"),
)
.arg(
Arg::with_name(options::MESG)
.long(options::MESG)
.short("T")
// .visible_short_alias('w') // TODO: requires clap "3.0.0-beta.2"
.visible_aliases(&["message", "writable"])
.help("add user's message status as +, - or ?"),
)
.arg(
Arg::with_name("w") // work around for `Arg::visible_short_alias`
.short("w")
.help("same as -T"),
)
.arg(
Arg::with_name(options::FILE)
.takes_value(true)
.min_values(1)
.max_values(2),
)
.get_matches_from(args);
let matches = opts.parse(args); let files: Vec<String> = matches
.values_of(options::FILE)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();
// If true, attempt to canonicalize hostnames via a DNS lookup. // If true, attempt to canonicalize hostnames via a DNS lookup.
let do_lookup = matches.opt_present("lookup"); let do_lookup = matches.is_present(options::LOOKUP);
// If true, display only a list of usernames and count of // If true, display only a list of usernames and count of
// the users logged on. // the users logged on.
// Ignored for 'who am i'. // Ignored for 'who am i'.
let short_list = matches.opt_present("q"); let short_list = matches.is_present(options::COUNT);
// If true, display only name, line, and time fields. // If true, display only name, line, and time fields.
let mut short_output = false; let mut short_output = false;
@ -95,12 +185,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let mut include_idle = false; let mut include_idle = false;
// If true, display a line at the top describing each field. // If true, display a line at the top describing each field.
let include_heading = matches.opt_present("H"); let include_heading = matches.is_present(options::HEADING);
// If true, display a '+' for each user if mesg y, a '-' if mesg n, // If true, display a '+' for each user if mesg y, a '-' if mesg n,
// or a '?' if their tty cannot be statted. // or a '?' if their tty cannot be statted.
let include_mesg = let include_mesg = matches.is_present(options::ALL)
matches.opt_present("a") || matches.opt_present("T") || matches.opt_present("w"); || matches.is_present(options::MESG)
|| matches.is_present("w");
// If true, display process termination & exit status. // If true, display process termination & exit status.
let mut include_exit = false; let mut include_exit = false;
@ -133,7 +224,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
#[allow(clippy::useless_let_if_seq)] #[allow(clippy::useless_let_if_seq)]
{ {
if matches.opt_present("a") { if matches.is_present(options::ALL) {
need_boottime = true; need_boottime = true;
need_deadprocs = true; need_deadprocs = true;
need_login = true; need_login = true;
@ -146,49 +237,49 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
assumptions = false; assumptions = false;
} }
if matches.opt_present("b") { if matches.is_present(options::BOOT) {
need_boottime = true; need_boottime = true;
assumptions = false; assumptions = false;
} }
if matches.opt_present("d") { if matches.is_present(options::DEAD) {
need_deadprocs = true; need_deadprocs = true;
include_idle = true; include_idle = true;
include_exit = true; include_exit = true;
assumptions = false; assumptions = false;
} }
if matches.opt_present("l") { if matches.is_present(options::LOGIN) {
need_login = true; need_login = true;
include_idle = true; include_idle = true;
assumptions = false; assumptions = false;
} }
if matches.opt_present("m") || matches.free.len() == 2 { if matches.is_present(options::ONLY_HOSTNAME_USER) || files.len() == 2 {
my_line_only = true; my_line_only = true;
} }
if matches.opt_present("p") { if matches.is_present(options::PROCESS) {
need_initspawn = true; need_initspawn = true;
assumptions = false; assumptions = false;
} }
if matches.opt_present("r") { if matches.is_present(options::RUNLEVEL) {
need_runlevel = true; need_runlevel = true;
include_idle = true; include_idle = true;
assumptions = false; assumptions = false;
} }
if matches.opt_present("s") { if matches.is_present(options::SHORT) {
short_output = true; short_output = true;
} }
if matches.opt_present("t") { if matches.is_present(options::TIME) {
need_clockchange = true; need_clockchange = true;
assumptions = false; assumptions = false;
} }
if matches.opt_present("u") { if matches.is_present(options::USERS) {
need_users = true; need_users = true;
include_idle = true; include_idle = true;
assumptions = false; assumptions = false;
@ -202,11 +293,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
if include_exit { if include_exit {
short_output = false; short_output = false;
} }
if matches.free.len() > 2 {
show_usage_error!("{}", msg_wrong_number_of_arguments!());
exit!(1);
}
} }
let mut who = Who { let mut who = Who {
@ -225,7 +311,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
need_runlevel, need_runlevel,
need_users, need_users,
my_line_only, my_line_only,
args: matches.free, args: files,
}; };
who.exec(); who.exec();

View file

@ -132,19 +132,15 @@ fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) {
(srwx, pos) (srwx, pos)
} }
pub fn parse_mode(mode: Option<String>) -> Result<mode_t, String> { pub fn parse_mode(mode: &str) -> Result<mode_t, String> {
let fperm = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; let fperm = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
if let Some(mode) = mode { let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
let arr: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; let result = if mode.contains(arr) {
let result = if mode.contains(arr) { parse_numeric(fperm as u32, mode)
parse_numeric(fperm as u32, mode.as_str())
} else {
parse_symbolic(fperm as u32, mode.as_str(), true)
};
result.map(|mode| mode as mode_t)
} else { } else {
Ok(fperm) parse_symbolic(fperm as u32, mode, true)
} };
result.map(|mode| mode as mode_t)
} }
#[cfg(test)] #[cfg(test)]
@ -152,20 +148,19 @@ mod test {
#[test] #[test]
fn symbolic_modes() { fn symbolic_modes() {
assert_eq!(super::parse_mode(Some("u+x".to_owned())).unwrap(), 0o766); assert_eq!(super::parse_mode("u+x").unwrap(), 0o766);
assert_eq!( assert_eq!(
super::parse_mode(Some("+x".to_owned())).unwrap(), super::parse_mode("+x").unwrap(),
if !crate::os::is_wsl_1() { 0o777 } else { 0o776 } if !crate::os::is_wsl_1() { 0o777 } else { 0o776 }
); );
assert_eq!(super::parse_mode(Some("a-w".to_owned())).unwrap(), 0o444); assert_eq!(super::parse_mode("a-w").unwrap(), 0o444);
assert_eq!(super::parse_mode(Some("g-r".to_owned())).unwrap(), 0o626); assert_eq!(super::parse_mode("g-r").unwrap(), 0o626);
} }
#[test] #[test]
fn numeric_modes() { fn numeric_modes() {
assert_eq!(super::parse_mode(Some("644".to_owned())).unwrap(), 0o644); assert_eq!(super::parse_mode("644").unwrap(), 0o644);
assert_eq!(super::parse_mode(Some("+100".to_owned())).unwrap(), 0o766); assert_eq!(super::parse_mode("+100").unwrap(), 0o766);
assert_eq!(super::parse_mode(Some("-4".to_owned())).unwrap(), 0o662); assert_eq!(super::parse_mode("-4").unwrap(), 0o662);
assert_eq!(super::parse_mode(None).unwrap(), 0o666);
} }
} }

View file

@ -98,7 +98,7 @@ fn test_wrap_bad_arg() {
.arg(wrap_param) .arg(wrap_param)
.arg("b") .arg("b")
.fails() .fails()
.stderr_only("base32: error: invalid wrap size: b: invalid digit found in string\n"); .stderr_only("base32: error: Invalid wrap size: b: invalid digit found in string\n");
} }
} }

View file

@ -7,6 +7,21 @@ fn test_encode() {
.pipe_in(input) .pipe_in(input)
.succeeds() .succeeds()
.stdout_only("aGVsbG8sIHdvcmxkIQ==\n"); .stdout_only("aGVsbG8sIHdvcmxkIQ==\n");
// Using '-' as our file
new_ucmd!()
.arg("-")
.pipe_in(input)
.succeeds()
.stdout_only("aGVsbG8sIHdvcmxkIQ==\n");
}
#[test]
fn test_base64_encode_file() {
new_ucmd!()
.arg("input-simple.txt")
.succeeds()
.stdout_only("SGVsbG8sIFdvcmxkIQo=\n");
} }
#[test] #[test]
@ -60,10 +75,9 @@ fn test_wrap() {
#[test] #[test]
fn test_wrap_no_arg() { fn test_wrap_no_arg() {
for wrap_param in vec!["-w", "--wrap"] { for wrap_param in vec!["-w", "--wrap"] {
new_ucmd!().arg(wrap_param).fails().stderr_only(format!( new_ucmd!().arg(wrap_param).fails().stderr_contains(
"base64: error: Argument to option '{}' missing\n", &"The argument '--wrap <wrap>' requires a value but none was supplied",
if wrap_param == "-w" { "w" } else { "wrap" } );
));
} }
} }
@ -74,6 +88,24 @@ fn test_wrap_bad_arg() {
.arg(wrap_param) .arg(wrap_param)
.arg("b") .arg("b")
.fails() .fails()
.stderr_only("base64: error: invalid wrap size: b: invalid digit found in string\n"); .stderr_only("base64: error: Invalid wrap size: b: invalid digit found in string\n");
} }
} }
#[test]
fn test_base64_extra_operand() {
// Expect a failure when multiple files are specified.
new_ucmd!()
.arg("a.txt")
.arg("a.txt")
.fails()
.stderr_only("base64: error: extra operand a.txt");
}
#[test]
fn test_base64_file_not_found() {
new_ucmd!()
.arg("a.txt")
.fails()
.stderr_only("base64: error: a.txt: No such file or directory");
}

View file

@ -1,6 +1,29 @@
use crate::common::util::*; use crate::common::util::*;
use std::ffi::OsStr; use std::ffi::OsStr;
#[test]
fn test_help() {
for help_flg in vec!["-h", "--help"] {
new_ucmd!()
.arg(&help_flg)
.succeeds()
.no_stderr()
.stdout_contains("USAGE:");
}
}
#[test]
fn test_version() {
for version_flg in vec!["-V", "--version"] {
assert!(new_ucmd!()
.arg(&version_flg)
.succeeds()
.no_stderr()
.stdout_str()
.starts_with("basename"));
}
}
#[test] #[test]
fn test_directory() { fn test_directory() {
new_ucmd!() new_ucmd!()
@ -81,11 +104,25 @@ fn test_no_args() {
expect_error(vec![]); expect_error(vec![]);
} }
#[test]
fn test_no_args_output() {
new_ucmd!()
.fails()
.stderr_is("basename: error: missing operand\nTry 'basename --help' for more information.");
}
#[test] #[test]
fn test_too_many_args() { fn test_too_many_args() {
expect_error(vec!["a", "b", "c"]); expect_error(vec!["a", "b", "c"]);
} }
#[test]
fn test_too_many_args_output() {
new_ucmd!().args(&["a", "b", "c"]).fails().stderr_is(
"basename: error: extra operand 'c'\nTry 'basename --help' for more information.",
);
}
fn test_invalid_utf8_args(os_str: &OsStr) { fn test_invalid_utf8_args(os_str: &OsStr) {
let test_vec = vec![os_str.to_os_string()]; let test_vec = vec![os_str.to_os_string()];
new_ucmd!().args(&test_vec).succeeds().stdout_is("fo<EFBFBD>o\n"); new_ucmd!().args(&test_vec).succeeds().stdout_is("fo<EFBFBD>o\n");

View file

@ -20,4 +20,16 @@ fn test_df_compatible_si() {
new_ucmd!().arg("-aH").succeeds(); new_ucmd!().arg("-aH").succeeds();
} }
#[test]
fn test_df_output() {
if cfg!(target_os = "macos") {
new_ucmd!().arg("-H").arg("-total").succeeds().
stdout_only("Filesystem Size Used Available Capacity Use% Mounted on \n");
} else {
new_ucmd!().arg("-H").arg("-total").succeeds().stdout_only(
"Filesystem Size Used Available Use% Mounted on \n"
);
}
}
// ToDO: more tests... // ToDO: more tests...

View file

@ -16,6 +16,21 @@ fn test_path_without_trailing_slashes() {
.stdout_is("/root/alpha/beta/gamma/delta/epsilon\n"); .stdout_is("/root/alpha/beta/gamma/delta/epsilon\n");
} }
#[test]
fn test_path_without_trailing_slashes_and_zero() {
new_ucmd!()
.arg("-z")
.arg("/root/alpha/beta/gamma/delta/epsilon/omega")
.succeeds()
.stdout_is("/root/alpha/beta/gamma/delta/epsilon\u{0}");
new_ucmd!()
.arg("--zero")
.arg("/root/alpha/beta/gamma/delta/epsilon/omega")
.succeeds()
.stdout_is("/root/alpha/beta/gamma/delta/epsilon\u{0}");
}
#[test] #[test]
fn test_root() { fn test_root() {
new_ucmd!().arg("/").run().stdout_is("/\n"); new_ucmd!().arg("/").run().stdout_is("/\n");

View file

@ -33,18 +33,16 @@ fn test_fmt_w_too_big() {
"fmt: error: invalid width: '2501': Numerical result out of range" "fmt: error: invalid width: '2501': Numerical result out of range"
); );
} }
/* #[test] #[test]
Fails for now, see https://github.com/uutils/coreutils/issues/1501
fn test_fmt_w() { fn test_fmt_w() {
let result = new_ucmd!() let result = new_ucmd!()
.arg("-w") .arg("-w")
.arg("10") .arg("10")
.arg("one-word-per-line.txt") .arg("one-word-per-line.txt")
.run(); .run();
//.stdout_is_fixture("call_graph.expected"); //.stdout_is_fixture("call_graph.expected");
assert_eq!(result.stdout_str().trim(), "this is a file with one word per line"); assert_eq!(
result.stdout_str().trim(),
"this is\na file\nwith one\nword per\nline"
);
} }
fmt is pretty broken in general, needs more works to have more tests
*/

View file

@ -1 +1,126 @@
// ToDO: add tests use crate::common::util::*;
use regex::Regex;
use std::os::unix::process::ExitStatusExt;
use std::process::{Child, Command};
// A child process the tests will try to kill.
struct Target {
child: Child,
killed: bool,
}
impl Target {
// Creates a target that will naturally die after some time if not killed
// fast enough.
// This timeout avoids hanging failing tests.
fn new() -> Target {
Target {
child: Command::new("sleep")
.arg("30")
.spawn()
.expect("cannot spawn target"),
killed: false,
}
}
// Waits for the target to complete and returns the signal it received if any.
fn wait_for_signal(&mut self) -> Option<i32> {
let sig = self.child.wait().expect("cannot wait on target").signal();
self.killed = true;
sig
}
fn pid(&self) -> u32 {
self.child.id()
}
}
impl Drop for Target {
// Terminates this target to avoid littering test boxes with zombi processes
// when a test fails after creating a target but before killing it.
fn drop(&mut self) {
if !self.killed {
self.child.kill().expect("cannot kill target");
}
}
}
#[test]
fn test_kill_list_all_signals() {
// Check for a few signals. Do not try to be comprehensive.
new_ucmd!()
.arg("-l")
.succeeds()
.stdout_contains("KILL")
.stdout_contains("TERM")
.stdout_contains("HUP");
}
#[test]
fn test_kill_list_all_signals_as_table() {
// Check for a few signals. Do not try to be comprehensive.
new_ucmd!()
.arg("-t")
.succeeds()
.stdout_contains("KILL")
.stdout_contains("TERM")
.stdout_contains("HUP");
}
#[test]
fn test_kill_list_one_signal_from_name() {
// Use SIGKILL because it is 9 on all unixes.
new_ucmd!()
.arg("-l")
.arg("KILL")
.succeeds()
.stdout_matches(&Regex::new("\\b9\\b").unwrap());
}
#[test]
fn test_kill_set_bad_signal_name() {
new_ucmd!()
.arg("-s")
.arg("IAMNOTASIGNAL")
.fails()
.stderr_contains("unknown signal");
}
#[test]
fn test_kill_with_default_signal() {
let mut target = Target::new();
new_ucmd!().arg(format!("{}", target.pid())).succeeds();
assert_eq!(target.wait_for_signal(), Some(libc::SIGTERM));
}
#[test]
fn test_kill_with_signal_number_old_form() {
let mut target = Target::new();
new_ucmd!()
.arg("-9")
.arg(format!("{}", target.pid()))
.succeeds();
assert_eq!(target.wait_for_signal(), Some(9));
}
#[test]
fn test_kill_with_signal_number_new_form() {
let mut target = Target::new();
new_ucmd!()
.arg("-s")
.arg("9")
.arg(format!("{}", target.pid()))
.succeeds();
assert_eq!(target.wait_for_signal(), Some(9));
}
#[test]
fn test_kill_with_signal_name_new_form() {
let mut target = Target::new();
new_ucmd!()
.arg("-s")
.arg("KILL")
.arg(format!("{}", target.pid()))
.succeeds();
assert_eq!(target.wait_for_signal(), Some(libc::SIGKILL));
}

View file

@ -39,3 +39,25 @@ fn test_link_nonexistent_file() {
assert!(!at.file_exists(file)); assert!(!at.file_exists(file));
assert!(!at.file_exists(link)); assert!(!at.file_exists(link));
} }
#[test]
fn test_link_one_argument() {
let (_, mut ucmd) = at_and_ucmd!();
let file = "test_link_argument";
ucmd.args(&[file]).fails().stderr_contains(
"error: The argument '<FILES>...' requires at least 2 values, but only 1 was provide",
);
}
#[test]
fn test_link_three_arguments() {
let (_, mut ucmd) = at_and_ucmd!();
let arguments = vec![
"test_link_argument1",
"test_link_argument2",
"test_link_argument3",
];
ucmd.args(&arguments[..]).fails().stderr_contains(
format!("error: The value '{}' was provided to '<FILES>...', but it wasn't expecting any more values", arguments[2]),
);
}

View file

@ -5,6 +5,7 @@ use crate::common::util::*;
extern crate regex; extern crate regex;
use self::regex::Regex; use self::regex::Regex;
use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use std::thread::sleep; use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
@ -43,23 +44,74 @@ fn test_ls_a() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures; let at = &scene.fixtures;
at.touch(".test-1"); at.touch(".test-1");
at.mkdir("some-dir");
at.touch(
Path::new("some-dir")
.join(".test-2")
.as_os_str()
.to_str()
.unwrap(),
);
let result = scene.ucmd().succeeds(); let re_pwd = Regex::new(r"^\.\n").unwrap();
let stdout = result.stdout_str();
assert!(!stdout.contains(".test-1")); // Using the present working directory
assert!(!stdout.contains("..")); scene
.ucmd()
.arg("-1")
.succeeds()
.stdout_does_not_contain(".test-1")
.stdout_does_not_contain("..")
.stdout_does_not_match(&re_pwd);
scene scene
.ucmd() .ucmd()
.arg("-a") .arg("-a")
.arg("-1")
.succeeds() .succeeds()
.stdout_contains(&".test-1") .stdout_contains(&".test-1")
.stdout_contains(&".."); .stdout_contains(&"..")
.stdout_matches(&re_pwd);
let result = scene.ucmd().arg("-A").succeeds(); scene
result.stdout_contains(".test-1"); .ucmd()
let stdout = result.stdout_str(); .arg("-A")
assert!(!stdout.contains("..")); .arg("-1")
.succeeds()
.stdout_contains(".test-1")
.stdout_does_not_contain("..")
.stdout_does_not_match(&re_pwd);
// Using a subdirectory
scene
.ucmd()
.arg("-1")
.arg("some-dir")
.succeeds()
.stdout_does_not_contain(".test-2")
.stdout_does_not_contain("..")
.stdout_does_not_match(&re_pwd);
scene
.ucmd()
.arg("-a")
.arg("-1")
.arg("some-dir")
.succeeds()
.stdout_contains(&".test-2")
.stdout_contains(&"..")
.no_stderr()
.stdout_matches(&re_pwd);
scene
.ucmd()
.arg("-A")
.arg("-1")
.arg("some-dir")
.succeeds()
.stdout_contains(".test-2")
.stdout_does_not_contain("..")
.stdout_does_not_match(&re_pwd);
} }
#[test] #[test]
@ -257,6 +309,50 @@ fn test_ls_long() {
} }
} }
#[test]
fn test_ls_long_total_size() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch(&at.plus_as_string("test-long"));
at.append("test-long", "1");
at.touch(&at.plus_as_string("test-long2"));
at.append("test-long2", "2");
let expected_prints: HashMap<_, _> = if cfg!(unix) {
[
("long_vanilla", "total 8"),
("long_human_readable", "total 8.0K"),
("long_si", "total 8.2k"),
]
.iter()
.cloned()
.collect()
} else {
[
("long_vanilla", "total 2"),
("long_human_readable", "total 2"),
("long_si", "total 2"),
]
.iter()
.cloned()
.collect()
};
for arg in &["-l", "--long", "--format=long", "--format=verbose"] {
let result = scene.ucmd().arg(arg).succeeds();
result.stdout_contains(expected_prints["long_vanilla"]);
for arg2 in &["-h", "--human-readable", "--si"] {
let result = scene.ucmd().arg(arg).arg(arg2).succeeds();
result.stdout_contains(if *arg2 == "--si" {
expected_prints["long_si"]
} else {
expected_prints["long_human_readable"]
});
}
}
}
#[test] #[test]
fn test_ls_long_formats() { fn test_ls_long_formats() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
@ -482,7 +578,6 @@ fn test_ls_sort_name() {
.succeeds() .succeeds()
.stdout_is(["test-1", "test-2", "test-3\n"].join(sep)); .stdout_is(["test-1", "test-2", "test-3\n"].join(sep));
// Order of a named sort ignores leading dots.
let scene_dot = TestScenario::new(util_name!()); let scene_dot = TestScenario::new(util_name!());
let at = &scene_dot.fixtures; let at = &scene_dot.fixtures;
at.touch(".a"); at.touch(".a");
@ -495,7 +590,7 @@ fn test_ls_sort_name() {
.arg("--sort=name") .arg("--sort=name")
.arg("-A") .arg("-A")
.succeeds() .succeeds()
.stdout_is([".a", "a", ".b", "b\n"].join(sep)); .stdout_is([".a", ".b", "a", "b\n"].join(sep));
} }
#[test] #[test]
@ -559,8 +654,7 @@ fn test_ls_long_ctime() {
} }
#[test] #[test]
#[cfg(not(windows))] #[ignore]
// This test is currently failing on windows
fn test_ls_order_birthtime() { fn test_ls_order_birthtime() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());
let at = &scene.fixtures; let at = &scene.fixtures;
@ -570,15 +664,11 @@ fn test_ls_order_birthtime() {
After creating the first file try to sync it. After creating the first file try to sync it.
This ensures the file gets created immediately instead of being saved This ensures the file gets created immediately instead of being saved
inside the OS's IO operation buffer. inside the OS's IO operation buffer.
Without this, both files might accidentally be created at the same time, Without this, both files might accidentally be created at the same time.
even though we placed a timeout between creating the two.
https://github.com/uutils/coreutils/pull/1986/#issuecomment-828490651
*/ */
at.make_file("test-birthtime-1").sync_all().unwrap(); at.make_file("test-birthtime-1").sync_all().unwrap();
std::thread::sleep(std::time::Duration::from_millis(1)); at.make_file("test-birthtime-2").sync_all().unwrap();
at.make_file("test-birthtime-2"); at.open("test-birthtime-1");
at.touch("test-birthtime-1");
let result = scene.ucmd().arg("--time=birth").arg("-t").run(); let result = scene.ucmd().arg("--time=birth").arg("-t").run();
@ -602,7 +692,7 @@ fn test_ls_styles() {
Regex::new(r"[a-z-]* \d* \w* \w* \d* \d{4}-\d{2}-\d{2} \d{2}:\d{2} test\n").unwrap(); Regex::new(r"[a-z-]* \d* \w* \w* \d* \d{4}-\d{2}-\d{2} \d{2}:\d{2} test\n").unwrap();
let re_iso = Regex::new(r"[a-z-]* \d* \w* \w* \d* \d{2}-\d{2} \d{2}:\d{2} test\n").unwrap(); let re_iso = Regex::new(r"[a-z-]* \d* \w* \w* \d* \d{2}-\d{2} \d{2}:\d{2} test\n").unwrap();
let re_locale = let re_locale =
Regex::new(r"[a-z-]* \d* \w* \w* \d* [A-Z][a-z]{2} \d{2} \d{2}:\d{2} test\n").unwrap(); Regex::new(r"[a-z-]* \d* \w* \w* \d* [A-Z][a-z]{2} ( |\d)\d \d{2}:\d{2} test\n").unwrap();
//full-iso //full-iso
let result = scene let result = scene

View file

@ -1 +1,124 @@
// ToDO: add tests use crate::common::util::*;
#[cfg(not(windows))]
#[test]
fn test_mknod_help() {
new_ucmd!()
.arg("--help")
.succeeds()
.no_stderr()
.stdout_contains("USAGE:");
}
#[test]
#[cfg(not(windows))]
fn test_mknod_version() {
assert!(new_ucmd!()
.arg("--version")
.succeeds()
.no_stderr()
.stdout_str()
.starts_with("mknod"));
}
#[test]
#[cfg(not(windows))]
fn test_mknod_fifo_default_writable() {
let ts = TestScenario::new(util_name!());
ts.ucmd().arg("test_file").arg("p").succeeds();
assert!(ts.fixtures.is_fifo("test_file"));
assert!(!ts.fixtures.metadata("test_file").permissions().readonly());
}
#[test]
#[cfg(not(windows))]
fn test_mknod_fifo_mnemonic_usage() {
let ts = TestScenario::new(util_name!());
ts.ucmd().arg("test_file").arg("pipe").succeeds();
assert!(ts.fixtures.is_fifo("test_file"));
}
#[test]
#[cfg(not(windows))]
fn test_mknod_fifo_read_only() {
let ts = TestScenario::new(util_name!());
ts.ucmd()
.arg("-m")
.arg("a=r")
.arg("test_file")
.arg("p")
.succeeds();
assert!(ts.fixtures.is_fifo("test_file"));
assert!(ts.fixtures.metadata("test_file").permissions().readonly());
}
#[test]
#[cfg(not(windows))]
fn test_mknod_fifo_invalid_extra_operand() {
new_ucmd!()
.arg("test_file")
.arg("p")
.arg("1")
.arg("2")
.fails()
.stderr_contains(&"Fifos do not have major and minor device numbers");
}
#[test]
#[cfg(not(windows))]
fn test_mknod_character_device_requires_major_and_minor() {
new_ucmd!()
.arg("test_file")
.arg("c")
.fails()
.status_code(1)
.stderr_contains(&"Special files require major and minor device numbers.");
new_ucmd!()
.arg("test_file")
.arg("c")
.arg("1")
.fails()
.status_code(1)
.stderr_contains(&"Special files require major and minor device numbers.");
new_ucmd!()
.arg("test_file")
.arg("c")
.arg("1")
.arg("c")
.fails()
.status_code(1)
.stderr_contains(&"Invalid value for '<MINOR>'");
new_ucmd!()
.arg("test_file")
.arg("c")
.arg("c")
.arg("1")
.fails()
.status_code(1)
.stderr_contains(&"Invalid value for '<MAJOR>'");
}
#[test]
#[cfg(not(windows))]
fn test_mknod_invalid_arg() {
new_ucmd!()
.arg("--foo")
.fails()
.status_code(1)
.no_stdout()
.stderr_contains(&"Found argument '--foo' which wasn't expected");
}
#[test]
#[cfg(not(windows))]
fn test_mknod_invalid_mode() {
new_ucmd!()
.arg("--mode")
.arg("rw")
.arg("test_file")
.arg("p")
.fails()
.no_stdout()
.status_code(1)
.stderr_contains(&"invalid mode");
}

View file

@ -34,6 +34,36 @@ fn test_long_format() {
)); ));
} }
#[cfg(target_os = "linux")]
#[test]
fn test_long_format_multiple_users() {
let scene = TestScenario::new(util_name!());
let expected = scene
.cmd_keepenv(util_name!())
.env("LANGUAGE", "C")
.arg("-l")
.arg("root")
.arg("root")
.arg("root")
.succeeds();
scene
.ucmd()
.arg("-l")
.arg("root")
.arg("root")
.arg("root")
.succeeds()
.stdout_is(expected.stdout_str());
}
#[test]
fn test_long_format_wo_user() {
// "no username specified; at least one must be specified when using -l"
new_ucmd!().arg("-l").fails().code_is(1);
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[test] #[test]
fn test_short_format_i() { fn test_short_format_i() {

View file

@ -103,7 +103,7 @@ fn test_relpath_with_from_with_d() {
at.mkdir_all(from); at.mkdir_all(from);
// d is part of subpath -> expect relative path // d is part of subpath -> expect relative path
let mut result_stdout = scene let mut _result_stdout = scene
.ucmd() .ucmd()
.arg(to) .arg(to)
.arg(from) .arg(from)
@ -112,17 +112,17 @@ fn test_relpath_with_from_with_d() {
.stdout_move_str(); .stdout_move_str();
// relax rules for windows test environment // relax rules for windows test environment
#[cfg(not(windows))] #[cfg(not(windows))]
assert!(Path::new(&result_stdout).is_relative()); assert!(Path::new(&_result_stdout).is_relative());
// d is not part of subpath -> expect absolut path // d is not part of subpath -> expect absolut path
result_stdout = scene _result_stdout = scene
.ucmd() .ucmd()
.arg(to) .arg(to)
.arg(from) .arg(from)
.arg("-dnon_existing") .arg("-dnon_existing")
.succeeds() .succeeds()
.stdout_move_str(); .stdout_move_str();
assert!(Path::new(&result_stdout).is_absolute()); assert!(Path::new(&_result_stdout).is_absolute());
} }
} }
@ -135,12 +135,12 @@ fn test_relpath_no_from_no_d() {
let to: &str = &convert_path(test.to); let to: &str = &convert_path(test.to);
at.mkdir_all(to); at.mkdir_all(to);
let result_stdout = scene.ucmd().arg(to).succeeds().stdout_move_str(); let _result_stdout = scene.ucmd().arg(to).succeeds().stdout_move_str();
#[cfg(not(windows))] #[cfg(not(windows))]
assert_eq!(result_stdout, format!("{}\n", to)); assert_eq!(_result_stdout, format!("{}\n", to));
// relax rules for windows test environment // relax rules for windows test environment
#[cfg(windows)] #[cfg(windows)]
assert!(result_stdout.ends_with(&format!("{}\n", to))); assert!(_result_stdout.ends_with(&format!("{}\n", to)));
} }
} }

View file

@ -37,7 +37,29 @@ fn test_larger_than_specified_segment() {
.arg("50K") .arg("50K")
.arg("ext_sort.txt") .arg("ext_sort.txt")
.succeeds() .succeeds()
.stdout_is_fixture(format!("{}", "ext_sort.expected")); .stdout_is_fixture("ext_sort.expected");
}
#[test]
fn test_smaller_than_specified_segment() {
new_ucmd!()
.arg("-n")
.arg("-S")
.arg("100M")
.arg("ext_sort.txt")
.succeeds()
.stdout_is_fixture("ext_sort.expected");
}
#[test]
fn test_extsort_zero_terminated() {
new_ucmd!()
.arg("-z")
.arg("-S")
.arg("10K")
.arg("zero-terminated.txt")
.succeeds()
.stdout_is_fixture("zero-terminated.expected");
} }
#[test] #[test]

View file

@ -198,9 +198,16 @@ fn test_terse_normal_format() {
let expect = expected_result(&args); let expect = expected_result(&args);
println!("actual: {:?}", actual); println!("actual: {:?}", actual);
println!("expect: {:?}", expect); println!("expect: {:?}", expect);
let v_actual: Vec<&str> = actual.split(' ').collect(); let v_actual: Vec<&str> = actual.trim().split(' ').collect();
let v_expect: Vec<&str> = expect.split(' ').collect(); let mut v_expect: Vec<&str> = expect.trim().split(' ').collect();
assert!(!v_expect.is_empty()); assert!(!v_expect.is_empty());
// uu_stat does not support selinux
if v_actual.len() == v_expect.len() - 1 && v_expect[v_expect.len() - 1].contains(":") {
// assume last element contains: `SELinux security context string`
v_expect.pop();
}
// * allow for inequality if `stat` (aka, expect) returns "0" (unknown value) // * allow for inequality if `stat` (aka, expect) returns "0" (unknown value)
assert!( assert!(
expect == "0" expect == "0"

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) mahkoh (ju.orth [at] gmail [dot] com) // (c) mahkoh (ju.orth [at] gmail [dot] com)
// (c) Daniel Rocco <drocco@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,11 +10,466 @@
use crate::common::util::*; use crate::common::util::*;
#[test]
fn test_empty_test_equivalent_to_false() {
new_ucmd!().run().status_code(1);
}
#[test]
fn test_empty_string_is_false() {
new_ucmd!().arg("").run().status_code(1);
}
#[test]
fn test_solo_not() {
new_ucmd!().arg("!").succeeds();
}
#[test]
fn test_solo_and_or_or_is_a_literal() {
// /bin/test '' -a '' => 1; so test(1) must interpret `-a` by itself as
// a literal string
new_ucmd!().arg("-a").succeeds();
new_ucmd!().arg("-o").succeeds();
}
#[test]
fn test_double_not_is_false() {
new_ucmd!().args(&["!", "!"]).run().status_code(1);
}
#[test]
fn test_and_not_is_false() {
new_ucmd!().args(&["-a", "!"]).run().status_code(1);
}
#[test]
fn test_not_and_is_false() {
// `-a` is a literal here & has nonzero length
new_ucmd!().args(&["!", "-a"]).run().status_code(1);
}
#[test]
fn test_not_and_not_succeeds() {
new_ucmd!().args(&["!", "-a", "!"]).succeeds();
}
#[test]
fn test_simple_or() {
new_ucmd!().args(&["foo", "-o", ""]).succeeds();
}
#[test]
fn test_negated_or() {
new_ucmd!()
.args(&["!", "foo", "-o", "bar"])
.run()
.status_code(1);
new_ucmd!().args(&["foo", "-o", "!", "bar"]).succeeds();
new_ucmd!()
.args(&["!", "foo", "-o", "!", "bar"])
.run()
.status_code(1);
}
#[test]
fn test_strlen_of_nothing() {
// odd but matches GNU, which must interpret -n as a literal here
new_ucmd!().arg("-n").succeeds();
}
#[test]
fn test_strlen_of_empty() {
new_ucmd!().args(&["-n", ""]).run().status_code(1);
// STRING equivalent to -n STRING
new_ucmd!().arg("").run().status_code(1);
}
#[test]
fn test_nothing_is_empty() {
// -z is a literal here and has nonzero length
new_ucmd!().arg("-z").succeeds();
}
#[test]
fn test_zero_len_of_empty() {
new_ucmd!().args(&["-z", ""]).succeeds();
}
#[test]
fn test_solo_paren_is_literal() {
let scenario = TestScenario::new(util_name!());
let tests = [["("], [")"]];
for test in &tests {
scenario.ucmd().args(&test[..]).succeeds();
}
}
#[test]
fn test_solo_empty_parenthetical_is_error() {
new_ucmd!().args(&["(", ")"]).run().status_code(2);
}
#[test]
fn test_zero_len_equals_zero_len() {
new_ucmd!().args(&["", "=", ""]).succeeds();
}
#[test]
fn test_zero_len_not_equals_zero_len_is_false() {
new_ucmd!().args(&["", "!=", ""]).run().status_code(1);
}
#[test]
fn test_string_comparison() {
let scenario = TestScenario::new(util_name!());
let tests = [
["foo", "!=", "bar"],
["contained\nnewline", "=", "contained\nnewline"],
["(", "=", "("],
["(", "!=", ")"],
["!", "=", "!"],
];
for test in &tests {
scenario.ucmd().args(&test[..]).succeeds();
}
}
#[test]
#[ignore = "fixme: error reporting"]
fn test_dangling_string_comparison_is_error() {
new_ucmd!()
.args(&["missing_something", "="])
.run()
.status_code(2)
.stderr_is("test: missing argument after =");
}
#[test]
fn test_stringop_is_literal_after_bang() {
let scenario = TestScenario::new(util_name!());
let tests = [
["!", "="],
["!", "!="],
["!", "-eq"],
["!", "-ne"],
["!", "-lt"],
["!", "-le"],
["!", "-gt"],
["!", "-ge"],
["!", "-ef"],
["!", "-nt"],
["!", "-ot"],
];
for test in &tests {
scenario.ucmd().args(&test[..]).run().status_code(1);
}
}
#[test]
fn test_a_bunch_of_not() {
new_ucmd!()
.args(&["!", "", "!=", "", "-a", "!", "", "!=", ""])
.succeeds();
}
#[test]
fn test_pseudofloat_equal() {
new_ucmd!().args(&["123.45", "=", "123.45"]).succeeds();
}
#[test]
fn test_pseudofloat_not_equal() {
new_ucmd!().args(&["123.45", "!=", "123.450"]).succeeds();
}
#[test]
fn test_negative_arg_is_a_string() {
new_ucmd!().arg("-12345").succeeds();
new_ucmd!().arg("--qwert").succeeds();
}
#[test]
fn test_some_int_compares() {
let scenario = TestScenario::new(util_name!());
let tests = [
["0", "-eq", "0"],
["0", "-ne", "1"],
["421", "-lt", "3720"],
["0", "-le", "0"],
["11", "-gt", "10"],
["1024", "-ge", "512"],
["9223372036854775806", "-le", "9223372036854775807"],
];
for test in &tests {
scenario.ucmd().args(&test[..]).succeeds();
}
}
#[test]
#[ignore = "fixme: evaluation error (code 1); GNU returns 0"]
fn test_values_greater_than_i64_allowed() {
new_ucmd!()
.args(&["9223372036854775808", "-gt", "0"])
.succeeds();
}
#[test]
fn test_negative_int_compare() {
let scenario = TestScenario::new(util_name!());
let tests = [
["-1", "-eq", "-1"],
["-1", "-ne", "-2"],
["-3720", "-lt", "-421"],
["-10", "-le", "-10"],
["-21", "-gt", "-22"],
["-128", "-ge", "-256"],
["-9223372036854775808", "-le", "-9223372036854775807"],
];
for test in &tests {
scenario.ucmd().args(&test[..]).succeeds();
}
}
#[test]
fn test_float_inequality_is_error() {
new_ucmd!()
.args(&["123.45", "-ge", "6"])
.run()
.status_code(2)
.stderr_is("test: invalid integer 123.45");
}
#[test]
#[cfg(not(windows))]
fn test_invalid_utf8_integer_compare() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
let source = [0x66, 0x6f, 0x80, 0x6f];
let arg = OsStr::from_bytes(&source[..]);
let mut cmd = new_ucmd!();
cmd.arg("123").arg("-ne");
cmd.raw.arg(arg);
cmd.run()
.status_code(2)
.stderr_is("test: invalid integer fo<66>o");
let mut cmd = new_ucmd!();
cmd.raw.arg(arg);
cmd.arg("-eq").arg("456");
cmd.run()
.status_code(2)
.stderr_is("test: invalid integer fo<66>o");
}
#[test]
#[ignore = "fixme: parse/evaluation error (code 2); GNU returns 1"]
fn test_file_is_itself() {
new_ucmd!()
.args(&["regular_file", "-ef", "regular_file"])
.succeeds();
}
#[test]
#[ignore = "fixme: parse/evaluation error (code 2); GNU returns 1"]
fn test_file_is_newer_than_and_older_than_itself() {
// odd but matches GNU
new_ucmd!()
.args(&["regular_file", "-nt", "regular_file"])
.run()
.status_code(1);
new_ucmd!()
.args(&["regular_file", "-ot", "regular_file"])
.run()
.status_code(1);
}
#[test]
#[ignore = "todo: implement these"]
fn test_newer_file() {
let scenario = TestScenario::new(util_name!());
scenario.cmd("touch").arg("newer_file").succeeds();
scenario
.cmd("touch")
.args(&["-m", "-d", "last Thursday", "regular_file"])
.succeeds();
scenario
.ucmd()
.args(&["newer_file", "-nt", "regular_file"])
.succeeds();
scenario
.ucmd()
.args(&["regular_file", "-ot", "newer_file"])
.succeeds();
}
#[test]
fn test_file_exists() {
new_ucmd!().args(&["-e", "regular_file"]).succeeds();
}
#[test]
fn test_nonexistent_file_does_not_exist() {
new_ucmd!()
.args(&["-e", "nonexistent_file"])
.run()
.status_code(1);
}
#[test]
fn test_nonexistent_file_is_not_regular() {
new_ucmd!()
.args(&["-f", "nonexistent_file"])
.run()
.status_code(1);
}
#[test]
fn test_file_exists_and_is_regular() {
new_ucmd!().args(&["-f", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))] // FIXME: implement on Windows
fn test_file_is_readable() {
new_ucmd!().args(&["-r", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))] // FIXME: implement on Windows
fn test_file_is_not_readable() {
let scenario = TestScenario::new(util_name!());
let mut ucmd = scenario.ucmd();
let mut chmod = scenario.cmd("chmod");
scenario.fixtures.touch("crypto_file");
chmod.args(&["u-r", "crypto_file"]).succeeds();
ucmd.args(&["!", "-r", "crypto_file"]).succeeds();
}
#[test]
#[cfg(not(windows))] // FIXME: implement on Windows
fn test_file_is_writable() {
new_ucmd!().args(&["-w", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))] // FIXME: implement on Windows
fn test_file_is_not_writable() {
let scenario = TestScenario::new(util_name!());
let mut ucmd = scenario.ucmd();
let mut chmod = scenario.cmd("chmod");
scenario.fixtures.touch("immutable_file");
chmod.args(&["u-w", "immutable_file"]).succeeds();
ucmd.args(&["!", "-w", "immutable_file"]).succeeds();
}
#[test]
fn test_file_is_not_executable() {
new_ucmd!().args(&["!", "-x", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))] // FIXME: implement on Windows
fn test_file_is_executable() {
let scenario = TestScenario::new(util_name!());
let mut chmod = scenario.cmd("chmod");
chmod.args(&["u+x", "regular_file"]).succeeds();
scenario.ucmd().args(&["-x", "regular_file"]).succeeds();
}
#[test]
fn test_is_not_empty() {
new_ucmd!().args(&["-s", "non_empty_file"]).succeeds();
}
#[test]
fn test_nonexistent_file_size_test_is_false() {
new_ucmd!()
.args(&["-s", "nonexistent_file"])
.run()
.status_code(1);
}
#[test]
fn test_not_is_not_empty() {
new_ucmd!().args(&["!", "-s", "regular_file"]).succeeds();
}
#[test]
#[cfg(not(windows))]
fn test_symlink_is_symlink() {
let scenario = TestScenario::new(util_name!());
let mut ln = scenario.cmd("ln");
// creating symlinks requires admin on Windows
ln.args(&["-s", "regular_file", "symlink"]).succeeds();
// FIXME: implement on Windows
scenario.ucmd().args(&["-h", "symlink"]).succeeds();
scenario.ucmd().args(&["-L", "symlink"]).succeeds();
}
#[test]
fn test_file_is_not_symlink() {
let scenario = TestScenario::new(util_name!());
scenario
.ucmd()
.args(&["!", "-h", "regular_file"])
.succeeds();
scenario
.ucmd()
.args(&["!", "-L", "regular_file"])
.succeeds();
}
#[test]
fn test_nonexistent_file_is_not_symlink() {
let scenario = TestScenario::new(util_name!());
scenario
.ucmd()
.args(&["!", "-h", "nonexistent_file"])
.succeeds();
scenario
.ucmd()
.args(&["!", "-L", "nonexistent_file"])
.succeeds();
}
#[test] #[test]
fn test_op_prec_and_or_1() { fn test_op_prec_and_or_1() {
new_ucmd!().args(&[" ", "-o", "", "-a", ""]).succeeds(); new_ucmd!().args(&[" ", "-o", "", "-a", ""]).succeeds();
} }
#[test]
fn test_op_prec_and_or_1_overridden_by_parentheses() {
new_ucmd!()
.args(&["(", " ", "-o", "", ")", "-a", ""])
.run()
.status_code(1);
}
#[test] #[test]
fn test_op_prec_and_or_2() { fn test_op_prec_and_or_2() {
new_ucmd!() new_ucmd!()
@ -22,6 +478,54 @@ fn test_op_prec_and_or_2() {
} }
#[test] #[test]
fn test_or_as_filename() { fn test_op_prec_and_or_2_overridden_by_parentheses() {
new_ucmd!().args(&["x", "-a", "-z", "-o"]).fails(); new_ucmd!()
.args(&["", "-a", "(", "", "-o", " ", ")", "-a", " "])
.run()
.status_code(1);
}
#[test]
#[ignore = "fixme: error reporting"]
fn test_dangling_parenthesis() {
new_ucmd!()
.args(&["(", "(", "a", "!=", "b", ")", "-o", "-n", "c"])
.run()
.status_code(2);
new_ucmd!()
.args(&["(", "(", "a", "!=", "b", ")", "-o", "-n", "c", ")"])
.succeeds();
}
#[test]
fn test_complicated_parenthesized_expression() {
new_ucmd!()
.args(&[
"(", "(", "!", "(", "a", "=", "b", ")", "-o", "c", "=", "d", ")", "-a", "(", "q", "!=",
"r", ")", ")",
])
.succeeds();
}
#[test]
fn test_erroneous_parenthesized_expression() {
new_ucmd!()
.args(&["a", "!=", "(", "b", "-a", "b", ")", "!=", "c"])
.run()
.status_code(2)
.stderr_is("test: extra argument b");
}
#[test]
fn test_or_as_filename() {
new_ucmd!()
.args(&["x", "-a", "-z", "-o"])
.run()
.status_code(1);
}
#[test]
#[ignore = "GNU considers this an error"]
fn test_strlen_and_nothing() {
new_ucmd!().args(&["-n", "a", "-a"]).run().status_code(2);
} }

View file

@ -45,6 +45,70 @@ fn test_delete_complement() {
.stdout_is("ac"); .stdout_is("ac");
} }
#[test]
fn test_delete_complement_2() {
new_ucmd!()
.args(&["-d", "-C", "0-9"])
.pipe_in("Phone: 01234 567890")
.succeeds()
.stdout_is("01234567890");
new_ucmd!()
.args(&["-d", "--complement", "0-9"])
.pipe_in("Phone: 01234 567890")
.succeeds()
.stdout_is("01234567890");
}
#[test]
fn test_complement1() {
new_ucmd!()
.args(&["-c", "a", "X"])
.pipe_in("ab")
.run()
.stdout_is("aX");
}
#[test]
fn test_complement2() {
new_ucmd!()
.args(&["-c", "0-9", "x"])
.pipe_in("Phone: 01234 567890")
.run()
.stdout_is("xxxxxxx01234x567890");
}
#[test]
fn test_complement3() {
new_ucmd!()
.args(&["-c", "abcdefgh", "123"])
.pipe_in("the cat and the bat")
.run()
.stdout_is("3he3ca33a3d33he3ba3");
}
#[test]
fn test_complement4() {
// $ echo -n '0x1y2z3' | tr -c '0-@' '*-~'
// 0~1~2~3
new_ucmd!()
.args(&["-c", "0-@", "*-~"])
.pipe_in("0x1y2z3")
.run()
.stdout_is("0~1~2~3");
}
#[test]
#[ignore = "fixme: GNU tr returns '0a1b2c3' instead of '0~1~2~3', see #2158"]
fn test_complement5() {
// $ echo '0x1y2z3' | tr -c '\0-@' '*-~'
// 0a1b2c3
new_ucmd!()
.args(&["-c", "\\0-@", "*-~"])
.pipe_in("0x1y2z3")
.run()
.stdout_is("0a1b2c3");
}
#[test] #[test]
fn test_squeeze() { fn test_squeeze() {
new_ucmd!() new_ucmd!()
@ -63,6 +127,24 @@ fn test_squeeze_complement() {
.stdout_is("aaBcDcc"); .stdout_is("aaBcDcc");
} }
#[test]
fn test_translate_and_squeeze() {
new_ucmd!()
.args(&["-s", "x", "y"])
.pipe_in("xx")
.run()
.stdout_is("y");
}
#[test]
fn test_translate_and_squeeze_multiple_lines() {
new_ucmd!()
.args(&["-s", "x", "y"])
.pipe_in("xxaax\nxaaxx")
.run()
.stdout_is("yaay\nyaay");
}
#[test] #[test]
fn test_delete_and_squeeze() { fn test_delete_and_squeeze() {
new_ucmd!() new_ucmd!()

View file

@ -224,8 +224,14 @@ fn test_size_and_reference() {
let mut file1 = at.make_file(TFILE1); let mut file1 = at.make_file(TFILE1);
let mut file2 = at.make_file(TFILE2); let mut file2 = at.make_file(TFILE2);
file1.write_all(b"1234567890").unwrap(); file1.write_all(b"1234567890").unwrap();
ucmd.args(&["--reference", TFILE1, "--size", "+5", TFILE2]).succeeds(); ucmd.args(&["--reference", TFILE1, "--size", "+5", TFILE2])
.succeeds();
file2.seek(SeekFrom::End(0)).unwrap(); file2.seek(SeekFrom::End(0)).unwrap();
let actual = file2.seek(SeekFrom::Current(0)).unwrap(); let actual = file2.seek(SeekFrom::Current(0)).unwrap();
assert!(expected == actual, "expected '{}' got '{}'", expected, actual); assert!(
expected == actual,
"expected '{}' got '{}'",
expected,
actual
);
} }

View file

@ -36,7 +36,12 @@ fn test_uname_kernel_version() {
fn test_uname_kernel() { fn test_uname_kernel() {
let (_, mut ucmd) = at_and_ucmd!(); let (_, mut ucmd) = at_and_ucmd!();
let result = ucmd.arg("-o").succeeds();
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
assert!(result.stdout_str().to_lowercase().contains("linux")); {
let result = ucmd.arg("-o").succeeds();
assert!(result.stdout_str().to_lowercase().contains("linux"));
}
#[cfg(not(target_os = "linux"))]
let result = ucmd.arg("-o").succeeds();
} }

View file

@ -112,3 +112,60 @@ fn test_multiple_default() {
alice_in_wonderland.txt\n 36 370 2189 total\n", alice_in_wonderland.txt\n 36 370 2189 total\n",
); );
} }
/// Test for an empty file.
#[test]
fn test_file_empty() {
// TODO There is a leading space in the output that should be
// removed; see issue #2173.
new_ucmd!()
.args(&["-clmwL", "emptyfile.txt"])
.run()
.stdout_is(" 0 0 0 0 0 emptyfile.txt\n");
}
/// Test for an file containing a single non-whitespace character
/// *without* a trailing newline.
#[test]
fn test_file_single_line_no_trailing_newline() {
// TODO There is a leading space in the output that should be
// removed; see issue #2173.
new_ucmd!()
.args(&["-clmwL", "notrailingnewline.txt"])
.run()
.stdout_is(" 1 1 2 2 1 notrailingnewline.txt\n");
}
/// Test for a file that has 100 empty lines (that is, the contents of
/// the file are the newline character repeated one hundred times).
#[test]
fn test_file_many_empty_lines() {
// TODO There is a leading space in the output that should be
// removed; see issue #2173.
new_ucmd!()
.args(&["-clmwL", "manyemptylines.txt"])
.run()
.stdout_is(" 100 0 100 100 0 manyemptylines.txt\n");
}
/// Test for a file that has one long line comprising only spaces.
#[test]
fn test_file_one_long_line_only_spaces() {
// TODO There is a leading space in the output that should be
// removed; see issue #2173.
new_ucmd!()
.args(&["-clmwL", "onelongemptyline.txt"])
.run()
.stdout_is(" 1 0 10001 10001 10000 onelongemptyline.txt\n");
}
/// Test for a file that has one long line comprising a single "word".
#[test]
fn test_file_one_long_word() {
// TODO There is a leading space in the output that should be
// removed; see issue #2173.
new_ucmd!()
.args(&["-clmwL", "onelongword.txt"])
.run()
.stdout_is(" 1 1 10001 10001 10000 onelongword.txt\n");
}

View file

@ -1,11 +1,13 @@
#[cfg(target_os = "linux")]
use crate::common::util::*; use crate::common::util::*;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[test] #[test]
fn test_count() { fn test_count() {
for opt in vec!["-q", "--count"] { for opt in vec!["-q", "--count"] {
new_ucmd!().arg(opt).run().stdout_is(expected_result(opt)); new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
} }
} }
@ -13,17 +15,21 @@ fn test_count() {
#[test] #[test]
fn test_boot() { fn test_boot() {
for opt in vec!["-b", "--boot"] { for opt in vec!["-b", "--boot"] {
new_ucmd!().arg(opt).run().stdout_is(expected_result(opt)); new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
} }
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[test] #[test]
fn test_heading() { fn test_heading() {
for opt in vec!["-H"] { for opt in vec!["-H", "--heading"] {
// allow whitespace variation // allow whitespace variation
// * minor whitespace differences occur between platform built-in outputs; specifically number of TABs between "TIME" and "COMMENT" may be variant // * minor whitespace differences occur between platform built-in outputs;
let actual = new_ucmd!().arg(opt).run().stdout_move_str(); // specifically number of TABs between "TIME" and "COMMENT" may be variant
let actual = new_ucmd!().arg(opt).succeeds().stdout_move_str();
let expect = expected_result(opt); let expect = expected_result(opt);
println!("actual: {:?}", actual); println!("actual: {:?}", actual);
println!("expect: {:?}", expect); println!("expect: {:?}", expect);
@ -37,7 +43,10 @@ fn test_heading() {
#[test] #[test]
fn test_short() { fn test_short() {
for opt in vec!["-s", "--short"] { for opt in vec!["-s", "--short"] {
new_ucmd!().arg(opt).run().stdout_is(expected_result(opt)); new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
} }
} }
@ -45,7 +54,10 @@ fn test_short() {
#[test] #[test]
fn test_login() { fn test_login() {
for opt in vec!["-l", "--login"] { for opt in vec!["-l", "--login"] {
new_ucmd!().arg(opt).run().stdout_is(expected_result(opt)); new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
} }
} }
@ -53,7 +65,110 @@ fn test_login() {
#[test] #[test]
fn test_m() { fn test_m() {
for opt in vec!["-m"] { for opt in vec!["-m"] {
new_ucmd!().arg(opt).run().stdout_is(expected_result(opt)); new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_process() {
for opt in vec!["-p", "--process"] {
new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_runlevel() {
for opt in vec!["-r", "--runlevel"] {
new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_time() {
for opt in vec!["-t", "--time"] {
new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_mesg() {
for opt in vec!["-w", "-T", "--users", "--message", "--writable"] {
new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_arg1_arg2() {
let scene = TestScenario::new(util_name!());
let expected = scene
.cmd_keepenv(util_name!())
.env("LANGUAGE", "C")
.arg("am")
.arg("i")
.succeeds();
scene
.ucmd()
.arg("am")
.arg("i")
.succeeds()
.stdout_is(expected.stdout_str());
}
#[test]
fn test_too_many_args() {
let expected =
"error: The value 'u' was provided to '<FILE>...', but it wasn't expecting any more values";
new_ucmd!()
.arg("am")
.arg("i")
.arg("u")
.fails()
.stderr_contains(expected);
}
#[cfg(target_os = "linux")]
#[test]
fn test_users() {
for opt in vec!["-u", "--users"] {
new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
}
}
#[cfg(target_os = "linux")]
#[test]
#[ignore]
fn test_lookup() {
for opt in vec!["--lookup"] {
new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
} }
} }
@ -61,15 +176,60 @@ fn test_m() {
#[test] #[test]
fn test_dead() { fn test_dead() {
for opt in vec!["-d", "--dead"] { for opt in vec!["-d", "--dead"] {
new_ucmd!().arg(opt).run().stdout_is(expected_result(opt)); new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
} }
} }
#[cfg(target_os = "linux")]
#[test]
fn test_all_separately() {
// -a, --all same as -b -d --login -p -r -t -T -u
let scene = TestScenario::new(util_name!());
let expected = scene
.cmd_keepenv(util_name!())
.env("LANGUAGE", "C")
.arg("-b")
.arg("-d")
.arg("--login")
.arg("-p")
.arg("-r")
.arg("-t")
.arg("-T")
.arg("-u")
.succeeds();
scene
.ucmd()
.arg("-b")
.arg("-d")
.arg("--login")
.arg("-p")
.arg("-r")
.arg("-t")
.arg("-T")
.arg("-u")
.succeeds()
.stdout_is(expected.stdout_str());
scene
.ucmd()
.arg("--all")
.succeeds()
.stdout_is(expected.stdout_str());
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[test] #[test]
fn test_all() { fn test_all() {
for opt in vec!["-a", "--all"] { for opt in vec!["-a", "--all"] {
new_ucmd!().arg(opt).run().stdout_is(expected_result(opt)); new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
} }
} }
@ -79,6 +239,6 @@ fn expected_result(arg: &str) -> String {
.cmd_keepenv(util_name!()) .cmd_keepenv(util_name!())
.env("LANGUAGE", "C") .env("LANGUAGE", "C")
.args(&[arg]) .args(&[arg])
.run() .succeeds()
.stdout_move_str() .stdout_move_str()
} }

View file

@ -163,7 +163,7 @@ impl CmdResult {
/// asserts that the command's exit code is the same as the given one /// asserts that the command's exit code is the same as the given one
pub fn status_code(&self, code: i32) -> &CmdResult { pub fn status_code(&self, code: i32) -> &CmdResult {
assert!(self.code == Some(code)); assert_eq!(self.code, Some(code));
self self
} }
@ -295,12 +295,22 @@ impl CmdResult {
} }
pub fn stdout_contains<T: AsRef<str>>(&self, cmp: T) -> &CmdResult { pub fn stdout_contains<T: AsRef<str>>(&self, cmp: T) -> &CmdResult {
assert!(self.stdout_str().contains(cmp.as_ref())); assert!(
self.stdout_str().contains(cmp.as_ref()),
"'{}' does not contain '{}'",
self.stdout_str(),
cmp.as_ref()
);
self self
} }
pub fn stderr_contains<T: AsRef<str>>(&self, cmp: T) -> &CmdResult { pub fn stderr_contains<T: AsRef<str>>(&self, cmp: T) -> &CmdResult {
assert!(self.stderr_str().contains(cmp.as_ref())); assert!(
self.stderr_str().contains(cmp.as_ref()),
"'{}' does not contain '{}'",
self.stderr_str(),
cmp.as_ref()
);
self self
} }

View file

@ -0,0 +1 @@
Hello, World!

1
tests/fixtures/test/non_empty_file vendored Normal file
View file

@ -0,0 +1 @@
Not empty!

0
tests/fixtures/test/regular_file vendored Normal file
View file

0
tests/fixtures/wc/emptyfile.txt vendored Normal file
View file

100
tests/fixtures/wc/manyemptylines.txt vendored Normal file
View file

@ -0,0 +1,100 @@

View file

@ -0,0 +1 @@
a

File diff suppressed because one or more lines are too long

1
tests/fixtures/wc/onelongword.txt vendored Normal file

File diff suppressed because one or more lines are too long