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

Merge branch 'master' into ls/device_information

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

3
.gitignore vendored
View file

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

86
Cargo.lock generated
View file

@ -12,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]]
@ -106,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",
]
@ -495,9 +495,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",
@ -508,9 +508,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",
@ -536,7 +536,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]]
@ -618,7 +618,7 @@ checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall 0.2.7",
"redox_syscall 0.2.8",
"winapi 0.3.9",
]
@ -868,9 +868,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"
@ -1089,7 +1089,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]]
@ -1259,9 +1259,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "redox_syscall"
version = "0.2.7"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85dd92e586f7355c633911e11f77f3d12f04b1b1bd76a198bd34ae3af8341ef2"
checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
dependencies = [
"bitflags",
]
@ -1272,17 +1272,17 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
dependencies = [
"redox_syscall 0.2.7",
"redox_syscall 0.2.8",
]
[[package]]
name = "regex"
version = "1.4.6"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [
"aho-corasick",
"memchr 2.3.4",
"memchr 2.4.0",
"regex-syntax",
]
@ -1297,9 +1297,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"
@ -1312,9 +1312,9 @@ dependencies = [
[[package]]
name = "retain_mut"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53552c6c49e1e13f1a203ef0080ab3bbef0beb570a528993e83df057a9d9bba1"
checksum = "e9c17925a9027d298a4603d286befe3f9dc0e8ed02523141914eb628798d6e5b"
[[package]]
name = "rust-ini"
@ -1372,9 +1372,6 @@ 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"
@ -1453,9 +1450,6 @@ name = "smallvec"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
dependencies = [
"serde",
]
[[package]]
name = "strsim"
@ -1483,13 +1477,13 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.71"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad184cc9470f9117b2ac6817bfe297307418819ba40552f9b3846f05c33d5373"
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
dependencies = [
"proc-macro2",
"quote 1.0.9",
"unicode-xid 0.2.1",
"unicode-xid 0.2.2",
]
[[package]]
@ -1543,7 +1537,7 @@ checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
dependencies = [
"libc",
"numtoa",
"redox_syscall 0.2.7",
"redox_syscall 0.2.8",
"redox_termios",
]
@ -1636,9 +1630,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"
@ -1688,6 +1682,8 @@ dependencies = [
name = "uu_base64"
version = "0.0.6"
dependencies = [
"clap",
"uu_base32",
"uucore",
"uucore_procs",
]
@ -1696,6 +1692,7 @@ dependencies = [
name = "uu_basename"
version = "0.0.6"
dependencies = [
"clap",
"uucore",
"uucore_procs",
]
@ -1806,7 +1803,7 @@ version = "0.0.6"
dependencies = [
"bstr",
"clap",
"memchr 2.3.4",
"memchr 2.4.0",
"uucore",
"uucore_procs",
]
@ -2041,6 +2038,7 @@ dependencies = [
name = "uu_kill"
version = "0.0.6"
dependencies = [
"clap",
"libc",
"uucore",
"uucore_procs",
@ -2050,6 +2048,7 @@ dependencies = [
name = "uu_link"
version = "0.0.6"
dependencies = [
"clap",
"libc",
"uucore",
"uucore_procs",
@ -2069,6 +2068,7 @@ dependencies = [
name = "uu_logname"
version = "0.0.6"
dependencies = [
"clap",
"libc",
"uucore",
"uucore_procs",
@ -2118,7 +2118,7 @@ dependencies = [
name = "uu_mknod"
version = "0.0.6"
dependencies = [
"getopts",
"clap",
"libc",
"uucore",
"uucore_procs",
@ -2175,7 +2175,7 @@ dependencies = [
"aho-corasick",
"clap",
"libc",
"memchr 2.3.4",
"memchr 2.4.0",
"regex",
"regex-syntax",
"uucore",
@ -2247,6 +2247,7 @@ dependencies = [
name = "uu_pinky"
version = "0.0.6"
dependencies = [
"clap",
"uucore",
"uucore_procs",
]
@ -2276,7 +2277,7 @@ dependencies = [
"aho-corasick",
"clap",
"libc",
"memchr 2.3.4",
"memchr 2.4.0",
"regex",
"regex-syntax",
"uucore",
@ -2391,8 +2392,6 @@ dependencies = [
"rand 0.7.3",
"rayon",
"semver",
"serde",
"serde_json",
"smallvec 1.6.1",
"tempdir",
"unicode-width",
@ -2648,6 +2647,7 @@ dependencies = [
name = "uu_who"
version = "0.0.6"
dependencies = [
"clap",
"uucore",
"uucore_procs",
]

View file

@ -429,6 +429,7 @@ This is an auto-generated table showing which binaries compile for each target-t
|windows-msvc|i686|y|y|y|y|y| | | | |y|y|y|y|y|y|y|y|y| |y|y|y| |y|y|y|y| |y|y|y|y| | |y| |y|y|y|y|y| | |y|y|y| |y| |y|y|y|y| | |y|y|y|y|y|y|y|y|y|y|y|y|y|y|y| | |y|y|y|y|y|y| |y|y|y|y|y| |y|y|y| |y| |y| |y|y|
|windows-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|

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,6 +15,7 @@ edition = "2018"
path = "src/basename.rs"
[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" }

View file

@ -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);
}

View file

@ -81,7 +81,7 @@ fn copy_exact(read_fd: RawFd, write_fd: RawFd, num_bytes: usize) -> nix::Result<
let mut buf = [0; BUF_SIZE];
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;

View file

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

View file

@ -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")))]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -39,8 +39,6 @@ use std::{
time::Duration,
};
use chrono;
use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
use unicode_width::UnicodeWidthStr;
@ -1122,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,
@ -1174,31 +1179,32 @@ impl PathData {
}
fn list(locs: Vec<String>, config: Config) -> i32 {
let number_of_locs = locs.len();
let mut files = Vec::<PathData>::new();
let mut dirs = Vec::<PathData>::new();
let mut has_failed = false;
let mut out = BufWriter::new(stdout());
for loc in locs {
for loc in &locs {
let p = PathBuf::from(&loc);
if !p.exists() {
show_error!("'{}': {}", &loc, "No such file or directory");
// We found an error, the return code of ls should not be 0
// And no need to continue the execution
/*
We found an error, the return code of ls should not be 0
And no need to continue the execution
*/
has_failed = true;
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()
} else {
has_failed = true;
false
let show_dir_contents = match path_data.file_type() {
Some(ft) => !config.directory && ft.is_dir(),
None => {
has_failed = true;
false
}
};
if show_dir_contents {
@ -1212,7 +1218,7 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
sort_entries(&mut dirs, &config);
for dir in dirs {
if number_of_locs > 1 {
if locs.len() > 1 {
let _ = writeln!(out, "\n{}:", dir.p_buf.display());
}
enter_directory(&dir, &config, &mut out);
@ -1237,14 +1243,8 @@ fn sort_entries(entries: &mut Vec<PathData>, config: &Config) {
entries.sort_by_key(|k| Reverse(k.md().as_ref().map(|md| md.len()).unwrap_or(0)))
}
// 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()
@ -1283,8 +1283,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![]
@ -1293,7 +1299,7 @@ fn enter_directory(dir: &PathData, config: &Config, out: &mut BufWriter<Stdout>)
let mut temp: Vec<_> = safe_unwrap!(fs::read_dir(&dir.p_buf))
.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);
@ -1326,7 +1332,7 @@ fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize) {
if let Some(md) = entry.md() {
(
display_symlink_count(&md).len(),
display_file_size(&md, config).len(),
display_size(md.len(), config).len(),
)
} else {
(0, 0)
@ -1339,14 +1345,22 @@ fn pad_left(string: String, count: usize) -> String {
fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout>) {
if config.format == Format::Long {
let (mut max_links, mut max_size) = (1, 1);
let (mut max_links, mut max_width) = (1, 1);
let mut total_size = 0;
for item in items {
let (links, size) = display_dir_entry_size(item, config);
let (links, width) = display_dir_entry_size(item, config);
max_links = links.max(max_links);
max_size = size.max(max_size);
max_width = width.max(max_width);
total_size += item.md().map_or(0, |md| get_block_size(md, config));
}
if total_size > 0 {
let _ = writeln!(out, "total {}", display_size(total_size, config));
}
for item in items {
display_item_long(item, max_links, max_size, config, out);
display_item_long(item, max_links, max_width, config, out);
}
} else {
let names = items.iter().filter_map(|i| display_file_name(&i, config));
@ -1391,6 +1405,29 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout
}
}
fn get_block_size(md: &Metadata, config: &Config) -> u64 {
/* GNU ls will display sizes in terms of block size
md.len() will differ from this value when the file has some holes
*/
#[cfg(unix)]
{
// hard-coded for now - enabling setting this remains a TODO
let ls_block_size = 1024;
return match config.size_format {
SizeFormat::Binary => md.blocks() * 512,
SizeFormat::Decimal => md.blocks() * 512,
SizeFormat::Bytes => md.blocks() * 512 / ls_block_size,
};
}
#[cfg(not(unix))]
{
let _ = config;
// no way to get block size for windows, fall-back to file size
md.len()
}
}
fn display_grid(
names: impl Iterator<Item = Cell>,
width: u16,
@ -1466,7 +1503,7 @@ fn display_item_long(
let _ = writeln!(
out,
" {} {} {}",
pad_left(display_file_size(&md, config), max_size),
pad_left(display_size(md.len(), config), max_size),
display_date(&md, config),
// unwrap is fine because it fails when metadata is not available
// but we already know that it is because it's checked at the
@ -1559,6 +1596,7 @@ fn get_system_time(md: &Metadata, config: &Config) -> Option<SystemTime> {
match config.time {
Time::Modification => md.modified().ok(),
Time::Access => md.accessed().ok(),
Time::Birth => md.created().ok(),
_ => None,
}
}
@ -1620,7 +1658,7 @@ fn format_prefixed(prefixed: NumberPrefix<f64>) -> String {
}
}
fn display_file_size(metadata: &Metadata, config: &Config) -> String {
fn display_size(metadata: &Metadata, config: &Config) -> String {
#[cfg(unix)]
{
let ft = metadata.file_type();
@ -1635,9 +1673,9 @@ fn display_file_size(metadata: &Metadata, config: &Config) -> String {
// NOTE: The human-readable behaviour deviates from the GNU ls.
// The GNU ls uses binary prefixes by default.
match config.size_format {
SizeFormat::Binary => format_prefixed(NumberPrefix::binary(metadata.len() as f64)),
SizeFormat::Decimal => format_prefixed(NumberPrefix::decimal(metadata.len() as f64)),
SizeFormat::Bytes => metadata.len().to_string(),
SizeFormat::Binary => format_prefixed(NumberPrefix::binary(len as f64)),
SizeFormat::Decimal => format_prefixed(NumberPrefix::decimal(len as f64)),
SizeFormat::Bytes => len.to_string(),
}
}

View file

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

View file

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

View file

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

View file

@ -17,6 +17,7 @@ path = "src/pinky.rs"
[dependencies]
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"

View file

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

View file

@ -158,7 +158,6 @@ pub fn base_conv_float(src: &[u8], radix_src: u8, _radix_dest: u8) -> f64 {
// to implement this for arbitrary string input.
// 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;

View file

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

View file

@ -15,15 +15,13 @@ edition = "2018"
path = "src/sort.rs"
[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 = { version="1.6.1", features=["serde"] }
smallvec = "1.6.1"
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" }

View file

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

View file

@ -1,50 +1,32 @@
use std::clone::Clone;
use std::cmp::Ordering::Less;
use std::cmp::Ordering;
use std::collections::VecDeque;
use std::error::Error;
use std::fs::{File, OpenOptions};
use std::io::SeekFrom::Start;
use std::io::SeekFrom;
use std::io::{BufRead, BufReader, BufWriter, Seek, Write};
use std::marker::PhantomData;
use std::path::PathBuf;
use std::path::Path;
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> {
pub struct ExtSortedIterator {
buffers: Vec<VecDeque<Line>>,
chunk_offsets: Vec<u64>,
max_per_chunk: u64,
chunks: u64,
max_per_chunk: usize,
chunks: usize,
tmp_dir: TempDir,
settings: GlobalSettings,
failed: bool,
}
impl Iterator for ExtSortedIterator<Line>
where
Line: ExternallySortable,
{
type Item = Result<Line, Box<dyn Error>>;
impl Iterator for ExtSortedIterator {
type Item = Line;
/// # Errors
///
/// This method can fail due to issues reading intermediate sorted chunks
/// from disk, or due to serde deserialization issues
/// from disk
fn next(&mut self) -> Option<Self::Item> {
if self.failed {
return None;
@ -53,29 +35,18 @@ where
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;
let mut f = crash_if_err!(
1,
File::open(self.tmp_dir.path().join(chunk_num.to_string()))
);
crash_if_err!(1, f.seek(SeekFrom::Start(self.chunk_offsets[chunk_num])));
let bytes_read = fill_buff(
&mut self.buffers[chunk_num as usize],
f,
self.max_per_chunk,
&self.settings,
);
self.chunk_offsets[chunk_num as usize] += bytes_read as u64;
if !self.buffers[chunk_num as usize].is_empty() {
empty = false;
}
@ -91,205 +62,150 @@ where
// 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)(
if !self.buffers[chunk_num].is_empty()
&& (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;
}
) == Ordering::Less)
{
idx = chunk_num;
}
}
// unwrap due to checks above
let r = self.buffers[idx].pop_front().unwrap();
Some(Ok(r))
Some(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,
}
/// 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.
pub fn ext_sort(
unsorted: impl Iterator<Item = Line>,
settings: &GlobalSettings,
) -> ExtSortedIterator {
let tmp_dir = crash_if_err!(1, TempDir::new_in(&settings.tmp_dir, "uutils_sort"));
impl ExternalSorter<Line>
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,
let mut iter = ExtSortedIterator {
buffers: Vec::new(),
chunk_offsets: Vec::new(),
max_per_chunk: 0,
chunks: 0,
tmp_dir,
settings: settings.clone(),
failed: false,
};
let mut total_read = 0;
let mut chunk = Vec::new();
// make the initial chunks on disk
for seq in unsorted {
let seq_size = seq.estimate_size();
total_read += seq_size;
chunk.push(seq);
if total_read >= settings.buffer_size {
super::sort_by(&mut chunk, &settings);
write_chunk(
settings,
&iter.tmp_dir.path().join(iter.chunks.to_string()),
&mut chunk,
);
chunk.clear();
total_read = 0;
iter.chunks += 1;
}
}
// write the last chunk
if !chunk.is_empty() {
super::sort_by(&mut chunk, &settings);
write_chunk(
settings,
}
&iter.tmp_dir.path().join(iter.chunks.to_string()),
&mut chunk,
);
iter.chunks += 1;
}
/// Sort (based on `compare`) the `T`s provided by `unsorted` and return an
/// 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,
};
// 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
{
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)
const MINIMUM_READBACK_BUFFER: usize = 8200;
let right_sized_buffer = settings
.buffer_size
.checked_div(iter.chunks)
.unwrap_or(settings.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];
iter.chunk_offsets = vec![0; iter.chunks];
for chunk_num in 0..iter.chunks {
let offset = fill_buff(
&mut iter.buffers[chunk_num],
crash_if_err!(
1,
File::open(iter.tmp_dir.path().join(chunk_num.to_string()))
),
iter.max_per_chunk,
&settings,
);
iter.chunk_offsets[chunk_num] = offset as u64;
}
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(())
}
iter
}
fn fill_buff<Line>(
fn write_chunk(settings: &GlobalSettings, file: &Path, chunk: &mut Vec<Line>) {
let new_file = crash_if_err!(1, OpenOptions::new().create(true).append(true).open(file));
let mut buf_write = BufWriter::new(new_file);
for s in chunk {
crash_if_err!(1, buf_write.write_all(s.line.as_bytes()));
crash_if_err!(
1,
buf_write.write_all(if settings.zero_terminated { "\0" } else { "\n" }.as_bytes(),)
);
}
crash_if_err!(1, buf_write.flush());
}
fn fill_buff(
vec: &mut VecDeque<Line>,
file: File,
max_bytes: u64,
) -> Result<u64, Box<dyn Error>>
where
Line: ExternallySortable,
{
max_bytes: usize,
settings: &GlobalSettings,
) -> usize {
let mut total_read = 0;
let mut bytes_read = 0;
for line in BufReader::new(file).lines() {
let line_s = line?;
for line in BufReader::new(file).split(if settings.zero_terminated {
b'\0'
} else {
b'\n'
}) {
let line_s = String::from_utf8(crash_if_err!(1, line)).unwrap();
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();
let deserialized = Line::new(line_s, settings);
total_read += deserialized.estimate_size();
vec.push_back(deserialized);
if total_read > max_bytes {
break;
}
}
Ok(bytes_read as u64)
bytes_read
}

View file

@ -14,21 +14,20 @@
//! More specifically, exponent can be understood so that the original number is in (1..10)*10^exponent.
//! 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, Serialize, Deserialize, Clone)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
enum Sign {
Negative,
Positive,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[derive(Debug, PartialEq, Clone)]
pub struct NumInfo {
exponent: i64,
sign: Sign,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[derive(Debug, PartialEq, Clone)]
pub struct NumInfoParseSettings {
pub accept_si_units: bool,
pub thousands_separator: Option<char>,

View file

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

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

View file

@ -1,144 +1,154 @@
// * This file is part of the uutils coreutils package.
// *
// * (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,

View file

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

View file

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

View file

@ -11,17 +11,17 @@
extern crate uucore;
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 {

View file

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

View file

@ -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();

View file

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

View file

@ -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");
}
}

View file

@ -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");
}

View file

@ -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");

View file

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

View file

@ -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");

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ use crate::common::util::*;
extern crate regex;
use self::regex::Regex;
use std::collections::HashMap;
use std::path::Path;
use std::thread::sleep;
use std::time::Duration;
@ -43,23 +44,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]
@ -257,6 +309,50 @@ fn test_ls_long() {
}
}
#[test]
fn test_ls_long_total_size() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch(&at.plus_as_string("test-long"));
at.append("test-long", "1");
at.touch(&at.plus_as_string("test-long2"));
at.append("test-long2", "2");
let expected_prints: HashMap<_, _> = if cfg!(unix) {
[
("long_vanilla", "total 8"),
("long_human_readable", "total 8.0K"),
("long_si", "total 8.2k"),
]
.iter()
.cloned()
.collect()
} else {
[
("long_vanilla", "total 2"),
("long_human_readable", "total 2"),
("long_si", "total 2"),
]
.iter()
.cloned()
.collect()
};
for arg in &["-l", "--long", "--format=long", "--format=verbose"] {
let result = scene.ucmd().arg(arg).succeeds();
result.stdout_contains(expected_prints["long_vanilla"]);
for arg2 in &["-h", "--human-readable", "--si"] {
let result = scene.ucmd().arg(arg).arg(arg2).succeeds();
result.stdout_contains(if *arg2 == "--si" {
expected_prints["long_si"]
} else {
expected_prints["long_human_readable"]
});
}
}
}
#[test]
fn test_ls_long_formats() {
let scene = TestScenario::new(util_name!());
@ -482,7 +578,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 +590,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]
@ -559,8 +654,7 @@ fn test_ls_long_ctime() {
}
#[test]
#[cfg(not(windows))]
// This test is currently failing on windows
#[ignore]
fn test_ls_order_birthtime() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
@ -570,15 +664,11 @@ fn test_ls_order_birthtime() {
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,
even though we placed a timeout between creating the two.
https://github.com/uutils/coreutils/pull/1986/#issuecomment-828490651
Without this, both files might accidentally be created at the same time.
*/
at.make_file("test-birthtime-1").sync_all().unwrap();
std::thread::sleep(std::time::Duration::from_millis(1));
at.make_file("test-birthtime-2");
at.touch("test-birthtime-1");
at.make_file("test-birthtime-2").sync_all().unwrap();
at.open("test-birthtime-1");
let result = scene.ucmd().arg("--time=birth").arg("-t").run();
@ -602,7 +692,7 @@ fn test_ls_styles() {
Regex::new(r"[a-z-]* \d* \w* \w* \d* \d{4}-\d{2}-\d{2} \d{2}:\d{2} test\n").unwrap();
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{2} \d{2}:\d{2} test\n").unwrap();
Regex::new(r"[a-z-]* \d* \w* \w* \d* [A-Z][a-z]{2} ( |\d)\d \d{2}:\d{2} test\n").unwrap();
//full-iso
let result = scene

View file

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

View file

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

View file

@ -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)));
}
}

View file

@ -16,15 +16,15 @@ fn test_helper(file_name: &str, args: &str) {
}
// 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
//
// 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
// 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
@ -37,7 +37,29 @@ fn test_larger_than_specified_segment() {
.arg("50K")
.arg("ext_sort.txt")
.succeeds()
.stdout_is_fixture(format!("{}", "ext_sort.expected"));
.stdout_is_fixture("ext_sort.expected");
}
#[test]
fn test_smaller_than_specified_segment() {
new_ucmd!()
.arg("-n")
.arg("-S")
.arg("100M")
.arg("ext_sort.txt")
.succeeds()
.stdout_is_fixture("ext_sort.expected");
}
#[test]
fn test_extsort_zero_terminated() {
new_ucmd!()
.arg("-z")
.arg("-S")
.arg("10K")
.arg("zero-terminated.txt")
.succeeds()
.stdout_is_fixture("zero-terminated.expected");
}
#[test]
@ -59,7 +81,7 @@ fn test_human_numeric_whitespace() {
test_helper("human-numeric-whitespace", "-h");
}
// This tests where serde often fails when reading back JSON
// This tests where serde often fails when reading back JSON
// if it finds a null value
#[test]
fn test_extsort_as64_bailout() {

View file

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

View file

@ -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);
}

View file

@ -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!()

View file

@ -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
);
}

View file

@ -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();
}

View file

@ -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");
}

View file

@ -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,110 @@ 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]
#[ignore]
fn test_lookup() {
for opt in vec!["--lookup"] {
new_ucmd!()
.arg(opt)
.succeeds()
.stdout_is(expected_result(opt));
}
}
@ -61,15 +176,60 @@ fn test_m() {
#[test]
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 +239,6 @@ fn expected_result(arg: &str) -> String {
.cmd_keepenv(util_name!())
.env("LANGUAGE", "C")
.args(&[arg])
.run()
.succeeds()
.stdout_move_str()
}

View file

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

View file

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

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

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

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

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

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

@ -0,0 +1,100 @@

View file

@ -0,0 +1 @@
a

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long