Merge branch 'master' into implement-more

This commit is contained in:
Sylvestre Ledru 2021-05-04 13:35:38 +02:00 committed by GitHub
commit 482e340e11
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 43638 additions and 1359 deletions

114
Cargo.lock generated
View file

@ -1,7 +1,5 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3
[[package]] [[package]]
name = "advapi32-sys" name = "advapi32-sys"
version = "0.2.0" version = "0.2.0"
@ -14,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]]
@ -108,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",
] ]
@ -165,13 +163,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.11" version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [ dependencies = [
"libc",
"num-integer", "num-integer",
"num-traits", "num-traits",
"time", "time",
"winapi 0.3.9",
] ]
[[package]] [[package]]
@ -334,6 +334,16 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "coz"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cef55b3fe2f5477d59e12bc792e8b3c95a25bd099eadcfae006ecea136de76e2"
dependencies = [
"libc",
"once_cell",
]
[[package]] [[package]]
name = "cpp" name = "cpp"
version = "0.5.6" version = "0.5.6"
@ -486,9 +496,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",
@ -499,9 +509,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",
@ -552,7 +562,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]]
@ -634,7 +644,7 @@ checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"redox_syscall 0.2.6", "redox_syscall 0.2.7",
"winapi 0.3.9", "winapi 0.3.9",
] ]
@ -846,6 +856,15 @@ dependencies = [
"scopeguard", "scopeguard",
] ]
[[package]]
name = "locale"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.14" version = "0.4.14"
@ -893,9 +912,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"
@ -1170,7 +1189,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]]
@ -1340,9 +1359,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.6" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041" checksum = "85dd92e586f7355c633911e11f77f3d12f04b1b1bd76a198bd34ae3af8341ef2"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
@ -1353,17 +1372,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.6", "redox_syscall 0.2.7",
] ]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.4.6" version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" checksum = "ce5f1ceb7f74abbce32601642fcf8e8508a8a8991e0621c7d750295b9095702b"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr 2.3.4", "memchr 2.4.0",
"regex-syntax", "regex-syntax",
] ]
@ -1378,9 +1397,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"
@ -1453,6 +1472,9 @@ 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"
@ -1551,6 +1573,9 @@ 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"
@ -1578,13 +1603,13 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.70" 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 = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883" 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]]
@ -1638,7 +1663,7 @@ checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
dependencies = [ dependencies = [
"libc", "libc",
"numtoa", "numtoa",
"redox_syscall 0.2.6", "redox_syscall 0.2.7",
"redox_termios", "redox_termios",
] ]
@ -1687,12 +1712,11 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.42" version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
dependencies = [ dependencies = [
"libc", "libc",
"redox_syscall 0.1.57",
"winapi 0.3.9", "winapi 0.3.9",
] ]
@ -1732,9 +1756,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"
@ -1784,6 +1808,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",
] ]
@ -1900,7 +1926,9 @@ dependencies = [
name = "uu_cut" name = "uu_cut"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"bstr",
"clap", "clap",
"memchr 2.4.0",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
] ]
@ -2002,6 +2030,7 @@ dependencies = [
name = "uu_factor" name = "uu_factor"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"coz",
"criterion", "criterion",
"num-traits", "num-traits",
"paste", "paste",
@ -2134,6 +2163,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",
@ -2143,6 +2173,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",
@ -2172,15 +2203,16 @@ name = "uu_ls"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"atty", "atty",
"chrono",
"clap", "clap",
"globset", "globset",
"lazy_static", "lazy_static",
"locale",
"lscolors", "lscolors",
"number_prefix", "number_prefix",
"once_cell", "once_cell",
"term_grid", "term_grid",
"termsize", "termsize",
"time",
"unicode-width", "unicode-width",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
@ -2268,7 +2300,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",
@ -2340,6 +2372,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",
] ]
@ -2369,7 +2402,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",
@ -2484,7 +2517,10 @@ 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",
"unicode-width", "unicode-width",
"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",
@ -231,4 +232,4 @@ if __name__ == "__main__":
prev_table, _, _ = load_csv(CACHE_PATH) prev_table, _, _ = load_csv(CACHE_PATH)
new_table = merge_tables(prev_table, table) new_table = merge_tables(prev_table, table)
save_csv(CACHE_PATH, new_table) save_csv(CACHE_PATH, new_table)

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

@ -0,0 +1,46 @@
## Benchmarking cut
### Performance profile
In normal use cases a significant amount of the total execution time of `cut`
is spent performing I/O. When invoked with the `-f` option (cut fields) some
CPU time is spent on detecting fields (in `Searcher::next`). Other than that
some small amount of CPU time is spent on breaking the input stream into lines.
### How to
When fixing bugs or adding features you might want to compare
performance before and after your code changes.
- `hyperfine` can be used to accurately measure and compare the total
execution time of one or more commands.
```
$ cargo build --release --package uu_cut
$ hyperfine -w3 "./target/release/cut -f2-4,8 -d' ' input.txt" "cut -f2-4,8 -d' ' input.txt"
```
You can put those two commands in a shell script to be sure that you don't
forget to build after making any changes.
When optimizing or fixing performance regressions seeing the number of times a
function is called, and the amount of time it takes can be useful.
- `cargo flamegraph` generates flame graphs from function level metrics it records using `perf` or `dtrace`
```
$ cargo flamegraph --bin cut --package uu_cut -- -f1,3-4 input.txt > /dev/null
```
### What to benchmark
There are four different performance paths in `cut` to benchmark.
- Byte ranges `-c`/`--characters` or `-b`/`--bytes` e.g. `cut -c 2,4,6-`
- Byte ranges with output delimiters e.g. `cut -c 4- --output-delimiter=/`
- Fields e.g. `cut -f -4`
- Fields with output delimiters e.g. `cut -f 7-10 --output-delimiter=:`
Choose a test input file with large number of lines so that program startup time does not significantly affect the benchmark.

View file

@ -18,6 +18,8 @@ path = "src/cut.rs"
clap = "2.33" 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" }
memchr = "2"
bstr = "0.2"
[[bin]] [[bin]]
name = "cut" name = "cut"

View file

@ -1,152 +0,0 @@
// This file is part of the uutils coreutils package.
//
// (c) Rolf Morel <rolfmorel@gmail.com>
// (c) kwantam <kwantam@gmail.com>
// * substantial rewrite to use the `std::io::BufReader` trait
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) SRes Newl
use std::io::Result as IoResult;
use std::io::{BufRead, BufReader, Read, Write};
#[allow(non_snake_case)]
pub mod Bytes {
use std::io::Write;
pub trait Select {
fn select<W: Write>(&mut self, bytes: usize, out: Option<&mut W>) -> Selected;
}
#[derive(PartialEq, Eq, Debug)]
pub enum Selected {
NewlineFound,
Complete(usize),
Partial(usize),
EndOfFile,
}
}
#[derive(Debug)]
pub struct ByteReader<R>
where
R: Read,
{
inner: BufReader<R>,
newline_char: u8,
}
impl<R: Read> ByteReader<R> {
pub fn new(read: R, newline_char: u8) -> ByteReader<R> {
ByteReader {
inner: BufReader::with_capacity(4096, read),
newline_char,
}
}
}
impl<R: Read> Read for ByteReader<R> {
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
self.inner.read(buf)
}
}
impl<R: Read> BufRead for ByteReader<R> {
fn fill_buf(&mut self) -> IoResult<&[u8]> {
self.inner.fill_buf()
}
fn consume(&mut self, amt: usize) {
self.inner.consume(amt)
}
}
impl<R: Read> ByteReader<R> {
pub fn consume_line(&mut self) -> usize {
let mut bytes_consumed = 0;
let mut consume_val;
let newline_char = self.newline_char;
loop {
{
// need filled_buf to go out of scope
let filled_buf = match self.fill_buf() {
Ok(b) => {
if b.is_empty() {
return bytes_consumed;
} else {
b
}
}
Err(e) => crash!(1, "read error: {}", e),
};
if let Some(idx) = filled_buf.iter().position(|byte| *byte == newline_char) {
consume_val = idx + 1;
bytes_consumed += consume_val;
break;
}
consume_val = filled_buf.len();
}
bytes_consumed += consume_val;
self.consume(consume_val);
}
self.consume(consume_val);
bytes_consumed
}
}
impl<R: Read> self::Bytes::Select for ByteReader<R> {
fn select<W: Write>(&mut self, bytes: usize, out: Option<&mut W>) -> Bytes::Selected {
enum SRes {
Comp,
Part,
Newl,
}
use self::Bytes::Selected::*;
let newline_char = self.newline_char;
let (res, consume_val) = {
let buffer = match self.fill_buf() {
Err(e) => crash!(1, "read error: {}", e),
Ok(b) => b,
};
let (res, consume_val) = match buffer.len() {
0 => return EndOfFile,
buf_used if bytes < buf_used => {
// because the output delimiter should only be placed between
// segments check if the byte after bytes is a newline
let buf_slice = &buffer[0..=bytes];
match buf_slice.iter().position(|byte| *byte == newline_char) {
Some(idx) => (SRes::Newl, idx + 1),
None => (SRes::Comp, bytes),
}
}
_ => match buffer.iter().position(|byte| *byte == newline_char) {
Some(idx) => (SRes::Newl, idx + 1),
None => (SRes::Part, buffer.len()),
},
};
if let Some(out) = out {
crash_if_err!(1, out.write_all(&buffer[0..consume_val]));
}
(res, consume_val)
};
self.consume(consume_val);
match res {
SRes::Comp => Complete(consume_val),
SRes::Part => Partial(consume_val),
SRes::Newl => NewlineFound,
}
}
}

View file

@ -10,16 +10,17 @@
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use bstr::io::BufReadExt;
use clap::{App, Arg}; use clap::{App, Arg};
use std::fs::File; use std::fs::File;
use std::io::{stdin, stdout, BufRead, BufReader, Read, Stdout, Write}; use std::io::{stdin, stdout, BufReader, BufWriter, Read, Write};
use std::path::Path; use std::path::Path;
use self::searcher::Searcher; use self::searcher::Searcher;
use uucore::fs::is_stdout_interactive;
use uucore::ranges::Range; use uucore::ranges::Range;
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
mod buffer;
mod searcher; mod searcher;
static NAME: &str = "cut"; static NAME: &str = "cut";
@ -126,6 +127,14 @@ enum Mode {
Fields(Vec<Range>, FieldOptions), Fields(Vec<Range>, FieldOptions),
} }
fn stdout_writer() -> Box<dyn Write> {
if is_stdout_interactive() {
Box::new(stdout())
} else {
Box::new(BufWriter::new(stdout())) as Box<dyn Write>
}
}
fn list_to_ranges(list: &str, complement: bool) -> Result<Vec<Range>, String> { fn list_to_ranges(list: &str, complement: bool) -> Result<Vec<Range>, String> {
if complement { if complement {
Range::from_list(list).map(|r| uucore::ranges::complement(&r)) Range::from_list(list).map(|r| uucore::ranges::complement(&r))
@ -135,72 +144,35 @@ fn list_to_ranges(list: &str, complement: bool) -> Result<Vec<Range>, String> {
} }
fn cut_bytes<R: Read>(reader: R, ranges: &[Range], opts: &Options) -> i32 { fn cut_bytes<R: Read>(reader: R, ranges: &[Range], opts: &Options) -> i32 {
use self::buffer::Bytes::Select;
use self::buffer::Bytes::Selected::*;
let newline_char = if opts.zero_terminated { b'\0' } else { b'\n' }; let newline_char = if opts.zero_terminated { b'\0' } else { b'\n' };
let mut buf_read = buffer::ByteReader::new(reader, newline_char); let buf_in = BufReader::new(reader);
let mut out = stdout(); let mut out = stdout_writer();
let delim = opts
.out_delim
.as_ref()
.map_or("", String::as_str)
.as_bytes();
'newline: loop { let res = buf_in.for_byte_record(newline_char, |line| {
let mut cur_pos = 1;
let mut print_delim = false; let mut print_delim = false;
for &Range { low, high } in ranges {
for &Range { low, high } in ranges.iter() { if low > line.len() {
// skip up to low break;
let orig_pos = cur_pos;
loop {
match buf_read.select(low - cur_pos, None::<&mut Stdout>) {
NewlineFound => {
crash_if_err!(1, out.write_all(&[newline_char]));
continue 'newline;
}
Complete(len) => {
cur_pos += len;
break;
}
Partial(len) => cur_pos += len,
EndOfFile => {
if orig_pos != cur_pos {
crash_if_err!(1, out.write_all(&[newline_char]));
}
break 'newline;
}
}
} }
if print_delim {
if let Some(ref delim) = opts.out_delim { out.write_all(delim)?;
if print_delim { } else if opts.out_delim.is_some() {
crash_if_err!(1, out.write_all(delim.as_bytes()));
}
print_delim = true; print_delim = true;
} }
// change `low` from 1-indexed value to 0-index value
// write out from low to high let low = low - 1;
loop { let high = high.min(line.len());
match buf_read.select(high - cur_pos + 1, Some(&mut out)) { out.write_all(&line[low..high])?;
NewlineFound => continue 'newline,
Partial(len) => cur_pos += len,
Complete(_) => {
cur_pos = high + 1;
break;
}
EndOfFile => {
if cur_pos != low || low == high {
crash_if_err!(1, out.write_all(&[newline_char]));
}
break 'newline;
}
}
}
} }
out.write_all(&[newline_char])?;
buf_read.consume_line(); Ok(true)
crash_if_err!(1, out.write_all(&[newline_char])); });
} crash_if_err!(1, res);
0 0
} }
@ -213,23 +185,11 @@ fn cut_fields_delimiter<R: Read>(
newline_char: u8, newline_char: u8,
out_delim: &str, out_delim: &str,
) -> i32 { ) -> i32 {
let mut buf_in = BufReader::new(reader); let buf_in = BufReader::new(reader);
let mut out = stdout(); let mut out = stdout_writer();
let mut buffer = Vec::new(); let input_delim_len = delim.len();
'newline: loop { let result = buf_in.for_byte_record_with_terminator(newline_char, |line| {
buffer.clear();
match buf_in.read_until(newline_char, &mut buffer) {
Ok(n) if n == 0 => break,
Err(e) => {
if buffer.is_empty() {
crash!(1, "read error: {}", e);
}
}
_ => (),
}
let line = &buffer[..];
let mut fields_pos = 1; let mut fields_pos = 1;
let mut low_idx = 0; let mut low_idx = 0;
let mut delim_search = Searcher::new(line, delim.as_bytes()).peekable(); let mut delim_search = Searcher::new(line, delim.as_bytes()).peekable();
@ -237,46 +197,46 @@ fn cut_fields_delimiter<R: Read>(
if delim_search.peek().is_none() { if delim_search.peek().is_none() {
if !only_delimited { if !only_delimited {
crash_if_err!(1, out.write_all(line)); out.write_all(line)?;
if line[line.len() - 1] != newline_char { if line[line.len() - 1] != newline_char {
crash_if_err!(1, out.write_all(&[newline_char])); out.write_all(&[newline_char])?;
} }
} }
continue; return Ok(true);
} }
for &Range { low, high } in ranges.iter() { for &Range { low, high } in ranges {
if low - fields_pos > 0 { if low - fields_pos > 0 {
low_idx = match delim_search.nth(low - fields_pos - 1) { low_idx = match delim_search.nth(low - fields_pos - 1) {
Some((_, beyond_delim)) => beyond_delim, Some(index) => index + input_delim_len,
None => break, None => break,
}; };
} }
for _ in 0..=high - low { for _ in 0..=high - low {
if print_delim { if print_delim {
crash_if_err!(1, out.write_all(out_delim.as_bytes())); out.write_all(out_delim.as_bytes())?;
} else {
print_delim = true;
} }
match delim_search.next() { match delim_search.next() {
Some((high_idx, next_low_idx)) => { Some(high_idx) => {
let segment = &line[low_idx..high_idx]; let segment = &line[low_idx..high_idx];
crash_if_err!(1, out.write_all(segment)); out.write_all(segment)?;
print_delim = true; low_idx = high_idx + input_delim_len;
low_idx = next_low_idx;
fields_pos = high + 1; fields_pos = high + 1;
} }
None => { None => {
let segment = &line[low_idx..]; let segment = &line[low_idx..];
crash_if_err!(1, out.write_all(segment)); out.write_all(segment)?;
if line[line.len() - 1] == newline_char { if line[line.len() - 1] == newline_char {
continue 'newline; return Ok(true);
} }
break; break;
} }
@ -284,9 +244,10 @@ fn cut_fields_delimiter<R: Read>(
} }
} }
crash_if_err!(1, out.write_all(&[newline_char])); out.write_all(&[newline_char])?;
} Ok(true)
});
crash_if_err!(1, result);
0 0
} }
@ -304,23 +265,11 @@ fn cut_fields<R: Read>(reader: R, ranges: &[Range], opts: &FieldOptions) -> i32
); );
} }
let mut buf_in = BufReader::new(reader); let buf_in = BufReader::new(reader);
let mut out = stdout(); let mut out = stdout_writer();
let mut buffer = Vec::new(); let delim_len = opts.delimiter.len();
'newline: loop { let result = buf_in.for_byte_record_with_terminator(newline_char, |line| {
buffer.clear();
match buf_in.read_until(newline_char, &mut buffer) {
Ok(n) if n == 0 => break,
Err(e) => {
if buffer.is_empty() {
crash!(1, "read error: {}", e);
}
}
_ => (),
}
let line = &buffer[..];
let mut fields_pos = 1; let mut fields_pos = 1;
let mut low_idx = 0; let mut low_idx = 0;
let mut delim_search = Searcher::new(line, opts.delimiter.as_bytes()).peekable(); let mut delim_search = Searcher::new(line, opts.delimiter.as_bytes()).peekable();
@ -328,53 +277,54 @@ fn cut_fields<R: Read>(reader: R, ranges: &[Range], opts: &FieldOptions) -> i32
if delim_search.peek().is_none() { if delim_search.peek().is_none() {
if !opts.only_delimited { if !opts.only_delimited {
crash_if_err!(1, out.write_all(line)); out.write_all(line)?;
if line[line.len() - 1] != newline_char { if line[line.len() - 1] != newline_char {
crash_if_err!(1, out.write_all(&[newline_char])); out.write_all(&[newline_char])?;
} }
} }
continue; return Ok(true);
} }
for &Range { low, high } in ranges.iter() { for &Range { low, high } in ranges {
if low - fields_pos > 0 { if low - fields_pos > 0 {
low_idx = match delim_search.nth(low - fields_pos - 1) { if let Some(delim_pos) = delim_search.nth(low - fields_pos - 1) {
Some((_, beyond_delim)) => beyond_delim, low_idx = if print_delim {
None => break, delim_pos
}; } else {
} delim_pos + delim_len
}
if print_delim && low_idx >= opts.delimiter.as_bytes().len() { } else {
low_idx -= opts.delimiter.as_bytes().len(); break;
}
} }
match delim_search.nth(high - low) { match delim_search.nth(high - low) {
Some((high_idx, next_low_idx)) => { Some(high_idx) => {
let segment = &line[low_idx..high_idx]; let segment = &line[low_idx..high_idx];
crash_if_err!(1, out.write_all(segment)); out.write_all(segment)?;
print_delim = true; print_delim = true;
low_idx = next_low_idx; low_idx = high_idx;
fields_pos = high + 1; fields_pos = high + 1;
} }
None => { None => {
let segment = &line[low_idx..line.len()]; let segment = &line[low_idx..line.len()];
crash_if_err!(1, out.write_all(segment)); out.write_all(segment)?;
if line[line.len() - 1] == newline_char { if line[line.len() - 1] == newline_char {
continue 'newline; return Ok(true);
} }
break; break;
} }
} }
} }
out.write_all(&[newline_char])?;
crash_if_err!(1, out.write_all(&[newline_char])); Ok(true)
} });
crash_if_err!(1, result);
0 0
} }

View file

