mirror of
https://github.com/uutils/coreutils
synced 2024-11-17 02:08:09 +00:00
Merge branch 'master' into implement-more
This commit is contained in:
commit
482e340e11
74 changed files with 43638 additions and 1359 deletions
114
Cargo.lock
generated
114
Cargo.lock
generated
|
@ -1,7 +1,5 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "advapi32-sys"
|
||||
version = "0.2.0"
|
||||
|
@ -14,11 +12,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.15"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
|
||||
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||
dependencies = [
|
||||
"memchr 2.3.4",
|
||||
"memchr 2.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -108,12 +106,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "0.2.15"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d"
|
||||
checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"memchr 2.3.4",
|
||||
"memchr 2.4.0",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
@ -165,13 +163,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.11"
|
||||
version = "0.4.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2"
|
||||
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"time",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -334,6 +334,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "cpp"
|
||||
version = "0.5.6"
|
||||
|
@ -486,9 +496,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.3"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12"
|
||||
checksum = "52fb27eab85b17fbb9f6fd667089e07d6a2eb8743d02639ee7f6a7a7729c9c94"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"crossbeam-utils",
|
||||
|
@ -499,9 +509,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49"
|
||||
checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if 1.0.0",
|
||||
|
@ -552,7 +562,7 @@ version = "0.1.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
|
||||
dependencies = [
|
||||
"memchr 2.3.4",
|
||||
"memchr 2.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -634,7 +644,7 @@ checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8"
|
|||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall 0.2.6",
|
||||
"redox_syscall 0.2.7",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
|
@ -846,6 +856,15 @@ dependencies = [
|
|||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "locale"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.14"
|
||||
|
@ -893,9 +912,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.3.4"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
|
||||
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
|
@ -1170,7 +1189,7 @@ version = "1.0.26"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
|
||||
dependencies = [
|
||||
"unicode-xid 0.2.1",
|
||||
"unicode-xid 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1340,9 +1359,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
|
|||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041"
|
||||
checksum = "85dd92e586f7355c633911e11f77f3d12f04b1b1bd76a198bd34ae3af8341ef2"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
@ -1353,17 +1372,17 @@ version = "0.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
|
||||
dependencies = [
|
||||
"redox_syscall 0.2.6",
|
||||
"redox_syscall 0.2.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.4.6"
|
||||
version = "1.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
|
||||
checksum = "ce5f1ceb7f74abbce32601642fcf8e8508a8a8991e0621c7d750295b9095702b"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr 2.3.4",
|
||||
"memchr 2.4.0",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
|
@ -1378,9 +1397,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.23"
|
||||
version = "0.6.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548"
|
||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
|
@ -1453,6 +1472,9 @@ name = "serde"
|
|||
version = "1.0.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_cbor"
|
||||
|
@ -1551,6 +1573,9 @@ name = "smallvec"
|
|||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
|
@ -1578,13 +1603,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.70"
|
||||
version = "1.0.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883"
|
||||
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.9",
|
||||
"unicode-xid 0.2.1",
|
||||
"unicode-xid 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1638,7 +1663,7 @@ checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
|
|||
dependencies = [
|
||||
"libc",
|
||||
"numtoa",
|
||||
"redox_syscall 0.2.6",
|
||||
"redox_syscall 0.2.7",
|
||||
"redox_termios",
|
||||
]
|
||||
|
||||
|
@ -1687,12 +1712,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.42"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
|
||||
checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_syscall 0.1.57",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
|
@ -1732,9 +1756,9 @@ checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
|
|||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||
|
||||
[[package]]
|
||||
name = "unindent"
|
||||
|
@ -1784,6 +1808,8 @@ dependencies = [
|
|||
name = "uu_base64"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"uu_base32",
|
||||
"uucore",
|
||||
"uucore_procs",
|
||||
]
|
||||
|
@ -1900,7 +1926,9 @@ dependencies = [
|
|||
name = "uu_cut"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"clap",
|
||||
"memchr 2.4.0",
|
||||
"uucore",
|
||||
"uucore_procs",
|
||||
]
|
||||
|
@ -2002,6 +2030,7 @@ dependencies = [
|
|||
name = "uu_factor"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"coz",
|
||||
"criterion",
|
||||
"num-traits",
|
||||
"paste",
|
||||
|
@ -2134,6 +2163,7 @@ dependencies = [
|
|||
name = "uu_kill"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"libc",
|
||||
"uucore",
|
||||
"uucore_procs",
|
||||
|
@ -2143,6 +2173,7 @@ dependencies = [
|
|||
name = "uu_link"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"libc",
|
||||
"uucore",
|
||||
"uucore_procs",
|
||||
|
@ -2172,15 +2203,16 @@ name = "uu_ls"
|
|||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"chrono",
|
||||
"clap",
|
||||
"globset",
|
||||
"lazy_static",
|
||||
"locale",
|
||||
"lscolors",
|
||||
"number_prefix",
|
||||
"once_cell",
|
||||
"term_grid",
|
||||
"termsize",
|
||||
"time",
|
||||
"unicode-width",
|
||||
"uucore",
|
||||
"uucore_procs",
|
||||
|
@ -2268,7 +2300,7 @@ dependencies = [
|
|||
"aho-corasick",
|
||||
"clap",
|
||||
"libc",
|
||||
"memchr 2.3.4",
|
||||
"memchr 2.4.0",
|
||||
"regex",
|
||||
"regex-syntax",
|
||||
"uucore",
|
||||
|
@ -2340,6 +2372,7 @@ dependencies = [
|
|||
name = "uu_pinky"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"uucore",
|
||||
"uucore_procs",
|
||||
]
|
||||
|
@ -2369,7 +2402,7 @@ dependencies = [
|
|||
"aho-corasick",
|
||||
"clap",
|
||||
"libc",
|
||||
"memchr 2.3.4",
|
||||
"memchr 2.4.0",
|
||||
"regex",
|
||||
"regex-syntax",
|
||||
"uucore",
|
||||
|
@ -2484,7 +2517,10 @@ dependencies = [
|
|||
"rand 0.7.3",
|
||||
"rayon",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec 1.6.1",
|
||||
"tempdir",
|
||||
"unicode-width",
|
||||
"uucore",
|
||||
"uucore_procs",
|
||||
|
|
|
@ -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-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|
|
||||
|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|
|
||||
|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|
|
||||
|
|
|
@ -26,6 +26,7 @@ TARGETS = [
|
|||
"x86_64-pc-windows-gnu",
|
||||
"x86_64-pc-windows-msvc",
|
||||
# Apple
|
||||
"aarch64-apple-darwin",
|
||||
"x86_64-apple-darwin",
|
||||
"aarch64-apple-ios",
|
||||
"x86_64-apple-ios",
|
||||
|
@ -231,4 +232,4 @@ if __name__ == "__main__":
|
|||
|
||||
prev_table, _, _ = load_csv(CACHE_PATH)
|
||||
new_table = merge_tables(prev_table, table)
|
||||
save_csv(CACHE_PATH, new_table)
|
||||
save_csv(CACHE_PATH, new_table)
|
||||
|
|
|
@ -7,19 +7,14 @@
|
|||
|
||||
#[macro_use]
|
||||
extern crate uucore;
|
||||
|
||||
use std::io::{stdin, Read};
|
||||
|
||||
use uucore::encoding::Format;
|
||||
use uucore::InvalidEncodingHandling;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{stdin, BufReader};
|
||||
use std::path::Path;
|
||||
pub mod base_common;
|
||||
|
||||
use clap::{App, Arg};
|
||||
|
||||
mod base_common;
|
||||
|
||||
static SUMMARY: &str = "Base32 encode or decode FILE, or standard input, to standard output.";
|
||||
static LONG_HELP: &str = "
|
||||
static ABOUT: &str = "
|
||||
With no FILE, or when FILE is -, read standard input.
|
||||
|
||||
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 BASE_CMD_PARSE_ERROR: i32 = 1;
|
||||
|
||||
fn get_usage() -> String {
|
||||
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 {
|
||||
let format = Format::Base32;
|
||||
let usage = get_usage();
|
||||
let app = App::new(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 name = executable!();
|
||||
|
||||
let arg_list = args
|
||||
.collect_str(InvalidEncodingHandling::ConvertLossy)
|
||||
.accept_any();
|
||||
let config: Config = Config::from(app.get_matches_from(arg_list));
|
||||
match config.to_read {
|
||||
// Read from file.
|
||||
Some(name) => {
|
||||
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,
|
||||
);
|
||||
let config_result: Result<base_common::Config, String> =
|
||||
base_common::parse_base_cmd_args(args, name, VERSION, ABOUT, &usage);
|
||||
|
||||
if config_result.is_err() {
|
||||
match config_result {
|
||||
Ok(_) => panic!(),
|
||||
Err(s) => crash!(BASE_CMD_PARSE_ERROR, "{}", s),
|
||||
}
|
||||
// stdin
|
||||
None => {
|
||||
base_common::handle_input(
|
||||
&mut stdin().lock(),
|
||||
format,
|
||||
config.wrap_cols,
|
||||
config.ignore_garbage,
|
||||
config.decode,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -10,6 +10,122 @@
|
|||
use std::io::{stdout, Read, Write};
|
||||
|
||||
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>(
|
||||
input: &mut R,
|
||||
|
@ -17,6 +133,7 @@ pub fn handle_input<R: Read>(
|
|||
line_wrap: Option<usize>,
|
||||
ignore_garbage: bool,
|
||||
decode: bool,
|
||||
name: &str,
|
||||
) {
|
||||
let mut data = Data::new(input, format).ignore_garbage(ignore_garbage);
|
||||
if let Some(wrap) = line_wrap {
|
||||
|
@ -31,10 +148,14 @@ pub fn handle_input<R: Read>(
|
|||
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");
|
||||
eprintln!("{}: error: Cannot write non-utf8 data", name);
|
||||
exit!(1)
|
||||
}
|
||||
}
|
||||
Err(_) => crash!(1, "invalid input"),
|
||||
Err(_) => {
|
||||
eprintln!("{}: error: invalid input", name);
|
||||
exit!(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,10 @@ edition = "2018"
|
|||
path = "src/base64.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33"
|
||||
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features = ["encoding"] }
|
||||
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
|
||||
uu_base32 = { version=">=0.0.6", package="uu_base32", path="../base32"}
|
||||
|
||||
[[bin]]
|
||||
name = "base64"
|
||||
|
|
|
@ -8,14 +8,14 @@
|
|||
|
||||
#[macro_use]
|
||||
extern crate uucore;
|
||||
|
||||
use uu_base32::base_common;
|
||||
|
||||
use uucore::encoding::Format;
|
||||
use uucore::InvalidEncodingHandling;
|
||||
|
||||
mod base_common;
|
||||
use std::io::{stdin, Read};
|
||||
|
||||
static SYNTAX: &str = "[OPTION]... [FILE]";
|
||||
static SUMMARY: &str = "Base64 encode or decode FILE, or standard input, to standard output.";
|
||||
static LONG_HELP: &str = "
|
||||
static ABOUT: &str = "
|
||||
With no FILE, or when FILE is -, read standard input.
|
||||
|
||||
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
|
||||
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 {
|
||||
base_common::execute(
|
||||
args.collect_str(InvalidEncodingHandling::Ignore)
|
||||
.accept_any(),
|
||||
SYNTAX,
|
||||
SUMMARY,
|
||||
LONG_HELP,
|
||||
Format::Base64,
|
||||
)
|
||||
let format = Format::Base64;
|
||||
let usage = get_usage();
|
||||
let name = executable!();
|
||||
let config_result: Result<base_common::Config, String> =
|
||||
base_common::parse_base_cmd_args(args, name, VERSION, ABOUT, &usage);
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ edition = "2018"
|
|||
path = "src/basename.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.2"
|
||||
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" }
|
||||
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
|
||||
|
||||
|
|
|
@ -10,83 +10,106 @@
|
|||
#[macro_use]
|
||||
extern crate uucore;
|
||||
|
||||
use clap::{App, Arg};
|
||||
use std::path::{is_separator, PathBuf};
|
||||
use uucore::InvalidEncodingHandling;
|
||||
|
||||
static NAME: &str = "basename";
|
||||
static SYNTAX: &str = "NAME [SUFFIX]";
|
||||
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
static SUMMARY: &str = "Print NAME with any leading directory components removed
|
||||
If specified, also remove a trailing SUFFIX";
|
||||
static LONG_HELP: &str = "";
|
||||
If specified, also remove a trailing SUFFIX";
|
||||
|
||||
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 {
|
||||
let args = args
|
||||
.collect_str(InvalidEncodingHandling::ConvertLossy)
|
||||
.accept_any();
|
||||
let usage = get_usage();
|
||||
//
|
||||
// Argument parsing
|
||||
//
|
||||
let matches = app!(SYNTAX, SUMMARY, LONG_HELP)
|
||||
.optflag(
|
||||
"a",
|
||||
"multiple",
|
||||
"Support more than one argument. Treat every argument as a name.",
|
||||
let matches = App::new(executable!())
|
||||
.version(VERSION)
|
||||
.about(SUMMARY)
|
||||
.usage(&usage[..])
|
||||
.arg(
|
||||
Arg::with_name(options::MULTIPLE)
|
||||
.short("a")
|
||||
.long(options::MULTIPLE)
|
||||
.help("support multiple arguments and treat each as a NAME"),
|
||||
)
|
||||
.optopt(
|
||||
"s",
|
||||
"suffix",
|
||||
"Remove a trailing suffix. This option implies the -a option.",
|
||||
"SUFFIX",
|
||||
.arg(Arg::with_name(options::NAME).multiple(true).hidden(true))
|
||||
.arg(
|
||||
Arg::with_name(options::SUFFIX)
|
||||
.short("s")
|
||||
.long(options::SUFFIX)
|
||||
.value_name("SUFFIX")
|
||||
.help("remove a trailing SUFFIX; implies -a"),
|
||||
)
|
||||
.optflag(
|
||||
"z",
|
||||
"zero",
|
||||
"Output a zero byte (ASCII NUL) at the end of each line, rather than a newline.",
|
||||
.arg(
|
||||
Arg::with_name(options::ZERO)
|
||||
.short("z")
|
||||
.long(options::ZERO)
|
||||
.help("end each output line with NUL, not newline"),
|
||||
)
|
||||
.parse(args);
|
||||
.get_matches_from(args);
|
||||
|
||||
// too few arguments
|
||||
if matches.free.is_empty() {
|
||||
if !matches.is_present(options::NAME) {
|
||||
crash!(
|
||||
1,
|
||||
"{0}: {1}\nTry '{0} --help' for more information.",
|
||||
NAME,
|
||||
"{1}\nTry '{0} --help' for more information.",
|
||||
executable!(),
|
||||
"missing operand"
|
||||
);
|
||||
}
|
||||
let opt_s = matches.opt_present("s");
|
||||
let opt_a = matches.opt_present("a");
|
||||
let opt_z = matches.opt_present("z");
|
||||
let multiple_paths = opt_s || opt_a;
|
||||
|
||||
let opt_suffix = matches.is_present(options::SUFFIX);
|
||||
let opt_multiple = matches.is_present(options::MULTIPLE);
|
||||
let opt_zero = matches.is_present(options::ZERO);
|
||||
let multiple_paths = opt_suffix || opt_multiple;
|
||||
// too many arguments
|
||||
if !multiple_paths && matches.free.len() > 2 {
|
||||
if !multiple_paths && matches.occurrences_of(options::NAME) > 2 {
|
||||
crash!(
|
||||
1,
|
||||
"{0}: extra operand '{1}'\nTry '{0} --help' for more information.",
|
||||
NAME,
|
||||
matches.free[2]
|
||||
"extra operand '{1}'\nTry '{0} --help' for more information.",
|
||||
executable!(),
|
||||
matches.values_of(options::NAME).unwrap().nth(2).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
let suffix = if opt_s {
|
||||
matches.opt_str("s").unwrap()
|
||||
} else if !opt_a && matches.free.len() > 1 {
|
||||
matches.free[1].clone()
|
||||
let suffix = if opt_suffix {
|
||||
matches.value_of(options::SUFFIX).unwrap()
|
||||
} else if !opt_multiple && matches.occurrences_of(options::NAME) > 1 {
|
||||
matches.values_of(options::NAME).unwrap().nth(1).unwrap()
|
||||
} else {
|
||||
"".to_owned()
|
||||
""
|
||||
};
|
||||
|
||||
//
|
||||
// Main Program Processing
|
||||
//
|
||||
|
||||
let paths = if multiple_paths {
|
||||
&matches.free[..]
|
||||
let paths: Vec<_> = if multiple_paths {
|
||||
matches.values_of(options::NAME).unwrap().collect()
|
||||
} 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 {
|
||||
print!("{}{}", basename(&path, &suffix), line_ending);
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ fn copy_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<
|
|||
let mut buf = [0; BUF_SIZE];
|
||||
loop {
|
||||
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;
|
||||
if left == 0 {
|
||||
break;
|
||||
|
|
|
@ -551,6 +551,10 @@ impl FromStr for Attribute {
|
|||
fn add_all_attributes() -> Vec<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];
|
||||
|
||||
#[cfg(unix)]
|
||||
|
|
46
src/uu/cut/BENCHMARKING.md
Normal file
46
src/uu/cut/BENCHMARKING.md
Normal 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.
|
|
@ -18,6 +18,8 @@ path = "src/cut.rs"
|
|||
clap = "2.33"
|
||||
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" }
|
||||
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
|
||||
memchr = "2"
|
||||
bstr = "0.2"
|
||||
|
||||
[[bin]]
|
||||
name = "cut"
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,16 +10,17 @@
|
|||
#[macro_use]
|
||||
extern crate uucore;
|
||||
|
||||
use bstr::io::BufReadExt;
|
||||
use clap::{App, Arg};
|
||||
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 self::searcher::Searcher;
|
||||
use uucore::fs::is_stdout_interactive;
|
||||
use uucore::ranges::Range;
|
||||
use uucore::InvalidEncodingHandling;
|
||||
|
||||
mod buffer;
|
||||
mod searcher;
|
||||
|
||||
static NAME: &str = "cut";
|
||||
|
@ -126,6 +127,14 @@ enum Mode {
|
|||
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> {
|
||||
if complement {
|
||||
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 {
|
||||
use self::buffer::Bytes::Select;
|
||||
use self::buffer::Bytes::Selected::*;
|
||||
|
||||
let newline_char = if opts.zero_terminated { b'\0' } else { b'\n' };
|
||||
let mut buf_read = buffer::ByteReader::new(reader, newline_char);
|
||||
let mut out = stdout();
|
||||
let buf_in = BufReader::new(reader);
|
||||
let mut out = stdout_writer();
|
||||
let delim = opts
|
||||
.out_delim
|
||||
.as_ref()
|
||||
.map_or("", String::as_str)
|
||||
.as_bytes();
|
||||
|
||||
'newline: loop {
|
||||
let mut cur_pos = 1;
|
||||
let res = buf_in.for_byte_record(newline_char, |line| {
|
||||
let mut print_delim = false;
|
||||
|
||||
for &Range { low, high } in ranges.iter() {
|
||||
// skip up to low
|
||||
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;
|
||||
}
|
||||
}
|
||||
for &Range { low, high } in ranges {
|
||||
if low > line.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(ref delim) = opts.out_delim {
|
||||
if print_delim {
|
||||
crash_if_err!(1, out.write_all(delim.as_bytes()));
|
||||
}
|
||||
if print_delim {
|
||||
out.write_all(delim)?;
|
||||
} else if opts.out_delim.is_some() {
|
||||
print_delim = true;
|
||||
}
|
||||
|
||||
// write out from low to high
|
||||
loop {
|
||||
match buf_read.select(high - cur_pos + 1, Some(&mut out)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// change `low` from 1-indexed value to 0-index value
|
||||
let low = low - 1;
|
||||
let high = high.min(line.len());
|
||||
out.write_all(&line[low..high])?;
|
||||
}
|
||||
|
||||
buf_read.consume_line();
|
||||
crash_if_err!(1, out.write_all(&[newline_char]));
|
||||
}
|
||||
|
||||
out.write_all(&[newline_char])?;
|
||||
Ok(true)
|
||||
});
|
||||
crash_if_err!(1, res);
|
||||
0
|
||||
}
|
||||
|
||||
|
@ -213,23 +185,11 @@ fn cut_fields_delimiter<R: Read>(
|
|||
newline_char: u8,
|
||||
out_delim: &str,
|
||||
) -> i32 {
|
||||
let mut buf_in = BufReader::new(reader);
|
||||
let mut out = stdout();
|
||||
let mut buffer = Vec::new();
|
||||
let buf_in = BufReader::new(reader);
|
||||
let mut out = stdout_writer();
|
||||
let input_delim_len = delim.len();
|
||||
|
||||
'newline: loop {
|
||||
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 result = buf_in.for_byte_record_with_terminator(newline_char, |line| {
|
||||
let mut fields_pos = 1;
|
||||
let mut low_idx = 0;
|
||||
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 !only_delimited {
|
||||
crash_if_err!(1, out.write_all(line));
|
||||
out.write_all(line)?;
|
||||
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 {
|
||||
low_idx = match delim_search.nth(low - fields_pos - 1) {
|
||||
Some((_, beyond_delim)) => beyond_delim,
|
||||
Some(index) => index + input_delim_len,
|
||||
None => break,
|
||||
};
|
||||
}
|
||||
|
||||
for _ in 0..=high - low {
|
||||
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() {
|
||||
Some((high_idx, next_low_idx)) => {
|
||||
Some(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 = next_low_idx;
|
||||
low_idx = high_idx + input_delim_len;
|
||||
fields_pos = high + 1;
|
||||
}
|
||||
None => {
|
||||
let segment = &line[low_idx..];
|
||||
|
||||
crash_if_err!(1, out.write_all(segment));
|
||||
out.write_all(segment)?;
|
||||
|
||||
if line[line.len() - 1] == newline_char {
|
||||
continue 'newline;
|
||||
return Ok(true);
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -304,23 +265,11 @@ fn cut_fields<R: Read>(reader: R, ranges: &[Range], opts: &FieldOptions) -> i32
|
|||
);
|
||||
}
|
||||
|
||||
let mut buf_in = BufReader::new(reader);
|
||||
let mut out = stdout();
|
||||
let mut buffer = Vec::new();
|
||||
let buf_in = BufReader::new(reader);
|
||||
let mut out = stdout_writer();
|
||||
let delim_len = opts.delimiter.len();
|
||||
|
||||
'newline: loop {
|
||||
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 result = buf_in.for_byte_record_with_terminator(newline_char, |line| {
|
||||
let mut fields_pos = 1;
|
||||
let mut low_idx = 0;
|
||||
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 !opts.only_delimited {
|
||||
crash_if_err!(1, out.write_all(line));
|
||||
out.write_all(line)?;
|
||||
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 {
|
||||
low_idx = match delim_search.nth(low - fields_pos - 1) {
|
||||
Some((_, beyond_delim)) => beyond_delim,
|
||||
None => break,
|
||||
};
|
||||
}
|
||||
|
||||
if print_delim && low_idx >= opts.delimiter.as_bytes().len() {
|
||||
low_idx -= opts.delimiter.as_bytes().len();
|
||||
if let Some(delim_pos) = delim_search.nth(low - fields_pos - 1) {
|
||||
low_idx = if print_delim {
|
||||
delim_pos
|
||||
} else {
|
||||
delim_pos + delim_len
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
match delim_search.nth(high - low) {
|
||||
Some((high_idx, next_low_idx)) => {
|
||||
Some(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 = next_low_idx;
|
||||
low_idx = high_idx;
|
||||
fields_pos = high + 1;
|
||||
}
|
||||
None => {
|
||||
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 {
|
||||
continue 'newline;
|
||||
return Ok(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crash_if_err!(1, out.write_all(&[newline_char]));
|
||||
}
|
||||
|
||||
out.write_all(&[newline_char])?;
|
||||
Ok(true)
|
||||
});
|
||||
crash_if_err!(1, result);
|
||||
0
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
#[derive(Clone)]
|
||||
use memchr::memchr;
|
||||
|
||||
pub struct Searcher<'a> {
|
||||
haystack: &'a [u8],
|
||||
needle: &'a [u8],
|
||||
|
@ -14,6 +15,7 @@ pub struct Searcher<'a> {
|
|||
|
||||
impl<'a> Searcher<'a> {
|
||||
pub fn new(haystack: &'a [u8], needle: &'a [u8]) -> Searcher<'a> {
|
||||
assert!(!needle.is_empty());
|
||||
Searcher {
|
||||
haystack,
|
||||
needle,
|
||||
|
@ -23,30 +25,81 @@ impl<'a> Searcher<'a> {
|
|||
}
|
||||
|
||||
impl<'a> Iterator for Searcher<'a> {
|
||||
type Item = (usize, usize);
|
||||
type Item = usize;
|
||||
|
||||
fn next(&mut self) -> Option<(usize, usize)> {
|
||||
if self.needle.len() == 1 {
|
||||
for offset in self.position..self.haystack.len() {
|
||||
if self.haystack[offset] == self.needle[0] {
|
||||
self.position = offset + 1;
|
||||
return Some((offset, offset + 1));
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if let Some(match_idx) = memchr(self.needle[0], self.haystack) {
|
||||
if self.needle.len() == 1
|
||||
|| self.haystack[match_idx + 1..].starts_with(&self.needle[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 {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -348,7 +348,7 @@ fn set_system_datetime(_date: DateTime<Utc>) -> i32 {
|
|||
#[cfg(target_os = "macos")]
|
||||
fn set_system_datetime(_date: DateTime<Utc>) -> i32 {
|
||||
eprintln!("date: setting the date is not supported by macOS");
|
||||
return 1;
|
||||
1
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
|
|
|
@ -916,6 +916,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
"Use%",
|
||||
]
|
||||
});
|
||||
if cfg!(target_os = "macos") && !opt.show_inode_instead {
|
||||
header.insert(header.len() - 1, "Capacity");
|
||||
}
|
||||
header.push("Mounted on");
|
||||
|
||||
for (idx, title) in header.iter().enumerate() {
|
||||
|
@ -970,6 +973,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
"{0: >12} ",
|
||||
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: <16}", fs.mountinfo.mount_dir);
|
||||
|
|
|
@ -12,37 +12,42 @@ use clap::{App, Arg};
|
|||
use std::path::Path;
|
||||
use uucore::InvalidEncodingHandling;
|
||||
|
||||
static NAME: &str = "dirname";
|
||||
static SYNTAX: &str = "[OPTION] NAME...";
|
||||
static SUMMARY: &str = "strip last component from file name";
|
||||
static ABOUT: &str = "strip last component from file name";
|
||||
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 {
|
||||
pub const ZERO: &str = "zero";
|
||||
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 {
|
||||
let args = args
|
||||
.collect_str(InvalidEncodingHandling::ConvertLossy)
|
||||
.accept_any();
|
||||
|
||||
let usage = get_usage();
|
||||
let after_help = get_long_usage();
|
||||
|
||||
let matches = App::new(executable!())
|
||||
.name(NAME)
|
||||
.usage(SYNTAX)
|
||||
.about(SUMMARY)
|
||||
.after_help(LONG_HELP)
|
||||
.about(ABOUT)
|
||||
.usage(&usage[..])
|
||||
.after_help(&after_help[..])
|
||||
.version(VERSION)
|
||||
.arg(
|
||||
Arg::with_name(options::ZERO)
|
||||
.short(options::ZERO)
|
||||
.long(options::ZERO)
|
||||
.short("z")
|
||||
.takes_value(false)
|
||||
.help("separate output with NUL rather than newline"),
|
||||
)
|
||||
.arg(Arg::with_name(options::DIR).hidden(true).multiple(true))
|
||||
|
|
|
@ -14,8 +14,8 @@ edition = "2018"
|
|||
[build-dependencies]
|
||||
num-traits = "0.2.13" # used in src/numerics.rs, which is included by build.rs
|
||||
|
||||
|
||||
[dependencies]
|
||||
coz = { version = "0.1.3", optional = true }
|
||||
num-traits = "0.2.13" # Needs at least version 0.2.13 for "OverflowingAdd"
|
||||
rand = { version="0.7", features=["small_rng"] }
|
||||
smallvec = { version="0.6.14, < 1.0" }
|
||||
|
|
|
@ -125,6 +125,8 @@ fn _factor<A: Arithmetic + miller_rabin::Basis>(num: u64, f: Factors) -> Factors
|
|||
let n = A::new(num);
|
||||
let divisor = match miller_rabin::test::<A>(n) {
|
||||
Prime => {
|
||||
#[cfg(feature = "coz")]
|
||||
coz::progress!("factor found");
|
||||
let mut r = f;
|
||||
r.push(num);
|
||||
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 {
|
||||
#[cfg(feature = "coz")]
|
||||
coz::begin!("factorization");
|
||||
let mut factors = Factors::one();
|
||||
|
||||
if n < 2 {
|
||||
|
@ -152,16 +156,23 @@ pub fn factor(mut n: u64) -> Factors {
|
|||
}
|
||||
|
||||
if n == 1 {
|
||||
#[cfg(feature = "coz")]
|
||||
coz::end!("factorization");
|
||||
return factors;
|
||||
}
|
||||
|
||||
let (factors, n) = table::factor(n, factors);
|
||||
|
||||
if n < (1 << 32) {
|
||||
let r = if n < (1 << 32) {
|
||||
_factor::<Montgomery<u32>>(n, factors)
|
||||
} else {
|
||||
_factor::<Montgomery<u64>>(n, factors)
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(feature = "coz")]
|
||||
coz::end!("factorization");
|
||||
|
||||
r
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -33,6 +33,8 @@ pub(crate) fn factor(mut num: u64, mut factors: Factors) -> (Factors, u64) {
|
|||
if x <= ceil {
|
||||
num = x;
|
||||
k += 1;
|
||||
#[cfg(feature = "coz")]
|
||||
coz::progress!("factor found");
|
||||
} else {
|
||||
if k > 0 {
|
||||
factors.add(prime, k);
|
||||
|
|
|
@ -15,6 +15,7 @@ edition = "2018"
|
|||
path = "src/kill.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33"
|
||||
libc = "0.2.42"
|
||||
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["signals"] }
|
||||
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
|
||||
|
|
|
@ -10,18 +10,26 @@
|
|||
#[macro_use]
|
||||
extern crate uucore;
|
||||
|
||||
use clap::{App, Arg};
|
||||
use libc::{c_int, pid_t};
|
||||
use std::io::Error;
|
||||
use uucore::signals::ALL_SIGNALS;
|
||||
use uucore::InvalidEncodingHandling;
|
||||
|
||||
static SYNTAX: &str = "[options] <pid> [...]";
|
||||
static SUMMARY: &str = "";
|
||||
static LONG_HELP: &str = "";
|
||||
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
static ABOUT: &str = "Send signal to processes or list information about signals.";
|
||||
|
||||
static EXIT_OK: i32 = 0;
|
||||
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)]
|
||||
pub enum Mode {
|
||||
Kill,
|
||||
|
@ -33,41 +41,70 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
let args = args
|
||||
.collect_str(InvalidEncodingHandling::Ignore)
|
||||
.accept_any();
|
||||
|
||||
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
|
||||
} else if matches.opt_present("list") {
|
||||
} else if matches.is_present(options::LIST) {
|
||||
Mode::List
|
||||
} else {
|
||||
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 {
|
||||
Mode::Kill => {
|
||||
return kill(
|
||||
&matches
|
||||
.opt_str("signal")
|
||||
.unwrap_or_else(|| obs_signal.unwrap_or_else(|| "9".to_owned())),
|
||||
matches.free,
|
||||
)
|
||||
let sig = match (obs_signal, matches.value_of(options::SIGNAL)) {
|
||||
(Some(s), Some(_)) => s, // -s takes precedence
|
||||
(Some(s), None) => s,
|
||||
(None, Some(s)) => s.to_owned(),
|
||||
(None, None) => "TERM".to_owned(),
|
||||
};
|
||||
return kill(&sig, &pids_or_signals);
|
||||
}
|
||||
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>) {
|
||||
|
@ -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 optional_signal_value = uucore::signals::signal_by_name_or_value(signalname);
|
||||
let signal_value = match optional_signal_value {
|
||||
Some(x) => x,
|
||||
None => crash!(EXIT_ERR, "unknown signal name {}", signalname),
|
||||
};
|
||||
for pid in &pids {
|
||||
for pid in pids {
|
||||
match pid.parse::<usize>() {
|
||||
Ok(x) => {
|
||||
if unsafe { libc::kill(x as pid_t, signal_value as c_int) } != 0 {
|
||||
|
|
|
@ -18,6 +18,7 @@ path = "src/link.rs"
|
|||
libc = "0.2.42"
|
||||
uucore = { version=">=0.0.8", package="uucore", path="../../uucore" }
|
||||
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
|
||||
clap = "2.33"
|
||||
|
||||
[[bin]]
|
||||
name = "link"
|
||||
|
|
|
@ -8,14 +8,21 @@
|
|||
#[macro_use]
|
||||
extern crate uucore;
|
||||
|
||||
use clap::{App, Arg};
|
||||
use std::fs::hard_link;
|
||||
use std::io::Error;
|
||||
use std::path::Path;
|
||||
use uucore::InvalidEncodingHandling;
|
||||
|
||||
static SYNTAX: &str = "[OPTIONS] FILE1 FILE2";
|
||||
static SUMMARY: &str = "Create a link named FILE2 to FILE1";
|
||||
static LONG_HELP: &str = "";
|
||||
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
static ABOUT: &str = "Call the link function to create a link named FILE2 to an existing FILE1.";
|
||||
|
||||
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 {
|
||||
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 {
|
||||
let matches = app!(SYNTAX, SUMMARY, LONG_HELP).parse(
|
||||
args.collect_str(InvalidEncodingHandling::Ignore)
|
||||
.accept_any(),
|
||||
);
|
||||
if matches.free.len() != 2 {
|
||||
crash!(1, "{}", msg_wrong_number_of_arguments!(2));
|
||||
}
|
||||
let usage = get_usage();
|
||||
let matches = App::new(executable!())
|
||||
.version(VERSION)
|
||||
.about(ABOUT)
|
||||
.usage(&usage[..])
|
||||
.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 new = Path::new(&matches.free[1]);
|
||||
let files: Vec<_> = matches
|
||||
.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) {
|
||||
Ok(_) => 0,
|
||||
|
|
|
@ -36,6 +36,8 @@ 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
|
||||
|
||||
- Another thing to look at would be system calls count using strace (on linux) or equivalent on other operating systems.
|
||||
|
|
|
@ -15,16 +15,17 @@ edition = "2018"
|
|||
path = "src/ls.rs"
|
||||
|
||||
[dependencies]
|
||||
locale = "0.2.2"
|
||||
chrono = "0.4.19"
|
||||
clap = "2.33"
|
||||
unicode-width = "0.1.8"
|
||||
number_prefix = "0.4"
|
||||
term_grid = "0.1.5"
|
||||
termsize = "0.1.6"
|
||||
time = "0.1.40"
|
||||
globset = "0.4.6"
|
||||
lscolors = { version="0.7.1", features=["ansi_term"] }
|
||||
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "fs"] }
|
||||
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
|
||||
lscolors = { version = "0.7.1", features = ["ansi_term"] }
|
||||
uucore = { version = ">=0.0.8", package = "uucore", path = "../../uucore", features = ["entries", "fs"] }
|
||||
uucore_procs = { version = ">=0.0.5", package = "uucore_procs", path = "../../uucore_procs" }
|
||||
once_cell = "1.7.2"
|
||||
atty = "0.2"
|
||||
|
||||
|
|
|
@ -38,8 +38,9 @@ use std::{
|
|||
os::unix::fs::{FileTypeExt, MetadataExt},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
|
||||
use time::{strftime, Timespec};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
#[cfg(unix)]
|
||||
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
|
||||
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 {
|
||||
format!("{0} [OPTION]... [FILE]...", executable!())
|
||||
|
@ -117,6 +120,8 @@ pub mod options {
|
|||
pub static COLOR: &str = "color";
|
||||
pub static PATHS: &str = "paths";
|
||||
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 IGNORE: &str = "ignore";
|
||||
}
|
||||
|
@ -156,6 +161,15 @@ enum Time {
|
|||
Modification,
|
||||
Access,
|
||||
Change,
|
||||
Birth,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TimeStyle {
|
||||
FullIso,
|
||||
LongIso,
|
||||
Iso,
|
||||
Locale,
|
||||
}
|
||||
|
||||
enum Dereference {
|
||||
|
@ -191,6 +205,7 @@ struct Config {
|
|||
width: Option<u16>,
|
||||
quoting_style: QuotingStyle,
|
||||
indicator_style: IndicatorStyle,
|
||||
time_style: TimeStyle,
|
||||
}
|
||||
|
||||
// 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_GROUP,
|
||||
options::format::LONG_NUMERIC_UID_GID,
|
||||
options::FULL_TIME,
|
||||
]
|
||||
.iter()
|
||||
.flat_map(|opt| options.indices_of(opt))
|
||||
|
@ -302,6 +318,7 @@ impl Config {
|
|||
match field {
|
||||
"ctime" | "status" => Time::Change,
|
||||
"access" | "atime" | "use" => Time::Access,
|
||||
"birth" | "creation" => Time::Birth,
|
||||
// below should never happen as clap already restricts the values.
|
||||
_ => unreachable!("Invalid field for --time"),
|
||||
}
|
||||
|
@ -439,6 +456,30 @@ impl Config {
|
|||
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();
|
||||
if options.is_present(options::IGNORE_BACKUPS) {
|
||||
ignore_patterns.add(Glob::new("*~").unwrap());
|
||||
|
@ -504,6 +545,7 @@ impl Config {
|
|||
width,
|
||||
quoting_style,
|
||||
indicator_style,
|
||||
time_style,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -696,10 +738,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
.long(options::TIME)
|
||||
.help("Show time in <field>:\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")
|
||||
.takes_value(true)
|
||||
.possible_values(&["atime", "access", "use", "ctime", "status"])
|
||||
.possible_values(&["atime", "access", "use", "ctime", "status", "birth", "creation"])
|
||||
.hide_possible_values(true)
|
||||
.require_equals(true)
|
||||
.overrides_with_all(&[
|
||||
|
@ -1020,9 +1063,34 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
options::indicator_style::CLASSIFY,
|
||||
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
|
||||
.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);
|
||||
|
||||
|
@ -1052,14 +1120,21 @@ impl PathData {
|
|||
fn new(
|
||||
p_buf: PathBuf,
|
||||
file_type: Option<std::io::Result<FileType>>,
|
||||
file_name: Option<String>,
|
||||
config: &Config,
|
||||
command_line: bool,
|
||||
) -> Self {
|
||||
let name = p_buf
|
||||
.file_name()
|
||||
.unwrap_or_else(|| p_buf.iter().next_back().unwrap())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
// We cannot use `Path::ends_with` or `Path::Components`, because they remove occurrences of '.'
|
||||
// For '..', the filename is None
|
||||
let name = if let Some(name) = file_name {
|
||||
name
|
||||
} 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 {
|
||||
Dereference::All => true,
|
||||
Dereference::Args => command_line,
|
||||
|
@ -1122,7 +1197,7 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
|
|||
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() {
|
||||
!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)))
|
||||
}
|
||||
// The default sort in GNU ls is case insensitive
|
||||
Sort::Name => entries.sort_by_cached_key(|k| {
|
||||
let has_dot: bool = k.file_name.starts_with('.');
|
||||
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::Name => entries.sort_by(|a, b| a.file_name.cmp(&b.file_name)),
|
||||
Sort::Version => entries.sort_by(|a, b| version_cmp::version_cmp(&a.p_buf, &b.p_buf)),
|
||||
Sort::Extension => entries.sort_by(|a, b| {
|
||||
a.p_buf
|
||||
.extension()
|
||||
|
@ -1213,8 +1282,14 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool {
|
|||
fn enter_directory(dir: &PathData, config: &Config, out: &mut BufWriter<Stdout>) {
|
||||
let mut entries: Vec<_> = if config.files == Files::All {
|
||||
vec![
|
||||
PathData::new(dir.p_buf.join("."), None, config, false),
|
||||
PathData::new(dir.p_buf.join(".."), None, config, false),
|
||||
PathData::new(
|
||||
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 {
|
||||
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))
|
||||
.map(|res| safe_unwrap!(res))
|
||||
.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();
|
||||
|
||||
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::Modification => md.modified().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 {
|
||||
Time::Modification => md.modified().ok(),
|
||||
Time::Access => md.accessed().ok(),
|
||||
Time::Birth => md.created().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_time(md: &Metadata, config: &Config) -> Option<time::Tm> {
|
||||
let duration = get_system_time(md, config)?
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()?;
|
||||
let secs = duration.as_secs() as i64;
|
||||
let nsec = duration.subsec_nanos() as i32;
|
||||
Some(time::at(Timespec::new(secs, nsec)))
|
||||
fn get_time(md: &Metadata, config: &Config) -> Option<chrono::DateTime<chrono::Local>> {
|
||||
let time = get_system_time(md, config)?;
|
||||
Some(time.into())
|
||||
}
|
||||
|
||||
fn display_date(metadata: &Metadata, config: &Config) -> String {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ path = "src/pinky.rs"
|
|||
[dependencies]
|
||||
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["utmpx", "entries"] }
|
||||
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
|
||||
clap = "2.33.3"
|
||||
|
||||
[[bin]]
|
||||
name = "pinky"
|
||||
|
|
|
@ -19,67 +19,110 @@ use std::io::BufReader;
|
|||
use std::fs::File;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use clap::{App, Arg};
|
||||
use std::path::PathBuf;
|
||||
use uucore::InvalidEncodingHandling;
|
||||
|
||||
static SYNTAX: &str = "[OPTION]... [USER]...";
|
||||
static SUMMARY: &str = "A lightweight 'finger' program; print user information.";
|
||||
|
||||
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 {
|
||||
let args = args
|
||||
.collect_str(InvalidEncodingHandling::Ignore)
|
||||
.accept_any();
|
||||
|
||||
let long_help = &format!(
|
||||
"
|
||||
-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
|
||||
let usage = get_usage();
|
||||
let after_help = get_long_usage();
|
||||
|
||||
The utmp file will be {}",
|
||||
utmpx::DEFAULT_FILE
|
||||
);
|
||||
let mut opts = app!(SYNTAX, SUMMARY, &long_help);
|
||||
opts.optflag(
|
||||
"l",
|
||||
"",
|
||||
"produce long format output for the specified USERs",
|
||||
);
|
||||
opts.optflag(
|
||||
"b",
|
||||
"",
|
||||
"omit the user's home directory and shell in long format",
|
||||
);
|
||||
opts.optflag("h", "", "omit the user's project file 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");
|
||||
opts.optflag("f", "", "omit the line of column headings in short format");
|
||||
opts.optflag("w", "", "omit the user's full name in short format");
|
||||
opts.optflag(
|
||||
"i",
|
||||
"",
|
||||
"omit the user's full name and remote host in short format",
|
||||
);
|
||||
opts.optflag(
|
||||
"q",
|
||||
"",
|
||||
"omit the user's full name, remote host and idle time in short format",
|
||||
);
|
||||
opts.optflag("", "help", "display this help and exit");
|
||||
opts.optflag("", "version", "output version information and exit");
|
||||
let matches = App::new(executable!())
|
||||
.version(VERSION)
|
||||
.about(ABOUT)
|
||||
.usage(&usage[..])
|
||||
.after_help(&after_help[..])
|
||||
.arg(
|
||||
Arg::with_name(options::LONG_FORMAT)
|
||||
.short("l")
|
||||
.requires(options::USER)
|
||||
.help("produce long format output for the specified USERs"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name(options::OMIT_HOME_DIR)
|
||||
.short("b")
|
||||
.help("omit the user's home directory and shell in long format"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name(options::OMIT_PROJECT_FILE)
|
||||
.short("h")
|
||||
.help("omit the user's project file in long format"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name(options::OMIT_PLAN_FILE)
|
||||
.short("p")
|
||||
.help("omit the user's plan file in long format"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name(options::SHORT_FORMAT)
|
||||
.short("s")
|
||||
.help("do short format output, this is the default"),
|
||||
)
|
||||
.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
|
||||
// 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;
|
||||
|
||||
// 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.
|
||||
let mut include_fullname = true;
|
||||
|
||||
// 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.
|
||||
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
|
||||
// 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.
|
||||
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. */
|
||||
let mut include_where = true;
|
||||
|
||||
if matches.opt_present("w") {
|
||||
if matches.is_present(options::OMIT_NAME) {
|
||||
include_fullname = false;
|
||||
}
|
||||
if matches.opt_present("i") {
|
||||
if matches.is_present(options::OMIT_NAME_HOST) {
|
||||
include_fullname = false;
|
||||
include_where = false;
|
||||
}
|
||||
if matches.opt_present("q") {
|
||||
if matches.is_present(options::OMIT_NAME_HOST_TIME) {
|
||||
include_fullname = false;
|
||||
include_idle = 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 {
|
||||
include_idle,
|
||||
include_heading,
|
||||
|
@ -134,7 +172,7 @@ The utmp file will be {}",
|
|||
include_plan,
|
||||
include_home_and_shell,
|
||||
include_where,
|
||||
names: matches.free,
|
||||
names: users,
|
||||
};
|
||||
|
||||
if do_short_format {
|
||||
|
|
|
@ -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.
|
||||
// until then, the below operates as an outline
|
||||
// of how it would work.
|
||||
let result: Vec<u8> = vec![0];
|
||||
let mut factor: f64 = 1_f64;
|
||||
let radix_src_float: f64 = f64::from(radix_src);
|
||||
let mut r: f64 = 0_f64;
|
||||
|
|
|
@ -15,16 +15,19 @@ edition = "2018"
|
|||
path = "src/sort.rs"
|
||||
|
||||
[dependencies]
|
||||
serde_json = { version = "1.0.64", default-features = false, features = ["alloc"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
rayon = "1.5"
|
||||
rand = "0.7"
|
||||
clap = "2.33"
|
||||
fnv = "1.0.7"
|
||||
itertools = "0.10.0"
|
||||
semver = "0.9.0"
|
||||
smallvec = "1.6.1"
|
||||
smallvec = { version="1.6.1", features=["serde"] }
|
||||
unicode-width = "0.1.8"
|
||||
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] }
|
||||
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
|
||||
tempdir = "0.3.7"
|
||||
|
||||
[[bin]]
|
||||
name = "sort"
|
||||
|
|
64
src/uu/sort/src/custom_str_cmp.rs
Normal file
64
src/uu/sort/src/custom_str_cmp.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
src/uu/sort/src/external_sort/LICENSE
Normal file
19
src/uu/sort/src/external_sort/LICENSE
Normal 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.
|
295
src/uu/sort/src/external_sort/mod.rs
Normal file
295
src/uu/sort/src/external_sort/mod.rs
Normal 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)
|
||||
}
|
|
@ -14,20 +14,21 @@
|
|||
//! 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]).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Ordering, ops::Range};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
|
||||
enum Sign {
|
||||
Negative,
|
||||
Positive,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
|
||||
pub struct NumInfo {
|
||||
exponent: i64,
|
||||
sign: Sign,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
|
||||
pub struct NumInfoParseSettings {
|
||||
pub accept_si_units: bool,
|
||||
pub thousands_separator: Option<char>,
|
||||
|
|
|
@ -15,9 +15,13 @@
|
|||
#[macro_use]
|
||||
extern crate uucore;
|
||||
|
||||
mod custom_str_cmp;
|
||||
mod external_sort;
|
||||
mod numeric_str_cmp;
|
||||
|
||||
use clap::{App, Arg};
|
||||
use custom_str_cmp::custom_str_cmp;
|
||||
use external_sort::{ExternalSorter, ExternallySortable};
|
||||
use fnv::FnvHasher;
|
||||
use itertools::Itertools;
|
||||
use numeric_str_cmp::{numeric_str_cmp, NumInfo, NumInfoParseSettings};
|
||||
|
@ -25,6 +29,7 @@ use rand::distributions::Alphanumeric;
|
|||
use rand::{thread_rng, Rng};
|
||||
use rayon::prelude::*;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use std::cmp::Ordering;
|
||||
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::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
use uucore::fs::is_stdin_interactive; // for Iterator::dedup()
|
||||
use uucore::InvalidEncodingHandling;
|
||||
|
@ -77,6 +83,8 @@ static OPT_RANDOM: &str = "random-sort";
|
|||
static OPT_ZERO_TERMINATED: &str = "zero-terminated";
|
||||
static OPT_PARALLEL: &str = "parallel";
|
||||
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";
|
||||
|
||||
|
@ -86,6 +94,8 @@ static THOUSANDS_SEP: char = ',';
|
|||
static NEGATIVE: char = '-';
|
||||
static POSITIVE: char = '+';
|
||||
|
||||
static DEFAULT_BUF_SIZE: usize = std::usize::MAX;
|
||||
|
||||
#[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy)]
|
||||
enum SortMode {
|
||||
Numeric,
|
||||
|
@ -95,7 +105,7 @@ enum SortMode {
|
|||
Version,
|
||||
Default,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct GlobalSettings {
|
||||
mode: SortMode,
|
||||
debug: bool,
|
||||
|
@ -116,6 +126,31 @@ struct GlobalSettings {
|
|||
separator: Option<char>,
|
||||
threads: String,
|
||||
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 {
|
||||
|
@ -140,10 +175,13 @@ impl Default for GlobalSettings {
|
|||
separator: None,
|
||||
threads: String::new(),
|
||||
zero_terminated: false,
|
||||
buffer_size: DEFAULT_BUF_SIZE,
|
||||
tmp_dir: PathBuf::new(),
|
||||
ext_sort: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct KeySettings {
|
||||
mode: SortMode,
|
||||
ignore_blanks: bool,
|
||||
|
@ -168,37 +206,29 @@ impl From<&GlobalSettings> for KeySettings {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
/// Represents the string selected by a FieldSelector.
|
||||
enum SelectionRange {
|
||||
/// If we had to transform this selection, we have to store a new string.
|
||||
String(String),
|
||||
/// If there was no transformation, we can store an index into the line.
|
||||
ByIndex(Range<usize>),
|
||||
struct SelectionRange {
|
||||
range: Range<usize>,
|
||||
}
|
||||
|
||||
impl SelectionRange {
|
||||
fn new(range: Range<usize>) -> Self {
|
||||
Self { range }
|
||||
}
|
||||
|
||||
/// Gets the actual string slice represented by this Selection.
|
||||
fn get_str<'a>(&'a self, line: &'a str) -> &'a str {
|
||||
match self {
|
||||
SelectionRange::String(string) => string.as_str(),
|
||||
SelectionRange::ByIndex(range) => &line[range.to_owned()],
|
||||
}
|
||||
fn get_str<'a>(&self, line: &'a str) -> &'a str {
|
||||
&line[self.range.to_owned()]
|
||||
}
|
||||
|
||||
fn shorten(&mut self, new_range: Range<usize>) {
|
||||
match self {
|
||||
SelectionRange::String(string) => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
self.range.end = self.range.start + new_range.end;
|
||||
self.range.start += new_range.start;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
enum NumCache {
|
||||
AsF64(GeneralF64ParseResult),
|
||||
WithInfo(NumInfo),
|
||||
|
@ -219,7 +249,7 @@ impl NumCache {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct Selection {
|
||||
range: SelectionRange,
|
||||
num_cache: NumCache,
|
||||
|
@ -234,12 +264,20 @@ impl Selection {
|
|||
|
||||
type Field = Range<usize>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct Line {
|
||||
line: String,
|
||||
// The common case is not to specify fields. Let's make this fast.
|
||||
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 {
|
||||
fn new(line: String, settings: &GlobalSettings) -> Self {
|
||||
let fields = if settings
|
||||
|
@ -257,14 +295,8 @@ impl Line {
|
|||
.selectors
|
||||
.iter()
|
||||
.map(|selector| {
|
||||
let range = selector.get_selection(&line, fields.as_deref());
|
||||
let mut range = if let Some(transformed) =
|
||||
transform(&line[range.to_owned()], &selector.settings)
|
||||
{
|
||||
SelectionRange::String(transformed)
|
||||
} else {
|
||||
SelectionRange::ByIndex(range)
|
||||
};
|
||||
let mut range =
|
||||
SelectionRange::new(selector.get_selection(&line, fields.as_deref()));
|
||||
let num_cache = if selector.settings.mode == SortMode::Numeric
|
||||
|| 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.
|
||||
fn tokenize(line: &str, separator: Option<char>) -> Vec<Field> {
|
||||
if let Some(separator) = separator {
|
||||
|
@ -489,6 +493,7 @@ fn tokenize_with_separator(line: &str, separator: char) -> Vec<Field> {
|
|||
tokens
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct KeyPosition {
|
||||
/// 1-indexed, 0 is invalid.
|
||||
field: usize,
|
||||
|
@ -578,7 +583,7 @@ impl KeyPosition {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FieldSelector {
|
||||
from: KeyPosition,
|
||||
to: Option<KeyPosition>,
|
||||
|
@ -912,6 +917,22 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
.takes_value(true)
|
||||
.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::with_name(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);
|
||||
}
|
||||
|
||||
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.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 file_merger = FileMerger::new(&settings);
|
||||
|
||||
|
@ -1105,6 +1149,13 @@ fn exec(files: Vec<String>, settings: &GlobalSettings) -> i32 {
|
|||
|
||||
if settings.check {
|
||||
return exec_check_file(&lines, &settings);
|
||||
}
|
||||
|
||||
// Only use ext_sorter when we need to.
|
||||
// Probably faster that we don't create
|
||||
// an owned value each run
|
||||
if settings.ext_sort {
|
||||
lines = ext_sort_by(lines, settings.clone());
|
||||
} else {
|
||||
sort_by(&mut lines, &settings);
|
||||
}
|
||||
|
@ -1112,7 +1163,7 @@ fn exec(files: Vec<String>, settings: &GlobalSettings) -> i32 {
|
|||
if settings.merge {
|
||||
if settings.unique {
|
||||
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,
|
||||
)
|
||||
} else {
|
||||
|
@ -1122,7 +1173,7 @@ fn exec(files: Vec<String>, settings: &GlobalSettings) -> i32 {
|
|||
print_sorted(
|
||||
lines
|
||||
.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,
|
||||
)
|
||||
} 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 {
|
||||
lines.par_sort_by(|a, b| compare_by(a, b, &settings))
|
||||
unsorted.par_sort_by(|a, b| compare_by(a, b, &settings))
|
||||
} 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::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 {
|
||||
|
@ -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 {
|
||||
Ordering::Equal
|
||||
} else {
|
||||
default_compare(&a.line, &b.line)
|
||||
a.line.cmp(&b.line)
|
||||
};
|
||||
|
||||
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.
|
||||
// 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.
|
||||
|
@ -1268,7 +1332,7 @@ fn get_leading_gen(input: &str) -> Range<usize> {
|
|||
leading_whitespace_len..input.len()
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, PartialOrd)]
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, PartialOrd)]
|
||||
enum GeneralF64ParseResult {
|
||||
Invalid,
|
||||
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) {
|
||||
let mut file: Box<dyn Write> = match settings.outfile {
|
||||
Some(ref filename) => match File::create(Path::new(&filename)) {
|
||||
|
@ -1491,14 +1539,6 @@ mod tests {
|
|||
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]
|
||||
fn test_month_compare() {
|
||||
let a = "JaN";
|
||||
|
|
292
src/uu/test/src/parser.rs
Normal file
292
src/uu/test/src/parser.rs
Normal 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)
|
||||
}
|
|
@ -1,144 +1,154 @@
|
|||
// * This file is part of the uutils coreutils package.
|
||||
// *
|
||||
// * (c) mahkoh (ju.orth [at] gmail [dot] com)
|
||||
// *
|
||||
// * For the full copyright and license information, please view the LICENSE
|
||||
// * file that was distributed with this source code.
|
||||
// This file is part of the uutils coreutils package.
|
||||
//
|
||||
// (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.
|
||||
|
||||
// spell-checker:ignore (ToDO) retval paren prec subprec cond
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::str::from_utf8;
|
||||
mod parser;
|
||||
|
||||
static NAME: &str = "test";
|
||||
use parser::{parse, Symbol};
|
||||
use std::ffi::{OsStr, OsString};
|
||||
|
||||
pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||
let args: Vec<_> = args.collect();
|
||||
// This is completely disregarding valid windows paths that aren't valid unicode
|
||||
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
|
||||
}
|
||||
}
|
||||
// TODO: handle being called as `[`
|
||||
let args: Vec<_> = args.skip(1).collect();
|
||||
|
||||
fn one(args: &[&[u8]]) -> bool {
|
||||
!args[0].is_empty()
|
||||
}
|
||||
let result = parse(args).and_then(|mut stack| eval(&mut stack));
|
||||
|
||||
fn two(args: &[&[u8]], error: &mut bool) -> bool {
|
||||
match args[0] {
|
||||
b"!" => !one(&args[1..]),
|
||||
b"-b" => path(args[1], PathCondition::BlockSpecial),
|
||||
b"-c" => path(args[1], PathCondition::CharacterSpecial),
|
||||
b"-d" => path(args[1], PathCondition::Directory),
|
||||
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
|
||||
match result {
|
||||
Ok(result) => {
|
||||
if result {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn four(args: &[&[u8]], error: &mut bool) -> bool {
|
||||
match args[0] {
|
||||
b"!" => !three(&args[1..], error),
|
||||
_ => {
|
||||
*error = true;
|
||||
false
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("test: {}", e);
|
||||
2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum IntegerCondition {
|
||||
Equal,
|
||||
Unequal,
|
||||
Greater,
|
||||
GreaterEqual,
|
||||
Less,
|
||||
LessEqual,
|
||||
}
|
||||
/// Evaluate a stack of Symbols, returning the result of the evaluation or
|
||||
/// an error message if evaluation failed.
|
||||
fn eval(stack: &mut Vec<Symbol>) -> Result<bool, String> {
|
||||
macro_rules! pop_literal {
|
||||
() => {
|
||||
match stack.pop() {
|
||||
Some(Symbol::Literal(s)) => s,
|
||||
_ => panic!(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn integers(a: &[u8], b: &[u8], cond: IntegerCondition) -> bool {
|
||||
let (a, b): (&str, &str) = match (from_utf8(a), from_utf8(b)) {
|
||||
(Ok(a), Ok(b)) => (a, b),
|
||||
_ => return false,
|
||||
};
|
||||
let (a, b): (i64, i64) = match (a.parse(), b.parse()) {
|
||||
(Ok(a), Ok(b)) => (a, b),
|
||||
_ => return false,
|
||||
};
|
||||
match cond {
|
||||
IntegerCondition::Equal => a == b,
|
||||
IntegerCondition::Unequal => a != b,
|
||||
IntegerCondition::Greater => a > b,
|
||||
IntegerCondition::GreaterEqual => a >= b,
|
||||
IntegerCondition::Less => a < b,
|
||||
IntegerCondition::LessEqual => a <= b,
|
||||
let s = stack.pop();
|
||||
|
||||
match s {
|
||||
Some(Symbol::Bang) => {
|
||||
let result = eval(stack)?;
|
||||
|
||||
Ok(!result)
|
||||
}
|
||||
Some(Symbol::StringOp(op)) => {
|
||||
let b = stack.pop();
|
||||
let a = stack.pop();
|
||||
Ok(if op == "=" { a == b } else { a != b })
|
||||
}
|
||||
Some(Symbol::IntOp(op)) => {
|
||||
let b = pop_literal!();
|
||||
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 {
|
||||
from_utf8(fd)
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.map_or(false, |i| {
|
||||
fn integers(a: &OsStr, b: &OsStr, cond: &OsStr) -> Result<bool, String> {
|
||||
let format_err = |value| format!("invalid integer ‘{}’", value);
|
||||
|
||||
let a = a.to_string_lossy();
|
||||
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"))]
|
||||
unsafe {
|
||||
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)]
|
||||
enum PathCondition {
|
||||
BlockSpecial,
|
||||
|
@ -334,14 +177,10 @@ enum PathCondition {
|
|||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn path(path: &[u8], cond: PathCondition) -> bool {
|
||||
use std::ffi::OsStr;
|
||||
fn path(path: &OsStr, cond: PathCondition) -> bool {
|
||||
use std::fs::{self, Metadata};
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
||||
|
||||
let path = OsStr::from_bytes(path);
|
||||
|
||||
const S_ISUID: u32 = 0o4000;
|
||||
const S_ISGID: u32 = 0o2000;
|
||||
|
||||
|
@ -403,13 +242,14 @@ fn path(path: &[u8], cond: PathCondition) -> bool {
|
|||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn path(path: &[u8], cond: PathCondition) -> bool {
|
||||
fn path(path: &OsStr, cond: PathCondition) -> bool {
|
||||
use std::fs::metadata;
|
||||
let path = from_utf8(path).unwrap();
|
||||
|
||||
let stat = match metadata(path) {
|
||||
Ok(s) => s,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
match cond {
|
||||
PathCondition::BlockSpecial => false,
|
||||
PathCondition::CharacterSpecial => false,
|
||||
|
|
|
@ -23,11 +23,9 @@ use std::io::{stdin, stdout, BufRead, BufWriter, Write};
|
|||
use crate::expand::ExpandSet;
|
||||
use uucore::InvalidEncodingHandling;
|
||||
|
||||
static NAME: &str = "tr";
|
||||
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
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;
|
||||
|
||||
mod options {
|
||||
|
@ -125,10 +123,17 @@ impl SymbolTranslator for DeleteAndSqueezeOperation {
|
|||
|
||||
struct TranslateOperation {
|
||||
translate_map: FnvHashMap<usize, char>,
|
||||
complement: bool,
|
||||
s2_last: char,
|
||||
}
|
||||
|
||||
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 s2_prev = '_';
|
||||
for i in set1 {
|
||||
|
@ -141,13 +146,54 @@ impl TranslateOperation {
|
|||
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 {
|
||||
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
|
||||
let filtered = buf.chars().filter_map(|c| {
|
||||
let res = translator.translate(c, prev_c);
|
||||
if res.is_some() {
|
||||
prev_c = c;
|
||||
// Set `prev_c` to the post-translate character. This
|
||||
// allows the squeeze operation to correctly function
|
||||
// after the translate operation.
|
||||
if let Some(rc) = res {
|
||||
prev_c = rc;
|
||||
}
|
||||
res
|
||||
});
|
||||
|
@ -186,24 +235,38 @@ fn get_usage() -> String {
|
|||
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 {
|
||||
let usage = get_usage();
|
||||
let args = args
|
||||
.collect_str(InvalidEncodingHandling::ConvertLossy)
|
||||
.accept_any();
|
||||
|
||||
let usage = get_usage();
|
||||
let after_help = get_long_usage();
|
||||
|
||||
let matches = App::new(executable!())
|
||||
.version(VERSION)
|
||||
.about(ABOUT)
|
||||
.usage(&usage[..])
|
||||
.after_help(LONG_HELP)
|
||||
.after_help(&after_help[..])
|
||||
.arg(
|
||||
Arg::with_name(options::COMPLEMENT)
|
||||
.short("C")
|
||||
// .visible_short_alias('C') // TODO: requires clap "3.0.0-beta.2"
|
||||
.short("c")
|
||||
.long(options::COMPLEMENT)
|
||||
.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::with_name(options::DELETE)
|
||||
.short("d")
|
||||
|
@ -216,8 +279,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
.short("s")
|
||||
.help(
|
||||
"replace each sequence of a repeated character that is
|
||||
listed in the last specified SET, with a single occurrence
|
||||
of that character",
|
||||
listed in the last specified SET, with a single occurrence
|
||||
of that character",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
|
@ -230,7 +293,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
.get_matches_from(args);
|
||||
|
||||
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 truncate_flag = matches.is_present(options::TRUNCATE);
|
||||
|
||||
|
@ -242,7 +305,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
if sets.is_empty() {
|
||||
show_error!(
|
||||
"missing operand\nTry `{} --help` for more information.",
|
||||
NAME
|
||||
executable!()
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
@ -251,16 +314,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
show_error!(
|
||||
"missing operand after ‘{}’\nTry `{} --help` for more information.",
|
||||
sets[0],
|
||||
NAME
|
||||
executable!()
|
||||
);
|
||||
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 mut locked_stdin = stdin.lock();
|
||||
let stdout = stdout();
|
||||
|
@ -278,12 +336,25 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
translate_input(&mut locked_stdin, &mut buffered_stdout, op);
|
||||
}
|
||||
} else if squeeze_flag {
|
||||
let op = SqueezeOperation::new(set1, complement_flag);
|
||||
translate_input(&mut locked_stdin, &mut buffered_stdout, op);
|
||||
if sets.len() < 2 {
|
||||
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 {
|
||||
let mut set2 = ExpandSet::new(sets[1].as_ref());
|
||||
let op = TranslateOperation::new(set1, &mut set2, truncate_flag);
|
||||
translate_input(&mut locked_stdin, &mut buffered_stdout, op)
|
||||
let op = TranslateOperation::new(set1, &mut set2, truncate_flag, complement_flag);
|
||||
translate_input(&mut locked_stdin, &mut buffered_stdout, op);
|
||||
}
|
||||
|
||||
0
|
||||
|
|
72
src/uu/wc/src/countable.rs
Normal file
72
src/uu/wc/src/countable.rs
Normal 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)),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,17 +11,17 @@
|
|||
extern crate uucore;
|
||||
|
||||
mod count_bytes;
|
||||
mod countable;
|
||||
use count_bytes::count_bytes_fast;
|
||||
use countable::WordCountable;
|
||||
|
||||
use clap::{App, Arg, ArgMatches};
|
||||
use thiserror::Error;
|
||||
|
||||
use std::cmp::max;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufRead, BufReader, Read, StdinLock, Write};
|
||||
use std::io::{self, Write};
|
||||
use std::ops::{Add, AddAssign};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::Path;
|
||||
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)]
|
||||
struct WordCount {
|
||||
bytes: usize,
|
||||
|
@ -270,25 +244,16 @@ fn word_count_from_reader<T: WordCountable>(
|
|||
let mut byte_count: usize = 0;
|
||||
let mut char_count: usize = 0;
|
||||
let mut longest_line_length: usize = 0;
|
||||
let mut raw_line = Vec::new();
|
||||
let mut ends_lf: bool;
|
||||
|
||||
// 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
|
||||
let mut buffered_reader = reader.get_buffered();
|
||||
loop {
|
||||
match buffered_reader.read_until(LF, &mut raw_line) {
|
||||
Ok(n) => {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(ref e) => {
|
||||
if !raw_line.is_empty() {
|
||||
show_warning!("Error while reading {}: {}", path, e);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
for line_result in reader.lines() {
|
||||
let raw_line = match line_result {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
show_warning!("Error while reading {}: {}", path, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -317,8 +282,6 @@ fn word_count_from_reader<T: WordCountable>(
|
|||
longest_line_length = current_char_count - (ends_lf as usize);
|
||||
}
|
||||
}
|
||||
|
||||
raw_line.truncate(0);
|
||||
}
|
||||
|
||||
Ok(WordCount {
|
||||
|
|
|
@ -17,6 +17,7 @@ path = "src/who.rs"
|
|||
[dependencies]
|
||||
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["utmpx"] }
|
||||
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
|
||||
clap = "2.33.3"
|
||||
|
||||
[[bin]]
|
||||
name = "who"
|
||||
|
|
|
@ -12,79 +12,169 @@ extern crate uucore;
|
|||
use uucore::libc::{ttyname, STDIN_FILENO, S_IWGRP};
|
||||
use uucore::utmpx::{self, time, Utmpx};
|
||||
|
||||
use clap::{App, Arg};
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::CStr;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::PathBuf;
|
||||
use uucore::InvalidEncodingHandling;
|
||||
|
||||
static SYNTAX: &str = "[OPTION]... [ FILE | ARG1 ARG2 ]";
|
||||
static SUMMARY: &str = "Print information about users who are currently logged in.";
|
||||
static LONG_HELP: &str = "
|
||||
-a, --all same as -b -d --login -p -r -t -T -u
|
||||
-b, --boot time of last system boot
|
||||
-d, --dead print dead processes
|
||||
-H, --heading print line of column headings
|
||||
-l, --login print system login processes
|
||||
--lookup attempt to canonicalize hostnames via DNS
|
||||
-m only hostname and user associated with stdin
|
||||
-p, --process print active processes spawned by init
|
||||
-q, --count all login names and number of users logged on
|
||||
-r, --runlevel print current runlevel (not available on BSDs)
|
||||
-s, --short print only name, line, and time (default)
|
||||
-t, --time print last system clock change
|
||||
-T, -w, --mesg add user's message status as +, - or ?
|
||||
-u, --users list users logged in
|
||||
--message same as -T
|
||||
--writable same as -T
|
||||
--help display this help and exit
|
||||
--version output version information and exit
|
||||
mod options {
|
||||
pub const ALL: &str = "all";
|
||||
pub const BOOT: &str = "boot";
|
||||
pub const DEAD: &str = "dead";
|
||||
pub const HEADING: &str = "heading";
|
||||
pub const LOGIN: &str = "login";
|
||||
pub const LOOKUP: &str = "lookup";
|
||||
pub const ONLY_HOSTNAME_USER: &str = "only_hostname_user";
|
||||
pub const PROCESS: &str = "process";
|
||||
pub const COUNT: &str = "count";
|
||||
#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
|
||||
pub const RUNLEVEL: &str = "runlevel";
|
||||
pub const SHORT: &str = "short";
|
||||
pub const TIME: &str = "time";
|
||||
pub const USERS: &str = "users";
|
||||
pub const MESG: &str = "mesg"; // aliases: --message, --writable
|
||||
pub const FILE: &str = "FILE"; // if length=1: FILE, if length=2: ARG1 ARG2
|
||||
}
|
||||
|
||||
If FILE is not specified, use /var/run/utmp. /var/log/wtmp as FILE is common.
|
||||
If ARG1 ARG2 given, -m presumed: 'am i' or 'mom likes' are usual.
|
||||
";
|
||||
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
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 {
|
||||
let args = args
|
||||
.collect_str(InvalidEncodingHandling::Ignore)
|
||||
.accept_any();
|
||||
|
||||
let mut opts = app!(SYNTAX, SUMMARY, LONG_HELP);
|
||||
opts.optflag("a", "all", "same as -b -d --login -p -r -t -T -u");
|
||||
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", "");
|
||||
let usage = get_usage();
|
||||
let after_help = get_long_usage();
|
||||
|
||||
opts.optflag("", "help", "display this help and exit");
|
||||
opts.optflag("", "version", "output version information and exit");
|
||||
let matches = App::new(executable!())
|
||||
.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.
|
||||
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
|
||||
// the users logged on.
|
||||
// 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.
|
||||
let mut short_output = false;
|
||||
|
@ -95,12 +185,13 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
let mut include_idle = false;
|
||||
|
||||
// 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,
|
||||
// or a '?' if their tty cannot be statted.
|
||||
let include_mesg =
|
||||
matches.opt_present("a") || matches.opt_present("T") || matches.opt_present("w");
|
||||
let include_mesg = matches.is_present(options::ALL)
|
||||
|| matches.is_present(options::MESG)
|
||||
|| matches.is_present("w");
|
||||
|
||||
// If true, display process termination & exit status.
|
||||
let mut include_exit = false;
|
||||
|
@ -133,7 +224,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
|
||||
#[allow(clippy::useless_let_if_seq)]
|
||||
{
|
||||
if matches.opt_present("a") {
|
||||
if matches.is_present(options::ALL) {
|
||||
need_boottime = true;
|
||||
need_deadprocs = true;
|
||||
need_login = true;
|
||||
|
@ -146,49 +237,49 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
assumptions = false;
|
||||
}
|
||||
|
||||
if matches.opt_present("b") {
|
||||
if matches.is_present(options::BOOT) {
|
||||
need_boottime = true;
|
||||
assumptions = false;
|
||||
}
|
||||
|
||||
if matches.opt_present("d") {
|
||||
if matches.is_present(options::DEAD) {
|
||||
need_deadprocs = true;
|
||||
include_idle = true;
|
||||
include_exit = true;
|
||||
assumptions = false;
|
||||
}
|
||||
|
||||
if matches.opt_present("l") {
|
||||
if matches.is_present(options::LOGIN) {
|
||||
need_login = true;
|
||||
include_idle = true;
|
||||
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;
|
||||
}
|
||||
|
||||
if matches.opt_present("p") {
|
||||
if matches.is_present(options::PROCESS) {
|
||||
need_initspawn = true;
|
||||
assumptions = false;
|
||||
}
|
||||
|
||||
if matches.opt_present("r") {
|
||||
if matches.is_present(options::RUNLEVEL) {
|
||||
need_runlevel = true;
|
||||
include_idle = true;
|
||||
assumptions = false;
|
||||
}
|
||||
|
||||
if matches.opt_present("s") {
|
||||
if matches.is_present(options::SHORT) {
|
||||
short_output = true;
|
||||
}
|
||||
|
||||
if matches.opt_present("t") {
|
||||
if matches.is_present(options::TIME) {
|
||||
need_clockchange = true;
|
||||
assumptions = false;
|
||||
}
|
||||
|
||||
if matches.opt_present("u") {
|
||||
if matches.is_present(options::USERS) {
|
||||
need_users = true;
|
||||
include_idle = true;
|
||||
assumptions = false;
|
||||
|
@ -202,11 +293,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
if include_exit {
|
||||
short_output = false;
|
||||
}
|
||||
|
||||
if matches.free.len() > 2 {
|
||||
show_usage_error!("{}", msg_wrong_number_of_arguments!());
|
||||
exit!(1);
|
||||
}
|
||||
}
|
||||
|
||||
let mut who = Who {
|
||||
|
@ -225,7 +311,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
|||
need_runlevel,
|
||||
need_users,
|
||||
my_line_only,
|
||||
args: matches.free,
|
||||
args: files,
|
||||
};
|
||||
|
||||
who.exec();
|
||||
|
|
|
@ -24,7 +24,7 @@ thiserror = { version="1.0", optional=true }
|
|||
lazy_static = { version="1.3", optional=true }
|
||||
nix = { version="<= 0.13", 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)
|
||||
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
|
||||
|
|
|
@ -98,7 +98,7 @@ fn test_wrap_bad_arg() {
|
|||
.arg(wrap_param)
|
||||
.arg("b")
|
||||
.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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,21 @@ fn test_encode() {
|
|||
.pipe_in(input)
|
||||
.succeeds()
|
||||
.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]
|
||||
|
@ -60,10 +75,9 @@ fn test_wrap() {
|
|||
#[test]
|
||||
fn test_wrap_no_arg() {
|
||||
for wrap_param in vec!["-w", "--wrap"] {
|
||||
new_ucmd!().arg(wrap_param).fails().stderr_only(format!(
|
||||
"base64: error: Argument to option '{}' missing\n",
|
||||
if wrap_param == "-w" { "w" } else { "wrap" }
|
||||
));
|
||||
new_ucmd!().arg(wrap_param).fails().stderr_contains(
|
||||
&"The argument '--wrap <wrap>' requires a value but none was supplied",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,6 +88,24 @@ fn test_wrap_bad_arg() {
|
|||
.arg(wrap_param)
|
||||
.arg("b")
|
||||
.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");
|
||||
}
|
||||
|
|
|
@ -1,6 +1,29 @@
|
|||
use crate::common::util::*;
|
||||
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]
|
||||
fn test_directory() {
|
||||
new_ucmd!()
|
||||
|
@ -81,11 +104,25 @@ fn test_no_args() {
|
|||
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]
|
||||
fn test_too_many_args() {
|
||||
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) {
|
||||
let test_vec = vec![os_str.to_os_string()];
|
||||
new_ucmd!().args(&test_vec).succeeds().stdout_is("fo<EFBFBD>o\n");
|
||||
|
|
|
@ -20,4 +20,16 @@ fn test_df_compatible_si() {
|
|||
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...
|
||||
|
|
|
@ -16,6 +16,21 @@ fn test_path_without_trailing_slashes() {
|
|||
.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]
|
||||
fn test_root() {
|
||||
new_ucmd!().arg("/").run().stdout_is("/\n");
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -39,3 +39,25 @@ fn test_link_nonexistent_file() {
|
|||
assert!(!at.file_exists(file));
|
||||
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]),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -43,23 +43,74 @@ fn test_ls_a() {
|
|||
let scene = TestScenario::new(util_name!());
|
||||
let at = &scene.fixtures;
|
||||
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 stdout = result.stdout_str();
|
||||
assert!(!stdout.contains(".test-1"));
|
||||
assert!(!stdout.contains(".."));
|
||||
let re_pwd = Regex::new(r"^\.\n").unwrap();
|
||||
|
||||
// Using the present working directory
|
||||
scene
|
||||
.ucmd()
|
||||
.arg("-1")
|
||||
.succeeds()
|
||||
.stdout_does_not_contain(".test-1")
|
||||
.stdout_does_not_contain("..")
|
||||
.stdout_does_not_match(&re_pwd);
|
||||
|
||||
scene
|
||||
.ucmd()
|
||||
.arg("-a")
|
||||
.arg("-1")
|
||||
.succeeds()
|
||||
.stdout_contains(&".test-1")
|
||||
.stdout_contains(&"..");
|
||||
.stdout_contains(&"..")
|
||||
.stdout_matches(&re_pwd);
|
||||
|
||||
let result = scene.ucmd().arg("-A").succeeds();
|
||||
result.stdout_contains(".test-1");
|
||||
let stdout = result.stdout_str();
|
||||
assert!(!stdout.contains(".."));
|
||||
scene
|
||||
.ucmd()
|
||||
.arg("-A")
|
||||
.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]
|
||||
|
@ -482,7 +533,6 @@ fn test_ls_sort_name() {
|
|||
.succeeds()
|
||||
.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 at = &scene_dot.fixtures;
|
||||
at.touch(".a");
|
||||
|
@ -495,7 +545,7 @@ fn test_ls_sort_name() {
|
|||
.arg("--sort=name")
|
||||
.arg("-A")
|
||||
.succeeds()
|
||||
.stdout_is([".a", "a", ".b", "b\n"].join(sep));
|
||||
.stdout_is([".a", ".b", "a", "b\n"].join(sep));
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn test_ls_order_time() {
|
||||
let scene = TestScenario::new(util_name!());
|
||||
|
|
|
@ -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")]
|
||||
#[test]
|
||||
fn test_short_format_i() {
|
||||
|
|
|
@ -103,7 +103,7 @@ fn test_relpath_with_from_with_d() {
|
|||
at.mkdir_all(from);
|
||||
|
||||
// d is part of subpath -> expect relative path
|
||||
let mut result_stdout = scene
|
||||
let mut _result_stdout = scene
|
||||
.ucmd()
|
||||
.arg(to)
|
||||
.arg(from)
|
||||
|
@ -112,17 +112,17 @@ fn test_relpath_with_from_with_d() {
|
|||
.stdout_move_str();
|
||||
// relax rules for windows test environment
|
||||
#[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
|
||||
result_stdout = scene
|
||||
_result_stdout = scene
|
||||
.ucmd()
|
||||
.arg(to)
|
||||
.arg(from)
|
||||
.arg("-dnon_existing")
|
||||
.succeeds()
|
||||
.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);
|
||||
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))]
|
||||
assert_eq!(result_stdout, format!("{}\n", to));
|
||||
assert_eq!(_result_stdout, format!("{}\n", to));
|
||||
// relax rules for windows test environment
|
||||
#[cfg(windows)]
|
||||
assert!(result_stdout.ends_with(&format!("{}\n", to)));
|
||||
assert!(_result_stdout.ends_with(&format!("{}\n", to)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,31 @@ fn test_helper(file_name: &str, args: &str) {
|
|||
.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]
|
||||
fn test_months_whitespace() {
|
||||
test_helper("months-whitespace", "-M");
|
||||
|
@ -34,6 +59,18 @@ fn test_human_numeric_whitespace() {
|
|||
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]
|
||||
fn test_multiple_decimals_general() {
|
||||
test_helper("multiple_decimals_general", "-g")
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// This file is part of the uutils coreutils package.
|
||||
//
|
||||
// (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.
|
||||
|
@ -9,11 +10,466 @@
|
|||
|
||||
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]
|
||||
fn test_op_prec_and_or_1() {
|
||||
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]
|
||||
fn test_op_prec_and_or_2() {
|
||||
new_ucmd!()
|
||||
|
@ -22,6 +478,54 @@ fn test_op_prec_and_or_2() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_or_as_filename() {
|
||||
new_ucmd!().args(&["x", "-a", "-z", "-o"]).fails();
|
||||
fn test_op_prec_and_or_2_overridden_by_parentheses() {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -45,6 +45,70 @@ fn test_delete_complement() {
|
|||
.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]
|
||||
fn test_squeeze() {
|
||||
new_ucmd!()
|
||||
|
@ -63,6 +127,24 @@ fn test_squeeze_complement() {
|
|||
.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]
|
||||
fn test_delete_and_squeeze() {
|
||||
new_ucmd!()
|
||||
|
|
|
@ -224,8 +224,14 @@ fn test_size_and_reference() {
|
|||
let mut file1 = at.make_file(TFILE1);
|
||||
let mut file2 = at.make_file(TFILE2);
|
||||
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();
|
||||
let actual = file2.seek(SeekFrom::Current(0)).unwrap();
|
||||
assert!(expected == actual, "expected '{}' got '{}'", expected, actual);
|
||||
assert!(
|
||||
expected == actual,
|
||||
"expected '{}' got '{}'",
|
||||
expected,
|
||||
actual
|
||||
);
|
||||
}
|
||||
|
|
|
@ -36,7 +36,12 @@ fn test_uname_kernel_version() {
|
|||
fn test_uname_kernel() {
|
||||
let (_, mut ucmd) = at_and_ucmd!();
|
||||
|
||||
let result = ucmd.arg("-o").succeeds();
|
||||
#[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();
|
||||
}
|
||||
|
|
|
@ -112,3 +112,60 @@ fn test_multiple_default() {
|
|||
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");
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
#[cfg(target_os = "linux")]
|
||||
use crate::common::util::*;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn test_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]
|
||||
fn test_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")]
|
||||
#[test]
|
||||
fn test_heading() {
|
||||
for opt in vec!["-H"] {
|
||||
for opt in vec!["-H", "--heading"] {
|
||||
// allow whitespace variation
|
||||
// * minor whitespace differences occur between platform built-in outputs; specifically number of TABs between "TIME" and "COMMENT" may be variant
|
||||
let actual = new_ucmd!().arg(opt).run().stdout_move_str();
|
||||
// * minor whitespace differences occur between platform built-in outputs;
|
||||
// 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);
|
||||
println!("actual: {:?}", actual);
|
||||
println!("expect: {:?}", expect);
|
||||
|
@ -37,7 +43,10 @@ fn test_heading() {
|
|||
#[test]
|
||||
fn test_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]
|
||||
fn test_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]
|
||||
fn test_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]
|
||||
fn test_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")]
|
||||
#[test]
|
||||
fn test_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!())
|
||||
.env("LANGUAGE", "C")
|
||||
.args(&[arg])
|
||||
.run()
|
||||
.succeeds()
|
||||
.stdout_move_str()
|
||||
}
|
||||
|
|
1
tests/fixtures/base64/input-simple.txt
vendored
Normal file
1
tests/fixtures/base64/input-simple.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
Hello, World!
|
20000
tests/fixtures/sort/ext_sort.expected
vendored
Normal file
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
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
1
tests/fixtures/test/non_empty_file
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
Not empty!
|
0
tests/fixtures/test/regular_file
vendored
Normal file
0
tests/fixtures/test/regular_file
vendored
Normal file
0
tests/fixtures/wc/emptyfile.txt
vendored
Normal file
0
tests/fixtures/wc/emptyfile.txt
vendored
Normal file
100
tests/fixtures/wc/manyemptylines.txt
vendored
Normal file
100
tests/fixtures/wc/manyemptylines.txt
vendored
Normal file
|
@ -0,0 +1,100 @@
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
1
tests/fixtures/wc/notrailingnewline.txt
vendored
Normal file
1
tests/fixtures/wc/notrailingnewline.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
a
|
1
tests/fixtures/wc/onelongemptyline.txt
vendored
Normal file
1
tests/fixtures/wc/onelongemptyline.txt
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/fixtures/wc/onelongword.txt
vendored
Normal file
1
tests/fixtures/wc/onelongword.txt
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue