mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-29 20:17:45 +00:00
Merge branch 'master' into id_selinux_context
This commit is contained in:
commit
010565c8fb
54 changed files with 2013 additions and 691 deletions
4
.github/workflows/CICD.yml
vendored
4
.github/workflows/CICD.yml
vendored
|
@ -13,8 +13,8 @@ env:
|
||||||
PROJECT_NAME: coreutils
|
PROJECT_NAME: coreutils
|
||||||
PROJECT_DESC: "Core universal (cross-platform) utilities"
|
PROJECT_DESC: "Core universal (cross-platform) utilities"
|
||||||
PROJECT_AUTH: "uutils"
|
PROJECT_AUTH: "uutils"
|
||||||
RUST_MIN_SRV: "1.51.0" ## MSRV v1.51.0
|
RUST_MIN_SRV: "1.47.0" ## MSRV v1.47.0
|
||||||
RUST_COV_SRV: "2020-08-01" ## (~v1.47.0) supported rust version for code coverage; (date required/used by 'coverage') ## !maint: refactor when code coverage support is included in the stable channel
|
RUST_COV_SRV: "2021-05-06" ## (~v1.52.0) supported rust version for code coverage; (date required/used by 'coverage') ## !maint: refactor when code coverage support is included in the stable channel
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
|
|
@ -151,6 +151,9 @@ Sylvestre Ledru
|
||||||
T Jameson Little
|
T Jameson Little
|
||||||
Jameson
|
Jameson
|
||||||
Little
|
Little
|
||||||
|
Thomas Queiroz
|
||||||
|
Thomas
|
||||||
|
Queiroz
|
||||||
Tobias Bohumir Schottdorf
|
Tobias Bohumir Schottdorf
|
||||||
Tobias
|
Tobias
|
||||||
Bohumir
|
Bohumir
|
||||||
|
|
|
@ -4,9 +4,10 @@ Contributions are very welcome, and should target Rust's master branch until the
|
||||||
standard libraries are stabilized. You may *claim* an item on the to-do list by
|
standard libraries are stabilized. You may *claim* an item on the to-do list by
|
||||||
following these steps:
|
following these steps:
|
||||||
|
|
||||||
1. Open an issue named "Implement [the utility of your choice]", e.g. "Implement ls"
|
1. Open an issue named "Implement [the utility of your choice]", e.g. "Implement
|
||||||
|
ls".
|
||||||
1. State that you are working on this utility.
|
1. State that you are working on this utility.
|
||||||
1. Develop the utility
|
1. Develop the utility.
|
||||||
1. Add integration tests.
|
1. Add integration tests.
|
||||||
1. Add the reference to your utility into Cargo.toml and Makefile.
|
1. Add the reference to your utility into Cargo.toml and Makefile.
|
||||||
1. Remove utility from the to-do list in the README.
|
1. Remove utility from the to-do list in the README.
|
||||||
|
@ -17,12 +18,20 @@ search the issues to make sure no one else is working on it.
|
||||||
|
|
||||||
## Best practices
|
## Best practices
|
||||||
|
|
||||||
1. Follow what GNU is doing in term of options and behavior.
|
1. Follow what GNU is doing in terms of options and behavior. It is recommended
|
||||||
1. If possible, look at the GNU test suite execution in the CI and make the test work if failing.
|
to look at the GNU Coreutils manual ([on the
|
||||||
|
web](https://www.gnu.org/software/coreutils/manual/html_node/index.html), or
|
||||||
|
locally using `info <utility>`). It is more in depth than the man pages and
|
||||||
|
provides a good description of available features and their implementation
|
||||||
|
details.
|
||||||
|
1. If possible, look at the GNU test suite execution in the CI and make the test
|
||||||
|
work if failing.
|
||||||
1. Use clap for argument management.
|
1. Use clap for argument management.
|
||||||
1. Make sure that the code coverage is covering all of the cases, including errors.
|
1. Make sure that the code coverage is covering all of the cases, including
|
||||||
|
errors.
|
||||||
1. The code must be clippy-warning-free and rustfmt-compliant.
|
1. The code must be clippy-warning-free and rustfmt-compliant.
|
||||||
1. Don't hesitate to move common functions into uucore if they can be reused by other binaries.
|
1. Don't hesitate to move common functions into uucore if they can be reused by
|
||||||
|
other binaries.
|
||||||
1. Unsafe code should be documented with Safety comments.
|
1. Unsafe code should be documented with Safety comments.
|
||||||
1. uutils is original code. It cannot contain code from existing GNU or Unix-like
|
1. uutils is original code. It cannot contain code from existing GNU or Unix-like
|
||||||
utilities, nor should it link to or reference GNU libraries.
|
utilities, nor should it link to or reference GNU libraries.
|
||||||
|
@ -99,9 +108,11 @@ project, a tool like `cargo-license` can be used to show their license details.
|
||||||
The following types of license are acceptable:
|
The following types of license are acceptable:
|
||||||
|
|
||||||
* MIT License
|
* MIT License
|
||||||
* Dual- or tri-license with an MIT License option ("Apache-2.0 or MIT" is a popular combination)
|
* Dual- or tri-license with an MIT License option ("Apache-2.0 or MIT" is a
|
||||||
|
popular combination)
|
||||||
* "MIT equivalent" license (2-clause BSD, 3-clause BSD, ISC)
|
* "MIT equivalent" license (2-clause BSD, 3-clause BSD, ISC)
|
||||||
* License less restrictive than the MIT License (CC0 1.0 Universal)
|
* License less restrictive than the MIT License (CC0 1.0 Universal)
|
||||||
|
* Apache License version 2.0
|
||||||
|
|
||||||
Licenses we will not use:
|
Licenses we will not use:
|
||||||
|
|
||||||
|
|
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -1,5 +1,7 @@
|
||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "Inflector"
|
name = "Inflector"
|
||||||
version = "0.11.4"
|
version = "0.11.4"
|
||||||
|
@ -2714,6 +2716,7 @@ version = "0.0.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"libc",
|
"libc",
|
||||||
|
"nix 0.20.0",
|
||||||
"redox_syscall 0.1.57",
|
"redox_syscall 0.1.57",
|
||||||
"uucore",
|
"uucore",
|
||||||
"uucore_procs",
|
"uucore_procs",
|
||||||
|
@ -2917,6 +2920,7 @@ dependencies = [
|
||||||
name = "uucore"
|
name = "uucore"
|
||||||
version = "0.0.9"
|
version = "0.0.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"clap",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
"dns-lookup",
|
"dns-lookup",
|
||||||
"dunce",
|
"dunce",
|
||||||
|
|
|
@ -6,9 +6,6 @@
|
||||||
// For the full copyright and license information, please view the LICENSE
|
// For the full copyright and license information, please view the LICENSE
|
||||||
// file that was distributed with this source code.
|
// file that was distributed with this source code.
|
||||||
|
|
||||||
// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422
|
|
||||||
#![allow(clippy::nonstandard_macro_braces)]
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,6 @@
|
||||||
|
|
||||||
// spell-checker:ignore (ToDO) nonprint nonblank nonprinting
|
// spell-checker:ignore (ToDO) nonprint nonblank nonprinting
|
||||||
|
|
||||||
// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422
|
|
||||||
#![allow(clippy::nonstandard_macro_braces)]
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
extern crate unix_socket;
|
extern crate unix_socket;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
|
|
@ -14,6 +14,8 @@ use uucore::fs::resolve_relative_path;
|
||||||
use uucore::libc::{gid_t, uid_t};
|
use uucore::libc::{gid_t, uid_t};
|
||||||
use uucore::perms::{wrap_chown, Verbosity};
|
use uucore::perms::{wrap_chown, Verbosity};
|
||||||
|
|
||||||
|
use uucore::error::{FromIo, UError, UResult, USimpleError};
|
||||||
|
|
||||||
use clap::{crate_version, App, Arg};
|
use clap::{crate_version, App, Arg};
|
||||||
|
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
@ -66,7 +68,8 @@ fn get_usage() -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uumain(args: impl uucore::Args) -> i32 {
|
#[uucore_procs::gen_uumain]
|
||||||
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let args = args
|
let args = args
|
||||||
.collect_str(InvalidEncodingHandling::Ignore)
|
.collect_str(InvalidEncodingHandling::Ignore)
|
||||||
.accept_any();
|
.accept_any();
|
||||||
|
@ -104,8 +107,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
if recursive {
|
if recursive {
|
||||||
if bit_flag == FTS_PHYSICAL {
|
if bit_flag == FTS_PHYSICAL {
|
||||||
if derefer == 1 {
|
if derefer == 1 {
|
||||||
show_error!("-R --dereference requires -H or -L");
|
return Err(USimpleError::new(
|
||||||
return 1;
|
1,
|
||||||
|
"-R --dereference requires -H or -L".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
derefer = 0;
|
derefer = 0;
|
||||||
}
|
}
|
||||||
|
@ -126,15 +131,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter = if let Some(spec) = matches.value_of(options::FROM) {
|
let filter = if let Some(spec) = matches.value_of(options::FROM) {
|
||||||
match parse_spec(spec) {
|
match parse_spec(spec)? {
|
||||||
Ok((Some(uid), None)) => IfFrom::User(uid),
|
(Some(uid), None) => IfFrom::User(uid),
|
||||||
Ok((None, Some(gid))) => IfFrom::Group(gid),
|
(None, Some(gid)) => IfFrom::Group(gid),
|
||||||
Ok((Some(uid), Some(gid))) => IfFrom::UserGroup(uid, gid),
|
(Some(uid), Some(gid)) => IfFrom::UserGroup(uid, gid),
|
||||||
Ok((None, None)) => IfFrom::All,
|
(None, None) => IfFrom::All,
|
||||||
Err(e) => {
|
|
||||||
show_error!("{}", e);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
IfFrom::All
|
IfFrom::All
|
||||||
|
@ -143,27 +144,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
let dest_uid: Option<u32>;
|
let dest_uid: Option<u32>;
|
||||||
let dest_gid: Option<u32>;
|
let dest_gid: Option<u32>;
|
||||||
if let Some(file) = matches.value_of(options::REFERENCE) {
|
if let Some(file) = matches.value_of(options::REFERENCE) {
|
||||||
match fs::metadata(&file) {
|
let meta = fs::metadata(&file)
|
||||||
Ok(meta) => {
|
.map_err_context(|| format!("failed to get attributes of '{}'", file))?;
|
||||||
dest_gid = Some(meta.gid());
|
dest_gid = Some(meta.gid());
|
||||||
dest_uid = Some(meta.uid());
|
dest_uid = Some(meta.uid());
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
show_error!("failed to get attributes of '{}': {}", file, e);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
match parse_spec(owner) {
|
let (u, g) = parse_spec(owner)?;
|
||||||
Ok((u, g)) => {
|
dest_uid = u;
|
||||||
dest_uid = u;
|
dest_gid = g;
|
||||||
dest_gid = g;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
show_error!("{}", e);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let executor = Chowner {
|
let executor = Chowner {
|
||||||
bit_flag,
|
bit_flag,
|
||||||
|
@ -275,7 +263,7 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_spec(spec: &str) -> Result<(Option<u32>, Option<u32>), String> {
|
fn parse_spec(spec: &str) -> UResult<(Option<u32>, Option<u32>)> {
|
||||||
let args = spec.split_terminator(':').collect::<Vec<_>>();
|
let args = spec.split_terminator(':').collect::<Vec<_>>();
|
||||||
let usr_only = args.len() == 1 && !args[0].is_empty();
|
let usr_only = args.len() == 1 && !args[0].is_empty();
|
||||||
let grp_only = args.len() == 2 && args[0].is_empty();
|
let grp_only = args.len() == 2 && args[0].is_empty();
|
||||||
|
@ -283,7 +271,7 @@ fn parse_spec(spec: &str) -> Result<(Option<u32>, Option<u32>), String> {
|
||||||
let uid = if usr_only || usr_grp {
|
let uid = if usr_only || usr_grp {
|
||||||
Some(
|
Some(
|
||||||
Passwd::locate(args[0])
|
Passwd::locate(args[0])
|
||||||
.map_err(|_| format!("invalid user: '{}'", spec))?
|
.map_err(|_| USimpleError::new(1, format!("invalid user: '{}'", spec)))?
|
||||||
.uid(),
|
.uid(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -292,7 +280,7 @@ fn parse_spec(spec: &str) -> Result<(Option<u32>, Option<u32>), String> {
|
||||||
let gid = if grp_only || usr_grp {
|
let gid = if grp_only || usr_grp {
|
||||||
Some(
|
Some(
|
||||||
Group::locate(args[1])
|
Group::locate(args[1])
|
||||||
.map_err(|_| format!("invalid group: '{}'", spec))?
|
.map_err(|_| USimpleError::new(1, format!("invalid group: '{}'", spec)))?
|
||||||
.gid(),
|
.gid(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -330,12 +318,15 @@ macro_rules! unwrap {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chowner {
|
impl Chowner {
|
||||||
fn exec(&self) -> i32 {
|
fn exec(&self) -> UResult<()> {
|
||||||
let mut ret = 0;
|
let mut ret = 0;
|
||||||
for f in &self.files {
|
for f in &self.files {
|
||||||
ret |= self.traverse(f);
|
ret |= self.traverse(f);
|
||||||
}
|
}
|
||||||
ret
|
if ret != 0 {
|
||||||
|
return Err(UError::from(ret));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn traverse<P: AsRef<Path>>(&self, root: P) -> i32 {
|
fn traverse<P: AsRef<Path>>(&self, root: P) -> i32 {
|
||||||
|
|
|
@ -55,7 +55,6 @@ use walkdir::WalkDir;
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
#[allow(clippy::missing_safety_doc)]
|
|
||||||
ioctl!(write ficlone with 0x94, 9; std::os::raw::c_int);
|
ioctl!(write ficlone with 0x94, 9; std::os::raw::c_int);
|
||||||
|
|
||||||
quick_error! {
|
quick_error! {
|
||||||
|
@ -98,6 +97,9 @@ quick_error! {
|
||||||
/// path, but those that are not implemented yet should return
|
/// path, but those that are not implemented yet should return
|
||||||
/// a NotImplemented error.
|
/// a NotImplemented error.
|
||||||
NotImplemented(opt: String) { display("Option '{}' not yet implemented.", opt) }
|
NotImplemented(opt: String) { display("Option '{}' not yet implemented.", opt) }
|
||||||
|
|
||||||
|
/// Invalid arguments to backup
|
||||||
|
Backup(description: String) { display("{}\nTry 'cp --help' for more information.", description) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,7 +361,6 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.require_equals(true)
|
.require_equals(true)
|
||||||
.min_values(0)
|
.min_values(0)
|
||||||
.possible_values(backup_control::BACKUP_CONTROL_VALUES)
|
|
||||||
.value_name("CONTROL")
|
.value_name("CONTROL")
|
||||||
)
|
)
|
||||||
.arg(Arg::with_name(options::BACKUP_NO_ARG)
|
.arg(Arg::with_name(options::BACKUP_NO_ARG)
|
||||||
|
@ -604,9 +605,17 @@ impl Options {
|
||||||
|| matches.is_present(options::ARCHIVE);
|
|| matches.is_present(options::ARCHIVE);
|
||||||
|
|
||||||
let backup_mode = backup_control::determine_backup_mode(
|
let backup_mode = backup_control::determine_backup_mode(
|
||||||
matches.is_present(options::BACKUP_NO_ARG) || matches.is_present(options::BACKUP),
|
matches.is_present(options::BACKUP_NO_ARG),
|
||||||
|
matches.is_present(options::BACKUP),
|
||||||
matches.value_of(options::BACKUP),
|
matches.value_of(options::BACKUP),
|
||||||
);
|
);
|
||||||
|
let backup_mode = match backup_mode {
|
||||||
|
Err(err) => {
|
||||||
|
return Err(Error::Backup(err));
|
||||||
|
}
|
||||||
|
Ok(mode) => mode,
|
||||||
|
};
|
||||||
|
|
||||||
let backup_suffix =
|
let backup_suffix =
|
||||||
backup_control::determine_backup_suffix(matches.value_of(options::SUFFIX));
|
backup_control::determine_backup_suffix(matches.value_of(options::SUFFIX));
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422
|
|
||||||
#![allow(clippy::nonstandard_macro_braces)]
|
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
use uucore::error::UCustomError;
|
||||||
|
use uucore::error::UResult;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use uucore::fsext::statfs_fn;
|
use uucore::fsext::statfs_fn;
|
||||||
use uucore::fsext::{read_fs_list, FsUsage, MountInfo};
|
use uucore::fsext::{read_fs_list, FsUsage, MountInfo};
|
||||||
|
@ -19,8 +21,10 @@ use std::cell::Cell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
|
use std::fmt::Display;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::mem;
|
use std::mem;
|
||||||
|
|
||||||
|
@ -33,9 +37,6 @@ use std::path::Path;
|
||||||
static ABOUT: &str = "Show information about the file system on which each FILE resides,\n\
|
static ABOUT: &str = "Show information about the file system on which each FILE resides,\n\
|
||||||
or all file systems by default.";
|
or all file systems by default.";
|
||||||
|
|
||||||
static EXIT_OK: i32 = 0;
|
|
||||||
static EXIT_ERR: i32 = 1;
|
|
||||||
|
|
||||||
static OPT_ALL: &str = "all";
|
static OPT_ALL: &str = "all";
|
||||||
static OPT_BLOCKSIZE: &str = "blocksize";
|
static OPT_BLOCKSIZE: &str = "blocksize";
|
||||||
static OPT_DIRECT: &str = "direct";
|
static OPT_DIRECT: &str = "direct";
|
||||||
|
@ -226,8 +227,8 @@ fn filter_mount_list(vmi: Vec<MountInfo>, paths: &[String], opt: &Options) -> Ve
|
||||||
/// Convert `value` to a human readable string based on `base`.
|
/// Convert `value` to a human readable string based on `base`.
|
||||||
/// e.g. It returns 1G when value is 1 * 1024 * 1024 * 1024 and base is 1024.
|
/// e.g. It returns 1G when value is 1 * 1024 * 1024 * 1024 and base is 1024.
|
||||||
/// Note: It returns `value` if `base` isn't positive.
|
/// Note: It returns `value` if `base` isn't positive.
|
||||||
fn human_readable(value: u64, base: i64) -> String {
|
fn human_readable(value: u64, base: i64) -> UResult<String> {
|
||||||
match base {
|
let base_str = match base {
|
||||||
d if d < 0 => value.to_string(),
|
d if d < 0 => value.to_string(),
|
||||||
|
|
||||||
// ref: [Binary prefix](https://en.wikipedia.org/wiki/Binary_prefix) @@ <https://archive.is/cnwmF>
|
// ref: [Binary prefix](https://en.wikipedia.org/wiki/Binary_prefix) @@ <https://archive.is/cnwmF>
|
||||||
|
@ -242,8 +243,10 @@ fn human_readable(value: u64, base: i64) -> String {
|
||||||
NumberPrefix::Prefixed(prefix, bytes) => format!("{:.1}{}", bytes, prefix.symbol()),
|
NumberPrefix::Prefixed(prefix, bytes) => format!("{:.1}{}", bytes, prefix.symbol()),
|
||||||
},
|
},
|
||||||
|
|
||||||
_ => crash!(EXIT_ERR, "Internal error: Unknown base value {}", base),
|
_ => return Err(DfError::InvalidBaseValue(base.to_string()).into()),
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Ok(base_str)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn use_size(free_size: u64, total_size: u64) -> String {
|
fn use_size(free_size: u64, total_size: u64) -> String {
|
||||||
|
@ -256,7 +259,31 @@ fn use_size(free_size: u64, total_size: u64) -> String {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uumain(args: impl uucore::Args) -> i32 {
|
#[derive(Debug)]
|
||||||
|
enum DfError {
|
||||||
|
InvalidBaseValue(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DfError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
DfError::InvalidBaseValue(s) => write!(f, "Internal error: Unknown base value {}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for DfError {}
|
||||||
|
|
||||||
|
impl UCustomError for DfError {
|
||||||
|
fn code(&self) -> i32 {
|
||||||
|
match self {
|
||||||
|
DfError::InvalidBaseValue(_) => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[uucore_procs::gen_uumain]
|
||||||
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let usage = get_usage();
|
let usage = get_usage();
|
||||||
let matches = uu_app().usage(&usage[..]).get_matches_from(args);
|
let matches = uu_app().usage(&usage[..]).get_matches_from(args);
|
||||||
|
|
||||||
|
@ -269,7 +296,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
{
|
{
|
||||||
if matches.is_present(OPT_INODES) {
|
if matches.is_present(OPT_INODES) {
|
||||||
println!("{}: doesn't support -i option", executable!());
|
println!("{}: doesn't support -i option", executable!());
|
||||||
return EXIT_OK;
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,15 +380,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
if opt.show_inode_instead {
|
if opt.show_inode_instead {
|
||||||
print!(
|
print!(
|
||||||
"{0: >12} ",
|
"{0: >12} ",
|
||||||
human_readable(fs.usage.files, opt.human_readable_base)
|
human_readable(fs.usage.files, opt.human_readable_base)?
|
||||||
);
|
);
|
||||||
print!(
|
print!(
|
||||||
"{0: >12} ",
|
"{0: >12} ",
|
||||||
human_readable(fs.usage.files - fs.usage.ffree, opt.human_readable_base)
|
human_readable(fs.usage.files - fs.usage.ffree, opt.human_readable_base)?
|
||||||
);
|
);
|
||||||
print!(
|
print!(
|
||||||
"{0: >12} ",
|
"{0: >12} ",
|
||||||
human_readable(fs.usage.ffree, opt.human_readable_base)
|
human_readable(fs.usage.ffree, opt.human_readable_base)?
|
||||||
);
|
);
|
||||||
print!(
|
print!(
|
||||||
"{0: >5} ",
|
"{0: >5} ",
|
||||||
|
@ -375,15 +402,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
let free_size = fs.usage.blocksize * fs.usage.bfree;
|
let free_size = fs.usage.blocksize * fs.usage.bfree;
|
||||||
print!(
|
print!(
|
||||||
"{0: >12} ",
|
"{0: >12} ",
|
||||||
human_readable(total_size, opt.human_readable_base)
|
human_readable(total_size, opt.human_readable_base)?
|
||||||
);
|
);
|
||||||
print!(
|
print!(
|
||||||
"{0: >12} ",
|
"{0: >12} ",
|
||||||
human_readable(total_size - free_size, opt.human_readable_base)
|
human_readable(total_size - free_size, opt.human_readable_base)?
|
||||||
);
|
);
|
||||||
print!(
|
print!(
|
||||||
"{0: >12} ",
|
"{0: >12} ",
|
||||||
human_readable(free_size, opt.human_readable_base)
|
human_readable(free_size, opt.human_readable_base)?
|
||||||
);
|
);
|
||||||
if cfg!(target_os = "macos") {
|
if cfg!(target_os = "macos") {
|
||||||
let used = fs.usage.blocks - fs.usage.bfree;
|
let used = fs.usage.blocks - fs.usage.bfree;
|
||||||
|
@ -396,7 +423,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
EXIT_OK
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uu_app() -> App<'static, 'static> {
|
pub fn uu_app() -> App<'static, 'static> {
|
||||||
|
|
|
@ -5,9 +5,6 @@
|
||||||
// For the full copyright and license information, please view the LICENSE
|
// For the full copyright and license information, please view the LICENSE
|
||||||
// file that was distributed with this source code.
|
// file that was distributed with this source code.
|
||||||
|
|
||||||
// Clippy bug: https://github.com/rust-lang/rust-clippy/issues/7422
|
|
||||||
#![allow(clippy::nonstandard_macro_braces)]
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,6 @@
|
||||||
// * For the full copyright and license information, please view the LICENSE
|
// * For the full copyright and license information, please view the LICENSE
|
||||||
// * file that was distributed with this source code.
|
// * file that was distributed with this source code.
|
||||||
|
|
||||||
// Clippy bug: https://github.com/rust-lang/rust-clippy/issues/7422
|
|
||||||
#![allow(clippy::nonstandard_macro_braces)]
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ use clap::{crate_version, App, Arg};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::iter::Peekable;
|
use std::iter::Peekable;
|
||||||
use std::str::Chars;
|
use std::str::Chars;
|
||||||
|
use uucore::error::{FromIo, UResult};
|
||||||
use uucore::InvalidEncodingHandling;
|
use uucore::InvalidEncodingHandling;
|
||||||
|
|
||||||
const NAME: &str = "echo";
|
const NAME: &str = "echo";
|
||||||
|
@ -113,7 +114,8 @@ fn print_escaped(input: &str, mut output: impl Write) -> io::Result<bool> {
|
||||||
Ok(should_stop)
|
Ok(should_stop)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uumain(args: impl uucore::Args) -> i32 {
|
#[uucore_procs::gen_uumain]
|
||||||
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let args = args
|
let args = args
|
||||||
.collect_str(InvalidEncodingHandling::ConvertLossy)
|
.collect_str(InvalidEncodingHandling::ConvertLossy)
|
||||||
.accept_any();
|
.accept_any();
|
||||||
|
@ -126,13 +128,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
None => vec!["".to_string()],
|
None => vec!["".to_string()],
|
||||||
};
|
};
|
||||||
|
|
||||||
match execute(no_newline, escaped, values) {
|
execute(no_newline, escaped, values).map_err_context(|| "could not write to stdout".to_string())
|
||||||
Ok(_) => 0,
|
|
||||||
Err(f) => {
|
|
||||||
show_error!("{}", f);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uu_app() -> App<'static, 'static> {
|
pub fn uu_app() -> App<'static, 'static> {
|
||||||
|
|
|
@ -36,28 +36,86 @@ fn get_usage() -> String {
|
||||||
format!("{0} [OPTION]... [FILE]...", executable!())
|
format!("{0} [OPTION]... [FILE]...", executable!())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tabstops_parse(s: String) -> Vec<usize> {
|
/// The mode to use when replacing tabs beyond the last one specified in
|
||||||
let words = s.split(',');
|
/// the `--tabs` argument.
|
||||||
|
enum RemainingMode {
|
||||||
|
None,
|
||||||
|
Slash,
|
||||||
|
Plus,
|
||||||
|
}
|
||||||
|
|
||||||
let nums = words
|
/// Decide whether the character is either a space or a comma.
|
||||||
.map(|sn| {
|
///
|
||||||
sn.parse::<usize>()
|
/// # Examples
|
||||||
.unwrap_or_else(|_| crash!(1, "{}\n", "tab size contains invalid character(s)"))
|
///
|
||||||
})
|
/// ```rust,ignore
|
||||||
.collect::<Vec<usize>>();
|
/// assert!(is_space_or_comma(' '))
|
||||||
|
/// assert!(is_space_or_comma(','))
|
||||||
|
/// assert!(!is_space_or_comma('a'))
|
||||||
|
/// ```
|
||||||
|
fn is_space_or_comma(c: char) -> bool {
|
||||||
|
c == ' ' || c == ','
|
||||||
|
}
|
||||||
|
|
||||||
if nums.iter().any(|&n| n == 0) {
|
/// Parse a list of tabstops from a `--tabs` argument.
|
||||||
crash!(1, "{}\n", "tab size cannot be 0");
|
///
|
||||||
|
/// This function returns both the vector of numbers appearing in the
|
||||||
|
/// comma- or space-separated list, and also an optional mode, specified
|
||||||
|
/// by either a "/" or a "+" character appearing before the final number
|
||||||
|
/// in the list. This mode defines the strategy to use for computing the
|
||||||
|
/// number of spaces to use for columns beyond the end of the tab stop
|
||||||
|
/// list specified here.
|
||||||
|
fn tabstops_parse(s: String) -> (RemainingMode, Vec<usize>) {
|
||||||
|
// Leading commas and spaces are ignored.
|
||||||
|
let s = s.trim_start_matches(is_space_or_comma);
|
||||||
|
|
||||||
|
// If there were only commas and spaces in the string, just use the
|
||||||
|
// default tabstops.
|
||||||
|
if s.is_empty() {
|
||||||
|
return (RemainingMode::None, vec![DEFAULT_TABSTOP]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (false, _) = nums
|
let mut nums = vec![];
|
||||||
.iter()
|
let mut remaining_mode = RemainingMode::None;
|
||||||
.fold((true, 0), |(acc, last), &n| (acc && last <= n, n))
|
for word in s.split(is_space_or_comma) {
|
||||||
{
|
let bytes = word.as_bytes();
|
||||||
crash!(1, "{}\n", "tab sizes must be ascending");
|
for i in 0..bytes.len() {
|
||||||
}
|
match bytes[i] {
|
||||||
|
b'+' => {
|
||||||
|
remaining_mode = RemainingMode::Plus;
|
||||||
|
}
|
||||||
|
b'/' => {
|
||||||
|
remaining_mode = RemainingMode::Slash;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Parse a number from the byte sequence.
|
||||||
|
let num = from_utf8(&bytes[i..]).unwrap().parse::<usize>().unwrap();
|
||||||
|
|
||||||
nums
|
// Tab size must be positive.
|
||||||
|
if num == 0 {
|
||||||
|
crash!(1, "{}\n", "tab size cannot be 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab sizes must be ascending.
|
||||||
|
if let Some(last_stop) = nums.last() {
|
||||||
|
if *last_stop >= num {
|
||||||
|
crash!(1, "tab sizes must be ascending");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append this tab stop to the list of all tabstops.
|
||||||
|
nums.push(num);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no numbers could be parsed (for example, if `s` were "+,+,+"),
|
||||||
|
// then just use the default tabstops.
|
||||||
|
if nums.is_empty() {
|
||||||
|
nums = vec![DEFAULT_TABSTOP];
|
||||||
|
}
|
||||||
|
(remaining_mode, nums)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Options {
|
struct Options {
|
||||||
|
@ -66,13 +124,17 @@ struct Options {
|
||||||
tspaces: String,
|
tspaces: String,
|
||||||
iflag: bool,
|
iflag: bool,
|
||||||
uflag: bool,
|
uflag: bool,
|
||||||
|
|
||||||
|
/// Strategy for expanding tabs for columns beyond those specified
|
||||||
|
/// in `tabstops`.
|
||||||
|
remaining_mode: RemainingMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
fn new(matches: &ArgMatches) -> Options {
|
fn new(matches: &ArgMatches) -> Options {
|
||||||
let tabstops = match matches.value_of(options::TABS) {
|
let (remaining_mode, tabstops) = match matches.value_of(options::TABS) {
|
||||||
Some(s) => tabstops_parse(s.to_string()),
|
Some(s) => tabstops_parse(s.to_string()),
|
||||||
None => vec![DEFAULT_TABSTOP],
|
None => (RemainingMode::None, vec![DEFAULT_TABSTOP]),
|
||||||
};
|
};
|
||||||
|
|
||||||
let iflag = matches.is_present(options::INITIAL);
|
let iflag = matches.is_present(options::INITIAL);
|
||||||
|
@ -102,6 +164,7 @@ impl Options {
|
||||||
tspaces,
|
tspaces,
|
||||||
iflag,
|
iflag,
|
||||||
uflag,
|
uflag,
|
||||||
|
remaining_mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,13 +222,41 @@ fn open(path: String) -> BufReader<Box<dyn Read + 'static>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_tabstop(tabstops: &[usize], col: usize) -> usize {
|
/// Compute the number of spaces to the next tabstop.
|
||||||
if tabstops.len() == 1 {
|
///
|
||||||
tabstops[0] - col % tabstops[0]
|
/// `tabstops` is the sequence of tabstop locations.
|
||||||
} else {
|
///
|
||||||
match tabstops.iter().find(|&&t| t > col) {
|
/// `col` is the index of the current cursor in the line being written.
|
||||||
|
///
|
||||||
|
/// If `remaining_mode` is [`RemainingMode::Plus`], then the last entry
|
||||||
|
/// in the `tabstops` slice is interpreted as a relative number of
|
||||||
|
/// spaces, which this function will return for every input value of
|
||||||
|
/// `col` beyond the end of the second-to-last element of `tabstops`.
|
||||||
|
///
|
||||||
|
/// If `remaining_mode` is [`RemainingMode::Plus`], then the last entry
|
||||||
|
/// in the `tabstops` slice is interpreted as a relative number of
|
||||||
|
/// spaces, which this function will return for every input value of
|
||||||
|
/// `col` beyond the end of the second-to-last element of `tabstops`.
|
||||||
|
fn next_tabstop(tabstops: &[usize], col: usize, remaining_mode: &RemainingMode) -> usize {
|
||||||
|
let num_tabstops = tabstops.len();
|
||||||
|
match remaining_mode {
|
||||||
|
RemainingMode::Plus => match tabstops[0..num_tabstops - 1].iter().find(|&&t| t > col) {
|
||||||
Some(t) => t - col,
|
Some(t) => t - col,
|
||||||
None => 1,
|
None => tabstops[num_tabstops - 1] - 1,
|
||||||
|
},
|
||||||
|
RemainingMode::Slash => match tabstops[0..num_tabstops - 1].iter().find(|&&t| t > col) {
|
||||||
|
Some(t) => t - col,
|
||||||
|
None => tabstops[num_tabstops - 1] - col % tabstops[num_tabstops - 1],
|
||||||
|
},
|
||||||
|
RemainingMode::None => {
|
||||||
|
if num_tabstops == 1 {
|
||||||
|
tabstops[0] - col % tabstops[0]
|
||||||
|
} else {
|
||||||
|
match tabstops.iter().find(|&&t| t > col) {
|
||||||
|
Some(t) => t - col,
|
||||||
|
None => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,12 +323,16 @@ fn expand(options: Options) {
|
||||||
match ctype {
|
match ctype {
|
||||||
Tab => {
|
Tab => {
|
||||||
// figure out how many spaces to the next tabstop
|
// figure out how many spaces to the next tabstop
|
||||||
let nts = next_tabstop(ts, col);
|
let nts = next_tabstop(ts, col, &options.remaining_mode);
|
||||||
col += nts;
|
col += nts;
|
||||||
|
|
||||||
// now dump out either spaces if we're expanding, or a literal tab if we're not
|
// now dump out either spaces if we're expanding, or a literal tab if we're not
|
||||||
if init || !options.iflag {
|
if init || !options.iflag {
|
||||||
safe_unwrap!(output.write_all(options.tspaces[..nts].as_bytes()));
|
if nts <= options.tspaces.len() {
|
||||||
|
safe_unwrap!(output.write_all(options.tspaces[..nts].as_bytes()));
|
||||||
|
} else {
|
||||||
|
safe_unwrap!(output.write_all(" ".repeat(nts).as_bytes()));
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
safe_unwrap!(output.write_all(&buf[byte..byte + nbytes]));
|
safe_unwrap!(output.write_all(&buf[byte..byte + nbytes]));
|
||||||
}
|
}
|
||||||
|
@ -269,3 +364,30 @@ fn expand(options: Options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::next_tabstop;
|
||||||
|
use super::RemainingMode;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_next_tabstop_remaining_mode_none() {
|
||||||
|
assert_eq!(next_tabstop(&[1, 5], 0, &RemainingMode::None), 1);
|
||||||
|
assert_eq!(next_tabstop(&[1, 5], 3, &RemainingMode::None), 2);
|
||||||
|
assert_eq!(next_tabstop(&[1, 5], 6, &RemainingMode::None), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_next_tabstop_remaining_mode_plus() {
|
||||||
|
assert_eq!(next_tabstop(&[1, 5], 0, &RemainingMode::Plus), 1);
|
||||||
|
assert_eq!(next_tabstop(&[1, 5], 3, &RemainingMode::Plus), 4);
|
||||||
|
assert_eq!(next_tabstop(&[1, 5], 6, &RemainingMode::Plus), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_next_tabstop_remaining_mode_slash() {
|
||||||
|
assert_eq!(next_tabstop(&[1, 5], 0, &RemainingMode::Slash), 1);
|
||||||
|
assert_eq!(next_tabstop(&[1, 5], 3, &RemainingMode::Slash), 2);
|
||||||
|
assert_eq!(next_tabstop(&[1, 5], 6, &RemainingMode::Slash), 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,12 +5,17 @@
|
||||||
// * For the full copyright and license information, please view the LICENSE
|
// * For the full copyright and license information, please view the LICENSE
|
||||||
// * file that was distributed with this source code.
|
// * file that was distributed with this source code.
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate uucore;
|
||||||
|
|
||||||
use clap::App;
|
use clap::App;
|
||||||
|
use uucore::error::{UError, UResult};
|
||||||
use uucore::executable;
|
use uucore::executable;
|
||||||
|
|
||||||
pub fn uumain(args: impl uucore::Args) -> i32 {
|
#[uucore_procs::gen_uumain]
|
||||||
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
uu_app().get_matches_from(args);
|
uu_app().get_matches_from(args);
|
||||||
1
|
Err(UError::from(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uu_app() -> App<'static, 'static> {
|
pub fn uu_app() -> App<'static, 'static> {
|
||||||
|
|
|
@ -7,9 +7,6 @@
|
||||||
|
|
||||||
// spell-checker:ignore (ToDO) gethostid
|
// spell-checker:ignore (ToDO) gethostid
|
||||||
|
|
||||||
// Clippy bug: https://github.com/rust-lang/rust-clippy/issues/7422
|
|
||||||
#![allow(clippy::nonstandard_macro_braces)]
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,6 @@
|
||||||
|
|
||||||
// spell-checker:ignore (ToDO) MAKEWORD addrs hashset
|
// spell-checker:ignore (ToDO) MAKEWORD addrs hashset
|
||||||
|
|
||||||
// Clippy bug: https://github.com/rust-lang/rust-clippy/issues/7422
|
|
||||||
#![allow(clippy::nonstandard_macro_braces)]
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
|
||||||
|
|
|
@ -308,15 +308,25 @@ fn behavior(matches: &ArgMatches) -> Result<Behavior, i32> {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let backup_mode = backup_control::determine_backup_mode(
|
||||||
|
matches.is_present(OPT_BACKUP_NO_ARG),
|
||||||
|
matches.is_present(OPT_BACKUP),
|
||||||
|
matches.value_of(OPT_BACKUP),
|
||||||
|
);
|
||||||
|
let backup_mode = match backup_mode {
|
||||||
|
Err(err) => {
|
||||||
|
show_usage_error!("{}", err);
|
||||||
|
return Err(1);
|
||||||
|
}
|
||||||
|
Ok(mode) => mode,
|
||||||
|
};
|
||||||
|
|
||||||
let target_dir = matches.value_of(OPT_TARGET_DIRECTORY).map(|d| d.to_owned());
|
let target_dir = matches.value_of(OPT_TARGET_DIRECTORY).map(|d| d.to_owned());
|
||||||
|
|
||||||
Ok(Behavior {
|
Ok(Behavior {
|
||||||
main_function,
|
main_function,
|
||||||
specified_mode,
|
specified_mode,
|
||||||
backup_mode: backup_control::determine_backup_mode(
|
backup_mode,
|
||||||
matches.is_present(OPT_BACKUP_NO_ARG) || matches.is_present(OPT_BACKUP),
|
|
||||||
matches.value_of(OPT_BACKUP),
|
|
||||||
),
|
|
||||||
suffix: backup_control::determine_backup_suffix(matches.value_of(OPT_SUFFIX)),
|
suffix: backup_control::determine_backup_suffix(matches.value_of(OPT_SUFFIX)),
|
||||||
owner: matches.value_of(OPT_OWNER).unwrap_or("").to_string(),
|
owner: matches.value_of(OPT_OWNER).unwrap_or("").to_string(),
|
||||||
group: matches.value_of(OPT_GROUP).unwrap_or("").to_string(),
|
group: matches.value_of(OPT_GROUP).unwrap_or("").to_string(),
|
||||||
|
|
|
@ -13,14 +13,12 @@ extern crate uucore;
|
||||||
use clap::{crate_version, App, Arg};
|
use clap::{crate_version, App, Arg};
|
||||||
use libc::{c_int, pid_t};
|
use libc::{c_int, pid_t};
|
||||||
use std::io::Error;
|
use std::io::Error;
|
||||||
|
use uucore::error::{UResult, USimpleError};
|
||||||
use uucore::signals::ALL_SIGNALS;
|
use uucore::signals::ALL_SIGNALS;
|
||||||
use uucore::InvalidEncodingHandling;
|
use uucore::InvalidEncodingHandling;
|
||||||
|
|
||||||
static ABOUT: &str = "Send signal to processes or list information about signals.";
|
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 mod options {
|
||||||
pub static PIDS_OR_SIGNALS: &str = "pids_of_signals";
|
pub static PIDS_OR_SIGNALS: &str = "pids_of_signals";
|
||||||
pub static LIST: &str = "list";
|
pub static LIST: &str = "list";
|
||||||
|
@ -36,7 +34,8 @@ pub enum Mode {
|
||||||
List,
|
List,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uumain(args: impl uucore::Args) -> i32 {
|
#[uucore_procs::gen_uumain]
|
||||||
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let args = args
|
let args = args
|
||||||
.collect_str(InvalidEncodingHandling::Ignore)
|
.collect_str(InvalidEncodingHandling::Ignore)
|
||||||
.accept_any();
|
.accept_any();
|
||||||
|
@ -66,13 +65,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
(None, Some(s)) => s.to_owned(),
|
(None, Some(s)) => s.to_owned(),
|
||||||
(None, None) => "TERM".to_owned(),
|
(None, None) => "TERM".to_owned(),
|
||||||
};
|
};
|
||||||
return kill(&sig, &pids_or_signals);
|
kill(&sig, &pids_or_signals)
|
||||||
|
}
|
||||||
|
Mode::Table => {
|
||||||
|
table();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
Mode::Table => table(),
|
|
||||||
Mode::List => list(pids_or_signals.get(0).cloned()),
|
Mode::List => list(pids_or_signals.get(0).cloned()),
|
||||||
}
|
}
|
||||||
|
|
||||||
EXIT_OK
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uu_app() -> App<'static, 'static> {
|
pub fn uu_app() -> App<'static, 'static> {
|
||||||
|
@ -139,20 +139,23 @@ fn table() {
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!()
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_signal(signal_name_or_value: &str) {
|
fn print_signal(signal_name_or_value: &str) -> UResult<()> {
|
||||||
for (value, &signal) in ALL_SIGNALS.iter().enumerate() {
|
for (value, &signal) in ALL_SIGNALS.iter().enumerate() {
|
||||||
if signal == signal_name_or_value || (format!("SIG{}", signal)) == signal_name_or_value {
|
if signal == signal_name_or_value || (format!("SIG{}", signal)) == signal_name_or_value {
|
||||||
println!("{}", value);
|
println!("{}", value);
|
||||||
exit!(EXIT_OK as i32)
|
return Ok(());
|
||||||
} else if signal_name_or_value == value.to_string() {
|
} else if signal_name_or_value == value.to_string() {
|
||||||
println!("{}", signal);
|
println!("{}", signal);
|
||||||
exit!(EXIT_OK as i32)
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
crash!(EXIT_ERR, "unknown signal name {}", signal_name_or_value)
|
Err(USimpleError::new(
|
||||||
|
1,
|
||||||
|
format!("unknown signal name {}", signal_name_or_value),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_signals() {
|
fn print_signals() {
|
||||||
|
@ -170,30 +173,41 @@ fn print_signals() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list(arg: Option<String>) {
|
fn list(arg: Option<String>) -> UResult<()> {
|
||||||
match arg {
|
match arg {
|
||||||
Some(ref x) => print_signal(x),
|
Some(ref x) => print_signal(x),
|
||||||
None => print_signals(),
|
None => {
|
||||||
};
|
print_signals();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn kill(signalname: &str, pids: &[String]) -> i32 {
|
fn kill(signalname: &str, pids: &[String]) -> UResult<()> {
|
||||||
let mut status = 0;
|
|
||||||
let optional_signal_value = uucore::signals::signal_by_name_or_value(signalname);
|
let optional_signal_value = uucore::signals::signal_by_name_or_value(signalname);
|
||||||
let signal_value = match optional_signal_value {
|
let signal_value = match optional_signal_value {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => crash!(EXIT_ERR, "unknown signal name {}", signalname),
|
None => {
|
||||||
|
return Err(USimpleError::new(
|
||||||
|
1,
|
||||||
|
format!("unknown signal name {}", signalname),
|
||||||
|
));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
for pid in pids {
|
for pid in pids {
|
||||||
match pid.parse::<usize>() {
|
match pid.parse::<usize>() {
|
||||||
Ok(x) => {
|
Ok(x) => {
|
||||||
if unsafe { libc::kill(x as pid_t, signal_value as c_int) } != 0 {
|
if unsafe { libc::kill(x as pid_t, signal_value as c_int) } != 0 {
|
||||||
show_error!("{}", Error::last_os_error());
|
show!(USimpleError::new(1, format!("{}", Error::last_os_error())));
|
||||||
status = 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => crash!(EXIT_ERR, "failed to parse argument {}: {}", pid, e),
|
Err(e) => {
|
||||||
|
return Err(USimpleError::new(
|
||||||
|
1,
|
||||||
|
format!("failed to parse argument {}: {}", pid, e),
|
||||||
|
));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
status
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,12 @@
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
|
||||||
use clap::{crate_version, App, Arg};
|
use clap::{crate_version, App, Arg};
|
||||||
|
use uucore::error::{UCustomError, UResult};
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::error::Error;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
|
use std::fmt::Display;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use std::io::{stdin, Result};
|
use std::io::{stdin, Result};
|
||||||
|
@ -22,6 +25,7 @@ use std::os::unix::fs::symlink;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::os::windows::fs::{symlink_dir, symlink_file};
|
use std::os::windows::fs::{symlink_dir, symlink_file};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use uucore::backup_control::{self, BackupMode};
|
||||||
use uucore::fs::{canonicalize, CanonicalizeMode};
|
use uucore::fs::{canonicalize, CanonicalizeMode};
|
||||||
|
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
|
@ -43,12 +47,49 @@ pub enum OverwriteMode {
|
||||||
Force,
|
Force,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Debug)]
|
||||||
pub enum BackupMode {
|
enum LnError {
|
||||||
NoBackup,
|
TargetIsDirectory(String),
|
||||||
SimpleBackup,
|
SomeLinksFailed,
|
||||||
NumberedBackup,
|
FailedToLink(String),
|
||||||
ExistingBackup,
|
MissingDestination(String),
|
||||||
|
ExtraOperand(String),
|
||||||
|
InvalidBackupMode(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for LnError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::TargetIsDirectory(s) => write!(f, "target '{}' is not a directory", s),
|
||||||
|
Self::FailedToLink(s) => write!(f, "failed to link '{}'", s),
|
||||||
|
Self::SomeLinksFailed => write!(f, "some links failed to create"),
|
||||||
|
Self::MissingDestination(s) => {
|
||||||
|
write!(f, "missing destination file operand after '{}'", s)
|
||||||
|
}
|
||||||
|
Self::ExtraOperand(s) => write!(
|
||||||
|
f,
|
||||||
|
"extra operand '{}'\nTry '{} --help' for more information.",
|
||||||
|
s,
|
||||||
|
executable!()
|
||||||
|
),
|
||||||
|
Self::InvalidBackupMode(s) => write!(f, "{}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for LnError {}
|
||||||
|
|
||||||
|
impl UCustomError for LnError {
|
||||||
|
fn code(&self) -> i32 {
|
||||||
|
match self {
|
||||||
|
Self::TargetIsDirectory(_) => 1,
|
||||||
|
Self::SomeLinksFailed => 1,
|
||||||
|
Self::FailedToLink(_) => 1,
|
||||||
|
Self::MissingDestination(_) => 1,
|
||||||
|
Self::ExtraOperand(_) => 1,
|
||||||
|
Self::InvalidBackupMode(_) => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_usage() -> String {
|
fn get_usage() -> String {
|
||||||
|
@ -78,7 +119,7 @@ fn get_long_usage() -> String {
|
||||||
static ABOUT: &str = "change file owner and group";
|
static ABOUT: &str = "change file owner and group";
|
||||||
|
|
||||||
mod options {
|
mod options {
|
||||||
pub const B: &str = "b";
|
pub const BACKUP_NO_ARG: &str = "b";
|
||||||
pub const BACKUP: &str = "backup";
|
pub const BACKUP: &str = "backup";
|
||||||
pub const FORCE: &str = "force";
|
pub const FORCE: &str = "force";
|
||||||
pub const INTERACTIVE: &str = "interactive";
|
pub const INTERACTIVE: &str = "interactive";
|
||||||
|
@ -93,13 +134,18 @@ mod options {
|
||||||
|
|
||||||
static ARG_FILES: &str = "files";
|
static ARG_FILES: &str = "files";
|
||||||
|
|
||||||
pub fn uumain(args: impl uucore::Args) -> i32 {
|
#[uucore_procs::gen_uumain]
|
||||||
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let usage = get_usage();
|
let usage = get_usage();
|
||||||
let long_usage = get_long_usage();
|
let long_usage = get_long_usage();
|
||||||
|
|
||||||
let matches = uu_app()
|
let matches = uu_app()
|
||||||
.usage(&usage[..])
|
.usage(&usage[..])
|
||||||
.after_help(&long_usage[..])
|
.after_help(&*format!(
|
||||||
|
"{}\n{}",
|
||||||
|
long_usage,
|
||||||
|
backup_control::BACKUP_CONTROL_LONG_HELP
|
||||||
|
))
|
||||||
.get_matches_from(args);
|
.get_matches_from(args);
|
||||||
|
|
||||||
/* the list of files */
|
/* the list of files */
|
||||||
|
@ -118,33 +164,24 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
OverwriteMode::NoClobber
|
OverwriteMode::NoClobber
|
||||||
};
|
};
|
||||||
|
|
||||||
let backup_mode = if matches.is_present(options::B) {
|
let backup_mode = backup_control::determine_backup_mode(
|
||||||
BackupMode::ExistingBackup
|
matches.is_present(options::BACKUP_NO_ARG),
|
||||||
} else if matches.is_present(options::BACKUP) {
|
matches.is_present(options::BACKUP),
|
||||||
match matches.value_of(options::BACKUP) {
|
matches.value_of(options::BACKUP),
|
||||||
None => BackupMode::ExistingBackup,
|
);
|
||||||
Some(mode) => match mode {
|
let backup_mode = match backup_mode {
|
||||||
"simple" | "never" => BackupMode::SimpleBackup,
|
Err(err) => {
|
||||||
"numbered" | "t" => BackupMode::NumberedBackup,
|
return Err(LnError::InvalidBackupMode(err).into());
|
||||||
"existing" | "nil" => BackupMode::ExistingBackup,
|
|
||||||
"none" | "off" => BackupMode::NoBackup,
|
|
||||||
_ => panic!(), // cannot happen as it is managed by clap
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
} else {
|
Ok(mode) => mode,
|
||||||
BackupMode::NoBackup
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let backup_suffix = if matches.is_present(options::SUFFIX) {
|
let backup_suffix = backup_control::determine_backup_suffix(matches.value_of(options::SUFFIX));
|
||||||
matches.value_of(options::SUFFIX).unwrap()
|
|
||||||
} else {
|
|
||||||
"~"
|
|
||||||
};
|
|
||||||
|
|
||||||
let settings = Settings {
|
let settings = Settings {
|
||||||
overwrite: overwrite_mode,
|
overwrite: overwrite_mode,
|
||||||
backup: backup_mode,
|
backup: backup_mode,
|
||||||
suffix: backup_suffix.to_string(),
|
suffix: backup_suffix,
|
||||||
symbolic: matches.is_present(options::SYMBOLIC),
|
symbolic: matches.is_present(options::SYMBOLIC),
|
||||||
relative: matches.is_present(options::RELATIVE),
|
relative: matches.is_present(options::RELATIVE),
|
||||||
target_dir: matches
|
target_dir: matches
|
||||||
|
@ -162,22 +199,19 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
App::new(executable!())
|
App::new(executable!())
|
||||||
.version(crate_version!())
|
.version(crate_version!())
|
||||||
.about(ABOUT)
|
.about(ABOUT)
|
||||||
.arg(Arg::with_name(options::B).short(options::B).help(
|
|
||||||
"make a backup of each file that would otherwise be overwritten or \
|
|
||||||
removed",
|
|
||||||
))
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::BACKUP)
|
Arg::with_name(options::BACKUP)
|
||||||
.long(options::BACKUP)
|
.long(options::BACKUP)
|
||||||
.help(
|
.help("make a backup of each existing destination file")
|
||||||
"make a backup of each file that would otherwise be overwritten \
|
|
||||||
or removed",
|
|
||||||
)
|
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.possible_values(&[
|
.require_equals(true)
|
||||||
"simple", "never", "numbered", "t", "existing", "nil", "none", "off",
|
.min_values(0)
|
||||||
])
|
.value_name("CONTROL"),
|
||||||
.value_name("METHOD"),
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(options::BACKUP_NO_ARG)
|
||||||
|
.short(options::BACKUP_NO_ARG)
|
||||||
|
.help("like --backup but does not accept an argument"),
|
||||||
)
|
)
|
||||||
// TODO: opts.arg(
|
// TODO: opts.arg(
|
||||||
// Arg::with_name(("d", "directory", "allow users with appropriate privileges to attempt \
|
// Arg::with_name(("d", "directory", "allow users with appropriate privileges to attempt \
|
||||||
|
@ -260,7 +294,7 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exec(files: &[PathBuf], settings: &Settings) -> i32 {
|
fn exec(files: &[PathBuf], settings: &Settings) -> UResult<()> {
|
||||||
// Handle cases where we create links in a directory first.
|
// Handle cases where we create links in a directory first.
|
||||||
if let Some(ref name) = settings.target_dir {
|
if let Some(ref name) = settings.target_dir {
|
||||||
// 4th form: a directory is specified by -t.
|
// 4th form: a directory is specified by -t.
|
||||||
|
@ -281,35 +315,22 @@ fn exec(files: &[PathBuf], settings: &Settings) -> i32 {
|
||||||
// 1st form. Now there should be only two operands, but if -T is
|
// 1st form. Now there should be only two operands, but if -T is
|
||||||
// specified we may have a wrong number of operands.
|
// specified we may have a wrong number of operands.
|
||||||
if files.len() == 1 {
|
if files.len() == 1 {
|
||||||
show_error!(
|
return Err(LnError::MissingDestination(files[0].to_string_lossy().into()).into());
|
||||||
"missing destination file operand after '{}'",
|
|
||||||
files[0].to_string_lossy()
|
|
||||||
);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
if files.len() > 2 {
|
if files.len() > 2 {
|
||||||
show_error!(
|
return Err(LnError::ExtraOperand(files[2].display().to_string()).into());
|
||||||
"extra operand '{}'\nTry '{} --help' for more information.",
|
|
||||||
files[2].display(),
|
|
||||||
executable!()
|
|
||||||
);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
assert!(!files.is_empty());
|
assert!(!files.is_empty());
|
||||||
|
|
||||||
match link(&files[0], &files[1], settings) {
|
match link(&files[0], &files[1], settings) {
|
||||||
Ok(_) => 0,
|
Ok(_) => Ok(()),
|
||||||
Err(e) => {
|
Err(e) => Err(LnError::FailedToLink(e.to_string()).into()),
|
||||||
show_error!("{}", e);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) -> i32 {
|
fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings) -> UResult<()> {
|
||||||
if !target_dir.is_dir() {
|
if !target_dir.is_dir() {
|
||||||
show_error!("target '{}' is not a directory", target_dir.display());
|
return Err(LnError::TargetIsDirectory(target_dir.display().to_string()).into());
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut all_successful = true;
|
let mut all_successful = true;
|
||||||
|
@ -368,9 +389,9 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &Path, settings: &Settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if all_successful {
|
if all_successful {
|
||||||
0
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
1
|
Err(LnError::SomeLinksFailed.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,6 @@
|
||||||
|
|
||||||
// spell-checker:ignore (ToDO) cpio svgz webm somegroup nlink rmvb xspf
|
// spell-checker:ignore (ToDO) cpio svgz webm somegroup nlink rmvb xspf
|
||||||
|
|
||||||
// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422
|
|
||||||
#![allow(clippy::nonstandard_macro_braces)]
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
|
|
@ -5,9 +5,6 @@
|
||||||
// * For the full copyright and license information, please view the LICENSE
|
// * For the full copyright and license information, please view the LICENSE
|
||||||
// * file that was distributed with this source code.
|
// * file that was distributed with this source code.
|
||||||
|
|
||||||
// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422
|
|
||||||
#![allow(clippy::nonstandard_macro_braces)]
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,6 @@
|
||||||
|
|
||||||
// spell-checker:ignore (paths) GPGHome
|
// spell-checker:ignore (paths) GPGHome
|
||||||
|
|
||||||
// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422
|
|
||||||
#![allow(clippy::nonstandard_macro_braces)]
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
|
||||||
|
|
|
@ -86,9 +86,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
|
|
||||||
let overwrite_mode = determine_overwrite_mode(&matches);
|
let overwrite_mode = determine_overwrite_mode(&matches);
|
||||||
let backup_mode = backup_control::determine_backup_mode(
|
let backup_mode = backup_control::determine_backup_mode(
|
||||||
matches.is_present(OPT_BACKUP_NO_ARG) || matches.is_present(OPT_BACKUP),
|
matches.is_present(OPT_BACKUP_NO_ARG),
|
||||||
|
matches.is_present(OPT_BACKUP),
|
||||||
matches.value_of(OPT_BACKUP),
|
matches.value_of(OPT_BACKUP),
|
||||||
);
|
);
|
||||||
|
let backup_mode = match backup_mode {
|
||||||
|
Err(err) => {
|
||||||
|
show_usage_error!("{}", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Ok(mode) => mode,
|
||||||
|
};
|
||||||
|
|
||||||
if overwrite_mode == OverwriteMode::NoClobber && backup_mode != BackupMode::NoBackup {
|
if overwrite_mode == OverwriteMode::NoClobber && backup_mode != BackupMode::NoBackup {
|
||||||
show_usage_error!("options --backup and --no-clobber are mutually exclusive");
|
show_usage_error!("options --backup and --no-clobber are mutually exclusive");
|
||||||
|
@ -135,7 +143,6 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.require_equals(true)
|
.require_equals(true)
|
||||||
.min_values(0)
|
.min_values(0)
|
||||||
.possible_values(backup_control::BACKUP_CONTROL_VALUES)
|
|
||||||
.value_name("CONTROL")
|
.value_name("CONTROL")
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
|
|
|
@ -170,7 +170,7 @@ fn check_basic(path: &[String]) -> bool {
|
||||||
fn check_extra(path: &[String]) -> bool {
|
fn check_extra(path: &[String]) -> bool {
|
||||||
// components: leading hyphens
|
// components: leading hyphens
|
||||||
for p in path {
|
for p in path {
|
||||||
if !no_leading_hyphen(p) {
|
if p.starts_with('-') {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut std::io::stderr(),
|
&mut std::io::stderr(),
|
||||||
"leading hyphen in file name component '{}'",
|
"leading hyphen in file name component '{}'",
|
||||||
|
@ -236,11 +236,6 @@ fn check_searchable(path: &str) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for a hyphen at the beginning of a path segment
|
|
||||||
fn no_leading_hyphen(path_segment: &str) -> bool {
|
|
||||||
!path_segment.starts_with('-')
|
|
||||||
}
|
|
||||||
|
|
||||||
// check whether a path segment contains only valid (read: portable) characters
|
// check whether a path segment contains only valid (read: portable) characters
|
||||||
fn check_portable_chars(path_segment: &str) -> bool {
|
fn check_portable_chars(path_segment: &str) -> bool {
|
||||||
const VALID_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
|
const VALID_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
|
||||||
|
|
|
@ -13,6 +13,8 @@ use std::env;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use uucore::error::{FromIo, UResult, USimpleError};
|
||||||
|
|
||||||
static ABOUT: &str = "Display the full filename of the current working directory.";
|
static ABOUT: &str = "Display the full filename of the current working directory.";
|
||||||
static OPT_LOGICAL: &str = "logical";
|
static OPT_LOGICAL: &str = "logical";
|
||||||
static OPT_PHYSICAL: &str = "physical";
|
static OPT_PHYSICAL: &str = "physical";
|
||||||
|
@ -36,7 +38,8 @@ fn get_usage() -> String {
|
||||||
format!("{0} [OPTION]... FILE...", executable!())
|
format!("{0} [OPTION]... FILE...", executable!())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uumain(args: impl uucore::Args) -> i32 {
|
#[uucore_procs::gen_uumain]
|
||||||
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let usage = get_usage();
|
let usage = get_usage();
|
||||||
|
|
||||||
let matches = uu_app().usage(&usage[..]).get_matches_from(args);
|
let matches = uu_app().usage(&usage[..]).get_matches_from(args);
|
||||||
|
@ -46,16 +49,20 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
if matches.is_present(OPT_LOGICAL) {
|
if matches.is_present(OPT_LOGICAL) {
|
||||||
println!("{}", logical_path.display());
|
println!("{}", logical_path.display());
|
||||||
} else {
|
} else {
|
||||||
match absolute_path(&logical_path) {
|
let physical_path = absolute_path(&logical_path)
|
||||||
Ok(physical_path) => println!("{}", physical_path.display()),
|
.map_err_context(|| "failed to get absolute path".to_string())?;
|
||||||
Err(e) => crash!(1, "failed to get absolute path {}", e),
|
println!("{}", physical_path.display());
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => crash!(1, "failed to get current directory {}", e),
|
Err(e) => {
|
||||||
|
return Err(USimpleError::new(
|
||||||
|
1,
|
||||||
|
format!("failed to get current directory {}", e),
|
||||||
|
))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
0
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uu_app() -> App<'static, 'static> {
|
pub fn uu_app() -> App<'static, 'static> {
|
||||||
|
|
|
@ -11,6 +11,8 @@ extern crate uucore;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use uucore::error::{UResult, USimpleError};
|
||||||
|
|
||||||
use clap::{crate_version, App, Arg};
|
use clap::{crate_version, App, Arg};
|
||||||
|
|
||||||
static ABOUT: &str = "Pause for NUMBER seconds.";
|
static ABOUT: &str = "Pause for NUMBER seconds.";
|
||||||
|
@ -32,17 +34,18 @@ fn get_usage() -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uumain(args: impl uucore::Args) -> i32 {
|
#[uucore_procs::gen_uumain]
|
||||||
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let usage = get_usage();
|
let usage = get_usage();
|
||||||
|
|
||||||
let matches = uu_app().usage(&usage[..]).get_matches_from(args);
|
let matches = uu_app().usage(&usage[..]).get_matches_from(args);
|
||||||
|
|
||||||
if let Some(values) = matches.values_of(options::NUMBER) {
|
if let Some(values) = matches.values_of(options::NUMBER) {
|
||||||
let numbers = values.collect();
|
let numbers = values.collect();
|
||||||
sleep(numbers);
|
return sleep(numbers);
|
||||||
}
|
}
|
||||||
|
|
||||||
0
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uu_app() -> App<'static, 'static> {
|
pub fn uu_app() -> App<'static, 'static> {
|
||||||
|
@ -61,15 +64,15 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sleep(args: Vec<&str>) {
|
fn sleep(args: Vec<&str>) -> UResult<()> {
|
||||||
let sleep_dur =
|
let sleep_dur =
|
||||||
args.iter().fold(
|
args.iter().try_fold(
|
||||||
Duration::new(0, 0),
|
Duration::new(0, 0),
|
||||||
|result, arg| match uucore::parse_time::from_str(&arg[..]) {
|
|result, arg| match uucore::parse_time::from_str(&arg[..]) {
|
||||||
Ok(m) => m + result,
|
Ok(m) => Ok(m + result),
|
||||||
Err(f) => crash!(1, "{}", f),
|
Err(f) => Err(USimpleError::new(1, f)),
|
||||||
},
|
},
|
||||||
);
|
)?;
|
||||||
|
|
||||||
thread::sleep(sleep_dur);
|
thread::sleep(sleep_dur);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,24 +9,33 @@
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
chunks::{self, Chunk, RecycledChunk},
|
chunks::{self, Chunk, RecycledChunk},
|
||||||
compare_by, open, GlobalSettings,
|
compare_by, open, GlobalSettings, SortError,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
|
ffi::OsStr,
|
||||||
io::Read,
|
io::Read,
|
||||||
iter,
|
iter,
|
||||||
sync::mpsc::{sync_channel, Receiver, SyncSender},
|
sync::mpsc::{sync_channel, Receiver, SyncSender},
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
use uucore::error::UResult;
|
||||||
|
|
||||||
/// Check if the file at `path` is ordered.
|
/// Check if the file at `path` is ordered.
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// The code we should exit with.
|
/// The code we should exit with.
|
||||||
pub fn check(path: &str, settings: &GlobalSettings) -> i32 {
|
pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> {
|
||||||
let file = open(path);
|
let max_allowed_cmp = if settings.unique {
|
||||||
|
// If `unique` is enabled, the previous line must compare _less_ to the next one.
|
||||||
|
Ordering::Less
|
||||||
|
} else {
|
||||||
|
// Otherwise, the line previous line must compare _less or equal_ to the next one.
|
||||||
|
Ordering::Equal
|
||||||
|
};
|
||||||
|
let file = open(path)?;
|
||||||
let (recycled_sender, recycled_receiver) = sync_channel(2);
|
let (recycled_sender, recycled_receiver) = sync_channel(2);
|
||||||
let (loaded_sender, loaded_receiver) = sync_channel(2);
|
let (loaded_sender, loaded_receiver) = sync_channel(2);
|
||||||
thread::spawn({
|
thread::spawn({
|
||||||
|
@ -34,7 +43,13 @@ pub fn check(path: &str, settings: &GlobalSettings) -> i32 {
|
||||||
move || reader(file, recycled_receiver, loaded_sender, &settings)
|
move || reader(file, recycled_receiver, loaded_sender, &settings)
|
||||||
});
|
});
|
||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
let _ = recycled_sender.send(RecycledChunk::new(100 * 1024));
|
let _ = recycled_sender.send(RecycledChunk::new(if settings.buffer_size < 100 * 1024 {
|
||||||
|
// when the buffer size is smaller than 100KiB we choose it instead of the default.
|
||||||
|
// this improves testability.
|
||||||
|
settings.buffer_size
|
||||||
|
} else {
|
||||||
|
100 * 1024
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut prev_chunk: Option<Chunk> = None;
|
let mut prev_chunk: Option<Chunk> = None;
|
||||||
|
@ -53,30 +68,35 @@ pub fn check(path: &str, settings: &GlobalSettings) -> i32 {
|
||||||
settings,
|
settings,
|
||||||
prev_chunk.line_data(),
|
prev_chunk.line_data(),
|
||||||
chunk.line_data(),
|
chunk.line_data(),
|
||||||
) == Ordering::Greater
|
) > max_allowed_cmp
|
||||||
{
|
{
|
||||||
if !settings.check_silent {
|
return Err(SortError::Disorder {
|
||||||
println!("sort: {}:{}: disorder: {}", path, line_idx, new_first.line);
|
file: path.to_owned(),
|
||||||
|
line_number: line_idx,
|
||||||
|
line: new_first.line.to_owned(),
|
||||||
|
silent: settings.check_silent,
|
||||||
}
|
}
|
||||||
return 1;
|
.into());
|
||||||
}
|
}
|
||||||
let _ = recycled_sender.send(prev_chunk.recycle());
|
let _ = recycled_sender.send(prev_chunk.recycle());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (a, b) in chunk.lines().iter().tuple_windows() {
|
for (a, b) in chunk.lines().iter().tuple_windows() {
|
||||||
line_idx += 1;
|
line_idx += 1;
|
||||||
if compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) == Ordering::Greater
|
if compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) > max_allowed_cmp {
|
||||||
{
|
return Err(SortError::Disorder {
|
||||||
if !settings.check_silent {
|
file: path.to_owned(),
|
||||||
println!("sort: {}:{}: disorder: {}", path, line_idx, b.line);
|
line_number: line_idx,
|
||||||
|
line: b.line.to_owned(),
|
||||||
|
silent: settings.check_silent,
|
||||||
}
|
}
|
||||||
return 1;
|
.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prev_chunk = Some(chunk);
|
prev_chunk = Some(chunk);
|
||||||
}
|
}
|
||||||
0
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The function running on the reader thread.
|
/// The function running on the reader thread.
|
||||||
|
@ -85,7 +105,7 @@ fn reader(
|
||||||
receiver: Receiver<RecycledChunk>,
|
receiver: Receiver<RecycledChunk>,
|
||||||
sender: SyncSender<Chunk>,
|
sender: SyncSender<Chunk>,
|
||||||
settings: &GlobalSettings,
|
settings: &GlobalSettings,
|
||||||
) {
|
) -> UResult<()> {
|
||||||
let mut carry_over = vec![];
|
let mut carry_over = vec![];
|
||||||
for recycled_chunk in receiver.iter() {
|
for recycled_chunk in receiver.iter() {
|
||||||
let should_continue = chunks::read(
|
let should_continue = chunks::read(
|
||||||
|
@ -101,9 +121,10 @@ fn reader(
|
||||||
b'\n'
|
b'\n'
|
||||||
},
|
},
|
||||||
settings,
|
settings,
|
||||||
);
|
)?;
|
||||||
if !should_continue {
|
if !should_continue {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,9 @@ use std::{
|
||||||
|
|
||||||
use memchr::memchr_iter;
|
use memchr::memchr_iter;
|
||||||
use ouroboros::self_referencing;
|
use ouroboros::self_referencing;
|
||||||
|
use uucore::error::{UResult, USimpleError};
|
||||||
|
|
||||||
use crate::{numeric_str_cmp::NumInfo, GeneralF64ParseResult, GlobalSettings, Line};
|
use crate::{numeric_str_cmp::NumInfo, GeneralF64ParseResult, GlobalSettings, Line, SortError};
|
||||||
|
|
||||||
/// The chunk that is passed around between threads.
|
/// The chunk that is passed around between threads.
|
||||||
/// `lines` consist of slices into `buffer`.
|
/// `lines` consist of slices into `buffer`.
|
||||||
|
@ -137,10 +138,10 @@ pub fn read<T: Read>(
|
||||||
max_buffer_size: Option<usize>,
|
max_buffer_size: Option<usize>,
|
||||||
carry_over: &mut Vec<u8>,
|
carry_over: &mut Vec<u8>,
|
||||||
file: &mut T,
|
file: &mut T,
|
||||||
next_files: &mut impl Iterator<Item = T>,
|
next_files: &mut impl Iterator<Item = UResult<T>>,
|
||||||
separator: u8,
|
separator: u8,
|
||||||
settings: &GlobalSettings,
|
settings: &GlobalSettings,
|
||||||
) -> bool {
|
) -> UResult<bool> {
|
||||||
let RecycledChunk {
|
let RecycledChunk {
|
||||||
lines,
|
lines,
|
||||||
selections,
|
selections,
|
||||||
|
@ -159,12 +160,12 @@ pub fn read<T: Read>(
|
||||||
max_buffer_size,
|
max_buffer_size,
|
||||||
carry_over.len(),
|
carry_over.len(),
|
||||||
separator,
|
separator,
|
||||||
);
|
)?;
|
||||||
carry_over.clear();
|
carry_over.clear();
|
||||||
carry_over.extend_from_slice(&buffer[read..]);
|
carry_over.extend_from_slice(&buffer[read..]);
|
||||||
|
|
||||||
if read != 0 {
|
if read != 0 {
|
||||||
let payload = Chunk::new(buffer, |buffer| {
|
let payload: UResult<Chunk> = Chunk::try_new(buffer, |buffer| {
|
||||||
let selections = unsafe {
|
let selections = unsafe {
|
||||||
// SAFETY: It is safe to transmute to an empty vector of selections with shorter lifetime.
|
// SAFETY: It is safe to transmute to an empty vector of selections with shorter lifetime.
|
||||||
// It was only temporarily transmuted to a Vec<Line<'static>> to make recycling possible.
|
// It was only temporarily transmuted to a Vec<Line<'static>> to make recycling possible.
|
||||||
|
@ -175,18 +176,19 @@ pub fn read<T: Read>(
|
||||||
// because it was only temporarily transmuted to a Vec<Line<'static>> to make recycling possible.
|
// because it was only temporarily transmuted to a Vec<Line<'static>> to make recycling possible.
|
||||||
std::mem::transmute::<Vec<Line<'static>>, Vec<Line<'_>>>(lines)
|
std::mem::transmute::<Vec<Line<'static>>, Vec<Line<'_>>>(lines)
|
||||||
};
|
};
|
||||||
let read = crash_if_err!(1, std::str::from_utf8(&buffer[..read]));
|
let read = std::str::from_utf8(&buffer[..read])
|
||||||
|
.map_err(|error| SortError::Uft8Error { error })?;
|
||||||
let mut line_data = LineData {
|
let mut line_data = LineData {
|
||||||
selections,
|
selections,
|
||||||
num_infos,
|
num_infos,
|
||||||
parsed_floats,
|
parsed_floats,
|
||||||
};
|
};
|
||||||
parse_lines(read, &mut lines, &mut line_data, separator, settings);
|
parse_lines(read, &mut lines, &mut line_data, separator, settings);
|
||||||
ChunkContents { lines, line_data }
|
Ok(ChunkContents { lines, line_data })
|
||||||
});
|
});
|
||||||
sender.send(payload).unwrap();
|
sender.send(payload?).unwrap();
|
||||||
}
|
}
|
||||||
should_continue
|
Ok(should_continue)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Split `read` into `Line`s, and add them to `lines`.
|
/// Split `read` into `Line`s, and add them to `lines`.
|
||||||
|
@ -242,12 +244,12 @@ fn parse_lines<'a>(
|
||||||
/// * Whether this function should be called again.
|
/// * Whether this function should be called again.
|
||||||
fn read_to_buffer<T: Read>(
|
fn read_to_buffer<T: Read>(
|
||||||
file: &mut T,
|
file: &mut T,
|
||||||
next_files: &mut impl Iterator<Item = T>,
|
next_files: &mut impl Iterator<Item = UResult<T>>,
|
||||||
buffer: &mut Vec<u8>,
|
buffer: &mut Vec<u8>,
|
||||||
max_buffer_size: Option<usize>,
|
max_buffer_size: Option<usize>,
|
||||||
start_offset: usize,
|
start_offset: usize,
|
||||||
separator: u8,
|
separator: u8,
|
||||||
) -> (usize, bool) {
|
) -> UResult<(usize, bool)> {
|
||||||
let mut read_target = &mut buffer[start_offset..];
|
let mut read_target = &mut buffer[start_offset..];
|
||||||
let mut last_file_target_size = read_target.len();
|
let mut last_file_target_size = read_target.len();
|
||||||
loop {
|
loop {
|
||||||
|
@ -274,7 +276,7 @@ fn read_to_buffer<T: Read>(
|
||||||
// We read enough lines.
|
// We read enough lines.
|
||||||
let end = last_line_end.unwrap();
|
let end = last_line_end.unwrap();
|
||||||
// We want to include the separator here, because it shouldn't be carried over.
|
// We want to include the separator here, because it shouldn't be carried over.
|
||||||
return (end + 1, true);
|
return Ok((end + 1, true));
|
||||||
} else {
|
} else {
|
||||||
// We need to read more lines
|
// We need to read more lines
|
||||||
let len = buffer.len();
|
let len = buffer.len();
|
||||||
|
@ -299,11 +301,11 @@ fn read_to_buffer<T: Read>(
|
||||||
if let Some(next_file) = next_files.next() {
|
if let Some(next_file) = next_files.next() {
|
||||||
// There is another file.
|
// There is another file.
|
||||||
last_file_target_size = leftover_len;
|
last_file_target_size = leftover_len;
|
||||||
*file = next_file;
|
*file = next_file?;
|
||||||
} else {
|
} else {
|
||||||
// This was the last file.
|
// This was the last file.
|
||||||
let read_len = buffer.len() - leftover_len;
|
let read_len = buffer.len() - leftover_len;
|
||||||
return (read_len, false);
|
return Ok((read_len, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -313,7 +315,7 @@ fn read_to_buffer<T: Read>(
|
||||||
Err(e) if e.kind() == ErrorKind::Interrupted => {
|
Err(e) if e.kind() == ErrorKind::Interrupted => {
|
||||||
// retry
|
// retry
|
||||||
}
|
}
|
||||||
Err(e) => crash!(1, "{}", e),
|
Err(e) => return Err(USimpleError::new(2, e.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,15 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use uucore::error::UResult;
|
||||||
|
|
||||||
use crate::chunks::RecycledChunk;
|
use crate::chunks::RecycledChunk;
|
||||||
use crate::merge::ClosedTmpFile;
|
use crate::merge::ClosedTmpFile;
|
||||||
use crate::merge::WriteableCompressedTmpFile;
|
use crate::merge::WriteableCompressedTmpFile;
|
||||||
use crate::merge::WriteablePlainTmpFile;
|
use crate::merge::WriteablePlainTmpFile;
|
||||||
use crate::merge::WriteableTmpFile;
|
use crate::merge::WriteableTmpFile;
|
||||||
|
use crate::Output;
|
||||||
|
use crate::SortError;
|
||||||
use crate::{
|
use crate::{
|
||||||
chunks::{self, Chunk},
|
chunks::{self, Chunk},
|
||||||
compare_by, merge, sort_by, GlobalSettings,
|
compare_by, merge, sort_by, GlobalSettings,
|
||||||
|
@ -38,7 +41,11 @@ use tempfile::TempDir;
|
||||||
const START_BUFFER_SIZE: usize = 8_000;
|
const START_BUFFER_SIZE: usize = 8_000;
|
||||||
|
|
||||||
/// Sort files by using auxiliary files for storing intermediate chunks (if needed), and output the result.
|
/// Sort files by using auxiliary files for storing intermediate chunks (if needed), and output the result.
|
||||||
pub fn ext_sort(files: &mut impl Iterator<Item = Box<dyn Read + Send>>, settings: &GlobalSettings) {
|
pub fn ext_sort(
|
||||||
|
files: &mut impl Iterator<Item = UResult<Box<dyn Read + Send>>>,
|
||||||
|
settings: &GlobalSettings,
|
||||||
|
output: Output,
|
||||||
|
) -> UResult<()> {
|
||||||
let (sorted_sender, sorted_receiver) = std::sync::mpsc::sync_channel(1);
|
let (sorted_sender, sorted_receiver) = std::sync::mpsc::sync_channel(1);
|
||||||
let (recycled_sender, recycled_receiver) = std::sync::mpsc::sync_channel(1);
|
let (recycled_sender, recycled_receiver) = std::sync::mpsc::sync_channel(1);
|
||||||
thread::spawn({
|
thread::spawn({
|
||||||
|
@ -51,23 +58,29 @@ pub fn ext_sort(files: &mut impl Iterator<Item = Box<dyn Read + Send>>, settings
|
||||||
settings,
|
settings,
|
||||||
sorted_receiver,
|
sorted_receiver,
|
||||||
recycled_sender,
|
recycled_sender,
|
||||||
);
|
output,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
reader_writer::<_, WriteablePlainTmpFile>(
|
reader_writer::<_, WriteablePlainTmpFile>(
|
||||||
files,
|
files,
|
||||||
settings,
|
settings,
|
||||||
sorted_receiver,
|
sorted_receiver,
|
||||||
recycled_sender,
|
recycled_sender,
|
||||||
);
|
output,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reader_writer<F: Iterator<Item = Box<dyn Read + Send>>, Tmp: WriteableTmpFile + 'static>(
|
fn reader_writer<
|
||||||
|
F: Iterator<Item = UResult<Box<dyn Read + Send>>>,
|
||||||
|
Tmp: WriteableTmpFile + 'static,
|
||||||
|
>(
|
||||||
files: F,
|
files: F,
|
||||||
settings: &GlobalSettings,
|
settings: &GlobalSettings,
|
||||||
receiver: Receiver<Chunk>,
|
receiver: Receiver<Chunk>,
|
||||||
sender: SyncSender<Chunk>,
|
sender: SyncSender<Chunk>,
|
||||||
) {
|
output: Output,
|
||||||
|
) -> UResult<()> {
|
||||||
let separator = if settings.zero_terminated {
|
let separator = if settings.zero_terminated {
|
||||||
b'\0'
|
b'\0'
|
||||||
} else {
|
} else {
|
||||||
|
@ -81,22 +94,20 @@ fn reader_writer<F: Iterator<Item = Box<dyn Read + Send>>, Tmp: WriteableTmpFile
|
||||||
files,
|
files,
|
||||||
&settings.tmp_dir,
|
&settings.tmp_dir,
|
||||||
separator,
|
separator,
|
||||||
// Heuristically chosen: Dividing by 10 seems to keep our memory usage roughly
|
|
||||||
// around settings.buffer_size as a whole.
|
|
||||||
buffer_size,
|
buffer_size,
|
||||||
settings,
|
settings,
|
||||||
receiver,
|
receiver,
|
||||||
sender,
|
sender,
|
||||||
);
|
)?;
|
||||||
match read_result {
|
match read_result {
|
||||||
ReadResult::WroteChunksToFile { tmp_files, tmp_dir } => {
|
ReadResult::WroteChunksToFile { tmp_files, tmp_dir } => {
|
||||||
let tmp_dir_size = tmp_files.len();
|
let tmp_dir_size = tmp_files.len();
|
||||||
let mut merger = merge::merge_with_file_limit::<_, _, Tmp>(
|
let merger = merge::merge_with_file_limit::<_, _, Tmp>(
|
||||||
tmp_files.into_iter().map(|c| c.reopen()),
|
tmp_files.into_iter().map(|c| c.reopen()),
|
||||||
settings,
|
settings,
|
||||||
Some((tmp_dir, tmp_dir_size)),
|
Some((tmp_dir, tmp_dir_size)),
|
||||||
);
|
)?;
|
||||||
merger.write_all(settings);
|
merger.write_all(settings, output)?;
|
||||||
}
|
}
|
||||||
ReadResult::SortedSingleChunk(chunk) => {
|
ReadResult::SortedSingleChunk(chunk) => {
|
||||||
if settings.unique {
|
if settings.unique {
|
||||||
|
@ -106,9 +117,10 @@ fn reader_writer<F: Iterator<Item = Box<dyn Read + Send>>, Tmp: WriteableTmpFile
|
||||||
== Ordering::Equal
|
== Ordering::Equal
|
||||||
}),
|
}),
|
||||||
settings,
|
settings,
|
||||||
|
output,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
print_sorted(chunk.lines().iter(), settings);
|
print_sorted(chunk.lines().iter(), settings, output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ReadResult::SortedTwoChunks([a, b]) => {
|
ReadResult::SortedTwoChunks([a, b]) => {
|
||||||
|
@ -128,15 +140,17 @@ fn reader_writer<F: Iterator<Item = Box<dyn Read + Send>>, Tmp: WriteableTmpFile
|
||||||
})
|
})
|
||||||
.map(|(line, _)| line),
|
.map(|(line, _)| line),
|
||||||
settings,
|
settings,
|
||||||
|
output,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
print_sorted(merged_iter.map(|(line, _)| line), settings);
|
print_sorted(merged_iter.map(|(line, _)| line), settings, output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ReadResult::EmptyInput => {
|
ReadResult::EmptyInput => {
|
||||||
// don't output anything
|
// don't output anything
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The function that is executed on the sorter thread.
|
/// The function that is executed on the sorter thread.
|
||||||
|
@ -145,7 +159,11 @@ fn sorter(receiver: Receiver<Chunk>, sender: SyncSender<Chunk>, settings: Global
|
||||||
payload.with_contents_mut(|contents| {
|
payload.with_contents_mut(|contents| {
|
||||||
sort_by(&mut contents.lines, &settings, &contents.line_data)
|
sort_by(&mut contents.lines, &settings, &contents.line_data)
|
||||||
});
|
});
|
||||||
sender.send(payload).unwrap();
|
if sender.send(payload).is_err() {
|
||||||
|
// The receiver has gone away, likely because the other thread hit an error.
|
||||||
|
// We stop silently because the actual error is printed by the other thread.
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,15 +183,15 @@ enum ReadResult<I: WriteableTmpFile> {
|
||||||
}
|
}
|
||||||
/// The function that is executed on the reader/writer thread.
|
/// The function that is executed on the reader/writer thread.
|
||||||
fn read_write_loop<I: WriteableTmpFile>(
|
fn read_write_loop<I: WriteableTmpFile>(
|
||||||
mut files: impl Iterator<Item = Box<dyn Read + Send>>,
|
mut files: impl Iterator<Item = UResult<Box<dyn Read + Send>>>,
|
||||||
tmp_dir_parent: &Path,
|
tmp_dir_parent: &Path,
|
||||||
separator: u8,
|
separator: u8,
|
||||||
buffer_size: usize,
|
buffer_size: usize,
|
||||||
settings: &GlobalSettings,
|
settings: &GlobalSettings,
|
||||||
receiver: Receiver<Chunk>,
|
receiver: Receiver<Chunk>,
|
||||||
sender: SyncSender<Chunk>,
|
sender: SyncSender<Chunk>,
|
||||||
) -> ReadResult<I> {
|
) -> UResult<ReadResult<I>> {
|
||||||
let mut file = files.next().unwrap();
|
let mut file = files.next().unwrap()?;
|
||||||
|
|
||||||
let mut carry_over = vec![];
|
let mut carry_over = vec![];
|
||||||
// kick things off with two reads
|
// kick things off with two reads
|
||||||
|
@ -191,14 +209,14 @@ fn read_write_loop<I: WriteableTmpFile>(
|
||||||
&mut files,
|
&mut files,
|
||||||
separator,
|
separator,
|
||||||
settings,
|
settings,
|
||||||
);
|
)?;
|
||||||
|
|
||||||
if !should_continue {
|
if !should_continue {
|
||||||
drop(sender);
|
drop(sender);
|
||||||
// We have already read the whole input. Since we are in our first two reads,
|
// We have already read the whole input. Since we are in our first two reads,
|
||||||
// this means that we can fit the whole input into memory. Bypass writing below and
|
// this means that we can fit the whole input into memory. Bypass writing below and
|
||||||
// handle this case in a more straightforward way.
|
// handle this case in a more straightforward way.
|
||||||
return if let Ok(first_chunk) = receiver.recv() {
|
return Ok(if let Ok(first_chunk) = receiver.recv() {
|
||||||
if let Ok(second_chunk) = receiver.recv() {
|
if let Ok(second_chunk) = receiver.recv() {
|
||||||
ReadResult::SortedTwoChunks([first_chunk, second_chunk])
|
ReadResult::SortedTwoChunks([first_chunk, second_chunk])
|
||||||
} else {
|
} else {
|
||||||
|
@ -206,16 +224,14 @@ fn read_write_loop<I: WriteableTmpFile>(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ReadResult::EmptyInput
|
ReadResult::EmptyInput
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tmp_dir = crash_if_err!(
|
let tmp_dir = tempfile::Builder::new()
|
||||||
1,
|
.prefix("uutils_sort")
|
||||||
tempfile::Builder::new()
|
.tempdir_in(tmp_dir_parent)
|
||||||
.prefix("uutils_sort")
|
.map_err(|_| SortError::TmpDirCreationFailed)?;
|
||||||
.tempdir_in(tmp_dir_parent)
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut sender_option = Some(sender);
|
let mut sender_option = Some(sender);
|
||||||
let mut file_number = 0;
|
let mut file_number = 0;
|
||||||
|
@ -224,7 +240,7 @@ fn read_write_loop<I: WriteableTmpFile>(
|
||||||
let mut chunk = match receiver.recv() {
|
let mut chunk = match receiver.recv() {
|
||||||
Ok(it) => it,
|
Ok(it) => it,
|
||||||
_ => {
|
_ => {
|
||||||
return ReadResult::WroteChunksToFile { tmp_files, tmp_dir };
|
return Ok(ReadResult::WroteChunksToFile { tmp_files, tmp_dir });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -233,7 +249,7 @@ fn read_write_loop<I: WriteableTmpFile>(
|
||||||
tmp_dir.path().join(file_number.to_string()),
|
tmp_dir.path().join(file_number.to_string()),
|
||||||
settings.compress_prog.as_deref(),
|
settings.compress_prog.as_deref(),
|
||||||
separator,
|
separator,
|
||||||
);
|
)?;
|
||||||
tmp_files.push(tmp_file);
|
tmp_files.push(tmp_file);
|
||||||
|
|
||||||
file_number += 1;
|
file_number += 1;
|
||||||
|
@ -250,7 +266,7 @@ fn read_write_loop<I: WriteableTmpFile>(
|
||||||
&mut files,
|
&mut files,
|
||||||
separator,
|
separator,
|
||||||
settings,
|
settings,
|
||||||
);
|
)?;
|
||||||
if !should_continue {
|
if !should_continue {
|
||||||
sender_option = None;
|
sender_option = None;
|
||||||
}
|
}
|
||||||
|
@ -265,8 +281,8 @@ fn write<I: WriteableTmpFile>(
|
||||||
file: PathBuf,
|
file: PathBuf,
|
||||||
compress_prog: Option<&str>,
|
compress_prog: Option<&str>,
|
||||||
separator: u8,
|
separator: u8,
|
||||||
) -> I::Closed {
|
) -> UResult<I::Closed> {
|
||||||
let mut tmp_file = I::create(file, compress_prog);
|
let mut tmp_file = I::create(file, compress_prog)?;
|
||||||
write_lines(chunk.lines(), tmp_file.as_write(), separator);
|
write_lines(chunk.lines(), tmp_file.as_write(), separator);
|
||||||
tmp_file.finished_writing()
|
tmp_file.finished_writing()
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,44 +9,85 @@
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
|
ffi::OsString,
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::{BufWriter, Read, Write},
|
io::{BufWriter, Read, Write},
|
||||||
iter,
|
iter,
|
||||||
path::PathBuf,
|
path::{Path, PathBuf},
|
||||||
process::{Child, ChildStdin, ChildStdout, Command, Stdio},
|
process::{Child, ChildStdin, ChildStdout, Command, Stdio},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::mpsc::{channel, sync_channel, Receiver, Sender, SyncSender},
|
sync::mpsc::{channel, sync_channel, Receiver, Sender, SyncSender},
|
||||||
thread,
|
thread::{self, JoinHandle},
|
||||||
};
|
};
|
||||||
|
|
||||||
use compare::Compare;
|
use compare::Compare;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
use uucore::error::UResult;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
chunks::{self, Chunk, RecycledChunk},
|
chunks::{self, Chunk, RecycledChunk},
|
||||||
compare_by, GlobalSettings,
|
compare_by, open, GlobalSettings, Output, SortError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// If the output file occurs in the input files as well, copy the contents of the output file
|
||||||
|
/// and replace its occurrences in the inputs with that copy.
|
||||||
|
fn replace_output_file_in_input_files(
|
||||||
|
files: &mut [OsString],
|
||||||
|
settings: &GlobalSettings,
|
||||||
|
output: Option<&str>,
|
||||||
|
) -> UResult<Option<(TempDir, usize)>> {
|
||||||
|
let mut copy: Option<(TempDir, PathBuf)> = None;
|
||||||
|
if let Some(Ok(output_path)) = output.map(|path| Path::new(path).canonicalize()) {
|
||||||
|
for file in files {
|
||||||
|
if let Ok(file_path) = Path::new(file).canonicalize() {
|
||||||
|
if file_path == output_path {
|
||||||
|
if let Some((_dir, copy)) = © {
|
||||||
|
*file = copy.clone().into_os_string();
|
||||||
|
} else {
|
||||||
|
let tmp_dir = tempfile::Builder::new()
|
||||||
|
.prefix("uutils_sort")
|
||||||
|
.tempdir_in(&settings.tmp_dir)
|
||||||
|
.map_err(|_| SortError::TmpDirCreationFailed)?;
|
||||||
|
let copy_path = tmp_dir.path().join("0");
|
||||||
|
std::fs::copy(file_path, ©_path)
|
||||||
|
.map_err(|error| SortError::OpenTmpFileFailed { error })?;
|
||||||
|
*file = copy_path.clone().into_os_string();
|
||||||
|
copy = Some((tmp_dir, copy_path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if we created a TempDir its size must be one.
|
||||||
|
Ok(copy.map(|(dir, _copy)| (dir, 1)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Merge pre-sorted `Box<dyn Read>`s.
|
/// Merge pre-sorted `Box<dyn Read>`s.
|
||||||
///
|
///
|
||||||
/// If `settings.merge_batch_size` is greater than the length of `files`, intermediate files will be used.
|
/// If `settings.merge_batch_size` is greater than the length of `files`, intermediate files will be used.
|
||||||
/// If `settings.compress_prog` is `Some`, intermediate files will be compressed with it.
|
/// If `settings.compress_prog` is `Some`, intermediate files will be compressed with it.
|
||||||
pub fn merge<Files: ExactSizeIterator<Item = Box<dyn Read + Send>>>(
|
pub fn merge<'a>(
|
||||||
files: Files,
|
files: &mut [OsString],
|
||||||
settings: &GlobalSettings,
|
settings: &'a GlobalSettings,
|
||||||
) -> FileMerger {
|
output: Option<&str>,
|
||||||
|
) -> UResult<FileMerger<'a>> {
|
||||||
|
let tmp_dir = replace_output_file_in_input_files(files, settings, output)?;
|
||||||
if settings.compress_prog.is_none() {
|
if settings.compress_prog.is_none() {
|
||||||
merge_with_file_limit::<_, _, WriteablePlainTmpFile>(
|
merge_with_file_limit::<_, _, WriteablePlainTmpFile>(
|
||||||
files.map(|file| PlainMergeInput { inner: file }),
|
files
|
||||||
|
.iter()
|
||||||
|
.map(|file| open(file).map(|file| PlainMergeInput { inner: file })),
|
||||||
settings,
|
settings,
|
||||||
None,
|
tmp_dir,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
merge_with_file_limit::<_, _, WriteableCompressedTmpFile>(
|
merge_with_file_limit::<_, _, WriteableCompressedTmpFile>(
|
||||||
files.map(|file| PlainMergeInput { inner: file }),
|
files
|
||||||
|
.iter()
|
||||||
|
.map(|file| open(file).map(|file| PlainMergeInput { inner: file })),
|
||||||
settings,
|
settings,
|
||||||
None,
|
tmp_dir,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,24 +95,25 @@ pub fn merge<Files: ExactSizeIterator<Item = Box<dyn Read + Send>>>(
|
||||||
// Merge already sorted `MergeInput`s.
|
// Merge already sorted `MergeInput`s.
|
||||||
pub fn merge_with_file_limit<
|
pub fn merge_with_file_limit<
|
||||||
M: MergeInput + 'static,
|
M: MergeInput + 'static,
|
||||||
F: ExactSizeIterator<Item = M>,
|
F: ExactSizeIterator<Item = UResult<M>>,
|
||||||
Tmp: WriteableTmpFile + 'static,
|
Tmp: WriteableTmpFile + 'static,
|
||||||
>(
|
>(
|
||||||
files: F,
|
files: F,
|
||||||
settings: &GlobalSettings,
|
settings: &GlobalSettings,
|
||||||
tmp_dir: Option<(TempDir, usize)>,
|
tmp_dir: Option<(TempDir, usize)>,
|
||||||
) -> FileMerger {
|
) -> UResult<FileMerger> {
|
||||||
if files.len() > settings.merge_batch_size {
|
if files.len() > settings.merge_batch_size {
|
||||||
// If we did not get a tmp_dir, create one.
|
// If we did not get a tmp_dir, create one.
|
||||||
let (tmp_dir, mut tmp_dir_size) = tmp_dir.unwrap_or_else(|| {
|
let (tmp_dir, mut tmp_dir_size) = match tmp_dir {
|
||||||
(
|
Some(x) => x,
|
||||||
|
None => (
|
||||||
tempfile::Builder::new()
|
tempfile::Builder::new()
|
||||||
.prefix("uutils_sort")
|
.prefix("uutils_sort")
|
||||||
.tempdir_in(&settings.tmp_dir)
|
.tempdir_in(&settings.tmp_dir)
|
||||||
.unwrap(),
|
.map_err(|_| SortError::TmpDirCreationFailed)?,
|
||||||
0,
|
0,
|
||||||
)
|
),
|
||||||
});
|
};
|
||||||
let mut remaining_files = files.len();
|
let mut remaining_files = files.len();
|
||||||
let batches = files.chunks(settings.merge_batch_size);
|
let batches = files.chunks(settings.merge_batch_size);
|
||||||
let mut batches = batches.into_iter();
|
let mut batches = batches.into_iter();
|
||||||
|
@ -79,14 +121,14 @@ pub fn merge_with_file_limit<
|
||||||
while remaining_files != 0 {
|
while remaining_files != 0 {
|
||||||
// Work around the fact that `Chunks` is not an `ExactSizeIterator`.
|
// Work around the fact that `Chunks` is not an `ExactSizeIterator`.
|
||||||
remaining_files = remaining_files.saturating_sub(settings.merge_batch_size);
|
remaining_files = remaining_files.saturating_sub(settings.merge_batch_size);
|
||||||
let mut merger = merge_without_limit(batches.next().unwrap(), settings);
|
let merger = merge_without_limit(batches.next().unwrap(), settings)?;
|
||||||
let mut tmp_file = Tmp::create(
|
let mut tmp_file = Tmp::create(
|
||||||
tmp_dir.path().join(tmp_dir_size.to_string()),
|
tmp_dir.path().join(tmp_dir_size.to_string()),
|
||||||
settings.compress_prog.as_deref(),
|
settings.compress_prog.as_deref(),
|
||||||
);
|
)?;
|
||||||
tmp_dir_size += 1;
|
tmp_dir_size += 1;
|
||||||
merger.write_all_to(settings, tmp_file.as_write());
|
merger.write_all_to(settings, tmp_file.as_write())?;
|
||||||
temporary_files.push(tmp_file.finished_writing());
|
temporary_files.push(tmp_file.finished_writing()?);
|
||||||
}
|
}
|
||||||
assert!(batches.next().is_none());
|
assert!(batches.next().is_none());
|
||||||
merge_with_file_limit::<_, _, Tmp>(
|
merge_with_file_limit::<_, _, Tmp>(
|
||||||
|
@ -94,7 +136,7 @@ pub fn merge_with_file_limit<
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(Box::new(|c: Tmp::Closed| c.reopen())
|
.map(Box::new(|c: Tmp::Closed| c.reopen())
|
||||||
as Box<
|
as Box<
|
||||||
dyn FnMut(Tmp::Closed) -> <Tmp::Closed as ClosedTmpFile>::Reopened,
|
dyn FnMut(Tmp::Closed) -> UResult<<Tmp::Closed as ClosedTmpFile>::Reopened>,
|
||||||
>),
|
>),
|
||||||
settings,
|
settings,
|
||||||
Some((tmp_dir, tmp_dir_size)),
|
Some((tmp_dir, tmp_dir_size)),
|
||||||
|
@ -108,10 +150,10 @@ pub fn merge_with_file_limit<
|
||||||
///
|
///
|
||||||
/// It is the responsibility of the caller to ensure that `files` yields only
|
/// It is the responsibility of the caller to ensure that `files` yields only
|
||||||
/// as many files as we are allowed to open concurrently.
|
/// as many files as we are allowed to open concurrently.
|
||||||
fn merge_without_limit<M: MergeInput + 'static, F: Iterator<Item = M>>(
|
fn merge_without_limit<M: MergeInput + 'static, F: Iterator<Item = UResult<M>>>(
|
||||||
files: F,
|
files: F,
|
||||||
settings: &GlobalSettings,
|
settings: &GlobalSettings,
|
||||||
) -> FileMerger {
|
) -> UResult<FileMerger> {
|
||||||
let (request_sender, request_receiver) = channel();
|
let (request_sender, request_receiver) = channel();
|
||||||
let mut reader_files = Vec::with_capacity(files.size_hint().0);
|
let mut reader_files = Vec::with_capacity(files.size_hint().0);
|
||||||
let mut loaded_receivers = Vec::with_capacity(files.size_hint().0);
|
let mut loaded_receivers = Vec::with_capacity(files.size_hint().0);
|
||||||
|
@ -119,7 +161,7 @@ fn merge_without_limit<M: MergeInput + 'static, F: Iterator<Item = M>>(
|
||||||
let (sender, receiver) = sync_channel(2);
|
let (sender, receiver) = sync_channel(2);
|
||||||
loaded_receivers.push(receiver);
|
loaded_receivers.push(receiver);
|
||||||
reader_files.push(Some(ReaderFile {
|
reader_files.push(Some(ReaderFile {
|
||||||
file,
|
file: file?,
|
||||||
sender,
|
sender,
|
||||||
carry_over: vec![],
|
carry_over: vec![],
|
||||||
}));
|
}));
|
||||||
|
@ -136,7 +178,7 @@ fn merge_without_limit<M: MergeInput + 'static, F: Iterator<Item = M>>(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
thread::spawn({
|
let reader_join_handle = thread::spawn({
|
||||||
let settings = settings.clone();
|
let settings = settings.clone();
|
||||||
move || {
|
move || {
|
||||||
reader(
|
reader(
|
||||||
|
@ -155,22 +197,25 @@ fn merge_without_limit<M: MergeInput + 'static, F: Iterator<Item = M>>(
|
||||||
let mut mergeable_files = vec![];
|
let mut mergeable_files = vec![];
|
||||||
|
|
||||||
for (file_number, receiver) in loaded_receivers.into_iter().enumerate() {
|
for (file_number, receiver) in loaded_receivers.into_iter().enumerate() {
|
||||||
mergeable_files.push(MergeableFile {
|
if let Ok(chunk) = receiver.recv() {
|
||||||
current_chunk: Rc::new(receiver.recv().unwrap()),
|
mergeable_files.push(MergeableFile {
|
||||||
file_number,
|
current_chunk: Rc::new(chunk),
|
||||||
line_idx: 0,
|
file_number,
|
||||||
receiver,
|
line_idx: 0,
|
||||||
})
|
receiver,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileMerger {
|
Ok(FileMerger {
|
||||||
heap: binary_heap_plus::BinaryHeap::from_vec_cmp(
|
heap: binary_heap_plus::BinaryHeap::from_vec_cmp(
|
||||||
mergeable_files,
|
mergeable_files,
|
||||||
FileComparator { settings },
|
FileComparator { settings },
|
||||||
),
|
),
|
||||||
request_sender,
|
request_sender,
|
||||||
prev: None,
|
prev: None,
|
||||||
}
|
reader_join_handle,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
/// The struct on the reader thread representing an input file
|
/// The struct on the reader thread representing an input file
|
||||||
struct ReaderFile<M: MergeInput> {
|
struct ReaderFile<M: MergeInput> {
|
||||||
|
@ -185,7 +230,7 @@ fn reader(
|
||||||
files: &mut [Option<ReaderFile<impl MergeInput>>],
|
files: &mut [Option<ReaderFile<impl MergeInput>>],
|
||||||
settings: &GlobalSettings,
|
settings: &GlobalSettings,
|
||||||
separator: u8,
|
separator: u8,
|
||||||
) {
|
) -> UResult<()> {
|
||||||
for (file_idx, recycled_chunk) in recycled_receiver.iter() {
|
for (file_idx, recycled_chunk) in recycled_receiver.iter() {
|
||||||
if let Some(ReaderFile {
|
if let Some(ReaderFile {
|
||||||
file,
|
file,
|
||||||
|
@ -202,15 +247,16 @@ fn reader(
|
||||||
&mut iter::empty(),
|
&mut iter::empty(),
|
||||||
separator,
|
separator,
|
||||||
settings,
|
settings,
|
||||||
);
|
)?;
|
||||||
if !should_continue {
|
if !should_continue {
|
||||||
// Remove the file from the list by replacing it with `None`.
|
// Remove the file from the list by replacing it with `None`.
|
||||||
let ReaderFile { file, .. } = files[file_idx].take().unwrap();
|
let ReaderFile { file, .. } = files[file_idx].take().unwrap();
|
||||||
// Depending on the kind of the `MergeInput`, this may delete the file:
|
// Depending on the kind of the `MergeInput`, this may delete the file:
|
||||||
file.finished_reading();
|
file.finished_reading()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
/// The struct on the main thread representing an input file
|
/// The struct on the main thread representing an input file
|
||||||
pub struct MergeableFile {
|
pub struct MergeableFile {
|
||||||
|
@ -234,17 +280,20 @@ pub struct FileMerger<'a> {
|
||||||
heap: binary_heap_plus::BinaryHeap<MergeableFile, FileComparator<'a>>,
|
heap: binary_heap_plus::BinaryHeap<MergeableFile, FileComparator<'a>>,
|
||||||
request_sender: Sender<(usize, RecycledChunk)>,
|
request_sender: Sender<(usize, RecycledChunk)>,
|
||||||
prev: Option<PreviousLine>,
|
prev: Option<PreviousLine>,
|
||||||
|
reader_join_handle: JoinHandle<UResult<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FileMerger<'a> {
|
impl<'a> FileMerger<'a> {
|
||||||
/// Write the merged contents to the output file.
|
/// Write the merged contents to the output file.
|
||||||
pub fn write_all(&mut self, settings: &GlobalSettings) {
|
pub fn write_all(self, settings: &GlobalSettings, output: Output) -> UResult<()> {
|
||||||
let mut out = settings.out_writer();
|
let mut out = output.into_write();
|
||||||
self.write_all_to(settings, &mut out);
|
self.write_all_to(settings, &mut out)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_all_to(&mut self, settings: &GlobalSettings, out: &mut impl Write) {
|
pub fn write_all_to(mut self, settings: &GlobalSettings, out: &mut impl Write) -> UResult<()> {
|
||||||
while self.write_next(settings, out) {}
|
while self.write_next(settings, out) {}
|
||||||
|
drop(self.request_sender);
|
||||||
|
self.reader_join_handle.join().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_next(&mut self, settings: &GlobalSettings, out: &mut impl Write) -> bool {
|
fn write_next(&mut self, settings: &GlobalSettings, out: &mut impl Write) -> bool {
|
||||||
|
@ -328,36 +377,41 @@ impl<'a> Compare<MergeableFile> for FileComparator<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the child to exit and check its exit code.
|
// Wait for the child to exit and check its exit code.
|
||||||
fn assert_child_success(mut child: Child, program: &str) {
|
fn check_child_success(mut child: Child, program: &str) -> UResult<()> {
|
||||||
if !matches!(
|
if !matches!(
|
||||||
child.wait().map(|e| e.code()),
|
child.wait().map(|e| e.code()),
|
||||||
Ok(Some(0)) | Ok(None) | Err(_)
|
Ok(Some(0)) | Ok(None) | Err(_)
|
||||||
) {
|
) {
|
||||||
crash!(2, "'{}' terminated abnormally", program)
|
Err(SortError::CompressProgTerminatedAbnormally {
|
||||||
|
prog: program.to_owned(),
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A temporary file that can be written to.
|
/// A temporary file that can be written to.
|
||||||
pub trait WriteableTmpFile {
|
pub trait WriteableTmpFile: Sized {
|
||||||
type Closed: ClosedTmpFile;
|
type Closed: ClosedTmpFile;
|
||||||
type InnerWrite: Write;
|
type InnerWrite: Write;
|
||||||
fn create(path: PathBuf, compress_prog: Option<&str>) -> Self;
|
fn create(path: PathBuf, compress_prog: Option<&str>) -> UResult<Self>;
|
||||||
/// Closes the temporary file.
|
/// Closes the temporary file.
|
||||||
fn finished_writing(self) -> Self::Closed;
|
fn finished_writing(self) -> UResult<Self::Closed>;
|
||||||
fn as_write(&mut self) -> &mut Self::InnerWrite;
|
fn as_write(&mut self) -> &mut Self::InnerWrite;
|
||||||
}
|
}
|
||||||
/// A temporary file that is (temporarily) closed, but can be reopened.
|
/// A temporary file that is (temporarily) closed, but can be reopened.
|
||||||
pub trait ClosedTmpFile {
|
pub trait ClosedTmpFile {
|
||||||
type Reopened: MergeInput;
|
type Reopened: MergeInput;
|
||||||
/// Reopens the temporary file.
|
/// Reopens the temporary file.
|
||||||
fn reopen(self) -> Self::Reopened;
|
fn reopen(self) -> UResult<Self::Reopened>;
|
||||||
}
|
}
|
||||||
/// A pre-sorted input for merging.
|
/// A pre-sorted input for merging.
|
||||||
pub trait MergeInput: Send {
|
pub trait MergeInput: Send {
|
||||||
type InnerRead: Read;
|
type InnerRead: Read;
|
||||||
/// Cleans this `MergeInput` up.
|
/// Cleans this `MergeInput` up.
|
||||||
/// Implementations may delete the backing file.
|
/// Implementations may delete the backing file.
|
||||||
fn finished_reading(self);
|
fn finished_reading(self) -> UResult<()>;
|
||||||
fn as_read(&mut self) -> &mut Self::InnerRead;
|
fn as_read(&mut self) -> &mut Self::InnerRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,15 +430,17 @@ impl WriteableTmpFile for WriteablePlainTmpFile {
|
||||||
type Closed = ClosedPlainTmpFile;
|
type Closed = ClosedPlainTmpFile;
|
||||||
type InnerWrite = BufWriter<File>;
|
type InnerWrite = BufWriter<File>;
|
||||||
|
|
||||||
fn create(path: PathBuf, _: Option<&str>) -> Self {
|
fn create(path: PathBuf, _: Option<&str>) -> UResult<Self> {
|
||||||
WriteablePlainTmpFile {
|
Ok(WriteablePlainTmpFile {
|
||||||
file: BufWriter::new(File::create(&path).unwrap()),
|
file: BufWriter::new(
|
||||||
|
File::create(&path).map_err(|error| SortError::OpenTmpFileFailed { error })?,
|
||||||
|
),
|
||||||
path,
|
path,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finished_writing(self) -> Self::Closed {
|
fn finished_writing(self) -> UResult<Self::Closed> {
|
||||||
ClosedPlainTmpFile { path: self.path }
|
Ok(ClosedPlainTmpFile { path: self.path })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_write(&mut self) -> &mut Self::InnerWrite {
|
fn as_write(&mut self) -> &mut Self::InnerWrite {
|
||||||
|
@ -393,18 +449,22 @@ impl WriteableTmpFile for WriteablePlainTmpFile {
|
||||||
}
|
}
|
||||||
impl ClosedTmpFile for ClosedPlainTmpFile {
|
impl ClosedTmpFile for ClosedPlainTmpFile {
|
||||||
type Reopened = PlainTmpMergeInput;
|
type Reopened = PlainTmpMergeInput;
|
||||||
fn reopen(self) -> Self::Reopened {
|
fn reopen(self) -> UResult<Self::Reopened> {
|
||||||
PlainTmpMergeInput {
|
Ok(PlainTmpMergeInput {
|
||||||
file: File::open(&self.path).unwrap(),
|
file: File::open(&self.path).map_err(|error| SortError::OpenTmpFileFailed { error })?,
|
||||||
path: self.path,
|
path: self.path,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl MergeInput for PlainTmpMergeInput {
|
impl MergeInput for PlainTmpMergeInput {
|
||||||
type InnerRead = File;
|
type InnerRead = File;
|
||||||
|
|
||||||
fn finished_reading(self) {
|
fn finished_reading(self) -> UResult<()> {
|
||||||
fs::remove_file(self.path).ok();
|
// we ignore failures to delete the temporary file,
|
||||||
|
// because there is a race at the end of the execution and the whole
|
||||||
|
// temporary directory might already be gone.
|
||||||
|
let _ = fs::remove_file(self.path);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_read(&mut self) -> &mut Self::InnerRead {
|
fn as_read(&mut self) -> &mut Self::InnerRead {
|
||||||
|
@ -432,35 +492,33 @@ impl WriteableTmpFile for WriteableCompressedTmpFile {
|
||||||
type Closed = ClosedCompressedTmpFile;
|
type Closed = ClosedCompressedTmpFile;
|
||||||
type InnerWrite = BufWriter<ChildStdin>;
|
type InnerWrite = BufWriter<ChildStdin>;
|
||||||
|
|
||||||
fn create(path: PathBuf, compress_prog: Option<&str>) -> Self {
|
fn create(path: PathBuf, compress_prog: Option<&str>) -> UResult<Self> {
|
||||||
let compress_prog = compress_prog.unwrap();
|
let compress_prog = compress_prog.unwrap();
|
||||||
let mut command = Command::new(compress_prog);
|
let mut command = Command::new(compress_prog);
|
||||||
command
|
let tmp_file =
|
||||||
.stdin(Stdio::piped())
|
File::create(&path).map_err(|error| SortError::OpenTmpFileFailed { error })?;
|
||||||
.stdout(File::create(&path).unwrap());
|
command.stdin(Stdio::piped()).stdout(tmp_file);
|
||||||
let mut child = crash_if_err!(
|
let mut child = command
|
||||||
2,
|
.spawn()
|
||||||
command.spawn().map_err(|err| format!(
|
.map_err(|err| SortError::CompressProgExecutionFailed {
|
||||||
"couldn't execute compress program: errno {}",
|
code: err.raw_os_error().unwrap(),
|
||||||
err.raw_os_error().unwrap()
|
})?;
|
||||||
))
|
|
||||||
);
|
|
||||||
let child_stdin = child.stdin.take().unwrap();
|
let child_stdin = child.stdin.take().unwrap();
|
||||||
WriteableCompressedTmpFile {
|
Ok(WriteableCompressedTmpFile {
|
||||||
path,
|
path,
|
||||||
compress_prog: compress_prog.to_owned(),
|
compress_prog: compress_prog.to_owned(),
|
||||||
child,
|
child,
|
||||||
child_stdin: BufWriter::new(child_stdin),
|
child_stdin: BufWriter::new(child_stdin),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finished_writing(self) -> Self::Closed {
|
fn finished_writing(self) -> UResult<Self::Closed> {
|
||||||
drop(self.child_stdin);
|
drop(self.child_stdin);
|
||||||
assert_child_success(self.child, &self.compress_prog);
|
check_child_success(self.child, &self.compress_prog)?;
|
||||||
ClosedCompressedTmpFile {
|
Ok(ClosedCompressedTmpFile {
|
||||||
path: self.path,
|
path: self.path,
|
||||||
compress_prog: self.compress_prog,
|
compress_prog: self.compress_prog,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_write(&mut self) -> &mut Self::InnerWrite {
|
fn as_write(&mut self) -> &mut Self::InnerWrite {
|
||||||
|
@ -470,33 +528,32 @@ impl WriteableTmpFile for WriteableCompressedTmpFile {
|
||||||
impl ClosedTmpFile for ClosedCompressedTmpFile {
|
impl ClosedTmpFile for ClosedCompressedTmpFile {
|
||||||
type Reopened = CompressedTmpMergeInput;
|
type Reopened = CompressedTmpMergeInput;
|
||||||
|
|
||||||
fn reopen(self) -> Self::Reopened {
|
fn reopen(self) -> UResult<Self::Reopened> {
|
||||||
let mut command = Command::new(&self.compress_prog);
|
let mut command = Command::new(&self.compress_prog);
|
||||||
let file = File::open(&self.path).unwrap();
|
let file = File::open(&self.path).unwrap();
|
||||||
command.stdin(file).stdout(Stdio::piped()).arg("-d");
|
command.stdin(file).stdout(Stdio::piped()).arg("-d");
|
||||||
let mut child = crash_if_err!(
|
let mut child = command
|
||||||
2,
|
.spawn()
|
||||||
command.spawn().map_err(|err| format!(
|
.map_err(|err| SortError::CompressProgExecutionFailed {
|
||||||
"couldn't execute compress program: errno {}",
|
code: err.raw_os_error().unwrap(),
|
||||||
err.raw_os_error().unwrap()
|
})?;
|
||||||
))
|
|
||||||
);
|
|
||||||
let child_stdout = child.stdout.take().unwrap();
|
let child_stdout = child.stdout.take().unwrap();
|
||||||
CompressedTmpMergeInput {
|
Ok(CompressedTmpMergeInput {
|
||||||
path: self.path,
|
path: self.path,
|
||||||
compress_prog: self.compress_prog,
|
compress_prog: self.compress_prog,
|
||||||
child,
|
child,
|
||||||
child_stdout,
|
child_stdout,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl MergeInput for CompressedTmpMergeInput {
|
impl MergeInput for CompressedTmpMergeInput {
|
||||||
type InnerRead = ChildStdout;
|
type InnerRead = ChildStdout;
|
||||||
|
|
||||||
fn finished_reading(self) {
|
fn finished_reading(self) -> UResult<()> {
|
||||||
drop(self.child_stdout);
|
drop(self.child_stdout);
|
||||||
assert_child_success(self.child, &self.compress_prog);
|
check_child_success(self.child, &self.compress_prog)?;
|
||||||
fs::remove_file(self.path).ok();
|
let _ = fs::remove_file(self.path);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_read(&mut self) -> &mut Self::InnerRead {
|
fn as_read(&mut self) -> &mut Self::InnerRead {
|
||||||
|
@ -509,7 +566,9 @@ pub struct PlainMergeInput<R: Read + Send> {
|
||||||
}
|
}
|
||||||
impl<R: Read + Send> MergeInput for PlainMergeInput<R> {
|
impl<R: Read + Send> MergeInput for PlainMergeInput<R> {
|
||||||
type InnerRead = R;
|
type InnerRead = R;
|
||||||
fn finished_reading(self) {}
|
fn finished_reading(self) -> UResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
fn as_read(&mut self) -> &mut Self::InnerRead {
|
fn as_read(&mut self) -> &mut Self::InnerRead {
|
||||||
&mut self.inner
|
&mut self.inner
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,19 +29,22 @@ use custom_str_cmp::custom_str_cmp;
|
||||||
use ext_sort::ext_sort;
|
use ext_sort::ext_sort;
|
||||||
use fnv::FnvHasher;
|
use fnv::FnvHasher;
|
||||||
use numeric_str_cmp::{human_numeric_str_cmp, numeric_str_cmp, NumInfo, NumInfoParseSettings};
|
use numeric_str_cmp::{human_numeric_str_cmp, numeric_str_cmp, NumInfo, NumInfoParseSettings};
|
||||||
use rand::distributions::Alphanumeric;
|
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsStr;
|
use std::error::Error;
|
||||||
use std::fs::File;
|
use std::ffi::{OsStr, OsString};
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::fs::{File, OpenOptions};
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write};
|
use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write};
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::str::Utf8Error;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
use uucore::error::{set_exit_code, UCustomError, UResult, USimpleError, UUsageError};
|
||||||
use uucore::parse_size::{parse_size, ParseSizeError};
|
use uucore::parse_size::{parse_size, ParseSizeError};
|
||||||
use uucore::version_cmp::version_cmp;
|
use uucore::version_cmp::version_cmp;
|
||||||
use uucore::InvalidEncodingHandling;
|
use uucore::InvalidEncodingHandling;
|
||||||
|
@ -121,6 +124,111 @@ const POSITIVE: char = '+';
|
||||||
// available memory into consideration, instead of relying on this constant only.
|
// available memory into consideration, instead of relying on this constant only.
|
||||||
const DEFAULT_BUF_SIZE: usize = 1_000_000_000; // 1 GB
|
const DEFAULT_BUF_SIZE: usize = 1_000_000_000; // 1 GB
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum SortError {
|
||||||
|
Disorder {
|
||||||
|
file: OsString,
|
||||||
|
line_number: usize,
|
||||||
|
line: String,
|
||||||
|
silent: bool,
|
||||||
|
},
|
||||||
|
OpenFailed {
|
||||||
|
path: String,
|
||||||
|
error: std::io::Error,
|
||||||
|
},
|
||||||
|
ReadFailed {
|
||||||
|
path: String,
|
||||||
|
error: std::io::Error,
|
||||||
|
},
|
||||||
|
ParseKeyError {
|
||||||
|
key: String,
|
||||||
|
msg: String,
|
||||||
|
},
|
||||||
|
OpenTmpFileFailed {
|
||||||
|
error: std::io::Error,
|
||||||
|
},
|
||||||
|
CompressProgExecutionFailed {
|
||||||
|
code: i32,
|
||||||
|
},
|
||||||
|
CompressProgTerminatedAbnormally {
|
||||||
|
prog: String,
|
||||||
|
},
|
||||||
|
TmpDirCreationFailed,
|
||||||
|
Uft8Error {
|
||||||
|
error: Utf8Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for SortError {}
|
||||||
|
|
||||||
|
impl UCustomError for SortError {
|
||||||
|
fn code(&self) -> i32 {
|
||||||
|
match self {
|
||||||
|
SortError::Disorder { .. } => 1,
|
||||||
|
_ => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SortError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
SortError::Disorder {
|
||||||
|
file,
|
||||||
|
line_number,
|
||||||
|
line,
|
||||||
|
silent,
|
||||||
|
} => {
|
||||||
|
if !silent {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}:{}: disorder: {}",
|
||||||
|
file.to_string_lossy(),
|
||||||
|
line_number,
|
||||||
|
line
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SortError::OpenFailed { path, error } => write!(
|
||||||
|
f,
|
||||||
|
"open failed: {}: {}",
|
||||||
|
path,
|
||||||
|
strip_errno(&error.to_string())
|
||||||
|
),
|
||||||
|
SortError::ParseKeyError { key, msg } => {
|
||||||
|
write!(f, "failed to parse key `{}`: {}", key, msg)
|
||||||
|
}
|
||||||
|
SortError::ReadFailed { path, error } => write!(
|
||||||
|
f,
|
||||||
|
"cannot read: {}: {}",
|
||||||
|
path,
|
||||||
|
strip_errno(&error.to_string())
|
||||||
|
),
|
||||||
|
SortError::OpenTmpFileFailed { error } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"failed to open temporary file: {}",
|
||||||
|
strip_errno(&error.to_string())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SortError::CompressProgExecutionFailed { code } => {
|
||||||
|
write!(f, "couldn't execute compress program: errno {}", code)
|
||||||
|
}
|
||||||
|
SortError::CompressProgTerminatedAbnormally { prog } => {
|
||||||
|
write!(f, "'{}' terminated abnormally", prog)
|
||||||
|
}
|
||||||
|
SortError::TmpDirCreationFailed => write!(f, "could not create temporary directory"),
|
||||||
|
SortError::Uft8Error { error } => write!(f, "{}", error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy, Debug)]
|
#[derive(Eq, Ord, PartialEq, PartialOrd, Clone, Copy, Debug)]
|
||||||
enum SortMode {
|
enum SortMode {
|
||||||
Numeric,
|
Numeric,
|
||||||
|
@ -146,6 +254,49 @@ impl SortMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Output {
|
||||||
|
file: Option<(String, File)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Output {
|
||||||
|
fn new(name: Option<&str>) -> UResult<Self> {
|
||||||
|
let file = if let Some(name) = name {
|
||||||
|
// This is different from `File::create()` because we don't truncate the output yet.
|
||||||
|
// This allows using the output file as an input file.
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.open(name)
|
||||||
|
.map_err(|e| SortError::OpenFailed {
|
||||||
|
path: name.to_owned(),
|
||||||
|
error: e,
|
||||||
|
})?;
|
||||||
|
Some((name.to_owned(), file))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Ok(Self { file })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_write(self) -> BufWriter<Box<dyn Write>> {
|
||||||
|
BufWriter::new(match self.file {
|
||||||
|
Some((_name, file)) => {
|
||||||
|
// truncate the file
|
||||||
|
let _ = file.set_len(0);
|
||||||
|
Box::new(file)
|
||||||
|
}
|
||||||
|
None => Box::new(stdout()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_output_name(&self) -> Option<&str> {
|
||||||
|
match &self.file {
|
||||||
|
Some((name, _file)) => Some(name),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GlobalSettings {
|
pub struct GlobalSettings {
|
||||||
mode: SortMode,
|
mode: SortMode,
|
||||||
|
@ -156,12 +307,11 @@ pub struct GlobalSettings {
|
||||||
ignore_non_printing: bool,
|
ignore_non_printing: bool,
|
||||||
merge: bool,
|
merge: bool,
|
||||||
reverse: bool,
|
reverse: bool,
|
||||||
output_file: Option<String>,
|
|
||||||
stable: bool,
|
stable: bool,
|
||||||
unique: bool,
|
unique: bool,
|
||||||
check: bool,
|
check: bool,
|
||||||
check_silent: bool,
|
check_silent: bool,
|
||||||
salt: String,
|
salt: Option<[u8; 16]>,
|
||||||
selectors: Vec<FieldSelector>,
|
selectors: Vec<FieldSelector>,
|
||||||
separator: Option<char>,
|
separator: Option<char>,
|
||||||
threads: String,
|
threads: String,
|
||||||
|
@ -209,19 +359,6 @@ impl GlobalSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn out_writer(&self) -> BufWriter<Box<dyn Write>> {
|
|
||||||
match self.output_file {
|
|
||||||
Some(ref filename) => match File::create(Path::new(&filename)) {
|
|
||||||
Ok(f) => BufWriter::new(Box::new(f) as Box<dyn Write>),
|
|
||||||
Err(e) => {
|
|
||||||
show_error!("{0}: {1}", filename, e.to_string());
|
|
||||||
panic!("Could not open output file");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => BufWriter::new(Box::new(stdout()) as Box<dyn Write>),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Precompute some data needed for sorting.
|
/// Precompute some data needed for sorting.
|
||||||
/// This function **must** be called before starting to sort, and `GlobalSettings` may not be altered
|
/// This function **must** be called before starting to sort, and `GlobalSettings` may not be altered
|
||||||
/// afterwards.
|
/// afterwards.
|
||||||
|
@ -253,12 +390,11 @@ impl Default for GlobalSettings {
|
||||||
ignore_non_printing: false,
|
ignore_non_printing: false,
|
||||||
merge: false,
|
merge: false,
|
||||||
reverse: false,
|
reverse: false,
|
||||||
output_file: None,
|
|
||||||
stable: false,
|
stable: false,
|
||||||
unique: false,
|
unique: false,
|
||||||
check: false,
|
check: false,
|
||||||
check_silent: false,
|
check_silent: false,
|
||||||
salt: String::new(),
|
salt: None,
|
||||||
selectors: vec![],
|
selectors: vec![],
|
||||||
separator: None,
|
separator: None,
|
||||||
threads: String::new(),
|
threads: String::new(),
|
||||||
|
@ -697,33 +833,37 @@ impl FieldSelector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(key: &str, global_settings: &GlobalSettings) -> Self {
|
fn parse(key: &str, global_settings: &GlobalSettings) -> UResult<Self> {
|
||||||
let mut from_to = key.split(',');
|
let mut from_to = key.split(',');
|
||||||
let (from, from_options) = Self::split_key_options(from_to.next().unwrap());
|
let (from, from_options) = Self::split_key_options(from_to.next().unwrap());
|
||||||
let to = from_to.next().map(|to| Self::split_key_options(to));
|
let to = from_to.next().map(|to| Self::split_key_options(to));
|
||||||
let options_are_empty = from_options.is_empty() && matches!(to, None | Some((_, "")));
|
let options_are_empty = from_options.is_empty() && matches!(to, None | Some((_, "")));
|
||||||
crash_if_err!(
|
|
||||||
2,
|
if options_are_empty {
|
||||||
if options_are_empty {
|
// Inherit the global settings if there are no options attached to this key.
|
||||||
// Inherit the global settings if there are no options attached to this key.
|
(|| {
|
||||||
(|| {
|
// This would be ideal for a try block, I think. In the meantime this closure allows
|
||||||
// This would be ideal for a try block, I think. In the meantime this closure allows
|
// to use the `?` operator here.
|
||||||
// to use the `?` operator here.
|
Self::new(
|
||||||
Self::new(
|
KeyPosition::new(from, 1, global_settings.ignore_leading_blanks)?,
|
||||||
KeyPosition::new(from, 1, global_settings.ignore_leading_blanks)?,
|
to.map(|(to, _)| {
|
||||||
to.map(|(to, _)| {
|
KeyPosition::new(to, 0, global_settings.ignore_leading_blanks)
|
||||||
KeyPosition::new(to, 0, global_settings.ignore_leading_blanks)
|
})
|
||||||
})
|
.transpose()?,
|
||||||
.transpose()?,
|
KeySettings::from(global_settings),
|
||||||
KeySettings::from(global_settings),
|
)
|
||||||
)
|
})()
|
||||||
})()
|
} else {
|
||||||
} else {
|
// Do not inherit from `global_settings`, as there are options attached to this key.
|
||||||
// Do not inherit from `global_settings`, as there are options attached to this key.
|
Self::parse_with_options((from, from_options), to)
|
||||||
Self::parse_with_options((from, from_options), to)
|
}
|
||||||
|
.map_err(|msg| {
|
||||||
|
SortError::ParseKeyError {
|
||||||
|
key: key.to_owned(),
|
||||||
|
msg,
|
||||||
}
|
}
|
||||||
.map_err(|e| format!("failed to parse key `{}`: {}", key, e))
|
.into()
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_with_options(
|
fn parse_with_options(
|
||||||
|
@ -916,9 +1056,7 @@ impl FieldSelector {
|
||||||
|
|
||||||
fn get_usage() -> String {
|
fn get_usage() -> String {
|
||||||
format!(
|
format!(
|
||||||
"{0}
|
"{0} [OPTION]... [FILE]...
|
||||||
Usage:
|
|
||||||
{0} [OPTION]... [FILE]...
|
|
||||||
Write the sorted concatenation of all FILE(s) to standard output.
|
Write the sorted concatenation of all FILE(s) to standard output.
|
||||||
Mandatory arguments for long options are mandatory for short options too.
|
Mandatory arguments for long options are mandatory for short options too.
|
||||||
With no FILE, or when FILE is -, read standard input.",
|
With no FILE, or when FILE is -, read standard input.",
|
||||||
|
@ -937,41 +1075,57 @@ fn make_sort_mode_arg<'a, 'b>(mode: &'a str, short: &'b str, help: &'b str) -> A
|
||||||
arg
|
arg
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uumain(args: impl uucore::Args) -> i32 {
|
#[uucore_procs::gen_uumain]
|
||||||
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let args = args
|
let args = args
|
||||||
.collect_str(InvalidEncodingHandling::Ignore)
|
.collect_str(InvalidEncodingHandling::Ignore)
|
||||||
.accept_any();
|
.accept_any();
|
||||||
let usage = get_usage();
|
let usage = get_usage();
|
||||||
let mut settings: GlobalSettings = Default::default();
|
let mut settings: GlobalSettings = Default::default();
|
||||||
|
|
||||||
let matches = uu_app().usage(&usage[..]).get_matches_from(args);
|
let matches = match uu_app().usage(&usage[..]).get_matches_from_safe(args) {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
// not all clap "Errors" are because of a failure to parse arguments.
|
||||||
|
// "--version" also causes an Error to be returned, but we should not print to stderr
|
||||||
|
// nor return with a non-zero exit code in this case (we should print to stdout and return 0).
|
||||||
|
// This logic is similar to the code in clap, but we return 2 as the exit code in case of real failure
|
||||||
|
// (clap returns 1).
|
||||||
|
if e.use_stderr() {
|
||||||
|
eprintln!("{}", e.message);
|
||||||
|
set_exit_code(2);
|
||||||
|
} else {
|
||||||
|
println!("{}", e.message);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
settings.debug = matches.is_present(options::DEBUG);
|
settings.debug = matches.is_present(options::DEBUG);
|
||||||
|
|
||||||
// check whether user specified a zero terminated list of files for input, otherwise read files from args
|
// check whether user specified a zero terminated list of files for input, otherwise read files from args
|
||||||
let mut files: Vec<String> = if matches.is_present(options::FILES0_FROM) {
|
let mut files: Vec<OsString> = if matches.is_present(options::FILES0_FROM) {
|
||||||
let files0_from: Vec<String> = matches
|
let files0_from: Vec<OsString> = matches
|
||||||
.values_of(options::FILES0_FROM)
|
.values_of_os(options::FILES0_FROM)
|
||||||
.map(|v| v.map(ToString::to_string).collect())
|
.map(|v| v.map(ToOwned::to_owned).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
for path in &files0_from {
|
for path in &files0_from {
|
||||||
let reader = open(path.as_str());
|
let reader = open(&path)?;
|
||||||
let buf_reader = BufReader::new(reader);
|
let buf_reader = BufReader::new(reader);
|
||||||
for line in buf_reader.split(b'\0').flatten() {
|
for line in buf_reader.split(b'\0').flatten() {
|
||||||
files.push(
|
files.push(OsString::from(
|
||||||
std::str::from_utf8(&line)
|
std::str::from_utf8(&line)
|
||||||
.expect("Could not parse string from zero terminated input.")
|
.expect("Could not parse string from zero terminated input."),
|
||||||
.to_string(),
|
));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
files
|
files
|
||||||
} else {
|
} else {
|
||||||
matches
|
matches
|
||||||
.values_of(options::FILES)
|
.values_of_os(options::FILES)
|
||||||
.map(|v| v.map(ToString::to_string).collect())
|
.map(|v| v.map(ToOwned::to_owned).collect())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -998,7 +1152,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
} else if matches.is_present(options::modes::RANDOM)
|
} else if matches.is_present(options::modes::RANDOM)
|
||||||
|| matches.value_of(options::modes::SORT) == Some("random")
|
|| matches.value_of(options::modes::SORT) == Some("random")
|
||||||
{
|
{
|
||||||
settings.salt = get_rand_string();
|
settings.salt = Some(get_rand_string());
|
||||||
SortMode::Random
|
SortMode::Random
|
||||||
} else {
|
} else {
|
||||||
SortMode::Default
|
SortMode::Default
|
||||||
|
@ -1015,12 +1169,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
env::set_var("RAYON_NUM_THREADS", &settings.threads);
|
env::set_var("RAYON_NUM_THREADS", &settings.threads);
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.buffer_size = matches
|
settings.buffer_size =
|
||||||
.value_of(options::BUF_SIZE)
|
matches
|
||||||
.map_or(DEFAULT_BUF_SIZE, |s| {
|
.value_of(options::BUF_SIZE)
|
||||||
GlobalSettings::parse_byte_count(s)
|
.map_or(Ok(DEFAULT_BUF_SIZE), |s| {
|
||||||
.unwrap_or_else(|e| crash!(2, "{}", format_error_message(e, s, options::BUF_SIZE)))
|
GlobalSettings::parse_byte_count(s).map_err(|e| {
|
||||||
});
|
USimpleError::new(2, format_error_message(e, s, options::BUF_SIZE))
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
settings.tmp_dir = matches
|
settings.tmp_dir = matches
|
||||||
.value_of(options::TMP_DIR)
|
.value_of(options::TMP_DIR)
|
||||||
|
@ -1030,9 +1186,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
settings.compress_prog = matches.value_of(options::COMPRESS_PROG).map(String::from);
|
settings.compress_prog = matches.value_of(options::COMPRESS_PROG).map(String::from);
|
||||||
|
|
||||||
if let Some(n_merge) = matches.value_of(options::BATCH_SIZE) {
|
if let Some(n_merge) = matches.value_of(options::BATCH_SIZE) {
|
||||||
settings.merge_batch_size = n_merge
|
settings.merge_batch_size = n_merge.parse().map_err(|_| {
|
||||||
.parse()
|
UUsageError::new(2, format!("invalid --batch-size argument '{}'", n_merge))
|
||||||
.unwrap_or_else(|_| crash!(2, "invalid --batch-size argument '{}'", n_merge));
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.zero_terminated = matches.is_present(options::ZERO_TERMINATED);
|
settings.zero_terminated = matches.is_present(options::ZERO_TERMINATED);
|
||||||
|
@ -1053,32 +1209,45 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
|
|
||||||
settings.ignore_leading_blanks = matches.is_present(options::IGNORE_LEADING_BLANKS);
|
settings.ignore_leading_blanks = matches.is_present(options::IGNORE_LEADING_BLANKS);
|
||||||
|
|
||||||
settings.output_file = matches.value_of(options::OUTPUT).map(String::from);
|
|
||||||
settings.reverse = matches.is_present(options::REVERSE);
|
settings.reverse = matches.is_present(options::REVERSE);
|
||||||
settings.stable = matches.is_present(options::STABLE);
|
settings.stable = matches.is_present(options::STABLE);
|
||||||
settings.unique = matches.is_present(options::UNIQUE);
|
settings.unique = matches.is_present(options::UNIQUE);
|
||||||
|
|
||||||
if files.is_empty() {
|
if files.is_empty() {
|
||||||
/* if no file, default to stdin */
|
/* if no file, default to stdin */
|
||||||
files.push("-".to_owned());
|
files.push("-".to_string().into());
|
||||||
} else if settings.check && files.len() != 1 {
|
} else if settings.check && files.len() != 1 {
|
||||||
crash!(1, "extra operand `{}' not allowed with -c", files[1])
|
return Err(UUsageError::new(
|
||||||
|
2,
|
||||||
|
format!(
|
||||||
|
"extra operand `{}' not allowed with -c",
|
||||||
|
files[1].to_string_lossy()
|
||||||
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(arg) = matches.args.get(options::SEPARATOR) {
|
if let Some(arg) = matches.args.get(options::SEPARATOR) {
|
||||||
let separator = arg.vals[0].to_string_lossy();
|
let separator = arg.vals[0].to_string_lossy();
|
||||||
let separator = separator;
|
let mut separator = separator.as_ref();
|
||||||
|
if separator == "\\0" {
|
||||||
|
separator = "\0";
|
||||||
|
}
|
||||||
if separator.len() != 1 {
|
if separator.len() != 1 {
|
||||||
crash!(1, "separator must be exactly one character long");
|
return Err(UUsageError::new(
|
||||||
|
2,
|
||||||
|
"separator must be exactly one character long".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
settings.separator = Some(separator.chars().next().unwrap())
|
settings.separator = Some(separator.chars().next().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(values) = matches.values_of(options::KEY) {
|
if let Some(values) = matches.values_of(options::KEY) {
|
||||||
for value in values {
|
for value in values {
|
||||||
settings
|
let selector = FieldSelector::parse(value, &settings)?;
|
||||||
.selectors
|
if selector.settings.mode == SortMode::Random && settings.salt.is_none() {
|
||||||
.push(FieldSelector::parse(value, &settings));
|
settings.salt = Some(get_rand_string());
|
||||||
|
}
|
||||||
|
settings.selectors.push(selector);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1099,9 +1268,19 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify that we can open all input files.
|
||||||
|
// It is the correct behavior to close all files afterwards,
|
||||||
|
// and to reopen them at a later point. This is different from how the output file is handled,
|
||||||
|
// probably to prevent running out of file descriptors.
|
||||||
|
for file in &files {
|
||||||
|
open(file)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = Output::new(matches.value_of(options::OUTPUT))?;
|
||||||
|
|
||||||
settings.init_precomputed();
|
settings.init_precomputed();
|
||||||
|
|
||||||
exec(&files, &settings)
|
exec(&mut files, &settings, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uu_app() -> App<'static, 'static> {
|
pub fn uu_app() -> App<'static, 'static> {
|
||||||
|
@ -1112,73 +1291,57 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
Arg::with_name(options::modes::SORT)
|
Arg::with_name(options::modes::SORT)
|
||||||
.long(options::modes::SORT)
|
.long(options::modes::SORT)
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.possible_values(
|
.possible_values(&[
|
||||||
&[
|
"general-numeric",
|
||||||
"general-numeric",
|
"human-numeric",
|
||||||
"human-numeric",
|
"month",
|
||||||
"month",
|
"numeric",
|
||||||
"numeric",
|
"version",
|
||||||
"version",
|
"random",
|
||||||
"random",
|
])
|
||||||
]
|
.conflicts_with_all(&options::modes::ALL_SORT_MODES),
|
||||||
)
|
|
||||||
.conflicts_with_all(&options::modes::ALL_SORT_MODES)
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
make_sort_mode_arg(
|
|
||||||
options::modes::HUMAN_NUMERIC,
|
|
||||||
"h",
|
|
||||||
"compare according to human readable sizes, eg 1M > 100k"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
make_sort_mode_arg(
|
|
||||||
options::modes::MONTH,
|
|
||||||
"M",
|
|
||||||
"compare according to month name abbreviation"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
make_sort_mode_arg(
|
|
||||||
options::modes::NUMERIC,
|
|
||||||
"n",
|
|
||||||
"compare according to string numerical value"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
make_sort_mode_arg(
|
|
||||||
options::modes::GENERAL_NUMERIC,
|
|
||||||
"g",
|
|
||||||
"compare according to string general numerical value"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
make_sort_mode_arg(
|
|
||||||
options::modes::VERSION,
|
|
||||||
"V",
|
|
||||||
"Sort by SemVer version number, eg 1.12.2 > 1.1.2",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
make_sort_mode_arg(
|
|
||||||
options::modes::RANDOM,
|
|
||||||
"R",
|
|
||||||
"shuffle in random order",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
.arg(make_sort_mode_arg(
|
||||||
|
options::modes::HUMAN_NUMERIC,
|
||||||
|
"h",
|
||||||
|
"compare according to human readable sizes, eg 1M > 100k",
|
||||||
|
))
|
||||||
|
.arg(make_sort_mode_arg(
|
||||||
|
options::modes::MONTH,
|
||||||
|
"M",
|
||||||
|
"compare according to month name abbreviation",
|
||||||
|
))
|
||||||
|
.arg(make_sort_mode_arg(
|
||||||
|
options::modes::NUMERIC,
|
||||||
|
"n",
|
||||||
|
"compare according to string numerical value",
|
||||||
|
))
|
||||||
|
.arg(make_sort_mode_arg(
|
||||||
|
options::modes::GENERAL_NUMERIC,
|
||||||
|
"g",
|
||||||
|
"compare according to string general numerical value",
|
||||||
|
))
|
||||||
|
.arg(make_sort_mode_arg(
|
||||||
|
options::modes::VERSION,
|
||||||
|
"V",
|
||||||
|
"Sort by SemVer version number, eg 1.12.2 > 1.1.2",
|
||||||
|
))
|
||||||
|
.arg(make_sort_mode_arg(
|
||||||
|
options::modes::RANDOM,
|
||||||
|
"R",
|
||||||
|
"shuffle in random order",
|
||||||
|
))
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::DICTIONARY_ORDER)
|
Arg::with_name(options::DICTIONARY_ORDER)
|
||||||
.short("d")
|
.short("d")
|
||||||
.long(options::DICTIONARY_ORDER)
|
.long(options::DICTIONARY_ORDER)
|
||||||
.help("consider only blanks and alphanumeric characters")
|
.help("consider only blanks and alphanumeric characters")
|
||||||
.conflicts_with_all(
|
.conflicts_with_all(&[
|
||||||
&[
|
options::modes::NUMERIC,
|
||||||
options::modes::NUMERIC,
|
options::modes::GENERAL_NUMERIC,
|
||||||
options::modes::GENERAL_NUMERIC,
|
options::modes::HUMAN_NUMERIC,
|
||||||
options::modes::HUMAN_NUMERIC,
|
options::modes::MONTH,
|
||||||
options::modes::MONTH,
|
]),
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::MERGE)
|
Arg::with_name(options::MERGE)
|
||||||
|
@ -1206,7 +1369,10 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
.short("C")
|
.short("C")
|
||||||
.long(options::check::CHECK_SILENT)
|
.long(options::check::CHECK_SILENT)
|
||||||
.conflicts_with(options::OUTPUT)
|
.conflicts_with(options::OUTPUT)
|
||||||
.help("exit successfully if the given file is already sorted, and exit with status 1 otherwise."),
|
.help(
|
||||||
|
"exit successfully if the given file is already sorted,\
|
||||||
|
and exit with status 1 otherwise.",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::IGNORE_CASE)
|
Arg::with_name(options::IGNORE_CASE)
|
||||||
|
@ -1219,14 +1385,12 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
.short("i")
|
.short("i")
|
||||||
.long(options::IGNORE_NONPRINTING)
|
.long(options::IGNORE_NONPRINTING)
|
||||||
.help("ignore nonprinting characters")
|
.help("ignore nonprinting characters")
|
||||||
.conflicts_with_all(
|
.conflicts_with_all(&[
|
||||||
&[
|
options::modes::NUMERIC,
|
||||||
options::modes::NUMERIC,
|
options::modes::GENERAL_NUMERIC,
|
||||||
options::modes::GENERAL_NUMERIC,
|
options::modes::HUMAN_NUMERIC,
|
||||||
options::modes::HUMAN_NUMERIC,
|
options::modes::MONTH,
|
||||||
options::modes::MONTH
|
]),
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::IGNORE_LEADING_BLANKS)
|
Arg::with_name(options::IGNORE_LEADING_BLANKS)
|
||||||
|
@ -1275,7 +1439,8 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
.short("t")
|
.short("t")
|
||||||
.long(options::SEPARATOR)
|
.long(options::SEPARATOR)
|
||||||
.help("custom separator for -k")
|
.help("custom separator for -k")
|
||||||
.takes_value(true))
|
.takes_value(true),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::ZERO_TERMINATED)
|
Arg::with_name(options::ZERO_TERMINATED)
|
||||||
.short("z")
|
.short("z")
|
||||||
|
@ -1310,13 +1475,13 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
.long(options::COMPRESS_PROG)
|
.long(options::COMPRESS_PROG)
|
||||||
.help("compress temporary files with PROG, decompress with PROG -d")
|
.help("compress temporary files with PROG, decompress with PROG -d")
|
||||||
.long_help("PROG has to take input from stdin and output to stdout")
|
.long_help("PROG has to take input from stdin and output to stdout")
|
||||||
.value_name("PROG")
|
.value_name("PROG"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::BATCH_SIZE)
|
Arg::with_name(options::BATCH_SIZE)
|
||||||
.long(options::BATCH_SIZE)
|
.long(options::BATCH_SIZE)
|
||||||
.help("Merge at most N_MERGE inputs at once.")
|
.help("Merge at most N_MERGE inputs at once.")
|
||||||
.value_name("N_MERGE")
|
.value_name("N_MERGE"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::FILES0_FROM)
|
Arg::with_name(options::FILES0_FROM)
|
||||||
|
@ -1331,24 +1496,27 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
.long(options::DEBUG)
|
.long(options::DEBUG)
|
||||||
.help("underline the parts of the line that are actually used for sorting"),
|
.help("underline the parts of the line that are actually used for sorting"),
|
||||||
)
|
)
|
||||||
.arg(Arg::with_name(options::FILES).multiple(true).takes_value(true))
|
.arg(
|
||||||
|
Arg::with_name(options::FILES)
|
||||||
|
.multiple(true)
|
||||||
|
.takes_value(true),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exec(files: &[String], settings: &GlobalSettings) -> i32 {
|
fn exec(files: &mut [OsString], settings: &GlobalSettings, output: Output) -> UResult<()> {
|
||||||
if settings.merge {
|
if settings.merge {
|
||||||
let mut file_merger = merge::merge(files.iter().map(open), settings);
|
let file_merger = merge::merge(files, settings, output.as_output_name())?;
|
||||||
file_merger.write_all(settings);
|
file_merger.write_all(settings, output)
|
||||||
} else if settings.check {
|
} else if settings.check {
|
||||||
if files.len() > 1 {
|
if files.len() > 1 {
|
||||||
crash!(1, "only one file allowed with -c");
|
Err(UUsageError::new(2, "only one file allowed with -c".into()))
|
||||||
|
} else {
|
||||||
|
check::check(files.first().unwrap(), settings)
|
||||||
}
|
}
|
||||||
return check::check(files.first().unwrap(), settings);
|
|
||||||
} else {
|
} else {
|
||||||
let mut lines = files.iter().map(open);
|
let mut lines = files.iter().map(open);
|
||||||
|
ext_sort(&mut lines, settings, output)
|
||||||
ext_sort(&mut lines, settings);
|
|
||||||
}
|
}
|
||||||
0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort_by<'a>(unsorted: &mut Vec<Line<'a>>, settings: &GlobalSettings, line_data: &LineData<'a>) {
|
fn sort_by<'a>(unsorted: &mut Vec<Line<'a>>, settings: &GlobalSettings, line_data: &LineData<'a>) {
|
||||||
|
@ -1387,7 +1555,22 @@ fn compare_by<'a>(
|
||||||
let settings = &selector.settings;
|
let settings = &selector.settings;
|
||||||
|
|
||||||
let cmp: Ordering = match settings.mode {
|
let cmp: Ordering = match settings.mode {
|
||||||
SortMode::Random => random_shuffle(a_str, b_str, &global_settings.salt),
|
SortMode::Random => {
|
||||||
|
// check if the two strings are equal
|
||||||
|
if custom_str_cmp(
|
||||||
|
a_str,
|
||||||
|
b_str,
|
||||||
|
settings.ignore_non_printing,
|
||||||
|
settings.dictionary_order,
|
||||||
|
settings.ignore_case,
|
||||||
|
) == Ordering::Equal
|
||||||
|
{
|
||||||
|
Ordering::Equal
|
||||||
|
} else {
|
||||||
|
// Only if they are not equal compare by the hash
|
||||||
|
random_shuffle(a_str, b_str, &global_settings.salt.unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
SortMode::Numeric => {
|
SortMode::Numeric => {
|
||||||
let a_num_info = &a_line_data.num_infos
|
let a_num_info = &a_line_data.num_infos
|
||||||
[a.index * global_settings.precomputed.num_infos_per_line + num_info_index];
|
[a.index * global_settings.precomputed.num_infos_per_line + num_info_index];
|
||||||
|
@ -1536,12 +1719,8 @@ fn general_numeric_compare(a: &GeneralF64ParseResult, b: &GeneralF64ParseResult)
|
||||||
a.partial_cmp(b).unwrap()
|
a.partial_cmp(b).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_rand_string() -> String {
|
fn get_rand_string() -> [u8; 16] {
|
||||||
thread_rng()
|
thread_rng().sample(rand::distributions::Standard)
|
||||||
.sample_iter(&Alphanumeric)
|
|
||||||
.take(16)
|
|
||||||
.map(char::from)
|
|
||||||
.collect::<String>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_hash<T: Hash>(t: &T) -> u64 {
|
fn get_hash<T: Hash>(t: &T) -> u64 {
|
||||||
|
@ -1550,10 +1729,9 @@ fn get_hash<T: Hash>(t: &T) -> u64 {
|
||||||
s.finish()
|
s.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn random_shuffle(a: &str, b: &str, salt: &str) -> Ordering {
|
fn random_shuffle(a: &str, b: &str, salt: &[u8]) -> Ordering {
|
||||||
let da = get_hash(&[a, salt].concat());
|
let da = get_hash(&(a, salt));
|
||||||
let db = get_hash(&[b, salt].concat());
|
let db = get_hash(&(b, salt));
|
||||||
|
|
||||||
da.cmp(&db)
|
da.cmp(&db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1618,26 +1796,38 @@ fn month_compare(a: &str, b: &str) -> Ordering {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_sorted<'a, T: Iterator<Item = &'a Line<'a>>>(iter: T, settings: &GlobalSettings) {
|
fn print_sorted<'a, T: Iterator<Item = &'a Line<'a>>>(
|
||||||
let mut writer = settings.out_writer();
|
iter: T,
|
||||||
|
settings: &GlobalSettings,
|
||||||
|
output: Output,
|
||||||
|
) {
|
||||||
|
let mut writer = output.into_write();
|
||||||
for line in iter {
|
for line in iter {
|
||||||
line.print(&mut writer, settings);
|
line.print(&mut writer, settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// from cat.rs
|
/// Strips the trailing " (os error XX)" from io error strings.
|
||||||
fn open(path: impl AsRef<OsStr>) -> Box<dyn Read + Send> {
|
fn strip_errno(err: &str) -> &str {
|
||||||
|
&err[..err.find(" (os error ").unwrap_or(err.len())]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open(path: impl AsRef<OsStr>) -> UResult<Box<dyn Read + Send>> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
if path == "-" {
|
if path == "-" {
|
||||||
let stdin = stdin();
|
let stdin = stdin();
|
||||||
return Box::new(stdin) as Box<dyn Read + Send>;
|
return Ok(Box::new(stdin) as Box<dyn Read + Send>);
|
||||||
}
|
}
|
||||||
|
|
||||||
match File::open(Path::new(path)) {
|
let path = Path::new(path);
|
||||||
Ok(f) => Box::new(f) as Box<dyn Read + Send>,
|
|
||||||
Err(e) => {
|
match File::open(path) {
|
||||||
crash!(2, "cannot read: {0:?}: {1}", path, e);
|
Ok(f) => Ok(Box::new(f) as Box<dyn Read + Send>),
|
||||||
|
Err(error) => Err(SortError::ReadFailed {
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
error,
|
||||||
}
|
}
|
||||||
|
.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -436,6 +436,7 @@ impl Stater {
|
||||||
'f' => tokens.push(Token::Char('\x0C')),
|
'f' => tokens.push(Token::Char('\x0C')),
|
||||||
'n' => tokens.push(Token::Char('\n')),
|
'n' => tokens.push(Token::Char('\n')),
|
||||||
'r' => tokens.push(Token::Char('\r')),
|
'r' => tokens.push(Token::Char('\r')),
|
||||||
|
't' => tokens.push(Token::Char('\t')),
|
||||||
'v' => tokens.push(Token::Char('\x0B')),
|
'v' => tokens.push(Token::Char('\x0B')),
|
||||||
c => {
|
c => {
|
||||||
show_warning!("unrecognized escape '\\{}'", c);
|
show_warning!("unrecognized escape '\\{}'", c);
|
||||||
|
|
|
@ -97,7 +97,7 @@ fn tac(filenames: Vec<String>, before: bool, _: bool, separator: &str) -> i32 {
|
||||||
let path = Path::new(filename);
|
let path = Path::new(filename);
|
||||||
if path.is_dir() || path.metadata().is_err() {
|
if path.is_dir() || path.metadata().is_err() {
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
show_error!("dir: read error: Invalid argument");
|
show_error!("{}: read error: Invalid argument", filename);
|
||||||
} else {
|
} else {
|
||||||
show_error!(
|
show_error!(
|
||||||
"failed to open '{}' for reading: No such file or directory",
|
"failed to open '{}' for reading: No such file or directory",
|
||||||
|
@ -139,9 +139,16 @@ fn tac(filenames: Vec<String>, before: bool, _: bool, separator: &str) -> i32 {
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// If the file contains no line separators, then simply write
|
||||||
|
// the contents of the file directly to stdout.
|
||||||
|
if offsets.is_empty() {
|
||||||
|
out.write_all(&data)
|
||||||
|
.unwrap_or_else(|e| crash!(1, "failed to write to stdout: {}", e));
|
||||||
|
return exit_code;
|
||||||
|
}
|
||||||
|
|
||||||
// if there isn't a separator at the end of the file, fake it
|
// if there isn't a separator at the end of the file, fake it
|
||||||
if offsets.is_empty() || *offsets.last().unwrap() < data.len() - slen {
|
if *offsets.last().unwrap() < data.len() - slen {
|
||||||
offsets.push(data.len());
|
offsets.push(data.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,10 @@ winapi = { version="0.3", features=["fileapi", "handleapi", "processthreadsapi",
|
||||||
[target.'cfg(target_os = "redox")'.dependencies]
|
[target.'cfg(target_os = "redox")'.dependencies]
|
||||||
redox_syscall = "0.1"
|
redox_syscall = "0.1"
|
||||||
|
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
nix = "0.20"
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "tail"
|
name = "tail"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
### Others
|
### Others
|
||||||
|
|
||||||
- [ ] The current implementation does not handle `-` as an alias for stdin.
|
- [ ] The current implementation doesn't follow stdin in non-unix platforms
|
||||||
|
|
||||||
## Possible optimizations
|
## Possible optimizations
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,14 @@
|
||||||
* This file is part of the uutils coreutils package.
|
* This file is part of the uutils coreutils package.
|
||||||
*
|
*
|
||||||
* (c) Alexander Batischev <eual.jp@gmail.com>
|
* (c) Alexander Batischev <eual.jp@gmail.com>
|
||||||
|
* (c) Thomas Queiroz <thomasqueirozb@gmail.com>
|
||||||
*
|
*
|
||||||
* For the full copyright and license information, please view the LICENSE
|
* For the full copyright and license information, please view the LICENSE
|
||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub use self::unix::{supports_pid_checks, Pid, ProcessChecker};
|
pub use self::unix::{stdin_is_pipe_or_fifo, supports_pid_checks, Pid, ProcessChecker};
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub use self::windows::{supports_pid_checks, Pid, ProcessChecker};
|
pub use self::windows::{supports_pid_checks, Pid, ProcessChecker};
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
* This file is part of the uutils coreutils package.
|
* This file is part of the uutils coreutils package.
|
||||||
*
|
*
|
||||||
* (c) Alexander Batischev <eual.jp@gmail.com>
|
* (c) Alexander Batischev <eual.jp@gmail.com>
|
||||||
|
* (c) Thomas Queiroz <thomasqueirozb@gmail.com>
|
||||||
*
|
*
|
||||||
* For the full copyright and license information, please view the LICENSE
|
* For the full copyright and license information, please view the LICENSE
|
||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
|
@ -9,7 +10,13 @@
|
||||||
|
|
||||||
// spell-checker:ignore (ToDO) errno EPERM ENOSYS
|
// spell-checker:ignore (ToDO) errno EPERM ENOSYS
|
||||||
|
|
||||||
use std::io::Error;
|
use std::io::{stdin, Error};
|
||||||
|
|
||||||
|
use std::os::unix::prelude::AsRawFd;
|
||||||
|
|
||||||
|
use nix::sys::stat::fstat;
|
||||||
|
|
||||||
|
use libc::{S_IFIFO, S_IFSOCK};
|
||||||
|
|
||||||
pub type Pid = libc::pid_t;
|
pub type Pid = libc::pid_t;
|
||||||
|
|
||||||
|
@ -40,3 +47,16 @@ pub fn supports_pid_checks(pid: self::Pid) -> bool {
|
||||||
fn get_errno() -> i32 {
|
fn get_errno() -> i32 {
|
||||||
Error::last_os_error().raw_os_error().unwrap()
|
Error::last_os_error().raw_os_error().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn stdin_is_pipe_or_fifo() -> bool {
|
||||||
|
let fd = stdin().lock().as_raw_fd();
|
||||||
|
fd >= 0 // GNU tail checks fd >= 0
|
||||||
|
&& match fstat(fd) {
|
||||||
|
Ok(stat) => {
|
||||||
|
let mode = stat.st_mode;
|
||||||
|
// NOTE: This is probably not the most correct way to check this
|
||||||
|
(mode & S_IFIFO != 0) || (mode & S_IFSOCK != 0)
|
||||||
|
}
|
||||||
|
Err(err) => panic!("{}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// *
|
// *
|
||||||
// * (c) Morten Olsen Lysgaard <morten@lysgaard.no>
|
// * (c) Morten Olsen Lysgaard <morten@lysgaard.no>
|
||||||
// * (c) Alexander Batischev <eual.jp@gmail.com>
|
// * (c) Alexander Batischev <eual.jp@gmail.com>
|
||||||
|
// * (c) Thomas Queiroz <thomasqueirozb@gmail.com>
|
||||||
// *
|
// *
|
||||||
// * For the full copyright and license information, please view the LICENSE
|
// * For the full copyright and license information, please view the LICENSE
|
||||||
// * file that was distributed with this source code.
|
// * file that was distributed with this source code.
|
||||||
|
@ -29,6 +30,9 @@ use std::time::Duration;
|
||||||
use uucore::parse_size::{parse_size, ParseSizeError};
|
use uucore::parse_size::{parse_size, ParseSizeError};
|
||||||
use uucore::ringbuffer::RingBuffer;
|
use uucore::ringbuffer::RingBuffer;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use crate::platform::stdin_is_pipe_or_fifo;
|
||||||
|
|
||||||
pub mod options {
|
pub mod options {
|
||||||
pub mod verbosity {
|
pub mod verbosity {
|
||||||
pub static QUIET: &str = "quiet";
|
pub static QUIET: &str = "quiet";
|
||||||
|
@ -130,25 +134,56 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
let files: Vec<String> = matches
|
let files: Vec<String> = matches
|
||||||
.values_of(options::ARG_FILES)
|
.values_of(options::ARG_FILES)
|
||||||
.map(|v| v.map(ToString::to_string).collect())
|
.map(|v| v.map(ToString::to_string).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_else(|| vec![String::from("-")]);
|
||||||
|
|
||||||
if files.is_empty() {
|
let multiple = files.len() > 1;
|
||||||
let mut buffer = BufReader::new(stdin());
|
let mut first_header = true;
|
||||||
unbounded_tail(&mut buffer, &settings);
|
let mut readers: Vec<(Box<dyn BufRead>, &String)> = Vec::new();
|
||||||
} else {
|
|
||||||
let multiple = files.len() > 1;
|
|
||||||
let mut first_header = true;
|
|
||||||
let mut readers = Vec::new();
|
|
||||||
|
|
||||||
for filename in &files {
|
#[cfg(unix)]
|
||||||
if (multiple || verbose) && !quiet {
|
let stdin_string = String::from("standard input");
|
||||||
if !first_header {
|
|
||||||
println!();
|
for filename in &files {
|
||||||
}
|
let use_stdin = filename.as_str() == "-";
|
||||||
|
if (multiple || verbose) && !quiet {
|
||||||
|
if !first_header {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
if use_stdin {
|
||||||
|
println!("==> standard input <==");
|
||||||
|
} else {
|
||||||
println!("==> {} <==", filename);
|
println!("==> {} <==", filename);
|
||||||
}
|
}
|
||||||
first_header = false;
|
}
|
||||||
|
first_header = false;
|
||||||
|
|
||||||
|
if use_stdin {
|
||||||
|
let mut reader = BufReader::new(stdin());
|
||||||
|
unbounded_tail(&mut reader, &settings);
|
||||||
|
|
||||||
|
// Don't follow stdin since there are no checks for pipes/FIFOs
|
||||||
|
//
|
||||||
|
// FIXME windows has GetFileType which can determine if the file is a pipe/FIFO
|
||||||
|
// so this check can also be performed
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
POSIX specification regarding tail -f
|
||||||
|
|
||||||
|
If the input file is a regular file or if the file operand specifies a FIFO, do not
|
||||||
|
terminate after the last line of the input file has been copied, but read and copy
|
||||||
|
further bytes from the input file when they become available. If no file operand is
|
||||||
|
specified and standard input is a pipe or FIFO, the -f option shall be ignored. If
|
||||||
|
the input file is not a FIFO, pipe, or regular file, it is unspecified whether or
|
||||||
|
not the -f option shall be ignored.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if settings.follow && !stdin_is_pipe_or_fifo() {
|
||||||
|
readers.push((Box::new(reader), &stdin_string));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
let path = Path::new(filename);
|
let path = Path::new(filename);
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
continue;
|
continue;
|
||||||
|
@ -158,20 +193,20 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
bounded_tail(&mut file, &settings);
|
bounded_tail(&mut file, &settings);
|
||||||
if settings.follow {
|
if settings.follow {
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
readers.push(reader);
|
readers.push((Box::new(reader), filename));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut reader = BufReader::new(file);
|
let mut reader = BufReader::new(file);
|
||||||
unbounded_tail(&mut reader, &settings);
|
unbounded_tail(&mut reader, &settings);
|
||||||
if settings.follow {
|
if settings.follow {
|
||||||
readers.push(reader);
|
readers.push((Box::new(reader), filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if settings.follow {
|
if settings.follow {
|
||||||
follow(&mut readers[..], &files[..], &settings);
|
follow(&mut readers[..], &settings);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
0
|
0
|
||||||
|
@ -248,8 +283,12 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn follow<T: Read>(readers: &mut [BufReader<T>], filenames: &[String], settings: &Settings) {
|
fn follow<T: BufRead>(readers: &mut [(T, &String)], settings: &Settings) {
|
||||||
assert!(settings.follow);
|
assert!(settings.follow);
|
||||||
|
if readers.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let mut last = readers.len() - 1;
|
let mut last = readers.len() - 1;
|
||||||
let mut read_some = false;
|
let mut read_some = false;
|
||||||
let mut process = platform::ProcessChecker::new(settings.pid);
|
let mut process = platform::ProcessChecker::new(settings.pid);
|
||||||
|
@ -260,7 +299,7 @@ fn follow<T: Read>(readers: &mut [BufReader<T>], filenames: &[String], settings:
|
||||||
let pid_is_dead = !read_some && settings.pid != 0 && process.is_dead();
|
let pid_is_dead = !read_some && settings.pid != 0 && process.is_dead();
|
||||||
read_some = false;
|
read_some = false;
|
||||||
|
|
||||||
for (i, reader) in readers.iter_mut().enumerate() {
|
for (i, (reader, filename)) in readers.iter_mut().enumerate() {
|
||||||
// Print all new content since the last pass
|
// Print all new content since the last pass
|
||||||
loop {
|
loop {
|
||||||
let mut datum = String::new();
|
let mut datum = String::new();
|
||||||
|
@ -269,7 +308,7 @@ fn follow<T: Read>(readers: &mut [BufReader<T>], filenames: &[String], settings:
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
read_some = true;
|
read_some = true;
|
||||||
if i != last {
|
if i != last {
|
||||||
println!("\n==> {} <==", filenames[i]);
|
println!("\n==> {} <==", filename);
|
||||||
last = i;
|
last = i;
|
||||||
}
|
}
|
||||||
print!("{}", datum);
|
print!("{}", datum);
|
||||||
|
|
|
@ -8,9 +8,6 @@
|
||||||
|
|
||||||
// spell-checker:ignore (ToDO) filetime strptime utcoff strs datetime MMDDhhmm
|
// spell-checker:ignore (ToDO) filetime strptime utcoff strs datetime MMDDhhmm
|
||||||
|
|
||||||
// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422
|
|
||||||
#![allow(clippy::nonstandard_macro_braces)]
|
|
||||||
|
|
||||||
pub extern crate filetime;
|
pub extern crate filetime;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
|
|
@ -5,12 +5,17 @@
|
||||||
// * For the full copyright and license information, please view the LICENSE
|
// * For the full copyright and license information, please view the LICENSE
|
||||||
// * file that was distributed with this source code.
|
// * file that was distributed with this source code.
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate uucore;
|
||||||
|
|
||||||
use clap::App;
|
use clap::App;
|
||||||
|
use uucore::error::UResult;
|
||||||
use uucore::executable;
|
use uucore::executable;
|
||||||
|
|
||||||
pub fn uumain(args: impl uucore::Args) -> i32 {
|
#[uucore_procs::gen_uumain]
|
||||||
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
uu_app().get_matches_from(args);
|
uu_app().get_matches_from(args);
|
||||||
0
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uu_app() -> App<'static, 'static> {
|
pub fn uu_app() -> App<'static, 'static> {
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
// *
|
// *
|
||||||
// * For the full copyright and license information, please view the LICENSE
|
// * For the full copyright and license information, please view the LICENSE
|
||||||
// * file that was distributed with this source code.
|
// * file that was distributed with this source code.
|
||||||
// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422
|
|
||||||
#![allow(clippy::nonstandard_macro_braces)]
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
|
|
@ -30,6 +30,10 @@ time = { version="<= 0.1.43", optional=true }
|
||||||
data-encoding = { version="~2.1", optional=true } ## data-encoding: require v2.1; but v2.2.0 breaks the build for MinSRV v1.31.0
|
data-encoding = { version="~2.1", optional=true } ## data-encoding: require v2.1; but v2.2.0 breaks the build for MinSRV v1.31.0
|
||||||
libc = { version="0.2.15, <= 0.2.85", optional=true } ## libc: initial utmp support added in v0.2.15; but v0.2.68 breaks the build for MinSRV v1.31.0
|
libc = { version="0.2.15, <= 0.2.85", optional=true } ## libc: initial utmp support added in v0.2.15; but v0.2.68 breaks the build for MinSRV v1.31.0
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
clap = "2.33.3"
|
||||||
|
lazy_static = "1.3"
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
winapi = { version = "0.3", features = ["errhandlingapi", "fileapi", "handleapi", "winerror"] }
|
winapi = { version = "0.3", features = ["errhandlingapi", "fileapi", "handleapi", "winerror"] }
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,6 @@
|
||||||
|
|
||||||
// spell-checker:ignore (strings) ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
// spell-checker:ignore (strings) ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
|
|
||||||
// clippy bug https://github.com/rust-lang/rust-clippy/issues/7422
|
|
||||||
#![allow(clippy::nonstandard_macro_braces)]
|
|
||||||
|
|
||||||
extern crate data_encoding;
|
extern crate data_encoding;
|
||||||
|
|
||||||
use self::data_encoding::{DecodeError, BASE32, BASE64};
|
use self::data_encoding::{DecodeError, BASE32, BASE64};
|
||||||
|
|
|
@ -7,19 +7,15 @@ pub static BACKUP_CONTROL_VALUES: &[&str] = &[
|
||||||
"simple", "never", "numbered", "t", "existing", "nil", "none", "off",
|
"simple", "never", "numbered", "t", "existing", "nil", "none", "off",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub static BACKUP_CONTROL_LONG_HELP: &str = "The backup suffix is '~', unless set with --suffix or SIMPLE_BACKUP_SUFFIX. Here are the version control values:
|
pub static BACKUP_CONTROL_LONG_HELP: &str =
|
||||||
|
"The backup suffix is '~', unless set with --suffix or SIMPLE_BACKUP_SUFFIX.
|
||||||
|
The version control method may be selected via the --backup option or through
|
||||||
|
the VERSION_CONTROL environment variable. Here are the values:
|
||||||
|
|
||||||
none, off
|
none, off never make backups (even if --backup is given)
|
||||||
never make backups (even if --backup is given)
|
numbered, t make numbered backups
|
||||||
|
existing, nil numbered if numbered backups exist, simple otherwise
|
||||||
numbered, t
|
simple, never always make simple backups";
|
||||||
make numbered backups
|
|
||||||
|
|
||||||
existing, nil
|
|
||||||
numbered if numbered backups exist, simple otherwise
|
|
||||||
|
|
||||||
simple, never
|
|
||||||
always make simple backups";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
pub enum BackupMode {
|
pub enum BackupMode {
|
||||||
|
@ -37,35 +33,177 @@ pub fn determine_backup_suffix(supplied_suffix: Option<&str>) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # TODO
|
/// Determine the "mode" for the backup operation to perform, if any.
|
||||||
///
|
///
|
||||||
/// This function currently deviates slightly from how the [manual][1] describes
|
/// Parses the backup options according to the [GNU manual][1], and converts
|
||||||
/// that it should work. In particular, the current implementation:
|
/// them to an instance of `BackupMode` for further processing.
|
||||||
///
|
///
|
||||||
/// 1. Doesn't strictly respect the order in which to determine the backup type,
|
/// For an explanation of what the arguments mean, refer to the examples below.
|
||||||
/// which is (in order of precedence)
|
|
||||||
/// 1. Take a valid value to the '--backup' option
|
|
||||||
/// 2. Take the value of the `VERSION_CONTROL` env var
|
|
||||||
/// 3. default to 'existing'
|
|
||||||
/// 2. Doesn't accept abbreviations to the 'backup_option' parameter
|
|
||||||
///
|
///
|
||||||
/// [1]: https://www.gnu.org/software/coreutils/manual/html_node/Backup-options.html
|
/// [1]: https://www.gnu.org/software/coreutils/manual/html_node/Backup-options.html
|
||||||
pub fn determine_backup_mode(backup_opt_exists: bool, backup_opt: Option<&str>) -> BackupMode {
|
///
|
||||||
if backup_opt_exists {
|
///
|
||||||
match backup_opt.map(String::from) {
|
/// # Errors
|
||||||
// default is existing, see:
|
///
|
||||||
// https://www.gnu.org/software/coreutils/manual/html_node/Backup-options.html
|
/// If an argument supplied directly to the long `backup` option, or read in
|
||||||
None => BackupMode::ExistingBackup,
|
/// through the `VERSION CONTROL` env var is ambiguous (i.e. may resolve to
|
||||||
Some(mode) => match &mode[..] {
|
/// multiple backup modes) or invalid, an error is returned. The error contains
|
||||||
"simple" | "never" => BackupMode::SimpleBackup,
|
/// the formatted error string which may then be passed to the
|
||||||
"numbered" | "t" => BackupMode::NumberedBackup,
|
/// [`show_usage_error`] macro.
|
||||||
"existing" | "nil" => BackupMode::ExistingBackup,
|
///
|
||||||
"none" | "off" => BackupMode::NoBackup,
|
///
|
||||||
_ => panic!(), // cannot happen as it is managed by clap
|
/// # Examples
|
||||||
},
|
///
|
||||||
|
/// Here's how one would integrate the backup mode determination into an
|
||||||
|
/// application.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// #[macro_use]
|
||||||
|
/// extern crate uucore;
|
||||||
|
/// use uucore::backup_control::{self, BackupMode};
|
||||||
|
/// use clap::{App, Arg};
|
||||||
|
///
|
||||||
|
/// fn main() {
|
||||||
|
/// let OPT_BACKUP: &str = "backup";
|
||||||
|
/// let OPT_BACKUP_NO_ARG: &str = "b";
|
||||||
|
/// let matches = App::new("app")
|
||||||
|
/// .arg(Arg::with_name(OPT_BACKUP_NO_ARG)
|
||||||
|
/// .short(OPT_BACKUP_NO_ARG))
|
||||||
|
/// .arg(Arg::with_name(OPT_BACKUP)
|
||||||
|
/// .long(OPT_BACKUP)
|
||||||
|
/// .takes_value(true)
|
||||||
|
/// .require_equals(true)
|
||||||
|
/// .min_values(0))
|
||||||
|
/// .get_matches_from(vec![
|
||||||
|
/// "app", "-b", "--backup=t"
|
||||||
|
/// ]);
|
||||||
|
///
|
||||||
|
/// let backup_mode = backup_control::determine_backup_mode(
|
||||||
|
/// matches.is_present(OPT_BACKUP_NO_ARG), matches.is_present(OPT_BACKUP),
|
||||||
|
/// matches.value_of(OPT_BACKUP)
|
||||||
|
/// );
|
||||||
|
/// let backup_mode = match backup_mode {
|
||||||
|
/// Err(err) => {
|
||||||
|
/// show_usage_error!("{}", err);
|
||||||
|
/// return;
|
||||||
|
/// },
|
||||||
|
/// Ok(mode) => mode,
|
||||||
|
/// };
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This example shows an ambiguous input, as 'n' may resolve to 4 different
|
||||||
|
/// backup modes.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// #[macro_use]
|
||||||
|
/// extern crate uucore;
|
||||||
|
/// use uucore::backup_control::{self, BackupMode};
|
||||||
|
/// use clap::{crate_version, App, Arg, ArgMatches};
|
||||||
|
///
|
||||||
|
/// fn main() {
|
||||||
|
/// let OPT_BACKUP: &str = "backup";
|
||||||
|
/// let OPT_BACKUP_NO_ARG: &str = "b";
|
||||||
|
/// let matches = App::new("app")
|
||||||
|
/// .arg(Arg::with_name(OPT_BACKUP_NO_ARG)
|
||||||
|
/// .short(OPT_BACKUP_NO_ARG))
|
||||||
|
/// .arg(Arg::with_name(OPT_BACKUP)
|
||||||
|
/// .long(OPT_BACKUP)
|
||||||
|
/// .takes_value(true)
|
||||||
|
/// .require_equals(true)
|
||||||
|
/// .min_values(0))
|
||||||
|
/// .get_matches_from(vec![
|
||||||
|
/// "app", "-b", "--backup=n"
|
||||||
|
/// ]);
|
||||||
|
///
|
||||||
|
/// let backup_mode = backup_control::determine_backup_mode(
|
||||||
|
/// matches.is_present(OPT_BACKUP_NO_ARG), matches.is_present(OPT_BACKUP),
|
||||||
|
/// matches.value_of(OPT_BACKUP)
|
||||||
|
/// );
|
||||||
|
/// let backup_mode = match backup_mode {
|
||||||
|
/// Err(err) => {
|
||||||
|
/// show_usage_error!("{}", err);
|
||||||
|
/// return;
|
||||||
|
/// },
|
||||||
|
/// Ok(mode) => mode,
|
||||||
|
/// };
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn determine_backup_mode(
|
||||||
|
short_opt_present: bool,
|
||||||
|
long_opt_present: bool,
|
||||||
|
long_opt_value: Option<&str>,
|
||||||
|
) -> Result<BackupMode, String> {
|
||||||
|
if long_opt_present {
|
||||||
|
// Use method to determine the type of backups to make. When this option
|
||||||
|
// is used but method is not specified, then the value of the
|
||||||
|
// VERSION_CONTROL environment variable is used. And if VERSION_CONTROL
|
||||||
|
// is not set, the default backup type is ‘existing’.
|
||||||
|
if let Some(method) = long_opt_value {
|
||||||
|
// Second argument is for the error string that is returned.
|
||||||
|
match_method(method, "backup type")
|
||||||
|
} else if let Ok(method) = env::var("VERSION_CONTROL") {
|
||||||
|
// Second argument is for the error string that is returned.
|
||||||
|
match_method(&method, "$VERSION_CONTROL")
|
||||||
|
} else {
|
||||||
|
Ok(BackupMode::ExistingBackup)
|
||||||
|
}
|
||||||
|
} else if short_opt_present {
|
||||||
|
// the short form of this option, -b does not accept any argument.
|
||||||
|
// Using -b is equivalent to using --backup=existing.
|
||||||
|
Ok(BackupMode::ExistingBackup)
|
||||||
|
} else {
|
||||||
|
// No option was present at all
|
||||||
|
Ok(BackupMode::NoBackup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Match a backup option string to a `BackupMode`.
|
||||||
|
///
|
||||||
|
/// The GNU manual specifies that abbreviations to options are valid as long as
|
||||||
|
/// they aren't ambiguous. This function matches the given `method` argument
|
||||||
|
/// against all valid backup options (via `starts_with`), and returns a valid
|
||||||
|
/// [`BackupMode`] if exactly one backup option matches the `method` given.
|
||||||
|
///
|
||||||
|
/// `origin` is required in order to format the generated error message
|
||||||
|
/// properly, when an error occurs.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// If `method` is ambiguous (i.e. may resolve to multiple backup modes) or
|
||||||
|
/// invalid, an error is returned. The error contains the formatted error string
|
||||||
|
/// which may then be passed to the [`show_usage_error`] macro.
|
||||||
|
fn match_method(method: &str, origin: &str) -> Result<BackupMode, String> {
|
||||||
|
let matches: Vec<&&str> = BACKUP_CONTROL_VALUES
|
||||||
|
.iter()
|
||||||
|
.filter(|val| val.starts_with(method))
|
||||||
|
.collect();
|
||||||
|
if matches.len() == 1 {
|
||||||
|
match *matches[0] {
|
||||||
|
"simple" | "never" => Ok(BackupMode::SimpleBackup),
|
||||||
|
"numbered" | "t" => Ok(BackupMode::NumberedBackup),
|
||||||
|
"existing" | "nil" => Ok(BackupMode::ExistingBackup),
|
||||||
|
"none" | "off" => Ok(BackupMode::NoBackup),
|
||||||
|
_ => unreachable!(), // cannot happen as we must have exactly one match
|
||||||
|
// from the list above.
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
BackupMode::NoBackup
|
let error_type = if matches.is_empty() {
|
||||||
|
"invalid"
|
||||||
|
} else {
|
||||||
|
"ambiguous"
|
||||||
|
};
|
||||||
|
Err(format!(
|
||||||
|
"{0} argument ‘{1}’ for ‘{2}’
|
||||||
|
Valid arguments are:
|
||||||
|
- ‘none’, ‘off’
|
||||||
|
- ‘simple’, ‘never’
|
||||||
|
- ‘existing’, ‘nil’
|
||||||
|
- ‘numbered’, ‘t’",
|
||||||
|
error_type, method, origin
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,13 +220,13 @@ pub fn get_backup_path(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn simple_backup_path(path: &Path, suffix: &str) -> PathBuf {
|
fn simple_backup_path(path: &Path, suffix: &str) -> PathBuf {
|
||||||
let mut p = path.to_string_lossy().into_owned();
|
let mut p = path.to_string_lossy().into_owned();
|
||||||
p.push_str(suffix);
|
p.push_str(suffix);
|
||||||
PathBuf::from(p)
|
PathBuf::from(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn numbered_backup_path(path: &Path) -> PathBuf {
|
fn numbered_backup_path(path: &Path) -> PathBuf {
|
||||||
for i in 1_u64.. {
|
for i in 1_u64.. {
|
||||||
let path_str = &format!("{}.~{}~", path.to_string_lossy(), i);
|
let path_str = &format!("{}.~{}~", path.to_string_lossy(), i);
|
||||||
let path = Path::new(path_str);
|
let path = Path::new(path_str);
|
||||||
|
@ -99,7 +237,7 @@ pub fn numbered_backup_path(path: &Path) -> PathBuf {
|
||||||
panic!("cannot create backup")
|
panic!("cannot create backup")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn existing_backup_path(path: &Path, suffix: &str) -> PathBuf {
|
fn existing_backup_path(path: &Path, suffix: &str) -> PathBuf {
|
||||||
let test_path_str = &format!("{}.~1~", path.to_string_lossy());
|
let test_path_str = &format!("{}.~1~", path.to_string_lossy());
|
||||||
let test_path = Path::new(test_path_str);
|
let test_path = Path::new(test_path_str);
|
||||||
if test_path.exists() {
|
if test_path.exists() {
|
||||||
|
@ -108,3 +246,210 @@ pub fn existing_backup_path(path: &Path, suffix: &str) -> PathBuf {
|
||||||
simple_backup_path(path, suffix)
|
simple_backup_path(path, suffix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Tests for this module
|
||||||
|
//
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::env;
|
||||||
|
// Required to instantiate mutex in shared context
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
// The mutex is required here as by default all tests are run as separate
|
||||||
|
// threads under the same parent process. As environment variables are
|
||||||
|
// specific to processes (and thus shared among threads), data races *will*
|
||||||
|
// occur if no precautions are taken. Thus we have all tests that rely on
|
||||||
|
// environment variables lock this empty mutex to ensure they don't access
|
||||||
|
// it concurrently.
|
||||||
|
lazy_static! {
|
||||||
|
static ref TEST_MUTEX: Mutex<()> = Mutex::new(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment variable for "VERSION_CONTROL"
|
||||||
|
static ENV_VERSION_CONTROL: &str = "VERSION_CONTROL";
|
||||||
|
|
||||||
|
// Defaults to --backup=existing
|
||||||
|
#[test]
|
||||||
|
fn test_backup_mode_short_only() {
|
||||||
|
let short_opt_present = true;
|
||||||
|
let long_opt_present = false;
|
||||||
|
let long_opt_value = None;
|
||||||
|
let _dummy = TEST_MUTEX.lock().unwrap();
|
||||||
|
|
||||||
|
let result =
|
||||||
|
determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result, BackupMode::ExistingBackup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --backup takes precedence over -b
|
||||||
|
#[test]
|
||||||
|
fn test_backup_mode_long_preferred_over_short() {
|
||||||
|
let short_opt_present = true;
|
||||||
|
let long_opt_present = true;
|
||||||
|
let long_opt_value = Some("none");
|
||||||
|
let _dummy = TEST_MUTEX.lock().unwrap();
|
||||||
|
|
||||||
|
let result =
|
||||||
|
determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result, BackupMode::NoBackup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --backup can be passed without an argument
|
||||||
|
#[test]
|
||||||
|
fn test_backup_mode_long_without_args_no_env() {
|
||||||
|
let short_opt_present = false;
|
||||||
|
let long_opt_present = true;
|
||||||
|
let long_opt_value = None;
|
||||||
|
let _dummy = TEST_MUTEX.lock().unwrap();
|
||||||
|
|
||||||
|
let result =
|
||||||
|
determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result, BackupMode::ExistingBackup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --backup can be passed with an argument only
|
||||||
|
#[test]
|
||||||
|
fn test_backup_mode_long_with_args() {
|
||||||
|
let short_opt_present = false;
|
||||||
|
let long_opt_present = true;
|
||||||
|
let long_opt_value = Some("simple");
|
||||||
|
let _dummy = TEST_MUTEX.lock().unwrap();
|
||||||
|
|
||||||
|
let result =
|
||||||
|
determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result, BackupMode::SimpleBackup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --backup errors on invalid argument
|
||||||
|
#[test]
|
||||||
|
fn test_backup_mode_long_with_args_invalid() {
|
||||||
|
let short_opt_present = false;
|
||||||
|
let long_opt_present = true;
|
||||||
|
let long_opt_value = Some("foobar");
|
||||||
|
let _dummy = TEST_MUTEX.lock().unwrap();
|
||||||
|
|
||||||
|
let result = determine_backup_mode(short_opt_present, long_opt_present, long_opt_value);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let text = result.unwrap_err();
|
||||||
|
assert!(text.contains("invalid argument ‘foobar’ for ‘backup type’"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --backup errors on ambiguous argument
|
||||||
|
#[test]
|
||||||
|
fn test_backup_mode_long_with_args_ambiguous() {
|
||||||
|
let short_opt_present = false;
|
||||||
|
let long_opt_present = true;
|
||||||
|
let long_opt_value = Some("n");
|
||||||
|
let _dummy = TEST_MUTEX.lock().unwrap();
|
||||||
|
|
||||||
|
let result = determine_backup_mode(short_opt_present, long_opt_present, long_opt_value);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let text = result.unwrap_err();
|
||||||
|
assert!(text.contains("ambiguous argument ‘n’ for ‘backup type’"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --backup accepts shortened arguments (si for simple)
|
||||||
|
#[test]
|
||||||
|
fn test_backup_mode_long_with_arg_shortened() {
|
||||||
|
let short_opt_present = false;
|
||||||
|
let long_opt_present = true;
|
||||||
|
let long_opt_value = Some("si");
|
||||||
|
let _dummy = TEST_MUTEX.lock().unwrap();
|
||||||
|
|
||||||
|
let result =
|
||||||
|
determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result, BackupMode::SimpleBackup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -b ignores the "VERSION_CONTROL" environment variable
|
||||||
|
#[test]
|
||||||
|
fn test_backup_mode_short_only_ignore_env() {
|
||||||
|
let short_opt_present = true;
|
||||||
|
let long_opt_present = false;
|
||||||
|
let long_opt_value = None;
|
||||||
|
let _dummy = TEST_MUTEX.lock().unwrap();
|
||||||
|
env::set_var(ENV_VERSION_CONTROL, "none");
|
||||||
|
|
||||||
|
let result =
|
||||||
|
determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result, BackupMode::ExistingBackup);
|
||||||
|
env::remove_var(ENV_VERSION_CONTROL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --backup can be passed without an argument, but reads env var if existent
|
||||||
|
#[test]
|
||||||
|
fn test_backup_mode_long_without_args_with_env() {
|
||||||
|
let short_opt_present = false;
|
||||||
|
let long_opt_present = true;
|
||||||
|
let long_opt_value = None;
|
||||||
|
let _dummy = TEST_MUTEX.lock().unwrap();
|
||||||
|
env::set_var(ENV_VERSION_CONTROL, "none");
|
||||||
|
|
||||||
|
let result =
|
||||||
|
determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result, BackupMode::NoBackup);
|
||||||
|
env::remove_var(ENV_VERSION_CONTROL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --backup errors on invalid VERSION_CONTROL env var
|
||||||
|
#[test]
|
||||||
|
fn test_backup_mode_long_with_env_var_invalid() {
|
||||||
|
let short_opt_present = false;
|
||||||
|
let long_opt_present = true;
|
||||||
|
let long_opt_value = None;
|
||||||
|
let _dummy = TEST_MUTEX.lock().unwrap();
|
||||||
|
env::set_var(ENV_VERSION_CONTROL, "foobar");
|
||||||
|
|
||||||
|
let result = determine_backup_mode(short_opt_present, long_opt_present, long_opt_value);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let text = result.unwrap_err();
|
||||||
|
assert!(text.contains("invalid argument ‘foobar’ for ‘$VERSION_CONTROL’"));
|
||||||
|
env::remove_var(ENV_VERSION_CONTROL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --backup errors on ambiguous VERSION_CONTROL env var
|
||||||
|
#[test]
|
||||||
|
fn test_backup_mode_long_with_env_var_ambiguous() {
|
||||||
|
let short_opt_present = false;
|
||||||
|
let long_opt_present = true;
|
||||||
|
let long_opt_value = None;
|
||||||
|
let _dummy = TEST_MUTEX.lock().unwrap();
|
||||||
|
env::set_var(ENV_VERSION_CONTROL, "n");
|
||||||
|
|
||||||
|
let result = determine_backup_mode(short_opt_present, long_opt_present, long_opt_value);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let text = result.unwrap_err();
|
||||||
|
assert!(text.contains("ambiguous argument ‘n’ for ‘$VERSION_CONTROL’"));
|
||||||
|
env::remove_var(ENV_VERSION_CONTROL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --backup accepts shortened env vars (si for simple)
|
||||||
|
#[test]
|
||||||
|
fn test_backup_mode_long_with_env_var_shortened() {
|
||||||
|
let short_opt_present = false;
|
||||||
|
let long_opt_present = true;
|
||||||
|
let long_opt_value = None;
|
||||||
|
let _dummy = TEST_MUTEX.lock().unwrap();
|
||||||
|
env::set_var(ENV_VERSION_CONTROL, "si");
|
||||||
|
|
||||||
|
let result =
|
||||||
|
determine_backup_mode(short_opt_present, long_opt_present, long_opt_value).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result, BackupMode::SimpleBackup);
|
||||||
|
env::remove_var(ENV_VERSION_CONTROL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -138,6 +138,12 @@ pub enum UError {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UError {
|
impl UError {
|
||||||
|
/// The error code of [`UResult`]
|
||||||
|
///
|
||||||
|
/// This function defines the error code associated with an instance of
|
||||||
|
/// [`UResult`]. To associate error codes for self-defined instances of
|
||||||
|
/// `UResult::Custom` (i.e. [`UCustomError`]), implement the
|
||||||
|
/// [`code`-function there](UCustomError::code).
|
||||||
pub fn code(&self) -> i32 {
|
pub fn code(&self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
UError::Common(e) => e.code(),
|
UError::Common(e) => e.code(),
|
||||||
|
@ -145,6 +151,13 @@ impl UError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether to print usage help for a [`UResult`]
|
||||||
|
///
|
||||||
|
/// Defines if a variant of [`UResult`] should print a short usage message
|
||||||
|
/// below the error. The usage message is printed when this function returns
|
||||||
|
/// `true`. To do this for self-defined instances of `UResult::Custom` (i.e.
|
||||||
|
/// [`UCustomError`]), implement the [`usage`-function
|
||||||
|
/// there](UCustomError::usage).
|
||||||
pub fn usage(&self) -> bool {
|
pub fn usage(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
UError::Common(e) => e.usage(),
|
UError::Common(e) => e.usage(),
|
||||||
|
@ -183,12 +196,13 @@ impl Display for UError {
|
||||||
/// Custom errors defined by the utils.
|
/// Custom errors defined by the utils.
|
||||||
///
|
///
|
||||||
/// All errors should implement [`std::error::Error`], [`std::fmt::Display`] and
|
/// All errors should implement [`std::error::Error`], [`std::fmt::Display`] and
|
||||||
/// [`std::fmt::Debug`] and have an additional `code` method that specifies the exit code of the
|
/// [`std::fmt::Debug`] and have an additional `code` method that specifies the
|
||||||
/// program if the error is returned from `uumain`.
|
/// exit code of the program if the error is returned from `uumain`.
|
||||||
///
|
///
|
||||||
/// An example of a custom error from `ls`:
|
/// An example of a custom error from `ls`:
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use uucore::error::{UCustomError};
|
/// use uucore::error::{UCustomError, UResult};
|
||||||
/// use std::{
|
/// use std::{
|
||||||
/// error::Error,
|
/// error::Error,
|
||||||
/// fmt::{Display, Debug},
|
/// fmt::{Display, Debug},
|
||||||
|
@ -221,13 +235,121 @@ impl Display for UError {
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
/// A crate like [`quick_error`](https://crates.io/crates/quick-error) might also be used, but will
|
///
|
||||||
/// still require an `impl` for the `code` method.
|
/// The main routine would look like this:
|
||||||
pub trait UCustomError: Error {
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// #[uucore_procs::gen_uumain]
|
||||||
|
/// pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
|
/// // Perform computations here ...
|
||||||
|
/// return Err(LsError::InvalidLineWidth(String::from("test")).into())
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The call to `into()` is required to convert the [`UCustomError`] to an
|
||||||
|
/// instance of [`UError`].
|
||||||
|
///
|
||||||
|
/// A crate like [`quick_error`](https://crates.io/crates/quick-error) might
|
||||||
|
/// also be used, but will still require an `impl` for the `code` method.
|
||||||
|
pub trait UCustomError: Error + Send {
|
||||||
|
/// Error code of a custom error.
|
||||||
|
///
|
||||||
|
/// Set a return value for each variant of an enum-type to associate an
|
||||||
|
/// error code (which is returned to the system shell) with an error
|
||||||
|
/// variant.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use uucore::error::{UCustomError};
|
||||||
|
/// use std::{
|
||||||
|
/// error::Error,
|
||||||
|
/// fmt::{Display, Debug},
|
||||||
|
/// path::PathBuf
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// #[derive(Debug)]
|
||||||
|
/// enum MyError {
|
||||||
|
/// Foo(String),
|
||||||
|
/// Bar(PathBuf),
|
||||||
|
/// Bing(),
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl UCustomError for MyError {
|
||||||
|
/// fn code(&self) -> i32 {
|
||||||
|
/// match self {
|
||||||
|
/// MyError::Foo(_) => 2,
|
||||||
|
/// // All other errors yield the same error code, there's no
|
||||||
|
/// // need to list them explicitly.
|
||||||
|
/// _ => 1,
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl Error for MyError {}
|
||||||
|
///
|
||||||
|
/// impl Display for MyError {
|
||||||
|
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
/// use MyError as ME;
|
||||||
|
/// match self {
|
||||||
|
/// ME::Foo(s) => write!(f, "Unknown Foo: '{}'", s),
|
||||||
|
/// ME::Bar(p) => write!(f, "Couldn't find Bar: '{}'", p.display()),
|
||||||
|
/// ME::Bing() => write!(f, "Exterminate!"),
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
fn code(&self) -> i32 {
|
fn code(&self) -> i32 {
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Print usage help to a custom error.
|
||||||
|
///
|
||||||
|
/// Return true or false to control whether a short usage help is printed
|
||||||
|
/// below the error message. The usage help is in the format: "Try '{name}
|
||||||
|
/// --help' for more information." and printed only if `true` is returned.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use uucore::error::{UCustomError};
|
||||||
|
/// use std::{
|
||||||
|
/// error::Error,
|
||||||
|
/// fmt::{Display, Debug},
|
||||||
|
/// path::PathBuf
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// #[derive(Debug)]
|
||||||
|
/// enum MyError {
|
||||||
|
/// Foo(String),
|
||||||
|
/// Bar(PathBuf),
|
||||||
|
/// Bing(),
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl UCustomError for MyError {
|
||||||
|
/// fn usage(&self) -> bool {
|
||||||
|
/// match self {
|
||||||
|
/// // This will have a short usage help appended
|
||||||
|
/// MyError::Bar(_) => true,
|
||||||
|
/// // These matches won't have a short usage help appended
|
||||||
|
/// _ => false,
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl Error for MyError {}
|
||||||
|
///
|
||||||
|
/// impl Display for MyError {
|
||||||
|
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
/// use MyError as ME;
|
||||||
|
/// match self {
|
||||||
|
/// ME::Foo(s) => write!(f, "Unknown Foo: '{}'", s),
|
||||||
|
/// ME::Bar(p) => write!(f, "Couldn't find Bar: '{}'", p.display()),
|
||||||
|
/// ME::Bing() => write!(f, "Exterminate!"),
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
fn usage(&self) -> bool {
|
fn usage(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
|
@ -398,8 +398,7 @@ fn test_du_time() {
|
||||||
let result = ts.ucmd().arg("--time=ctime").arg("date_test").succeeds();
|
let result = ts.ucmd().arg("--time=ctime").arg("date_test").succeeds();
|
||||||
result.stdout_only("0\t2016-06-16 00:00\tdate_test\n");
|
result.stdout_only("0\t2016-06-16 00:00\tdate_test\n");
|
||||||
|
|
||||||
#[cfg(not(target_env = "musl"))]
|
if birth_supported() {
|
||||||
{
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
let re_birth =
|
let re_birth =
|
||||||
|
@ -409,6 +408,16 @@ fn test_du_time() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "touch")]
|
||||||
|
fn birth_supported() -> bool {
|
||||||
|
let ts = TestScenario::new(util_name!());
|
||||||
|
let m = match std::fs::metadata(ts.fixtures.subdir) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => panic!("{}", e),
|
||||||
|
};
|
||||||
|
m.created().is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
#[cfg(feature = "chmod")]
|
#[cfg(feature = "chmod")]
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::common::util::*;
|
use crate::common::util::*;
|
||||||
|
// spell-checker:ignore (ToDO) taaaa tbbbb tcccc
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_with_tab() {
|
fn test_with_tab() {
|
||||||
|
@ -53,3 +54,140 @@ fn test_with_multiple_files() {
|
||||||
.stdout_contains(" return")
|
.stdout_contains(" return")
|
||||||
.stdout_contains(" ");
|
.stdout_contains(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tabs_space_separated_list() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["--tabs", "3 6 9"])
|
||||||
|
.pipe_in("a\tb\tc\td\te")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_is("a b c d e");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tabs_mixed_style_list() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["--tabs", ", 3,6 9"])
|
||||||
|
.pipe_in("a\tb\tc\td\te")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_is("a b c d e");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tabs_empty_string() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["--tabs", ""])
|
||||||
|
.pipe_in("a\tb\tc")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_is("a b c");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tabs_comma_only() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["--tabs", ","])
|
||||||
|
.pipe_in("a\tb\tc")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_is("a b c");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tabs_space_only() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["--tabs", " "])
|
||||||
|
.pipe_in("a\tb\tc")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_is("a b c");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tabs_slash() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["--tabs", "/"])
|
||||||
|
.pipe_in("a\tb\tc")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_is("a b c");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tabs_plus() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["--tabs", "+"])
|
||||||
|
.pipe_in("a\tb\tc")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_is("a b c");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tabs_trailing_slash() {
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("--tabs=1,/5")
|
||||||
|
.pipe_in("\ta\tb\tc")
|
||||||
|
.succeeds()
|
||||||
|
// 0 1
|
||||||
|
// 01234567890
|
||||||
|
.stdout_is(" a b c");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tabs_trailing_slash_long_columns() {
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("--tabs=1,/3")
|
||||||
|
.pipe_in("\taaaa\tbbbb\tcccc")
|
||||||
|
.succeeds()
|
||||||
|
// 0 1
|
||||||
|
// 01234567890123456
|
||||||
|
.stdout_is(" aaaa bbbb cccc");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tabs_trailing_plus() {
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("--tabs=1,+5")
|
||||||
|
.pipe_in("\ta\tb\tc")
|
||||||
|
.succeeds()
|
||||||
|
// 0 1
|
||||||
|
// 012345678901
|
||||||
|
.stdout_is(" a b c");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tabs_trailing_plus_long_columns() {
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("--tabs=1,+3")
|
||||||
|
.pipe_in("\taaaa\tbbbb\tcccc")
|
||||||
|
.succeeds()
|
||||||
|
// 0 1
|
||||||
|
// 012345678901234567
|
||||||
|
.stdout_is(" aaaa bbbb cccc");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tabs_must_be_ascending() {
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("--tabs=1,1")
|
||||||
|
.fails()
|
||||||
|
.stderr_contains("tab sizes must be ascending");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tabs_keep_last_trailing_specifier() {
|
||||||
|
// If there are multiple trailing specifiers, use only the last one
|
||||||
|
// before the number.
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("--tabs=1,+/+/5")
|
||||||
|
.pipe_in("\ta\tb\tc")
|
||||||
|
.succeeds()
|
||||||
|
// 0 1
|
||||||
|
// 01234567890
|
||||||
|
.stdout_is(" a b c");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tabs_comma_separated_no_numbers() {
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("--tabs=+,/,+,/")
|
||||||
|
.pipe_in("\ta\tb\tc")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_is(" a b c");
|
||||||
|
}
|
||||||
|
|
|
@ -110,3 +110,8 @@ fn test_sleep_sum_duration_many() {
|
||||||
let duration = before_test.elapsed();
|
let duration = before_test.elapsed();
|
||||||
assert!(duration >= millis_900);
|
assert!(duration >= millis_900);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sleep_wrong_time() {
|
||||||
|
new_ucmd!().args(&["0.1s", "abc"]).fails();
|
||||||
|
}
|
||||||
|
|
|
@ -181,7 +181,7 @@ fn test_check_zero_terminated_failure() {
|
||||||
.arg("-c")
|
.arg("-c")
|
||||||
.arg("zero-terminated.txt")
|
.arg("zero-terminated.txt")
|
||||||
.fails()
|
.fails()
|
||||||
.stdout_is("sort: zero-terminated.txt:2: disorder: ../../fixtures/du\n");
|
.stderr_only("sort: zero-terminated.txt:2: disorder: ../../fixtures/du\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -220,32 +220,29 @@ fn test_random_shuffle_contains_all_lines() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_random_shuffle_two_runs_not_the_same() {
|
fn test_random_shuffle_two_runs_not_the_same() {
|
||||||
// check to verify that two random shuffles are not equal; this has the
|
for arg in &["-R", "-k1,1R"] {
|
||||||
// potential to fail in the very unlikely event that the random order is the same
|
// check to verify that two random shuffles are not equal; this has the
|
||||||
// as the starting order, or if both random sorts end up having the same order.
|
// potential to fail in the very unlikely event that the random order is the same
|
||||||
const FILE: &str = "default_unsorted_ints.expected";
|
// as the starting order, or if both random sorts end up having the same order.
|
||||||
let (at, _ucmd) = at_and_ucmd!();
|
const FILE: &str = "default_unsorted_ints.expected";
|
||||||
let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str();
|
let (at, _ucmd) = at_and_ucmd!();
|
||||||
let expected = at.read(FILE);
|
let result = new_ucmd!().arg(arg).arg(FILE).run().stdout_move_str();
|
||||||
let unexpected = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str();
|
let expected = at.read(FILE);
|
||||||
|
let unexpected = new_ucmd!().arg(arg).arg(FILE).run().stdout_move_str();
|
||||||
|
|
||||||
assert_ne!(result, expected);
|
assert_ne!(result, expected);
|
||||||
assert_ne!(result, unexpected);
|
assert_ne!(result, unexpected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_random_shuffle_contains_two_runs_not_the_same() {
|
fn test_random_ignore_case() {
|
||||||
// check to verify that two random shuffles are not equal; this has the
|
let input = "ABC\nABc\nAbC\nAbc\naBC\naBc\nabC\nabc\n";
|
||||||
// potential to fail in the unlikely event that random order is the same
|
new_ucmd!()
|
||||||
// as the starting order, or if both random sorts end up having the same order.
|
.args(&["-fR"])
|
||||||
const FILE: &str = "default_unsorted_ints.expected";
|
.pipe_in(input)
|
||||||
let (at, _ucmd) = at_and_ucmd!();
|
.succeeds()
|
||||||
let result = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str();
|
.stdout_is(input);
|
||||||
let expected = at.read(FILE);
|
|
||||||
let unexpected = new_ucmd!().arg("-R").arg(FILE).run().stdout_move_str();
|
|
||||||
|
|
||||||
assert_ne!(result, expected);
|
|
||||||
assert_ne!(result, unexpected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -774,14 +771,15 @@ fn test_check() {
|
||||||
new_ucmd!()
|
new_ucmd!()
|
||||||
.arg(diagnose_arg)
|
.arg(diagnose_arg)
|
||||||
.arg("check_fail.txt")
|
.arg("check_fail.txt")
|
||||||
|
.arg("--buffer-size=10b")
|
||||||
.fails()
|
.fails()
|
||||||
.stdout_is("sort: check_fail.txt:6: disorder: 5\n");
|
.stderr_only("sort: check_fail.txt:6: disorder: 5\n");
|
||||||
|
|
||||||
new_ucmd!()
|
new_ucmd!()
|
||||||
.arg(diagnose_arg)
|
.arg(diagnose_arg)
|
||||||
.arg("multiple_files.expected")
|
.arg("multiple_files.expected")
|
||||||
.succeeds()
|
.succeeds()
|
||||||
.stdout_is("");
|
.stderr_is("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -796,6 +794,18 @@ fn test_check_silent() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_check_unique() {
|
||||||
|
// Due to a clap bug the combination "-cu" does not work. "-c -u" works.
|
||||||
|
// See https://github.com/clap-rs/clap/issues/2624
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-c", "-u"])
|
||||||
|
.pipe_in("A\nA\n")
|
||||||
|
.fails()
|
||||||
|
.code_is(1)
|
||||||
|
.stderr_only("sort: -:2: disorder: A");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dictionary_and_nonprinting_conflicts() {
|
fn test_dictionary_and_nonprinting_conflicts() {
|
||||||
let conflicting_args = ["n", "h", "g", "M"];
|
let conflicting_args = ["n", "h", "g", "M"];
|
||||||
|
@ -839,9 +849,9 @@ fn test_nonexistent_file() {
|
||||||
.status_code(2)
|
.status_code(2)
|
||||||
.stderr_only(
|
.stderr_only(
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
"sort: cannot read: \"nonexistent.txt\": No such file or directory (os error 2)",
|
"sort: cannot read: nonexistent.txt: No such file or directory",
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
"sort: cannot read: \"nonexistent.txt\": The system cannot find the file specified. (os error 2)",
|
"sort: cannot read: nonexistent.txt: The system cannot find the file specified.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -883,6 +893,29 @@ fn test_compress() {
|
||||||
.stdout_only_fixture("ext_sort.expected");
|
.stdout_only_fixture("ext_sort.expected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn test_compress_merge() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&[
|
||||||
|
"--compress-program",
|
||||||
|
"gzip",
|
||||||
|
"-S",
|
||||||
|
"10",
|
||||||
|
"--batch-size=2",
|
||||||
|
"-m",
|
||||||
|
"--unique",
|
||||||
|
"merge_ints_interleaved_1.txt",
|
||||||
|
"merge_ints_interleaved_2.txt",
|
||||||
|
"merge_ints_interleaved_3.txt",
|
||||||
|
"merge_ints_interleaved_3.txt",
|
||||||
|
"merge_ints_interleaved_2.txt",
|
||||||
|
"merge_ints_interleaved_1.txt",
|
||||||
|
])
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only_fixture("merge_ints_interleaved.expected");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_compress_fail() {
|
fn test_compress_fail() {
|
||||||
TestScenario::new(util_name!())
|
TestScenario::new(util_name!())
|
||||||
|
@ -959,3 +992,102 @@ fn test_key_takes_one_arg() {
|
||||||
.succeeds()
|
.succeeds()
|
||||||
.stdout_is_fixture("keys_open_ended.expected");
|
.stdout_is_fixture("keys_open_ended.expected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verifies_out_file() {
|
||||||
|
let inputs = ["" /* no input */, "some input"];
|
||||||
|
for &input in &inputs {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-o", "nonexistent_dir/nonexistent_file"])
|
||||||
|
.pipe_in(input)
|
||||||
|
.ignore_stdin_write_error()
|
||||||
|
.fails()
|
||||||
|
.status_code(2)
|
||||||
|
.stderr_only(
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
"sort: open failed: nonexistent_dir/nonexistent_file: No such file or directory",
|
||||||
|
#[cfg(windows)]
|
||||||
|
"sort: open failed: nonexistent_dir/nonexistent_file: The system cannot find the path specified.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verifies_files_after_keys() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&[
|
||||||
|
"-o",
|
||||||
|
"nonexistent_dir/nonexistent_file",
|
||||||
|
"-k",
|
||||||
|
"0",
|
||||||
|
"nonexistent_dir/input_file",
|
||||||
|
])
|
||||||
|
.fails()
|
||||||
|
.status_code(2)
|
||||||
|
.stderr_contains("failed to parse key");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn test_verifies_input_files() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["/dev/random", "nonexistent_file"])
|
||||||
|
.fails()
|
||||||
|
.status_code(2)
|
||||||
|
.stderr_is("sort: cannot read: nonexistent_file: No such file or directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_separator_null() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-k1,1", "-k3,3", "-t", "\\0"])
|
||||||
|
.pipe_in("z\0a\0b\nz\0b\0a\na\0z\0z\n")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only("a\0z\0z\nz\0b\0a\nz\0a\0b\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_is_input() {
|
||||||
|
let input = "a\nb\nc\n";
|
||||||
|
let (at, mut cmd) = at_and_ucmd!();
|
||||||
|
at.touch("file");
|
||||||
|
at.append("file", input);
|
||||||
|
cmd.args(&["-m", "-u", "-o", "file", "file", "file", "file"])
|
||||||
|
.succeeds();
|
||||||
|
assert_eq!(at.read("file"), input);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn test_output_device() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-o", "/dev/null"])
|
||||||
|
.pipe_in("input")
|
||||||
|
.succeeds();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_empty_input() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-m", "empty.txt"])
|
||||||
|
.succeeds()
|
||||||
|
.no_stderr()
|
||||||
|
.no_stdout();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_error_for_version() {
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("--version")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_contains("sort");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wrong_args_exit_code() {
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("--misspelled")
|
||||||
|
.fails()
|
||||||
|
.status_code(2)
|
||||||
|
.stderr_contains("--misspelled");
|
||||||
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ mod test_generate_tokens {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn printf_format() {
|
fn printf_format() {
|
||||||
let s = "%-# 15a\\r\\\"\\\\\\a\\b\\e\\f\\v%+020.-23w\\x12\\167\\132\\112\\n";
|
let s = "%-# 15a\\t\\r\\\"\\\\\\a\\b\\e\\f\\v%+020.-23w\\x12\\167\\132\\112\\n";
|
||||||
let expected = vec![
|
let expected = vec![
|
||||||
Token::Directive {
|
Token::Directive {
|
||||||
flag: F_LEFT | F_ALTER | F_SPACE,
|
flag: F_LEFT | F_ALTER | F_SPACE,
|
||||||
|
@ -72,6 +72,7 @@ mod test_generate_tokens {
|
||||||
precision: -1,
|
precision: -1,
|
||||||
format: 'a',
|
format: 'a',
|
||||||
},
|
},
|
||||||
|
Token::Char('\t'),
|
||||||
Token::Char('\r'),
|
Token::Char('\r'),
|
||||||
Token::Char('"'),
|
Token::Char('"'),
|
||||||
Token::Char('\\'),
|
Token::Char('\\'),
|
||||||
|
|
|
@ -66,5 +66,10 @@ fn test_invalid_input() {
|
||||||
.ucmd()
|
.ucmd()
|
||||||
.arg("a")
|
.arg("a")
|
||||||
.fails()
|
.fails()
|
||||||
.stderr_contains("dir: read error: Invalid argument");
|
.stderr_contains("a: read error: Invalid argument");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_line_separators() {
|
||||||
|
new_ucmd!().pipe_in("a").succeeds().stdout_is("a");
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,15 @@ fn test_stdin_default() {
|
||||||
.stdout_is_fixture("foobar_stdin_default.expected");
|
.stdout_is_fixture("foobar_stdin_default.expected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stdin_explicit() {
|
||||||
|
new_ucmd!()
|
||||||
|
.pipe_in_fixture(FOOBAR_TXT)
|
||||||
|
.arg("-")
|
||||||
|
.run()
|
||||||
|
.stdout_is_fixture("foobar_stdin_default.expected");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_single_default() {
|
fn test_single_default() {
|
||||||
new_ucmd!()
|
new_ucmd!()
|
||||||
|
|
0
tests/fixtures/sort/empty.txt
vendored
Normal file
0
tests/fixtures/sort/empty.txt
vendored
Normal file
Loading…
Add table
Add a link
Reference in a new issue