@ -5,7 +5,8 @@
// 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.
#[derive(Clone)] use memchr::memchr;
pub struct Searcher<'a> { pub struct Searcher<'a> {
haystack: &'a [u8], haystack: &'a [u8],
needle: &'a [u8], needle: &'a [u8],
@ -14,6 +15,7 @@ pub struct Searcher<'a> {
impl<'a> Searcher<'a> { impl<'a> Searcher<'a> {
pub fn new(haystack: &'a [u8], needle: &'a [u8]) -> Searcher<'a> { pub fn new(haystack: &'a [u8], needle: &'a [u8]) -> Searcher<'a> {
assert!(!needle.is_empty());
Searcher { Searcher {
haystack, haystack,
needle, needle,
@ -23,30 +25,81 @@ impl<'a> Searcher<'a> {
} }
impl<'a> Iterator for Searcher<'a> { impl<'a> Iterator for Searcher<'a> {
type Item = (usize, usize); type Item = usize;
fn next(&mut self) -> Option<(usize, usize)> { fn next(&mut self) -> Option<Self::Item> {
if self.needle.len() == 1 { loop {
for offset in self.position..self.haystack.len() { if let Some(match_idx) = memchr(self.needle[0], self.haystack) {
if self.haystack[offset] == self.needle[0] { if self.needle.len() == 1
self.position = offset + 1; || self.haystack[match_idx + 1..].starts_with(&self.needle[1..])
return Some((offset, offset + 1)); {
let match_pos = self.position + match_idx;
let skip = match_idx + self.needle.len();
self.haystack = &self.haystack[skip..];
self.position += skip;
return Some(match_pos);
} else {
let skip = match_idx + 1;
self.haystack = &self.haystack[skip..];
self.position += skip;
// continue
} }
}
self.position = self.haystack.len();
return None;
}
while self.position + self.needle.len() <= self.haystack.len() {
if &self.haystack[self.position..self.position + self.needle.len()] == self.needle {
let match_pos = self.position;
self.position += self.needle.len();
return Some((match_pos, match_pos + self.needle.len()));
} else { } else {
self.position += 1; return None;
} }
} }
None }
}
#[cfg(test)]
mod tests {
use super::*;
const NEEDLE: &[u8] = "ab".as_bytes();
#[test]
fn test_normal() {
let iter = Searcher::new("a.a.a".as_bytes(), "a".as_bytes());
let items: Vec<usize> = iter.collect();
assert_eq!(vec![0, 2, 4], items);
}
#[test]
fn test_empty() {
let iter = Searcher::new("".as_bytes(), "a".as_bytes());
let items: Vec<usize> = iter.collect();
assert_eq!(vec![] as Vec<usize>, items);
}
fn test_multibyte(line: &[u8], expected: Vec<usize>) {
let iter = Searcher::new(line, NEEDLE);
let items: Vec<usize> = iter.collect();
assert_eq!(expected, items);
}
#[test]
fn test_multibyte_normal() {
test_multibyte("...ab...ab...".as_bytes(), vec![3, 8]);
}
#[test]
fn test_multibyte_needle_head_at_end() {
test_multibyte("a".as_bytes(), vec![]);
}
#[test]
fn test_multibyte_starting_needle() {
test_multibyte("ab...ab...".as_bytes(), vec![0, 5]);
}
#[test]
fn test_multibyte_trailing_needle() {
test_multibyte("...ab...ab".as_bytes(), vec![3, 8]);
}
#[test]
fn test_multibyte_first_byte_false_match() {
test_multibyte("aA..aCaC..ab..aD".as_bytes(), vec![10]);
} }
} }

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

@ -14,8 +14,8 @@ edition = "2018"
[build-dependencies] [build-dependencies]
num-traits = "0.2.13" # used in src/numerics.rs, which is included by build.rs num-traits = "0.2.13" # used in src/numerics.rs, which is included by build.rs
[dependencies] [dependencies]
coz = { version = "0.1.3", optional = true }
num-traits = "0.2.13" # Needs at least version 0.2.13 for "OverflowingAdd" num-traits = "0.2.13" # Needs at least version 0.2.13 for "OverflowingAdd"
rand = { version="0.7", features=["small_rng"] } rand = { version="0.7", features=["small_rng"] }
smallvec = { version="0.6.14, < 1.0" } smallvec = { version="0.6.14, < 1.0" }

View file

@ -125,6 +125,8 @@ 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")]
coz::progress!("factor found");
let mut r = f; let mut r = f;
r.push(num); r.push(num);
return r; return r;
@ -139,6 +141,8 @@ 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")]
coz::begin!("factorization");
let mut factors = Factors::one(); let mut factors = Factors::one();
if n < 2 { if n < 2 {
@ -152,16 +156,23 @@ pub fn factor(mut n: u64) -> Factors {
} }
if n == 1 { if n == 1 {
#[cfg(feature = "coz")]
coz::end!("factorization");
return factors; return factors;
} }
let (factors, n) = table::factor(n, factors); let (factors, n) = table::factor(n, factors);
if n < (1 << 32) { let r = if n < (1 << 32) {
_factor::<Montgomery<u32>>(n, factors) _factor::<Montgomery<u32>>(n, factors)
} else { } else {
_factor::<Montgomery<u64>>(n, factors) _factor::<Montgomery<u64>>(n, factors)
} };
#[cfg(feature = "coz")]
coz::end!("factorization");
r
} }
#[cfg(test)] #[cfg(test)]

View file

@ -33,6 +33,8 @@ 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")]
coz::progress!("factor found");
} else { } else {
if k > 0 { if k > 0 {
factors.add(prime, k); factors.add(prime, k);

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

@ -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

@ -15,16 +15,17 @@ edition = "2018"
path = "src/ls.rs" path = "src/ls.rs"
[dependencies] [dependencies]
locale = "0.2.2"
chrono = "0.4.19"
clap = "2.33" clap = "2.33"
unicode-width = "0.1.8" unicode-width = "0.1.8"
number_prefix = "0.4" number_prefix = "0.4"
term_grid = "0.1.5" term_grid = "0.1.5"
termsize = "0.1.6" termsize = "0.1.6"
time = "0.1.40"
globset = "0.4.6" globset = "0.4.6"
lscolors = { version="0.7.1", features=["ansi_term"] } lscolors = { version = "0.7.1", features = ["ansi_term"] }
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "fs"] } uucore = { version = ">=0.0.8", package = "uucore", path = "../../uucore", features = ["entries", "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" }
once_cell = "1.7.2" once_cell = "1.7.2"
atty = "0.2" atty = "0.2"

View file

@ -38,8 +38,9 @@ use std::{
os::unix::fs::{FileTypeExt, MetadataExt}, os::unix::fs::{FileTypeExt, MetadataExt},
time::Duration, time::Duration,
}; };
use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
use time::{strftime, Timespec};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
#[cfg(unix)] #[cfg(unix)]
use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR};
@ -50,6 +51,8 @@ static ABOUT: &str = "
the command line, expect that it will ignore files and directories the command line, expect that it will ignore files and directories
whose names start with '.' whose names start with '.'
"; ";
static AFTER_HELP: &str = "The TIME_STYLE argument can be full-iso, long-iso, iso.
Also the TIME_STYLE environment variable sets the default style to use.";
fn get_usage() -> String { fn get_usage() -> String {
format!("{0} [OPTION]... [FILE]...", executable!()) format!("{0} [OPTION]... [FILE]...", executable!())
@ -117,6 +120,8 @@ pub mod options {
pub static COLOR: &str = "color"; pub static COLOR: &str = "color";
pub static PATHS: &str = "paths"; pub static PATHS: &str = "paths";
pub static INDICATOR_STYLE: &str = "indicator-style"; pub static INDICATOR_STYLE: &str = "indicator-style";
pub static TIME_STYLE: &str = "time-style";
pub static FULL_TIME: &str = "full-time";
pub static HIDE: &str = "hide"; pub static HIDE: &str = "hide";
pub static IGNORE: &str = "ignore"; pub static IGNORE: &str = "ignore";
} }
@ -156,6 +161,15 @@ enum Time {
Modification, Modification,
Access, Access,
Change, Change,
Birth,
}
#[derive(Debug)]
enum TimeStyle {
FullIso,
LongIso,
Iso,
Locale,
} }
enum Dereference { enum Dereference {
@ -191,6 +205,7 @@ struct Config {
width: Option<u16>, width: Option<u16>,
quoting_style: QuotingStyle, quoting_style: QuotingStyle,
indicator_style: IndicatorStyle, indicator_style: IndicatorStyle,
time_style: TimeStyle,
} }
// Fields that can be removed or added to the long format // Fields that can be removed or added to the long format
@ -251,6 +266,7 @@ impl Config {
options::format::LONG_NO_OWNER, options::format::LONG_NO_OWNER,
options::format::LONG_NO_GROUP, options::format::LONG_NO_GROUP,
options::format::LONG_NUMERIC_UID_GID, options::format::LONG_NUMERIC_UID_GID,
options::FULL_TIME,
] ]
.iter() .iter()
.flat_map(|opt| options.indices_of(opt)) .flat_map(|opt| options.indices_of(opt))
@ -302,6 +318,7 @@ impl Config {
match field { match field {
"ctime" | "status" => Time::Change, "ctime" | "status" => Time::Change,
"access" | "atime" | "use" => Time::Access, "access" | "atime" | "use" => Time::Access,
"birth" | "creation" => Time::Birth,
// below should never happen as clap already restricts the values. // below should never happen as clap already restricts the values.
_ => unreachable!("Invalid field for --time"), _ => unreachable!("Invalid field for --time"),
} }
@ -439,6 +456,30 @@ impl Config {
IndicatorStyle::None IndicatorStyle::None
}; };
let time_style = if let Some(field) = options.value_of(options::TIME_STYLE) {
//If both FULL_TIME and TIME_STYLE are present
//The one added last is dominant
if options.is_present(options::FULL_TIME)
&& options.indices_of(options::FULL_TIME).unwrap().last()
> options.indices_of(options::TIME_STYLE).unwrap().last()
{
TimeStyle::FullIso
} else {
//Clap handles the env variable "TIME_STYLE"
match field {
"full-iso" => TimeStyle::FullIso,
"long-iso" => TimeStyle::LongIso,
"iso" => TimeStyle::Iso,
"locale" => TimeStyle::Locale,
// below should never happen as clap already restricts the values.
_ => unreachable!("Invalid field for --time-style"),
}
}
} else if options.is_present(options::FULL_TIME) {
TimeStyle::FullIso
} else {
TimeStyle::Locale
};
let mut ignore_patterns = GlobSetBuilder::new(); let mut ignore_patterns = GlobSetBuilder::new();
if options.is_present(options::IGNORE_BACKUPS) { if options.is_present(options::IGNORE_BACKUPS) {
ignore_patterns.add(Glob::new("*~").unwrap()); ignore_patterns.add(Glob::new("*~").unwrap());
@ -504,6 +545,7 @@ impl Config {
width, width,
quoting_style, quoting_style,
indicator_style, indicator_style,
time_style,
} }
} }
} }
@ -696,10 +738,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.long(options::TIME) .long(options::TIME)
.help("Show time in <field>:\n\ .help("Show time in <field>:\n\
\taccess time (-u): atime, access, use;\n\ \taccess time (-u): atime, access, use;\n\
\tchange time (-t): ctime, status.") \tchange time (-t): ctime, status.\n\
\tbirth time: birth, creation;")
.value_name("field") .value_name("field")
.takes_value(true) .takes_value(true)
.possible_values(&["atime", "access", "use", "ctime", "status"]) .possible_values(&["atime", "access", "use", "ctime", "status", "birth", "creation"])
.hide_possible_values(true) .hide_possible_values(true)
.require_equals(true) .require_equals(true)
.overrides_with_all(&[ .overrides_with_all(&[
@ -1020,9 +1063,34 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
options::indicator_style::CLASSIFY, options::indicator_style::CLASSIFY,
options::INDICATOR_STYLE, options::INDICATOR_STYLE,
])) ]))
.arg(
//This still needs support for posix-*, +FORMAT
Arg::with_name(options::TIME_STYLE)
.long(options::TIME_STYLE)
.help("time/date format with -l; see TIME_STYLE below")
.value_name("TIME_STYLE")
.env("TIME_STYLE")
.possible_values(&[
"full-iso",
"long-iso",
"iso",
"locale",
])
.overrides_with_all(&[
options::TIME_STYLE
])
)
.arg(
Arg::with_name(options::FULL_TIME)
.long(options::FULL_TIME)
.overrides_with(options::FULL_TIME)
.help("like -l --time-style=full-iso")
)
// Positional arguments // Positional arguments
.arg(Arg::with_name(options::PATHS).multiple(true).takes_value(true)); .arg(Arg::with_name(options::PATHS).multiple(true).takes_value(true))
.after_help(AFTER_HELP);
let matches = app.get_matches_from(args); let matches = app.get_matches_from(args);
@ -1052,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,
@ -1122,7 +1197,7 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
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 = if let Some(ft) = path_data.file_type() {
!config.directory && ft.is_dir() !config.directory && ft.is_dir()
@ -1167,14 +1242,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()
@ -1213,8 +1282,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![]
@ -1223,7 +1298,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);
@ -1480,6 +1555,7 @@ fn get_system_time(md: &Metadata, config: &Config) -> Option<SystemTime> {
Time::Change => Some(UNIX_EPOCH + Duration::new(md.ctime() as u64, md.ctime_nsec() as u32)), Time::Change => Some(UNIX_EPOCH + Duration::new(md.ctime() as u64, md.ctime_nsec() as u32)),
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(),
} }
} }
@ -1488,22 +1564,40 @@ 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,
} }
} }
fn get_time(md: &Metadata, config: &Config) -> Option<time::Tm> { fn get_time(md: &Metadata, config: &Config) -> Option<chrono::DateTime<chrono::Local>> {
let duration = get_system_time(md, config)? let time = get_system_time(md, config)?;
.duration_since(UNIX_EPOCH) Some(time.into())
.ok()?;
let secs = duration.as_secs() as i64;
let nsec = duration.subsec_nanos() as i32;
Some(time::at(Timespec::new(secs, nsec)))
} }
fn display_date(metadata: &Metadata, config: &Config) -> String { fn display_date(metadata: &Metadata, config: &Config) -> String {
match get_time(metadata, config) { match get_time(metadata, config) {
Some(time) => strftime("%F %R", &time).unwrap(), Some(time) => {
//Date is recent if from past 6 months
//According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average.
let recent = time + chrono::Duration::seconds(31556952 / 2) > chrono::Local::now();
match config.time_style {
TimeStyle::FullIso => time.format("%Y-%m-%d %H:%M:%S.%f %z"),
TimeStyle::LongIso => time.format("%Y-%m-%d %H:%M"),
TimeStyle::Iso => time.format(if recent { "%m-%d %H:%M" } else { "%Y-%m-%d " }),
TimeStyle::Locale => {
let fmt = if recent { "%b %e %H:%M" } else { "%b %e %Y" };
//In this version of chrono translating can be done
//The function is chrono::datetime::DateTime::format_localized
//However it's currently still hard to get the current pure-rust-locale
//So it's not yet implemented
time.format(fmt)
}
}
.to_string()
}
None => "???".into(), None => "???".into(),
} }
} }

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

