Merge branch 'master' of https://github.com/uutils/coreutils into sort-no-json-extsort

This commit is contained in:
Michael Debertol 2021-05-02 18:08:15 +02:00
commit e99f157e6a
35 changed files with 1775 additions and 795 deletions

16
Cargo.lock generated
View file

@ -106,9 +106,9 @@ 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.4.0",
@ -1277,9 +1277,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.5.1"
version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a068b905b8cb93815aa3069ae48653d90f382308aebd1d33d940ac1f1d771e2d"
checksum = "ce5f1ceb7f74abbce32601642fcf8e8508a8a8991e0621c7d750295b9095702b"
dependencies = [
"aho-corasick",
"memchr 2.4.0",
@ -1297,9 +1297,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.6.24"
version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00efb87459ba4f6fb2169d20f68565555688e1250ee6825cdf6254f8b48fafb2"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "remove_dir_all"
@ -1682,6 +1682,8 @@ dependencies = [
name = "uu_base64"
version = "0.0.6"
dependencies = [
"clap",
"uu_base32",
"uucore",
"uucore_procs",
]
@ -2044,6 +2046,7 @@ dependencies = [
name = "uu_link"
version = "0.0.6"
dependencies = [
"clap",
"libc",
"uucore",
"uucore_procs",
@ -2241,6 +2244,7 @@ dependencies = [
name = "uu_pinky"
version = "0.0.6"
dependencies = [
"clap",
"uucore",
"uucore_procs",
]

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

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

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

@ -172,7 +172,7 @@ pub fn factor(mut n: u64) -> Factors {
#[cfg(feature = "coz")]
coz::end!("factorization");
return r;
r
}
#[cfg(test)]

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

@ -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,
@ -1192,7 +1197,7 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
continue;
}
let path_data = PathData::new(p, None, &config, true);
let path_data = PathData::new(p, None, None, &config, true);
let show_dir_contents = if let Some(ft) = path_data.file_type() {
!config.directory && ft.is_dir()
@ -1237,14 +1242,8 @@ fn sort_entries(entries: &mut Vec<PathData>, config: &Config) {
entries.sort_by_key(|k| Reverse(k.md().as_ref().map(|md| md.len()).unwrap_or(0)))
}
// The default sort in GNU ls is case insensitive
Sort::Name => entries.sort_by_cached_key(|k| {
let has_dot: bool = k.file_name.starts_with('.');
let filename_nodot: &str = &k.file_name[if has_dot { 1 } else { 0 }..];
// We want hidden files to appear before regular files of the same
// name, so we need to negate the "has_dot" variable.
(filename_nodot.to_lowercase(), !has_dot)
}),
Sort::Version => entries.sort_by(|k, j| version_cmp::version_cmp(&k.p_buf, &j.p_buf)),
Sort::Name => entries.sort_by(|a, b| a.file_name.cmp(&b.file_name)),
Sort::Version => entries.sort_by(|a, b| version_cmp::version_cmp(&a.p_buf, &b.p_buf)),
Sort::Extension => entries.sort_by(|a, b| {
a.p_buf
.extension()
@ -1283,8 +1282,14 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool {
fn enter_directory(dir: &PathData, config: &Config, out: &mut BufWriter<Stdout>) {
let mut entries: Vec<_> = if config.files == Files::All {
vec![
PathData::new(dir.p_buf.join("."), None, config, false),
PathData::new(dir.p_buf.join(".."), None, config, false),
PathData::new(
dir.p_buf.clone(),
Some(Ok(*dir.file_type().unwrap())),
Some(".".into()),
config,
false,
),
PathData::new(dir.p_buf.join(".."), None, Some("..".into()), config, false),
]
} else {
vec![]
@ -1293,7 +1298,7 @@ fn enter_directory(dir: &PathData, config: &Config, out: &mut BufWriter<Stdout>)
let mut temp: Vec<_> = safe_unwrap!(fs::read_dir(&dir.p_buf))
.map(|res| safe_unwrap!(res))
.filter(|e| should_display(e, config))
.map(|e| PathData::new(DirEntry::path(&e), Some(e.file_type()), config, false))
.map(|e| PathData::new(DirEntry::path(&e), Some(e.file_type()), None, config, false))
.collect();
sort_entries(&mut temp, config);

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

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

@ -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::ext_sort;
use custom_str_cmp::custom_str_cmp;
use fnv::FnvHasher;
use itertools::Itertools;
use numeric_str_cmp::{numeric_str_cmp, NumInfo, NumInfoParseSettings};
@ -205,33 +207,23 @@ impl From<&GlobalSettings> for KeySettings {
#[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;
}
}
@ -302,14 +294,8 @@ impl Line {
.selectors
.iter()
.map(|selector| {
let range = selector.get_selection(&line, fields.as_deref());
let mut range = if let Some(transformed) =
transform(&line[range.to_owned()], &selector.settings)
{
SelectionRange::String(transformed)
} else {
SelectionRange::ByIndex(range)
};
let mut range =
SelectionRange::new(selector.get_selection(&line, fields.as_deref()));
let num_cache = if selector.settings.mode == SortMode::Numeric
|| selector.settings.mode == SortMode::HumanNumeric
{
@ -459,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 {
@ -1294,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 {
@ -1311,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 {
@ -1321,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.
@ -1514,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)) {
@ -1596,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

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

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

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

@ -43,23 +43,74 @@ fn test_ls_a() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch(".test-1");
at.mkdir("some-dir");
at.touch(
Path::new("some-dir")
.join(".test-2")
.as_os_str()
.to_str()
.unwrap(),
);
let result = scene.ucmd().succeeds();
let stdout = result.stdout_str();
assert!(!stdout.contains(".test-1"));
assert!(!stdout.contains(".."));
let re_pwd = Regex::new(r"^\.\n").unwrap();
// Using the present working directory
scene
.ucmd()
.arg("-1")
.succeeds()
.stdout_does_not_contain(".test-1")
.stdout_does_not_contain("..")
.stdout_does_not_match(&re_pwd);
scene
.ucmd()
.arg("-a")
.arg("-1")
.succeeds()
.stdout_contains(&".test-1")
.stdout_contains(&"..");
.stdout_contains(&"..")
.stdout_matches(&re_pwd);
let result = scene.ucmd().arg("-A").succeeds();
result.stdout_contains(".test-1");
let stdout = result.stdout_str();
assert!(!stdout.contains(".."));
scene
.ucmd()
.arg("-A")
.arg("-1")
.succeeds()
.stdout_contains(".test-1")
.stdout_does_not_contain("..")
.stdout_does_not_match(&re_pwd);
// Using a subdirectory
scene
.ucmd()
.arg("-1")
.arg("some-dir")
.succeeds()
.stdout_does_not_contain(".test-2")
.stdout_does_not_contain("..")
.stdout_does_not_match(&re_pwd);
scene
.ucmd()
.arg("-a")
.arg("-1")
.arg("some-dir")
.succeeds()
.stdout_contains(&".test-2")
.stdout_contains(&"..")
.no_stderr()
.stdout_matches(&re_pwd);
scene
.ucmd()
.arg("-A")
.arg("-1")
.arg("some-dir")
.succeeds()
.stdout_contains(".test-2")
.stdout_does_not_contain("..")
.stdout_does_not_match(&re_pwd);
}
#[test]
@ -482,7 +533,6 @@ fn test_ls_sort_name() {
.succeeds()
.stdout_is(["test-1", "test-2", "test-3\n"].join(sep));
// Order of a named sort ignores leading dots.
let scene_dot = TestScenario::new(util_name!());
let at = &scene_dot.fixtures;
at.touch(".a");
@ -495,7 +545,7 @@ fn test_ls_sort_name() {
.arg("--sort=name")
.arg("-A")
.succeeds()
.stdout_is([".a", "a", ".b", "b\n"].join(sep));
.stdout_is([".a", ".b", "a", "b\n"].join(sep));
}
#[test]
@ -584,7 +634,6 @@ fn test_ls_order_birthtime() {
}
#[test]
#[ignore]
fn test_ls_styles() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
@ -598,7 +647,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

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

@ -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,67 @@ 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");
// TODO: fix this
// $ 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 +124,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

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

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