@ -15,16 +15,19 @@ 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 = "1.6.1" smallvec = { version="1.6.1", features=["serde"] }
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" }
tempdir = "0.3.7"
[[bin]] [[bin]]
name = "sort" name = "sort"

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

@ -0,0 +1,19 @@
Copyright 2018 Battelle Memorial Institute
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,295 @@
use std::clone::Clone;
use std::cmp::Ordering::Less;
use std::collections::VecDeque;
use std::error::Error;
use std::fs::{File, OpenOptions};
use std::io::SeekFrom::Start;
use std::io::{BufRead, BufReader, BufWriter, Seek, Write};
use std::marker::PhantomData;
use std::path::PathBuf;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json;
use tempdir::TempDir;
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
pub struct ExtSortedIterator<Line> {
buffers: Vec<VecDeque<Line>>,
chunk_offsets: Vec<u64>,
max_per_chunk: u64,
chunks: u64,
tmp_dir: TempDir,
settings: GlobalSettings,
failed: bool,
}
impl Iterator for ExtSortedIterator<Line>
where
Line: ExternallySortable,
{
type Item = Result<Line, Box<dyn Error>>;
/// # Errors
///
/// This method can fail due to issues reading intermediate sorted chunks
/// from disk, or due to serde deserialization issues
fn next(&mut self) -> Option<Self::Item> {
if self.failed {
return None;
}
// fill up any empty buffers
let mut empty = true;
for chunk_num in 0..self.chunks {
if self.buffers[chunk_num as usize].is_empty() {
let mut f = match File::open(self.tmp_dir.path().join(chunk_num.to_string())) {
Ok(f) => f,
Err(e) => {
self.failed = true;
return Some(Err(Box::new(e)));
}
};
match f.seek(Start(self.chunk_offsets[chunk_num as usize])) {
Ok(_) => (),
Err(e) => {
self.failed = true;
return Some(Err(Box::new(e)));
}
}
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() {
empty = false;
}
} else {
empty = false;
}
}
if empty {
return None;
}
// find the next record to write
// check is_empty() before unwrap()ing
let mut idx = 0;
for chunk_num in 0..self.chunks as usize {
if !self.buffers[chunk_num].is_empty() {
if self.buffers[idx].is_empty()
|| (super::compare_by)(
self.buffers[chunk_num].front().unwrap(),
self.buffers[idx].front().unwrap(),
&self.settings,
) == Less
{
idx = chunk_num;
}
}
}
// unwrap due to checks above
let r = self.buffers[idx].pop_front().unwrap();
Some(Ok(r))
}
}
/// Perform an external sort on an unsorted stream of incoming data
pub struct ExternalSorter<Line>
where
Line: ExternallySortable,
{
tmp_dir: Option<PathBuf>,
buffer_bytes: u64,
phantom: PhantomData<Line>,
settings: GlobalSettings,
}
impl ExternalSorter<Line>
where
Line: ExternallySortable,
{
/// Create a new `ExternalSorter` with a specified memory buffer and
/// temporary directory
pub fn new(
buffer_bytes: u64,
tmp_dir: Option<PathBuf>,
settings: GlobalSettings,
) -> ExternalSorter<Line> {
ExternalSorter {
buffer_bytes,
tmp_dir,
phantom: PhantomData,
settings,
}
}
/// Sort (based on `compare`) the `T`s provided by `unsorted` and return an
/// iterator
///
/// # Errors
///
/// This method can fail due to issues writing intermediate sorted chunks
/// 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,
};
{
let mut total_read = 0;
let mut chunk = Vec::new();
// Initial buffer is specified by user
let mut adjusted_buffer_size = self.buffer_bytes;
let (iter_size, _) = unsorted.size_hint();
// make the initial chunks on disk
for seq in unsorted {
let seq_size = seq.get_size();
total_read += seq_size;
// GNU minimum is 16 * (sizeof struct + 2), but GNU uses about
// 1/10 the memory that we do. And GNU even says in the code it may
// not work on small buffer sizes.
//
// The following seems to work pretty well, and has about the same max
// RSS as lower minimum values.
//
let minimum_buffer_size: u64 = iter_size as u64 * seq_size / 8;
adjusted_buffer_size =
// Grow buffer size for a struct/Line larger than buffer
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>> {
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>(
vec: &mut VecDeque<Line>,
file: File,
max_bytes: u64,
) -> Result<u64, Box<dyn Error>>
where
Line: ExternallySortable,
{
let mut total_read = 0;
let mut bytes_read = 0;
for line in BufReader::new(file).lines() {
let line_s = line?;
bytes_read += line_s.len() + 1;
// This is where the bad stuff happens usually
let deserialized: Line = serde_json::from_str(&line_s).expect("JSON read error: ");
total_read += deserialized.get_size();
vec.push_back(deserialized);
if total_read > max_bytes {
break;
}
}
Ok(bytes_read as u64)
}

View file

@ -14,20 +14,21 @@
//! 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)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
enum Sign { enum Sign {
Negative, Negative,
Positive, Positive,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct NumInfo { pub struct NumInfo {
exponent: i64, exponent: i64,
sign: Sign, sign: Sign,
} }
#[derive(Debug, PartialEq, Serialize, Deserialize, 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,9 +15,13 @@
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
mod custom_str_cmp;
mod external_sort;
mod numeric_str_cmp; mod numeric_str_cmp;
use clap::{App, Arg}; use clap::{App, Arg};
use custom_str_cmp::custom_str_cmp;
use external_sort::{ExternalSorter, ExternallySortable};
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};
@ -25,6 +29,7 @@ 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;
@ -35,6 +40,7 @@ use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Lines, 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;
use std::path::PathBuf;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use uucore::fs::is_stdin_interactive; // for Iterator::dedup() use uucore::fs::is_stdin_interactive; // for Iterator::dedup()
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
@ -77,6 +83,8 @@ static OPT_RANDOM: &str = "random-sort";
static OPT_ZERO_TERMINATED: &str = "zero-terminated"; static OPT_ZERO_TERMINATED: &str = "zero-terminated";
static OPT_PARALLEL: &str = "parallel"; static OPT_PARALLEL: &str = "parallel";
static OPT_FILES0_FROM: &str = "files0-from"; static OPT_FILES0_FROM: &str = "files0-from";
static OPT_BUF_SIZE: &str = "buffer-size";
static OPT_TMP_DIR: &str = "temporary-directory";
static ARG_FILES: &str = "files"; static ARG_FILES: &str = "files";
@ -86,6 +94,8 @@ static THOUSANDS_SEP: char = ',';
static NEGATIVE: char = '-'; static NEGATIVE: char = '-';
static POSITIVE: char = '+'; static POSITIVE: char = '+';
static DEFAULT_BUF_SIZE: usize = std::usize::MAX;
#[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy)] #[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy)]
enum SortMode { enum SortMode {
Numeric, Numeric,
@ -95,7 +105,7 @@ enum SortMode {
Version, Version,
Default, Default,
} }
#[derive(Clone)]
struct GlobalSettings { struct GlobalSettings {
mode: SortMode, mode: SortMode,
debug: bool, debug: bool,
@ -116,6 +126,31 @@ struct GlobalSettings {
separator: Option<char>, separator: Option<char>,
threads: String, threads: String,
zero_terminated: bool, zero_terminated: bool,
buffer_size: usize,
tmp_dir: PathBuf,
ext_sort: bool,
}
impl GlobalSettings {
// It's back to do conversions for command line opts!
// Probably want to do through numstrcmp somehow now?
fn human_numeric_convert(a: &str) -> usize {
let num_str = &a[get_leading_gen(a)];
let (_, suf_str) = a.split_at(num_str.len());
let num_usize = num_str
.parse::<usize>()
.expect("Error parsing buffer size: ");
let suf_usize: usize = match suf_str.to_uppercase().as_str() {
// SI Units
"B" => 1usize,
"K" => 1000usize,
"M" => 1000000usize,
"G" => 1000000000usize,
// GNU regards empty human numeric values as K by default
_ => 1000usize,
};
num_usize * suf_usize
}
} }
impl Default for GlobalSettings { impl Default for GlobalSettings {
@ -140,10 +175,13 @@ impl Default for GlobalSettings {
separator: None, separator: None,
threads: String::new(), threads: String::new(),
zero_terminated: false, zero_terminated: false,
buffer_size: DEFAULT_BUF_SIZE,
tmp_dir: PathBuf::new(),
ext_sort: false,
} }
} }
} }
#[derive(Clone)]
struct KeySettings { struct KeySettings {
mode: SortMode, mode: SortMode,
ignore_blanks: bool, ignore_blanks: bool,
@ -168,37 +206,29 @@ impl From<&GlobalSettings> for KeySettings {
} }
} }
#[derive(Debug, Serialize, Deserialize, 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)]
enum NumCache { enum NumCache {
AsF64(GeneralF64ParseResult), AsF64(GeneralF64ParseResult),
WithInfo(NumInfo), WithInfo(NumInfo),
@ -219,7 +249,7 @@ impl NumCache {
} }
} }
} }
#[derive(Serialize, Deserialize, Clone)]
struct Selection { struct Selection {
range: SelectionRange, range: SelectionRange,
num_cache: NumCache, num_cache: NumCache,
@ -234,12 +264,20 @@ impl Selection {
type Field = Range<usize>; type Field = Range<usize>;
#[derive(Serialize, Deserialize, Clone)]
struct Line { 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 { fn new(line: String, settings: &GlobalSettings) -> Self {
let fields = if settings let fields = if settings
@ -257,14 +295,8 @@ impl Line {
.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
{ {
@ -414,34 +446,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 {
@ -489,6 +493,7 @@ fn tokenize_with_separator(line: &str, separator: char) -> Vec<Field> {
tokens tokens
} }
#[derive(Clone)]
struct KeyPosition { struct KeyPosition {
/// 1-indexed, 0 is invalid. /// 1-indexed, 0 is invalid.
field: usize, field: usize,
@ -578,7 +583,7 @@ impl KeyPosition {
} }
} }
} }
#[derive(Clone)]
struct FieldSelector { struct FieldSelector {
from: KeyPosition, from: KeyPosition,
to: Option<KeyPosition>, to: Option<KeyPosition>,
@ -912,6 +917,22 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.takes_value(true) .takes_value(true)
.value_name("NUM_THREADS"), .value_name("NUM_THREADS"),
) )
.arg(
Arg::with_name(OPT_BUF_SIZE)
.short("S")
.long(OPT_BUF_SIZE)
.help("sets the maximum SIZE of each segment in number of sorted items")
.takes_value(true)
.value_name("SIZE"),
)
.arg(
Arg::with_name(OPT_TMP_DIR)
.short("T")
.long(OPT_TMP_DIR)
.help("use DIR for temporaries, not $TMPDIR or /tmp")
.takes_value(true)
.value_name("DIR"),
)
.arg( .arg(
Arg::with_name(OPT_FILES0_FROM) Arg::with_name(OPT_FILES0_FROM)
.long(OPT_FILES0_FROM) .long(OPT_FILES0_FROM)
@ -982,6 +1003,29 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
env::set_var("RAYON_NUM_THREADS", &settings.threads); env::set_var("RAYON_NUM_THREADS", &settings.threads);
} }
if matches.is_present(OPT_BUF_SIZE) {
settings.buffer_size = {
let input = matches
.value_of(OPT_BUF_SIZE)
.map(String::from)
.unwrap_or(format!("{}", DEFAULT_BUF_SIZE));
GlobalSettings::human_numeric_convert(&input)
};
settings.ext_sort = true;
}
if matches.is_present(OPT_TMP_DIR) {
let result = matches
.value_of(OPT_TMP_DIR)
.map(String::from)
.unwrap_or(format!("{}", env::temp_dir().display()));
settings.tmp_dir = PathBuf::from(result);
settings.ext_sort = true;
} else {
settings.tmp_dir = env::temp_dir();
}
settings.zero_terminated = matches.is_present(OPT_ZERO_TERMINATED); settings.zero_terminated = matches.is_present(OPT_ZERO_TERMINATED);
settings.merge = matches.is_present(OPT_MERGE); settings.merge = matches.is_present(OPT_MERGE);
@ -1066,10 +1110,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
}); });
} }
exec(files, &settings) exec(files, settings)
} }
fn exec(files: Vec<String>, settings: &GlobalSettings) -> i32 { fn exec(files: Vec<String>, settings: GlobalSettings) -> i32 {
let mut lines = Vec::new(); let mut lines = Vec::new();
let mut file_merger = FileMerger::new(&settings); let mut file_merger = FileMerger::new(&settings);
@ -1105,6 +1149,13 @@ fn exec(files: Vec<String>, settings: &GlobalSettings) -> i32 {
if settings.check { if settings.check {
return exec_check_file(&lines, &settings); 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 { } else {
sort_by(&mut lines, &settings); sort_by(&mut lines, &settings);
} }
@ -1112,7 +1163,7 @@ fn exec(files: Vec<String>, settings: &GlobalSettings) -> i32 {
if settings.merge { if settings.merge {
if settings.unique { if settings.unique {
print_sorted( print_sorted(
file_merger.dedup_by(|a, b| compare_by(a, b, settings) == Ordering::Equal), file_merger.dedup_by(|a, b| compare_by(a, b, &settings) == Ordering::Equal),
&settings, &settings,
) )
} else { } else {
@ -1122,7 +1173,7 @@ fn exec(files: Vec<String>, settings: &GlobalSettings) -> i32 {
print_sorted( print_sorted(
lines lines
.into_iter() .into_iter()
.dedup_by(|a, b| compare_by(a, b, settings) == Ordering::Equal), .dedup_by(|a, b| compare_by(a, b, &settings) == Ordering::Equal),
&settings, &settings,
) )
} else { } else {
@ -1164,11 +1215,25 @@ fn exec_check_file(unwrapped_lines: &[Line], settings: &GlobalSettings) -> i32 {
} }
} }
fn sort_by(lines: &mut Vec<Line>, settings: &GlobalSettings) { 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)
.unwrap()
.map(|x| x.unwrap())
.collect::<Vec<Line>>();
iter
}
fn sort_by(unsorted: &mut Vec<Line>, settings: &GlobalSettings) {
if settings.stable || settings.unique { if settings.stable || settings.unique {
lines.par_sort_by(|a, b| compare_by(a, b, &settings)) unsorted.par_sort_by(|a, b| compare_by(a, b, &settings))
} else { } else {
lines.par_sort_unstable_by(|a, b| compare_by(a, b, &settings)) unsorted.par_sort_unstable_by(|a, b| compare_by(a, b, &settings))
} }
} }
@ -1194,7 +1259,13 @@ fn compare_by(a: &Line, b: &Line, global_settings: &GlobalSettings) -> Ordering
), ),
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 {
@ -1206,7 +1277,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 {
@ -1216,13 +1287,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.
@ -1268,7 +1332,7 @@ fn get_leading_gen(input: &str) -> Range<usize> {
leading_whitespace_len..input.len() leading_whitespace_len..input.len()
} }
#[derive(Copy, Clone, PartialEq, PartialOrd)] #[derive(Serialize, Deserialize, Copy, Clone, PartialEq, PartialOrd)]
enum GeneralF64ParseResult { enum GeneralF64ParseResult {
Invalid, Invalid,
NaN, NaN,
@ -1409,22 +1473,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)) {
@ -1491,14 +1539,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

@ -24,7 +24,7 @@ thiserror = { version="1.0", optional=true }
lazy_static = { version="1.3", optional=true } lazy_static = { version="1.3", optional=true }
nix = { version="<= 0.13", optional=true } nix = { version="<= 0.13", optional=true }
platform-info = { version="<= 0.1", optional=true } platform-info = { version="<= 0.1", optional=true }
time = { version="<= 0.1.42", optional=true } time = { version="<= 0.1.43", optional=true }
# * "problem" dependencies (pinned) # * "problem" dependencies (pinned)
data-encoding = { version="~2.1", optional=true } ## data-encoding: require v2.1; but v2.2.0 breaks the build for MinSRV v1.31.0 data-encoding = { version="~2.1", optional=true } ## data-encoding: require v2.1; but v2.2.0 breaks the build for MinSRV v1.31.0
libc = { version="0.2.15, <= 0.2.85", optional=true } ## libc: initial utmp support added in v0.2.15; but v0.2.68 breaks the build for MinSRV v1.31.0 libc = { version="0.2.15, <= 0.2.85", optional=true } ## libc: initial utmp support added in v0.2.15; but v0.2.68 breaks the build for MinSRV v1.31.0

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

@ -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

@ -43,23 +43,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]
@ -482,7 +533,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 +545,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]
@ -558,6 +608,113 @@ fn test_ls_long_ctime() {
} }
} }
#[test]
#[ignore]
fn test_ls_order_birthtime() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
/*
Here we make 2 files with a timeout in between.
After creating the first file try to sync it.
This ensures the file gets created immediately instead of being saved
inside the OS's IO operation buffer.
Without this, both files might accidentally be created at the same time.
*/
at.make_file("test-birthtime-1").sync_all().unwrap();
at.make_file("test-birthtime-2").sync_all().unwrap();
at.open("test-birthtime-1");
let result = scene.ucmd().arg("--time=birth").arg("-t").run();
#[cfg(not(windows))]
assert_eq!(result.stdout_str(), "test-birthtime-2\ntest-birthtime-1\n");
#[cfg(windows)]
assert_eq!(result.stdout_str(), "test-birthtime-2 test-birthtime-1\n");
}
#[test]
fn test_ls_styles() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("test");
let re_full = Regex::new(
r"[a-z-]* \d* \w* \w* \d* \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d* \+\d{4} test\n",
)
.unwrap();
let re_long =
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_locale =
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
let result = scene
.ucmd()
.arg("-l")
.arg("--time-style=full-iso")
.succeeds();
assert!(re_full.is_match(&result.stdout_str()));
//long-iso
let result = scene
.ucmd()
.arg("-l")
.arg("--time-style=long-iso")
.succeeds();
assert!(re_long.is_match(&result.stdout_str()));
//iso
let result = scene.ucmd().arg("-l").arg("--time-style=iso").succeeds();
assert!(re_iso.is_match(&result.stdout_str()));
//locale
let result = scene.ucmd().arg("-l").arg("--time-style=locale").succeeds();
assert!(re_locale.is_match(&result.stdout_str()));
//Overwrite options tests
let result = scene
.ucmd()
.arg("-l")
.arg("--time-style=long-iso")
.arg("--time-style=iso")
.succeeds();
assert!(re_iso.is_match(&result.stdout_str()));
let result = scene
.ucmd()
.arg("--time-style=iso")
.arg("--full-time")
.succeeds();
assert!(re_full.is_match(&result.stdout_str()));
let result = scene
.ucmd()
.arg("--full-time")
.arg("--time-style=iso")
.succeeds();
assert!(re_iso.is_match(&result.stdout_str()));
let result = scene
.ucmd()
.arg("--full-time")
.arg("--time-style=iso")
.arg("--full-time")
.succeeds();
assert!(re_full.is_match(&result.stdout_str()));
let result = scene
.ucmd()
.arg("--full-time")
.arg("-x")
.arg("-l")
.succeeds();
assert!(re_full.is_match(&result.stdout_str()));
at.touch("test2");
let result = scene.ucmd().arg("--full-time").arg("-x").succeeds();
#[cfg(not(windows))]
assert_eq!(result.stdout_str(), "test\ntest2\n");
#[cfg(windows)]
assert_eq!(result.stdout_str(), "test test2\n");
}
#[test] #[test]
fn test_ls_order_time() { fn test_ls_order_time() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());

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

@ -15,6 +15,31 @@ fn test_helper(file_name: &str, args: &str) {
.stdout_is_fixture(format!("{}.expected.debug", file_name)); .stdout_is_fixture(format!("{}.expected.debug", file_name));
} }
// FYI, the initialization size of our Line struct is 96 bytes.
//
// At very small buffer sizes, with that overhead we are certainly going
// to overrun our buffer way, way, way too quickly because of these excess
// bytes for the struct.
//
// For instance, seq 0..20000 > ...text = 108894 bytes
// But overhead is 1920000 + 108894 = 2028894 bytes
//
// Or kjvbible-random.txt = 4332506 bytes, but minimum size of its
// 99817 lines in memory * 96 bytes = 9582432 bytes
//
// Here, we test 108894 bytes with a 50K buffer
//
#[test]
fn test_larger_than_specified_segment() {
new_ucmd!()
.arg("-n")
.arg("-S")
.arg("50K")
.arg("ext_sort.txt")
.succeeds()
.stdout_is_fixture(format!("{}", "ext_sort.expected"));
}
#[test] #[test]
fn test_months_whitespace() { fn test_months_whitespace() {
test_helper("months-whitespace", "-M"); test_helper("months-whitespace", "-M");
@ -34,6 +59,18 @@ fn test_human_numeric_whitespace() {
test_helper("human-numeric-whitespace", "-h"); test_helper("human-numeric-whitespace", "-h");
} }
// This tests where serde often fails when reading back JSON
// if it finds a null value
#[test]
fn test_extsort_as64_bailout() {
new_ucmd!()
.arg("-g")
.arg("-S 5K")
.arg("multiple_decimals_general.txt")
.succeeds()
.stdout_is_fixture("multiple_decimals_general.expected");
}
#[test] #[test]
fn test_multiple_decimals_general() { fn test_multiple_decimals_general() {
test_helper("multiple_decimals_general", "-g") test_helper("multiple_decimals_general", "-g")

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,109 @@ 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]
fn test_lookup() {
for opt in vec!["--lookup"] {
new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
} }
} }
@ -61,15 +175,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 +238,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

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

20000
tests/fixtures/sort/ext_sort.expected vendored Normal file

File diff suppressed because it is too large Load diff

20000
tests/fixtures/sort/ext_sort.txt vendored Normal file

File diff suppressed because it is too large Load diff

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