mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
Merge branch 'main' into whitespace
This commit is contained in:
commit
bae78e62e0
25 changed files with 1507 additions and 393 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2660,6 +2660,7 @@ version = "0.0.16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"fs_extra",
|
"fs_extra",
|
||||||
|
"indicatif",
|
||||||
"uucore",
|
"uucore",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2984,6 +2985,7 @@ dependencies = [
|
||||||
name = "uu_tail"
|
name = "uu_tail"
|
||||||
version = "0.0.16"
|
version = "0.0.16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"atty",
|
||||||
"clap",
|
"clap",
|
||||||
"libc",
|
"libc",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
25
build.rs
25
build.rs
|
@ -6,10 +6,8 @@ use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
// println!("cargo:warning=Running build.rs");
|
|
||||||
|
|
||||||
if let Ok(profile) = env::var("PROFILE") {
|
if let Ok(profile) = env::var("PROFILE") {
|
||||||
println!("cargo:rustc-cfg=build={:?}", profile);
|
println!("cargo:rustc-cfg=build={profile:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
const ENV_FEATURE_PREFIX: &str = "CARGO_FEATURE_";
|
const ENV_FEATURE_PREFIX: &str = "CARGO_FEATURE_";
|
||||||
|
@ -17,7 +15,6 @@ pub fn main() {
|
||||||
const OVERRIDE_PREFIX: &str = "uu_";
|
const OVERRIDE_PREFIX: &str = "uu_";
|
||||||
|
|
||||||
let out_dir = env::var("OUT_DIR").unwrap();
|
let out_dir = env::var("OUT_DIR").unwrap();
|
||||||
// println!("cargo:warning=out_dir={}", out_dir);
|
|
||||||
|
|
||||||
let mut crates = Vec::new();
|
let mut crates = Vec::new();
|
||||||
for (key, val) in env::vars() {
|
for (key, val) in env::vars() {
|
||||||
|
@ -48,7 +45,7 @@ pub fn main() {
|
||||||
|
|
||||||
let mut phf_map = phf_codegen::Map::<&str>::new();
|
let mut phf_map = phf_codegen::Map::<&str>::new();
|
||||||
for krate in &crates {
|
for krate in &crates {
|
||||||
let map_value = format!("({krate}::uumain, {krate}::uu_app)", krate = krate);
|
let map_value = format!("({krate}::uumain, {krate}::uu_app)");
|
||||||
match krate.as_ref() {
|
match krate.as_ref() {
|
||||||
// 'test' is named uu_test to avoid collision with rust core crate 'test'.
|
// 'test' is named uu_test to avoid collision with rust core crate 'test'.
|
||||||
// It can also be invoked by name '[' for the '[ expr ] syntax'.
|
// It can also be invoked by name '[' for the '[ expr ] syntax'.
|
||||||
|
@ -60,22 +57,14 @@ pub fn main() {
|
||||||
phf_map.entry(&k[OVERRIDE_PREFIX.len()..], &map_value);
|
phf_map.entry(&k[OVERRIDE_PREFIX.len()..], &map_value);
|
||||||
}
|
}
|
||||||
"false" | "true" => {
|
"false" | "true" => {
|
||||||
phf_map.entry(
|
phf_map.entry(krate, &format!("(r#{krate}::uumain, r#{krate}::uu_app)"));
|
||||||
krate,
|
|
||||||
&format!("(r#{krate}::uumain, r#{krate}::uu_app)", krate = krate),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
"hashsum" => {
|
"hashsum" => {
|
||||||
phf_map.entry(
|
phf_map.entry(krate, &format!("({krate}::uumain, {krate}::uu_app_custom)"));
|
||||||
krate,
|
|
||||||
&format!("({krate}::uumain, {krate}::uu_app_custom)", krate = krate),
|
|
||||||
);
|
|
||||||
|
|
||||||
let map_value = format!("({krate}::uumain, {krate}::uu_app_common)", krate = krate);
|
let map_value = format!("({krate}::uumain, {krate}::uu_app_common)");
|
||||||
let map_value_bits =
|
let map_value_bits = format!("({krate}::uumain, {krate}::uu_app_bits)");
|
||||||
format!("({krate}::uumain, {krate}::uu_app_bits)", krate = krate);
|
let map_value_b3sum = format!("({krate}::uumain, {krate}::uu_app_b3sum)");
|
||||||
let map_value_b3sum =
|
|
||||||
format!("({krate}::uumain, {krate}::uu_app_b3sum)", krate = krate);
|
|
||||||
phf_map.entry("md5sum", &map_value);
|
phf_map.entry("md5sum", &map_value);
|
||||||
phf_map.entry("sha1sum", &map_value);
|
phf_map.entry("sha1sum", &map_value);
|
||||||
phf_map.entry("sha224sum", &map_value);
|
phf_map.entry("sha224sum", &map_value);
|
||||||
|
|
|
@ -9,6 +9,10 @@ extensions.
|
||||||
|
|
||||||
`cp` can display a progress bar when the `-g`/`--progress` flag is set.
|
`cp` can display a progress bar when the `-g`/`--progress` flag is set.
|
||||||
|
|
||||||
|
## `mv`
|
||||||
|
|
||||||
|
`mv` can display a progress bar when the `-g`/`--progress` flag is set.
|
||||||
|
|
||||||
## `hashsum`
|
## `hashsum`
|
||||||
|
|
||||||
This utility does not exist in GNU coreutils. `hashsum` is a utility that
|
This utility does not exist in GNU coreutils. `hashsum` is a utility that
|
||||||
|
|
|
@ -20,8 +20,8 @@ const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
include!(concat!(env!("OUT_DIR"), "/uutils_map.rs"));
|
include!(concat!(env!("OUT_DIR"), "/uutils_map.rs"));
|
||||||
|
|
||||||
fn usage<T>(utils: &UtilityMap<T>, name: &str) {
|
fn usage<T>(utils: &UtilityMap<T>, name: &str) {
|
||||||
println!("{} {} (multi-call binary)\n", name, VERSION);
|
println!("{name} {VERSION} (multi-call binary)\n");
|
||||||
println!("Usage: {} [function [arguments...]]\n", name);
|
println!("Usage: {name} [function [arguments...]]\n");
|
||||||
println!("Currently defined functions:\n");
|
println!("Currently defined functions:\n");
|
||||||
#[allow(clippy::map_clone)]
|
#[allow(clippy::map_clone)]
|
||||||
let mut utils: Vec<&str> = utils.keys().map(|&s| s).collect();
|
let mut utils: Vec<&str> = utils.keys().map(|&s| s).collect();
|
||||||
|
@ -153,7 +153,7 @@ fn gen_completions<T: uucore::Args>(
|
||||||
.get_matches_from(std::iter::once(OsString::from("completion")).chain(args));
|
.get_matches_from(std::iter::once(OsString::from("completion")).chain(args));
|
||||||
|
|
||||||
let utility = matches.get_one::<String>("utility").unwrap();
|
let utility = matches.get_one::<String>("utility").unwrap();
|
||||||
let shell = matches.get_one::<Shell>("shell").unwrap().to_owned();
|
let shell = *matches.get_one::<Shell>("shell").unwrap();
|
||||||
|
|
||||||
let mut command = if utility == "coreutils" {
|
let mut command = if utility == "coreutils" {
|
||||||
gen_coreutils_app(util_map)
|
gen_coreutils_app(util_map)
|
||||||
|
|
|
@ -287,7 +287,7 @@ where
|
||||||
OutputFmt::Shell => result.push_str("LS_COLORS='"),
|
OutputFmt::Shell => result.push_str("LS_COLORS='"),
|
||||||
OutputFmt::CShell => result.push_str("setenv LS_COLORS '"),
|
OutputFmt::CShell => result.push_str("setenv LS_COLORS '"),
|
||||||
OutputFmt::Display => (),
|
OutputFmt::Display => (),
|
||||||
_ => unreachable!(),
|
OutputFmt::Unknown => unreachable!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut table: HashMap<&str, &str> = HashMap::with_capacity(48);
|
let mut table: HashMap<&str, &str> = HashMap::with_capacity(48);
|
||||||
|
@ -405,7 +405,7 @@ where
|
||||||
// remove latest "\n"
|
// remove latest "\n"
|
||||||
result.pop();
|
result.pop();
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
OutputFmt::Unknown => unreachable!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
|
|
|
@ -634,7 +634,7 @@ where
|
||||||
match bad_format.cmp(&1) {
|
match bad_format.cmp(&1) {
|
||||||
Ordering::Equal => show_warning!("{} line is improperly formatted", bad_format),
|
Ordering::Equal => show_warning!("{} line is improperly formatted", bad_format),
|
||||||
Ordering::Greater => show_warning!("{} lines are improperly formatted", bad_format),
|
Ordering::Greater => show_warning!("{} lines are improperly formatted", bad_format),
|
||||||
_ => {}
|
Ordering::Less => {}
|
||||||
};
|
};
|
||||||
if failed_cksum > 0 {
|
if failed_cksum > 0 {
|
||||||
show_warning!("{} computed checksum did NOT match", failed_cksum);
|
show_warning!("{} computed checksum did NOT match", failed_cksum);
|
||||||
|
@ -644,7 +644,7 @@ where
|
||||||
Ordering::Greater => {
|
Ordering::Greater => {
|
||||||
show_warning!("{} listed files could not be read", failed_open_file);
|
show_warning!("{} listed files could not be read", failed_open_file);
|
||||||
}
|
}
|
||||||
_ => {}
|
Ordering::Less => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,8 @@ path = "src/mv.rs"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.0", features = ["wrap_help", "cargo"] }
|
clap = { version = "4.0", features = ["wrap_help", "cargo"] }
|
||||||
fs_extra = "1.1.0"
|
fs_extra = "1.1.0"
|
||||||
|
indicatif = "0.17"
|
||||||
|
|
||||||
uucore = { version=">=0.0.16", package="uucore", path="../../uucore" }
|
uucore = { version=">=0.0.16", package="uucore", path="../../uucore" }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
|
@ -12,6 +12,7 @@ mod error;
|
||||||
|
|
||||||
use clap::builder::ValueParser;
|
use clap::builder::ValueParser;
|
||||||
use clap::{crate_version, error::ErrorKind, Arg, ArgAction, ArgMatches, Command};
|
use clap::{crate_version, error::ErrorKind, Arg, ArgAction, ArgMatches, Command};
|
||||||
|
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
@ -24,9 +25,12 @@ use std::path::{Path, PathBuf};
|
||||||
use uucore::backup_control::{self, BackupMode};
|
use uucore::backup_control::{self, BackupMode};
|
||||||
use uucore::display::Quotable;
|
use uucore::display::Quotable;
|
||||||
use uucore::error::{FromIo, UError, UResult, USimpleError, UUsageError};
|
use uucore::error::{FromIo, UError, UResult, USimpleError, UUsageError};
|
||||||
use uucore::{format_usage, prompt_yes, show, show_if_err};
|
use uucore::{format_usage, prompt_yes, show};
|
||||||
|
|
||||||
use fs_extra::dir::{move_dir, CopyOptions as DirCopyOptions};
|
use fs_extra::dir::{
|
||||||
|
get_size as dir_get_size, move_dir, move_dir_with_progress, CopyOptions as DirCopyOptions,
|
||||||
|
TransitProcess, TransitProcessResult,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::error::MvError;
|
use crate::error::MvError;
|
||||||
|
|
||||||
|
@ -39,6 +43,7 @@ pub struct Behavior {
|
||||||
no_target_dir: bool,
|
no_target_dir: bool,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
strip_slashes: bool,
|
strip_slashes: bool,
|
||||||
|
progress_bar: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq)]
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
@ -63,7 +68,7 @@ static OPT_TARGET_DIRECTORY: &str = "target-directory";
|
||||||
static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory";
|
static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory";
|
||||||
static OPT_UPDATE: &str = "update";
|
static OPT_UPDATE: &str = "update";
|
||||||
static OPT_VERBOSE: &str = "verbose";
|
static OPT_VERBOSE: &str = "verbose";
|
||||||
|
static OPT_PROGRESS: &str = "progress";
|
||||||
static ARG_FILES: &str = "files";
|
static ARG_FILES: &str = "files";
|
||||||
|
|
||||||
#[uucore::main]
|
#[uucore::main]
|
||||||
|
@ -122,6 +127,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
no_target_dir: matches.get_flag(OPT_NO_TARGET_DIRECTORY),
|
no_target_dir: matches.get_flag(OPT_NO_TARGET_DIRECTORY),
|
||||||
verbose: matches.get_flag(OPT_VERBOSE),
|
verbose: matches.get_flag(OPT_VERBOSE),
|
||||||
strip_slashes: matches.get_flag(OPT_STRIP_TRAILING_SLASHES),
|
strip_slashes: matches.get_flag(OPT_STRIP_TRAILING_SLASHES),
|
||||||
|
progress_bar: matches.get_flag(OPT_PROGRESS),
|
||||||
};
|
};
|
||||||
|
|
||||||
exec(&files[..], &behavior)
|
exec(&files[..], &behavior)
|
||||||
|
@ -197,6 +203,16 @@ pub fn uu_app() -> Command {
|
||||||
.help("explain what is being done")
|
.help("explain what is being done")
|
||||||
.action(ArgAction::SetTrue),
|
.action(ArgAction::SetTrue),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(OPT_PROGRESS)
|
||||||
|
.short('g')
|
||||||
|
.long(OPT_PROGRESS)
|
||||||
|
.help(
|
||||||
|
"Display a progress bar. \n\
|
||||||
|
Note: this feature is not supported by GNU coreutils.",
|
||||||
|
)
|
||||||
|
.action(ArgAction::SetTrue),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(ARG_FILES)
|
Arg::new(ARG_FILES)
|
||||||
.action(ArgAction::Append)
|
.action(ArgAction::Append)
|
||||||
|
@ -271,7 +287,7 @@ fn exec(files: &[OsString], b: &Behavior) -> UResult<()> {
|
||||||
if !source.is_dir() {
|
if !source.is_dir() {
|
||||||
Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into())
|
Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into())
|
||||||
} else {
|
} else {
|
||||||
rename(source, target, b).map_err_context(|| {
|
rename(source, target, b, None).map_err_context(|| {
|
||||||
format!("cannot move {} to {}", source.quote(), target.quote())
|
format!("cannot move {} to {}", source.quote(), target.quote())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -294,7 +310,7 @@ fn exec(files: &[OsString], b: &Behavior) -> UResult<()> {
|
||||||
)
|
)
|
||||||
.into())
|
.into())
|
||||||
} else {
|
} else {
|
||||||
rename(source, target, b).map_err(|e| USimpleError::new(1, format!("{}", e)))
|
rename(source, target, b, None).map_err(|e| USimpleError::new(1, format!("{}", e)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -321,7 +337,27 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.unwrap_or_else(|_| target_dir.to_path_buf());
|
.unwrap_or_else(|_| target_dir.to_path_buf());
|
||||||
|
|
||||||
|
let multi_progress = b.progress_bar.then(MultiProgress::new);
|
||||||
|
|
||||||
|
let count_progress = if let Some(ref multi_progress) = multi_progress {
|
||||||
|
if files.len() > 1 {
|
||||||
|
Some(multi_progress.add(
|
||||||
|
ProgressBar::new(files.len().try_into().unwrap()).with_style(
|
||||||
|
ProgressStyle::with_template("moving {msg} {wide_bar} {pos}/{len}").unwrap(),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
for sourcepath in files.iter() {
|
for sourcepath in files.iter() {
|
||||||
|
if let Some(ref pb) = count_progress {
|
||||||
|
pb.set_message(sourcepath.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
let targetpath = match sourcepath.file_name() {
|
let targetpath = match sourcepath.file_name() {
|
||||||
Some(name) => target_dir.join(name),
|
Some(name) => target_dir.join(name),
|
||||||
None => {
|
None => {
|
||||||
|
@ -352,18 +388,35 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
show_if_err!(
|
let rename_result = rename(sourcepath, &targetpath, b, multi_progress.as_ref())
|
||||||
rename(sourcepath, &targetpath, b).map_err_context(|| format!(
|
.map_err_context(|| {
|
||||||
"cannot move {} to {}",
|
format!(
|
||||||
sourcepath.quote(),
|
"cannot move {} to {}",
|
||||||
targetpath.quote()
|
sourcepath.quote(),
|
||||||
))
|
targetpath.quote()
|
||||||
);
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(e) = rename_result {
|
||||||
|
match multi_progress {
|
||||||
|
Some(ref pb) => pb.suspend(|| show!(e)),
|
||||||
|
None => show!(e),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ref pb) = count_progress {
|
||||||
|
pb.inc(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rename(from: &Path, to: &Path, b: &Behavior) -> io::Result<()> {
|
fn rename(
|
||||||
|
from: &Path,
|
||||||
|
to: &Path,
|
||||||
|
b: &Behavior,
|
||||||
|
multi_progress: Option<&MultiProgress>,
|
||||||
|
) -> io::Result<()> {
|
||||||
let mut backup_path = None;
|
let mut backup_path = None;
|
||||||
|
|
||||||
if to.exists() {
|
if to.exists() {
|
||||||
|
@ -385,7 +438,7 @@ fn rename(from: &Path, to: &Path, b: &Behavior) -> io::Result<()> {
|
||||||
|
|
||||||
backup_path = backup_control::get_backup_path(b.backup, to, &b.suffix);
|
backup_path = backup_control::get_backup_path(b.backup, to, &b.suffix);
|
||||||
if let Some(ref backup_path) = backup_path {
|
if let Some(ref backup_path) = backup_path {
|
||||||
rename_with_fallback(to, backup_path)?;
|
rename_with_fallback(to, backup_path, multi_progress)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.update && fs::metadata(from)?.modified()? <= fs::metadata(to)?.modified()? {
|
if b.update && fs::metadata(from)?.modified()? <= fs::metadata(to)?.modified()? {
|
||||||
|
@ -405,21 +458,36 @@ fn rename(from: &Path, to: &Path, b: &Behavior) -> io::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rename_with_fallback(from, to)?;
|
rename_with_fallback(from, to, multi_progress)?;
|
||||||
|
|
||||||
if b.verbose {
|
if b.verbose {
|
||||||
print!("{} -> {}", from.quote(), to.quote());
|
let message = match backup_path {
|
||||||
match backup_path {
|
Some(path) => format!(
|
||||||
Some(path) => println!(" (backup: {})", path.quote()),
|
"{} -> {} (backup: {})",
|
||||||
None => println!(),
|
from.quote(),
|
||||||
}
|
to.quote(),
|
||||||
|
path.quote()
|
||||||
|
),
|
||||||
|
None => format!("{} -> {}", from.quote(), to.quote()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match multi_progress {
|
||||||
|
Some(pb) => pb.suspend(|| {
|
||||||
|
println!("{}", message);
|
||||||
|
}),
|
||||||
|
None => println!("{}", message),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A wrapper around `fs::rename`, so that if it fails, we try falling back on
|
/// A wrapper around `fs::rename`, so that if it fails, we try falling back on
|
||||||
/// copying and removing.
|
/// copying and removing.
|
||||||
fn rename_with_fallback(from: &Path, to: &Path) -> io::Result<()> {
|
fn rename_with_fallback(
|
||||||
|
from: &Path,
|
||||||
|
to: &Path,
|
||||||
|
multi_progress: Option<&MultiProgress>,
|
||||||
|
) -> io::Result<()> {
|
||||||
if fs::rename(from, to).is_err() {
|
if fs::rename(from, to).is_err() {
|
||||||
// Get metadata without following symlinks
|
// Get metadata without following symlinks
|
||||||
let metadata = from.symlink_metadata()?;
|
let metadata = from.symlink_metadata()?;
|
||||||
|
@ -441,7 +509,39 @@ fn rename_with_fallback(from: &Path, to: &Path) -> io::Result<()> {
|
||||||
copy_inside: true,
|
copy_inside: true,
|
||||||
..DirCopyOptions::new()
|
..DirCopyOptions::new()
|
||||||
};
|
};
|
||||||
if let Err(err) = move_dir(from, to, &options) {
|
|
||||||
|
// Calculate total size of directory
|
||||||
|
// Silently degrades:
|
||||||
|
// If finding the total size fails for whatever reason,
|
||||||
|
// the progress bar wont be shown for this file / dir.
|
||||||
|
// (Move will probably fail due to permission error later?)
|
||||||
|
let total_size = dir_get_size(from).ok();
|
||||||
|
|
||||||
|
let progress_bar =
|
||||||
|
if let (Some(multi_progress), Some(total_size)) = (multi_progress, total_size) {
|
||||||
|
let bar = ProgressBar::new(total_size).with_style(
|
||||||
|
ProgressStyle::with_template(
|
||||||
|
"{msg}: [{elapsed_precise}] {wide_bar} {bytes:>7}/{total_bytes:7}",
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(multi_progress.add(bar))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = if let Some(ref pb) = progress_bar {
|
||||||
|
move_dir_with_progress(from, to, &options, |process_info: TransitProcess| {
|
||||||
|
pb.set_position(process_info.copied_bytes);
|
||||||
|
pb.set_message(process_info.file_name);
|
||||||
|
TransitProcessResult::ContinueOrAbort
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
move_dir(from, to, &options)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = result {
|
||||||
return match err.kind {
|
return match err.kind {
|
||||||
fs_extra::error::ErrorKind::PermissionDenied => Err(io::Error::new(
|
fs_extra::error::ErrorKind::PermissionDenied => Err(io::Error::new(
|
||||||
io::ErrorKind::PermissionDenied,
|
io::ErrorKind::PermissionDenied,
|
||||||
|
|
|
@ -126,7 +126,7 @@ impl<'a> BytesGenerator<'a> {
|
||||||
fn new(total_bytes: u64, gen_type: PassType<'a>, exact: bool) -> BytesGenerator {
|
fn new(total_bytes: u64, gen_type: PassType<'a>, exact: bool) -> BytesGenerator {
|
||||||
let rng = match gen_type {
|
let rng = match gen_type {
|
||||||
PassType::Random => Some(RefCell::new(rand::thread_rng())),
|
PassType::Random => Some(RefCell::new(rand::thread_rng())),
|
||||||
_ => None,
|
PassType::Pattern(_) => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let bytes = [0; BLOCK_SIZE];
|
let bytes = [0; BLOCK_SIZE];
|
||||||
|
|
|
@ -22,6 +22,7 @@ memchr = "2.5.0"
|
||||||
notify = { version = "=5.0.0", features=["macos_kqueue"]}
|
notify = { version = "=5.0.0", features=["macos_kqueue"]}
|
||||||
uucore = { version=">=0.0.16", package="uucore", path="../../uucore", features=["ringbuffer", "lines"] }
|
uucore = { version=">=0.0.16", package="uucore", path="../../uucore", features=["ringbuffer", "lines"] }
|
||||||
same-file = "1.0.6"
|
same-file = "1.0.6"
|
||||||
|
atty = "0.2"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows-sys = { version = "0.42.0", default-features = false, features = ["Win32_System_Threading", "Win32_Foundation"] }
|
windows-sys = { version = "0.42.0", default-features = false, features = ["Win32_System_Threading", "Win32_Foundation"] }
|
||||||
|
|
|
@ -7,8 +7,10 @@
|
||||||
|
|
||||||
use crate::paths::Input;
|
use crate::paths::Input;
|
||||||
use crate::{parse, platform, Quotable};
|
use crate::{parse, platform, Quotable};
|
||||||
|
use atty::Stream;
|
||||||
use clap::crate_version;
|
use clap::crate_version;
|
||||||
use clap::{parser::ValueSource, Arg, ArgAction, ArgMatches, Command};
|
use clap::{parser::ValueSource, Arg, ArgAction, ArgMatches, Command};
|
||||||
|
use same_file::Handle;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -113,6 +115,13 @@ pub enum FollowMode {
|
||||||
Name,
|
Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum VerificationResult {
|
||||||
|
Ok,
|
||||||
|
CannotFollowStdinByName,
|
||||||
|
NoOutput,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub follow: Option<FollowMode>,
|
pub follow: Option<FollowMode>,
|
||||||
|
@ -149,10 +158,6 @@ impl Settings {
|
||||||
settings.retry =
|
settings.retry =
|
||||||
matches.get_flag(options::RETRY) || matches.get_flag(options::FOLLOW_RETRY);
|
matches.get_flag(options::RETRY) || matches.get_flag(options::FOLLOW_RETRY);
|
||||||
|
|
||||||
if settings.retry && settings.follow.is_none() {
|
|
||||||
show_warning!("--retry ignored; --retry is useful only when following");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(s) = matches.get_one::<String>(options::SLEEP_INT) {
|
if let Some(s) = matches.get_one::<String>(options::SLEEP_INT) {
|
||||||
settings.sleep_sec = match s.parse::<f32>() {
|
settings.sleep_sec = match s.parse::<f32>() {
|
||||||
Ok(s) => Duration::from_secs_f32(s),
|
Ok(s) => Duration::from_secs_f32(s),
|
||||||
|
@ -194,14 +199,8 @@ impl Settings {
|
||||||
format!("invalid PID: {}", pid_str.quote()),
|
format!("invalid PID: {}", pid_str.quote()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.pid = pid;
|
settings.pid = pid;
|
||||||
if settings.follow.is_none() {
|
|
||||||
show_warning!("PID ignored; --pid=PID is useful only when following");
|
|
||||||
}
|
|
||||||
if !platform::supports_pid_checks(settings.pid) {
|
|
||||||
show_warning!("--pid=PID is not supported on this system");
|
|
||||||
settings.pid = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(USimpleError::new(
|
return Err(USimpleError::new(
|
||||||
|
@ -214,16 +213,6 @@ impl Settings {
|
||||||
|
|
||||||
settings.mode = FilterMode::from(matches)?;
|
settings.mode = FilterMode::from(matches)?;
|
||||||
|
|
||||||
// Mimic GNU's tail for -[nc]0 without -f and exit immediately
|
|
||||||
if settings.follow.is_none()
|
|
||||||
&& matches!(
|
|
||||||
settings.mode,
|
|
||||||
FilterMode::Lines(Signum::MinusZero, _) | FilterMode::Bytes(Signum::MinusZero)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
std::process::exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut inputs: VecDeque<Input> = matches
|
let mut inputs: VecDeque<Input> = matches
|
||||||
.get_many::<String>(options::ARG_FILES)
|
.get_many::<String>(options::ARG_FILES)
|
||||||
.map(|v| v.map(|string| Input::from(string.clone())).collect())
|
.map(|v| v.map(|string| Input::from(string.clone())).collect())
|
||||||
|
@ -243,6 +232,81 @@ impl Settings {
|
||||||
|
|
||||||
Ok(settings)
|
Ok(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_only_stdin(&self) -> bool {
|
||||||
|
self.inputs.iter().all(|input| input.is_stdin())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_stdin(&self) -> bool {
|
||||||
|
self.inputs.iter().any(|input| input.is_stdin())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn num_inputs(&self) -> usize {
|
||||||
|
self.inputs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check [`Settings`] for problematic configurations of tail originating from user provided
|
||||||
|
/// command line arguments and print appropriate warnings.
|
||||||
|
pub fn check_warnings(&self) {
|
||||||
|
if self.retry {
|
||||||
|
if self.follow.is_none() {
|
||||||
|
show_warning!("--retry ignored; --retry is useful only when following");
|
||||||
|
} else if self.follow == Some(FollowMode::Descriptor) {
|
||||||
|
show_warning!("--retry only effective for the initial open");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.pid != 0 {
|
||||||
|
if self.follow.is_none() {
|
||||||
|
show_warning!("PID ignored; --pid=PID is useful only when following");
|
||||||
|
} else if !platform::supports_pid_checks(self.pid) {
|
||||||
|
show_warning!("--pid=PID is not supported on this system");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This warning originates from gnu's tail implementation of the equivalent warning. If the
|
||||||
|
// user wants to follow stdin, but tail is blocking indefinitely anyways, because of stdin
|
||||||
|
// as `tty` (but no otherwise blocking stdin), then we print a warning that `--follow`
|
||||||
|
// cannot be applied under these circumstances and is therefore ineffective.
|
||||||
|
if self.follow.is_some() && self.has_stdin() {
|
||||||
|
let blocking_stdin = self.pid == 0
|
||||||
|
&& self.follow == Some(FollowMode::Descriptor)
|
||||||
|
&& self.num_inputs() == 1
|
||||||
|
&& Handle::stdin().map_or(false, |handle| {
|
||||||
|
handle
|
||||||
|
.as_file()
|
||||||
|
.metadata()
|
||||||
|
.map_or(false, |meta| !meta.is_file())
|
||||||
|
});
|
||||||
|
|
||||||
|
if !blocking_stdin && atty::is(Stream::Stdin) {
|
||||||
|
show_warning!("following standard input indefinitely is ineffective");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify [`Settings`] and try to find unsolvable misconfigurations of tail originating from
|
||||||
|
/// user provided command line arguments. In contrast to [`Settings::check_warnings`] these
|
||||||
|
/// misconfigurations usually lead to the immediate exit or abortion of the running `tail`
|
||||||
|
/// process.
|
||||||
|
pub fn verify(&self) -> VerificationResult {
|
||||||
|
// Mimic GNU's tail for `tail -F`
|
||||||
|
if self.inputs.iter().any(|i| i.is_stdin()) && self.follow == Some(FollowMode::Name) {
|
||||||
|
return VerificationResult::CannotFollowStdinByName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mimic GNU's tail for -[nc]0 without -f and exit immediately
|
||||||
|
if self.follow.is_none()
|
||||||
|
&& matches!(
|
||||||
|
self.mode,
|
||||||
|
FilterMode::Lines(Signum::MinusZero, _) | FilterMode::Bytes(Signum::MinusZero)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return VerificationResult::NoOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
VerificationResult::Ok
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn arg_iterate<'a>(
|
pub fn arg_iterate<'a>(
|
||||||
|
@ -298,19 +362,6 @@ fn parse_num(src: &str) -> Result<Signum, ParseSizeError> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stdin_is_pipe_or_fifo() -> bool {
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
platform::stdin_is_pipe_or_fifo()
|
|
||||||
}
|
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
winapi_util::file::typ(winapi_util::HandleRef::stdin())
|
|
||||||
.map(|t| t.is_disk() || t.is_pipe())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_args(args: impl uucore::Args) -> UResult<Settings> {
|
pub fn parse_args(args: impl uucore::Args) -> UResult<Settings> {
|
||||||
let matches = uu_app().try_get_matches_from(arg_iterate(args)?)?;
|
let matches = uu_app().try_get_matches_from(arg_iterate(args)?)?;
|
||||||
Settings::from(&matches)
|
Settings::from(&matches)
|
||||||
|
|
|
@ -7,7 +7,9 @@
|
||||||
//! or at the end of piped stdin with [`LinesChunk`] or [`BytesChunk`].
|
//! or at the end of piped stdin with [`LinesChunk`] or [`BytesChunk`].
|
||||||
//!
|
//!
|
||||||
//! Use [`ReverseChunks::new`] to create a new iterator over chunks of bytes from the file.
|
//! Use [`ReverseChunks::new`] to create a new iterator over chunks of bytes from the file.
|
||||||
|
|
||||||
// spell-checker:ignore (ToDO) filehandle BUFSIZ
|
// spell-checker:ignore (ToDO) filehandle BUFSIZ
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{BufRead, Read, Seek, SeekFrom, Write};
|
use std::io::{BufRead, Read, Seek, SeekFrom, Write};
|
||||||
|
|
|
@ -13,7 +13,6 @@ use std::collections::hash_map::Keys;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::{File, Metadata};
|
use std::fs::{File, Metadata};
|
||||||
use std::io::{stdout, BufRead, BufReader, BufWriter};
|
use std::io::{stdout, BufRead, BufReader, BufWriter};
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use uucore::error::UResult;
|
use uucore::error::UResult;
|
||||||
|
|
||||||
|
|
|
@ -6,4 +6,4 @@
|
||||||
mod files;
|
mod files;
|
||||||
mod watch;
|
mod watch;
|
||||||
|
|
||||||
pub use watch::{follow, WatcherService};
|
pub use watch::{follow, Observer};
|
||||||
|
|
|
@ -13,8 +13,7 @@ use notify::{RecommendedWatcher, RecursiveMode, Watcher, WatcherKind};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::io::BufRead;
|
use std::io::BufRead;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc::{self, channel, Receiver};
|
||||||
use std::sync::mpsc::{channel, Receiver};
|
|
||||||
use uucore::display::Quotable;
|
use uucore::display::Quotable;
|
||||||
use uucore::error::{set_exit_code, UResult, USimpleError};
|
use uucore::error::{set_exit_code, UResult, USimpleError};
|
||||||
use uucore::show_error;
|
use uucore::show_error;
|
||||||
|
@ -81,7 +80,7 @@ impl WatcherRx {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WatcherService {
|
pub struct Observer {
|
||||||
/// Whether --retry was given on the command line
|
/// Whether --retry was given on the command line
|
||||||
pub retry: bool,
|
pub retry: bool,
|
||||||
|
|
||||||
|
@ -92,18 +91,28 @@ pub struct WatcherService {
|
||||||
/// platform specific event driven method. Since `use_polling` is subject to
|
/// platform specific event driven method. Since `use_polling` is subject to
|
||||||
/// change during runtime it is moved out of [`Settings`].
|
/// change during runtime it is moved out of [`Settings`].
|
||||||
pub use_polling: bool,
|
pub use_polling: bool,
|
||||||
|
|
||||||
pub watcher_rx: Option<WatcherRx>,
|
pub watcher_rx: Option<WatcherRx>,
|
||||||
pub orphans: Vec<PathBuf>,
|
pub orphans: Vec<PathBuf>,
|
||||||
pub files: FileHandling,
|
pub files: FileHandling,
|
||||||
|
|
||||||
|
pub pid: platform::Pid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WatcherService {
|
impl Observer {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
retry: bool,
|
retry: bool,
|
||||||
follow: Option<FollowMode>,
|
follow: Option<FollowMode>,
|
||||||
use_polling: bool,
|
use_polling: bool,
|
||||||
files: FileHandling,
|
files: FileHandling,
|
||||||
|
pid: platform::Pid,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let pid = if platform::supports_pid_checks(pid) {
|
||||||
|
pid
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
retry,
|
retry,
|
||||||
follow,
|
follow,
|
||||||
|
@ -111,6 +120,7 @@ impl WatcherService {
|
||||||
watcher_rx: None,
|
watcher_rx: None,
|
||||||
orphans: Vec::new(),
|
orphans: Vec::new(),
|
||||||
files,
|
files,
|
||||||
|
pid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +130,7 @@ impl WatcherService {
|
||||||
settings.follow,
|
settings.follow,
|
||||||
settings.use_polling,
|
settings.use_polling,
|
||||||
FileHandling::from(settings),
|
FileHandling::from(settings),
|
||||||
|
settings.pid,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -460,14 +471,12 @@ impl WatcherService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResult<()> {
|
pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> {
|
||||||
if watcher_service.files.no_files_remaining(settings)
|
if observer.files.no_files_remaining(settings) && !observer.files.only_stdin_remaining() {
|
||||||
&& !watcher_service.files.only_stdin_remaining()
|
|
||||||
{
|
|
||||||
return Err(USimpleError::new(1, text::NO_FILES_REMAINING.to_string()));
|
return Err(USimpleError::new(1, text::NO_FILES_REMAINING.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut process = platform::ProcessChecker::new(settings.pid);
|
let mut process = platform::ProcessChecker::new(observer.pid);
|
||||||
|
|
||||||
let mut _event_counter = 0;
|
let mut _event_counter = 0;
|
||||||
let mut _timeout_counter = 0;
|
let mut _timeout_counter = 0;
|
||||||
|
@ -478,7 +487,7 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu
|
||||||
|
|
||||||
// If `--pid=p`, tail checks whether process p
|
// If `--pid=p`, tail checks whether process p
|
||||||
// is alive at least every `--sleep-interval=N` seconds
|
// is alive at least every `--sleep-interval=N` seconds
|
||||||
if settings.follow.is_some() && settings.pid != 0 && process.is_dead() {
|
if settings.follow.is_some() && observer.pid != 0 && process.is_dead() {
|
||||||
// p is dead, tail will also terminate
|
// p is dead, tail will also terminate
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -487,22 +496,20 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu
|
||||||
// If a path becomes an orphan during runtime, it will be added to orphans.
|
// If a path becomes an orphan during runtime, it will be added to orphans.
|
||||||
// To be able to differentiate between the cases of test_retry8 and test_retry9,
|
// To be able to differentiate between the cases of test_retry8 and test_retry9,
|
||||||
// here paths will not be removed from orphans if the path becomes available.
|
// here paths will not be removed from orphans if the path becomes available.
|
||||||
if watcher_service.follow_name_retry() {
|
if observer.follow_name_retry() {
|
||||||
for new_path in &watcher_service.orphans {
|
for new_path in &observer.orphans {
|
||||||
if new_path.exists() {
|
if new_path.exists() {
|
||||||
let pd = watcher_service.files.get(new_path);
|
let pd = observer.files.get(new_path);
|
||||||
let md = new_path.metadata().unwrap();
|
let md = new_path.metadata().unwrap();
|
||||||
if md.is_tailable() && pd.reader.is_none() {
|
if md.is_tailable() && pd.reader.is_none() {
|
||||||
show_error!(
|
show_error!(
|
||||||
"{} has appeared; following new file",
|
"{} has appeared; following new file",
|
||||||
pd.display_name.quote()
|
pd.display_name.quote()
|
||||||
);
|
);
|
||||||
watcher_service.files.update_metadata(new_path, Some(md));
|
observer.files.update_metadata(new_path, Some(md));
|
||||||
watcher_service.files.update_reader(new_path)?;
|
observer.files.update_reader(new_path)?;
|
||||||
_read_some = watcher_service
|
_read_some = observer.files.tail_file(new_path, settings.verbose)?;
|
||||||
.files
|
observer
|
||||||
.tail_file(new_path, settings.verbose)?;
|
|
||||||
watcher_service
|
|
||||||
.watcher_rx
|
.watcher_rx
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -514,7 +521,7 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu
|
||||||
|
|
||||||
// With -f, sleep for approximately N seconds (default 1.0) between iterations;
|
// With -f, sleep for approximately N seconds (default 1.0) between iterations;
|
||||||
// We wake up if Notify sends an Event or if we wait more than `sleep_sec`.
|
// We wake up if Notify sends an Event or if we wait more than `sleep_sec`.
|
||||||
let rx_result = watcher_service
|
let rx_result = observer
|
||||||
.watcher_rx
|
.watcher_rx
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -529,9 +536,9 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu
|
||||||
match rx_result {
|
match rx_result {
|
||||||
Ok(Ok(event)) => {
|
Ok(Ok(event)) => {
|
||||||
if let Some(event_path) = event.paths.first() {
|
if let Some(event_path) = event.paths.first() {
|
||||||
if watcher_service.files.contains_key(event_path) {
|
if observer.files.contains_key(event_path) {
|
||||||
// Handle Event if it is about a path that we are monitoring
|
// Handle Event if it is about a path that we are monitoring
|
||||||
paths = watcher_service.handle_event(&event, settings)?;
|
paths = observer.handle_event(&event, settings)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -540,8 +547,8 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu
|
||||||
paths,
|
paths,
|
||||||
})) if e.kind() == std::io::ErrorKind::NotFound => {
|
})) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||||
if let Some(event_path) = paths.first() {
|
if let Some(event_path) = paths.first() {
|
||||||
if watcher_service.files.contains_key(event_path) {
|
if observer.files.contains_key(event_path) {
|
||||||
let _ = watcher_service
|
let _ = observer
|
||||||
.watcher_rx
|
.watcher_rx
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -566,16 +573,16 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu
|
||||||
Err(e) => return Err(USimpleError::new(1, format!("RecvTimeoutError: {}", e))),
|
Err(e) => return Err(USimpleError::new(1, format!("RecvTimeoutError: {}", e))),
|
||||||
}
|
}
|
||||||
|
|
||||||
if watcher_service.use_polling && settings.follow.is_some() {
|
if observer.use_polling && settings.follow.is_some() {
|
||||||
// Consider all files to potentially have new content.
|
// Consider all files to potentially have new content.
|
||||||
// This is a workaround because `Notify::PollWatcher`
|
// This is a workaround because `Notify::PollWatcher`
|
||||||
// does not recognize the "renaming" of files.
|
// does not recognize the "renaming" of files.
|
||||||
paths = watcher_service.files.keys().cloned().collect::<Vec<_>>();
|
paths = observer.files.keys().cloned().collect::<Vec<_>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// main print loop
|
// main print loop
|
||||||
for path in &paths {
|
for path in &paths {
|
||||||
_read_some = watcher_service.files.tail_file(path, settings.verbose)?;
|
_read_some = observer.files.tail_file(path, settings.verbose)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if _timeout_counter == settings.max_unchanged_stats {
|
if _timeout_counter == settings.max_unchanged_stats {
|
||||||
|
|
|
@ -5,24 +5,14 @@
|
||||||
|
|
||||||
// spell-checker:ignore tailable seekable stdlib (stdlib)
|
// spell-checker:ignore tailable seekable stdlib (stdlib)
|
||||||
|
|
||||||
#[cfg(unix)]
|
use crate::text;
|
||||||
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
use std::fs::{File, Metadata};
|
use std::fs::{File, Metadata};
|
||||||
use std::io::{Seek, SeekFrom};
|
use std::io::{Seek, SeekFrom};
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use uucore::error::UResult;
|
use uucore::error::UResult;
|
||||||
|
|
||||||
use crate::args::Settings;
|
|
||||||
use crate::text;
|
|
||||||
|
|
||||||
// * This file is part of the uutils coreutils package.
|
|
||||||
// *
|
|
||||||
// * For the full copyright and license information, please view the LICENSE
|
|
||||||
// * file that was distributed with this source code.
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum InputKind {
|
pub enum InputKind {
|
||||||
File(PathBuf),
|
File(PathBuf),
|
||||||
|
@ -36,6 +26,7 @@ pub struct Input {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
impl Input {
|
||||||
|
// TODO: from &str may be the better choice
|
||||||
pub fn from(string: String) -> Self {
|
pub fn from(string: String) -> Self {
|
||||||
let kind = if string == text::DASH {
|
let kind = if string == text::DASH {
|
||||||
InputKind::Stdin
|
InputKind::Stdin
|
||||||
|
@ -132,44 +123,6 @@ impl HeaderPrinter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct InputService {
|
|
||||||
pub inputs: VecDeque<Input>,
|
|
||||||
pub presume_input_pipe: bool,
|
|
||||||
pub header_printer: HeaderPrinter,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputService {
|
|
||||||
pub fn new(verbose: bool, presume_input_pipe: bool, inputs: VecDeque<Input>) -> Self {
|
|
||||||
Self {
|
|
||||||
inputs,
|
|
||||||
presume_input_pipe,
|
|
||||||
header_printer: HeaderPrinter::new(verbose, true),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from(settings: &Settings) -> Self {
|
|
||||||
Self::new(
|
|
||||||
settings.verbose,
|
|
||||||
settings.presume_input_pipe,
|
|
||||||
settings.inputs.clone(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_stdin(&mut self) -> bool {
|
|
||||||
self.inputs.iter().any(|input| input.is_stdin())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_only_stdin(&self) -> bool {
|
|
||||||
self.inputs.iter().all(|input| input.is_stdin())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn print_header(&mut self, input: &Input) {
|
|
||||||
self.header_printer.print_input(input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait FileExtTail {
|
pub trait FileExtTail {
|
||||||
#[allow(clippy::wrong_self_convention)]
|
#[allow(clippy::wrong_self_convention)]
|
||||||
fn is_seekable(&mut self, current_offset: u64) -> bool;
|
fn is_seekable(&mut self, current_offset: u64) -> bool;
|
||||||
|
@ -228,9 +181,11 @@ impl MetadataExtTail for Metadata {
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
|
// TODO: `file_index` requires unstable library feature `windows_by_handle`
|
||||||
// use std::os::windows::prelude::*;
|
// use std::os::windows::prelude::*;
|
||||||
// if let Some(self_id) = self.file_index() {
|
// if let Some(self_id) = self.file_index() {
|
||||||
// if let Some(other_id) = other.file_index() {
|
// if let Some(other_id) = other.file_index() {
|
||||||
|
// // TODO: not sure this is the equivalent of comparing inode numbers
|
||||||
//
|
//
|
||||||
// return self_id.eq(&other_id);
|
// return self_id.eq(&other_id);
|
||||||
// }
|
// }
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub use self::unix::{
|
pub use self::unix::{
|
||||||
//stdin_is_bad_fd, stdin_is_pipe_or_fifo, supports_pid_checks, Pid, ProcessChecker,
|
//stdin_is_bad_fd, stdin_is_pipe_or_fifo, supports_pid_checks, Pid, ProcessChecker,
|
||||||
stdin_is_pipe_or_fifo,
|
|
||||||
supports_pid_checks,
|
supports_pid_checks,
|
||||||
Pid,
|
Pid,
|
||||||
ProcessChecker,
|
ProcessChecker,
|
||||||
|
|
|
@ -11,8 +11,6 @@
|
||||||
// spell-checker:ignore (ToDO) stdlib, ISCHR, GETFD
|
// spell-checker:ignore (ToDO) stdlib, ISCHR, GETFD
|
||||||
// spell-checker:ignore (options) EPERM, ENOSYS
|
// spell-checker:ignore (options) EPERM, ENOSYS
|
||||||
|
|
||||||
use libc::S_IFCHR;
|
|
||||||
use nix::sys::stat::fstat;
|
|
||||||
use std::io::Error;
|
use std::io::Error;
|
||||||
|
|
||||||
pub type Pid = libc::pid_t;
|
pub type Pid = libc::pid_t;
|
||||||
|
@ -45,13 +43,6 @@ 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()
|
||||||
}
|
}
|
||||||
#[inline]
|
|
||||||
pub fn stdin_is_pipe_or_fifo() -> bool {
|
|
||||||
// IFCHR means the file (stdin) is a character input device, which is the case of a terminal.
|
|
||||||
// We just need to check if stdin is not a character device here, because we are not interested
|
|
||||||
// in the type of stdin itself.
|
|
||||||
fstat(libc::STDIN_FILENO).map_or(false, |file| file.st_mode as libc::mode_t & S_IFCHR == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
//pub fn stdin_is_bad_fd() -> bool {
|
//pub fn stdin_is_bad_fd() -> bool {
|
||||||
// FIXME: Detect a closed file descriptor, e.g.: `tail <&-`
|
// FIXME: Detect a closed file descriptor, e.g.: `tail <&-`
|
||||||
|
|
|
@ -24,58 +24,57 @@ mod paths;
|
||||||
mod platform;
|
mod platform;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
|
||||||
|
pub use args::uu_app;
|
||||||
|
use args::{parse_args, FilterMode, Settings, Signum};
|
||||||
|
use chunks::ReverseChunks;
|
||||||
|
use follow::Observer;
|
||||||
|
use paths::{FileExtTail, HeaderPrinter, Input, InputKind, MetadataExtTail};
|
||||||
use same_file::Handle;
|
use same_file::Handle;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{self, stdin, stdout, BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
|
use std::io::{self, stdin, stdout, BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use uucore::{show, show_error, show_warning};
|
|
||||||
|
|
||||||
use uucore::display::Quotable;
|
use uucore::display::Quotable;
|
||||||
use uucore::error::{get_exit_code, set_exit_code, FromIo, UError, UResult, USimpleError};
|
use uucore::error::{get_exit_code, set_exit_code, FromIo, UError, UResult, USimpleError};
|
||||||
|
use uucore::{show, show_error};
|
||||||
pub use args::uu_app;
|
|
||||||
use args::{parse_args, FilterMode, Settings, Signum};
|
|
||||||
use chunks::ReverseChunks;
|
|
||||||
use follow::WatcherService;
|
|
||||||
use paths::{FileExtTail, Input, InputKind, InputService, MetadataExtTail};
|
|
||||||
|
|
||||||
#[uucore::main]
|
#[uucore::main]
|
||||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let settings = parse_args(args)?;
|
let settings = parse_args(args)?;
|
||||||
|
|
||||||
|
settings.check_warnings();
|
||||||
|
|
||||||
|
match settings.verify() {
|
||||||
|
args::VerificationResult::CannotFollowStdinByName => {
|
||||||
|
return Err(USimpleError::new(
|
||||||
|
1,
|
||||||
|
format!("cannot follow {} by name", text::DASH.quote()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
// Exit early if we do not output anything. Note, that this may break a pipe
|
||||||
|
// when tail is on the receiving side.
|
||||||
|
args::VerificationResult::NoOutput => return Ok(()),
|
||||||
|
args::VerificationResult::Ok => {}
|
||||||
|
}
|
||||||
|
|
||||||
uu_tail(&settings)
|
uu_tail(&settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uu_tail(settings: &Settings) -> UResult<()> {
|
fn uu_tail(settings: &Settings) -> UResult<()> {
|
||||||
// Mimic GNU's tail for `tail -F` and exit immediately
|
let mut printer = HeaderPrinter::new(settings.verbose, true);
|
||||||
let mut input_service = InputService::from(settings);
|
let mut observer = Observer::from(settings);
|
||||||
let mut watcher_service = WatcherService::from(settings);
|
|
||||||
|
|
||||||
if input_service.has_stdin() && watcher_service.follow_name() {
|
observer.start(settings)?;
|
||||||
return Err(USimpleError::new(
|
|
||||||
1,
|
|
||||||
format!("cannot follow {} by name", text::DASH.quote()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
watcher_service.start(settings)?;
|
|
||||||
// Do an initial tail print of each path's content.
|
// Do an initial tail print of each path's content.
|
||||||
// Add `path` and `reader` to `files` map if `--follow` is selected.
|
// Add `path` and `reader` to `files` map if `--follow` is selected.
|
||||||
for input in &input_service.inputs.clone() {
|
for input in &settings.inputs.clone() {
|
||||||
match input.kind() {
|
match input.kind() {
|
||||||
InputKind::File(path) if cfg!(not(unix)) || path != &PathBuf::from(text::DEV_STDIN) => {
|
InputKind::File(path) if cfg!(not(unix)) || path != &PathBuf::from(text::DEV_STDIN) => {
|
||||||
tail_file(
|
tail_file(settings, &mut printer, input, path, &mut observer, 0)?;
|
||||||
settings,
|
|
||||||
&mut input_service,
|
|
||||||
input,
|
|
||||||
path,
|
|
||||||
&mut watcher_service,
|
|
||||||
0,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
// File points to /dev/stdin here
|
// File points to /dev/stdin here
|
||||||
InputKind::File(_) | InputKind::Stdin => {
|
InputKind::File(_) | InputKind::Stdin => {
|
||||||
tail_stdin(settings, &mut input_service, input, &mut watcher_service)?;
|
tail_stdin(settings, &mut printer, input, &mut observer)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,9 +89,8 @@ fn uu_tail(settings: &Settings) -> UResult<()> {
|
||||||
the input file is not a FIFO, pipe, or regular file, it is unspecified whether or
|
the input file is not a FIFO, pipe, or regular file, it is unspecified whether or
|
||||||
not the -f option shall be ignored.
|
not the -f option shall be ignored.
|
||||||
*/
|
*/
|
||||||
|
if !settings.has_only_stdin() {
|
||||||
if !input_service.has_only_stdin() {
|
follow::follow(observer, settings)?;
|
||||||
follow::follow(watcher_service, settings)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,16 +103,12 @@ fn uu_tail(settings: &Settings) -> UResult<()> {
|
||||||
|
|
||||||
fn tail_file(
|
fn tail_file(
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
input_service: &mut InputService,
|
header_printer: &mut HeaderPrinter,
|
||||||
input: &Input,
|
input: &Input,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
watcher_service: &mut WatcherService,
|
observer: &mut Observer,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
) -> UResult<()> {
|
) -> UResult<()> {
|
||||||
if watcher_service.follow_descriptor_retry() {
|
|
||||||
show_warning!("--retry only effective for the initial open");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
set_exit_code(1);
|
set_exit_code(1);
|
||||||
show_error!(
|
show_error!(
|
||||||
|
@ -122,11 +116,11 @@ fn tail_file(
|
||||||
input.display_name,
|
input.display_name,
|
||||||
text::NO_SUCH_FILE
|
text::NO_SUCH_FILE
|
||||||
);
|
);
|
||||||
watcher_service.add_bad_path(path, input.display_name.as_str(), false)?;
|
observer.add_bad_path(path, input.display_name.as_str(), false)?;
|
||||||
} else if path.is_dir() {
|
} else if path.is_dir() {
|
||||||
set_exit_code(1);
|
set_exit_code(1);
|
||||||
|
|
||||||
input_service.print_header(input);
|
header_printer.print_input(input);
|
||||||
let err_msg = "Is a directory".to_string();
|
let err_msg = "Is a directory".to_string();
|
||||||
|
|
||||||
show_error!("error reading '{}': {}", input.display_name, err_msg);
|
show_error!("error reading '{}': {}", input.display_name, err_msg);
|
||||||
|
@ -142,16 +136,16 @@ fn tail_file(
|
||||||
msg
|
msg
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if !(watcher_service.follow_name_retry()) {
|
if !(observer.follow_name_retry()) {
|
||||||
// skip directory if not retry
|
// skip directory if not retry
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
watcher_service.add_bad_path(path, input.display_name.as_str(), false)?;
|
observer.add_bad_path(path, input.display_name.as_str(), false)?;
|
||||||
} else if input.is_tailable() {
|
} else if input.is_tailable() {
|
||||||
let metadata = path.metadata().ok();
|
let metadata = path.metadata().ok();
|
||||||
match File::open(path) {
|
match File::open(path) {
|
||||||
Ok(mut file) => {
|
Ok(mut file) => {
|
||||||
input_service.print_header(input);
|
header_printer.print_input(input);
|
||||||
let mut reader;
|
let mut reader;
|
||||||
if !settings.presume_input_pipe
|
if !settings.presume_input_pipe
|
||||||
&& file.is_seekable(if input.is_stdin() { offset } else { 0 })
|
&& file.is_seekable(if input.is_stdin() { offset } else { 0 })
|
||||||
|
@ -163,7 +157,7 @@ fn tail_file(
|
||||||
reader = BufReader::new(file);
|
reader = BufReader::new(file);
|
||||||
unbounded_tail(&mut reader, settings)?;
|
unbounded_tail(&mut reader, settings)?;
|
||||||
}
|
}
|
||||||
watcher_service.add_path(
|
observer.add_path(
|
||||||
path,
|
path,
|
||||||
input.display_name.as_str(),
|
input.display_name.as_str(),
|
||||||
Some(Box::new(reader)),
|
Some(Box::new(reader)),
|
||||||
|
@ -171,20 +165,20 @@ fn tail_file(
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||||
watcher_service.add_bad_path(path, input.display_name.as_str(), false)?;
|
observer.add_bad_path(path, input.display_name.as_str(), false)?;
|
||||||
show!(e.map_err_context(|| {
|
show!(e.map_err_context(|| {
|
||||||
format!("cannot open '{}' for reading", input.display_name)
|
format!("cannot open '{}' for reading", input.display_name)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
watcher_service.add_bad_path(path, input.display_name.as_str(), false)?;
|
observer.add_bad_path(path, input.display_name.as_str(), false)?;
|
||||||
return Err(e.map_err_context(|| {
|
return Err(e.map_err_context(|| {
|
||||||
format!("cannot open '{}' for reading", input.display_name)
|
format!("cannot open '{}' for reading", input.display_name)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
watcher_service.add_bad_path(path, input.display_name.as_str(), false)?;
|
observer.add_bad_path(path, input.display_name.as_str(), false)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -192,9 +186,9 @@ fn tail_file(
|
||||||
|
|
||||||
fn tail_stdin(
|
fn tail_stdin(
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
input_service: &mut InputService,
|
header_printer: &mut HeaderPrinter,
|
||||||
input: &Input,
|
input: &Input,
|
||||||
watcher_service: &mut WatcherService,
|
observer: &mut Observer,
|
||||||
) -> UResult<()> {
|
) -> UResult<()> {
|
||||||
match input.resolve() {
|
match input.resolve() {
|
||||||
// fifo
|
// fifo
|
||||||
|
@ -211,24 +205,20 @@ fn tail_stdin(
|
||||||
}
|
}
|
||||||
tail_file(
|
tail_file(
|
||||||
settings,
|
settings,
|
||||||
input_service,
|
header_printer,
|
||||||
input,
|
input,
|
||||||
&path,
|
&path,
|
||||||
watcher_service,
|
observer,
|
||||||
stdin_offset,
|
stdin_offset,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
// pipe
|
// pipe
|
||||||
None => {
|
None => {
|
||||||
input_service.print_header(input);
|
header_printer.print_input(input);
|
||||||
if !paths::stdin_is_bad_fd() {
|
if !paths::stdin_is_bad_fd() {
|
||||||
let mut reader = BufReader::new(stdin());
|
let mut reader = BufReader::new(stdin());
|
||||||
unbounded_tail(&mut reader, settings)?;
|
unbounded_tail(&mut reader, settings)?;
|
||||||
watcher_service.add_stdin(
|
observer.add_stdin(input.display_name.as_str(), Some(Box::new(reader)), true)?;
|
||||||
input.display_name.as_str(),
|
|
||||||
Some(Box::new(reader)),
|
|
||||||
true,
|
|
||||||
)?;
|
|
||||||
} else {
|
} else {
|
||||||
set_exit_code(1);
|
set_exit_code(1);
|
||||||
show_error!(
|
show_error!(
|
||||||
|
@ -417,7 +407,7 @@ fn unbounded_tail<T: Read>(reader: &mut BufReader<T>, settings: &Settings) -> UR
|
||||||
FilterMode::Lines(Signum::Negative(count), sep) => {
|
FilterMode::Lines(Signum::Negative(count), sep) => {
|
||||||
let mut chunks = chunks::LinesChunkBuffer::new(*sep, *count);
|
let mut chunks = chunks::LinesChunkBuffer::new(*sep, *count);
|
||||||
chunks.fill(reader)?;
|
chunks.fill(reader)?;
|
||||||
chunks.print(writer)?;
|
chunks.print(&mut writer)?;
|
||||||
}
|
}
|
||||||
FilterMode::Lines(Signum::PlusZero | Signum::Positive(1), _) => {
|
FilterMode::Lines(Signum::PlusZero | Signum::Positive(1), _) => {
|
||||||
io::copy(reader, &mut writer)?;
|
io::copy(reader, &mut writer)?;
|
||||||
|
@ -441,7 +431,7 @@ fn unbounded_tail<T: Read>(reader: &mut BufReader<T>, settings: &Settings) -> UR
|
||||||
FilterMode::Bytes(Signum::Negative(count)) => {
|
FilterMode::Bytes(Signum::Negative(count)) => {
|
||||||
let mut chunks = chunks::BytesChunkBuffer::new(*count);
|
let mut chunks = chunks::BytesChunkBuffer::new(*count);
|
||||||
chunks.fill(reader)?;
|
chunks.fill(reader)?;
|
||||||
chunks.print(writer)?;
|
chunks.print(&mut writer)?;
|
||||||
}
|
}
|
||||||
FilterMode::Bytes(Signum::PlusZero | Signum::Positive(1)) => {
|
FilterMode::Bytes(Signum::PlusZero | Signum::Positive(1)) => {
|
||||||
io::copy(reader, &mut writer)?;
|
io::copy(reader, &mut writer)?;
|
||||||
|
|
|
@ -19,3 +19,6 @@ pub static BACKEND: &str = "kqueue";
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub static BACKEND: &str = "ReadDirectoryChanges";
|
pub static BACKEND: &str = "ReadDirectoryChanges";
|
||||||
pub static FD0: &str = "/dev/fd/0";
|
pub static FD0: &str = "/dev/fd/0";
|
||||||
|
pub static IS_A_DIRECTORY: &str = "Is a directory";
|
||||||
|
pub static DEV_TTY: &str = "/dev/tty";
|
||||||
|
pub static DEV_PTMX: &str = "/dev/ptmx";
|
||||||
|
|
|
@ -396,7 +396,7 @@ pub fn canonicalize<P: AsRef<Path>>(
|
||||||
read_dir(parent)?;
|
read_dir(parent)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
MissingHandling::Missing => {}
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
|
@ -278,7 +278,7 @@ pub fn escape_name(name: &OsStr, style: &QuotingStyle) -> String {
|
||||||
match quotes {
|
match quotes {
|
||||||
Quotes::Single => format!("'{}'", escaped_str),
|
Quotes::Single => format!("'{}'", escaped_str),
|
||||||
Quotes::Double => format!("\"{}\"", escaped_str),
|
Quotes::Double => format!("\"{}\"", escaped_str),
|
||||||
_ => escaped_str,
|
Quotes::None => escaped_str,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QuotingStyle::Shell {
|
QuotingStyle::Shell {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,7 @@
|
||||||
// * 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.
|
||||||
|
|
||||||
/// Platform-independent helper for constructing a PathBuf from individual elements
|
/// Platform-independent helper for constructing a `PathBuf` from individual elements
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! path_concat {
|
macro_rules! path_concat {
|
||||||
($e:expr, ..$n:expr) => {{
|
($e:expr, ..$n:expr) => {{
|
||||||
|
|
|
@ -147,7 +147,7 @@ impl CmdResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the program's exit code
|
/// Returns the program's exit code
|
||||||
/// Panics if not run or has not finished yet for example when run with run_no_wait()
|
/// Panics if not run or has not finished yet for example when run with `run_no_wait()`
|
||||||
pub fn code(&self) -> i32 {
|
pub fn code(&self) -> i32 {
|
||||||
self.code
|
self.code
|
||||||
.expect("Program must be run first or has not finished, yet")
|
.expect("Program must be run first or has not finished, yet")
|
||||||
|
@ -158,7 +158,7 @@ impl CmdResult {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the program's TempDir
|
/// Returns the program's `TempDir`
|
||||||
/// Panics if not present
|
/// Panics if not present
|
||||||
pub fn tmpd(&self) -> Rc<TempDir> {
|
pub fn tmpd(&self) -> Rc<TempDir> {
|
||||||
match &self.tmpd {
|
match &self.tmpd {
|
||||||
|
@ -201,7 +201,7 @@ impl CmdResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// asserts that the command resulted in empty (zero-length) stderr stream output
|
/// asserts that the command resulted in empty (zero-length) stderr stream output
|
||||||
/// generally, it's better to use stdout_only() instead,
|
/// generally, it's better to use `stdout_only()` instead,
|
||||||
/// but you might find yourself using this function if
|
/// but you might find yourself using this function if
|
||||||
/// 1. you can not know exactly what stdout will be or
|
/// 1. you can not know exactly what stdout will be or
|
||||||
/// 2. you know that stdout will also be empty
|
/// 2. you know that stdout will also be empty
|
||||||
|
@ -215,8 +215,8 @@ impl CmdResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// asserts that the command resulted in empty (zero-length) stderr stream output
|
/// asserts that the command resulted in empty (zero-length) stderr stream output
|
||||||
/// unless asserting there was neither stdout or stderr, stderr_only is usually a better choice
|
/// unless asserting there was neither stdout or stderr, `stderr_only` is usually a better choice
|
||||||
/// generally, it's better to use stderr_only() instead,
|
/// generally, it's better to use `stderr_only()` instead,
|
||||||
/// but you might find yourself using this function if
|
/// but you might find yourself using this function if
|
||||||
/// 1. you can not know exactly what stderr will be or
|
/// 1. you can not know exactly what stderr will be or
|
||||||
/// 2. you know that stderr will also be empty
|
/// 2. you know that stderr will also be empty
|
||||||
|
@ -236,7 +236,7 @@ impl CmdResult {
|
||||||
|
|
||||||
/// asserts that the command resulted in stdout stream output that equals the
|
/// asserts that the command resulted in stdout stream output that equals the
|
||||||
/// passed in value, trailing whitespace are kept to force strict comparison (#1235)
|
/// passed in value, trailing whitespace are kept to force strict comparison (#1235)
|
||||||
/// stdout_only is a better choice unless stderr may or will be non-empty
|
/// `stdout_only()` is a better choice unless stderr may or will be non-empty
|
||||||
pub fn stdout_is<T: AsRef<str>>(&self, msg: T) -> &Self {
|
pub fn stdout_is<T: AsRef<str>>(&self, msg: T) -> &Self {
|
||||||
assert_eq!(self.stdout_str(), String::from(msg.as_ref()));
|
assert_eq!(self.stdout_str(), String::from(msg.as_ref()));
|
||||||
self
|
self
|
||||||
|
@ -244,13 +244,12 @@ impl CmdResult {
|
||||||
|
|
||||||
/// like `stdout_is`, but succeeds if any elements of `expected` matches stdout.
|
/// like `stdout_is`, but succeeds if any elements of `expected` matches stdout.
|
||||||
pub fn stdout_is_any<T: AsRef<str> + std::fmt::Debug>(&self, expected: &[T]) -> &Self {
|
pub fn stdout_is_any<T: AsRef<str> + std::fmt::Debug>(&self, expected: &[T]) -> &Self {
|
||||||
if !expected.iter().any(|msg| self.stdout_str() == msg.as_ref()) {
|
assert!(
|
||||||
panic!(
|
expected.iter().any(|msg| self.stdout_str() == msg.as_ref()),
|
||||||
"stdout was {}\nExpected any of {:#?}",
|
"stdout was {}\nExpected any of {:#?}",
|
||||||
self.stdout_str(),
|
self.stdout_str(),
|
||||||
expected
|
expected
|
||||||
);
|
);
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,7 +267,7 @@ impl CmdResult {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// like stdout_is(...), but expects the contents of the file at the provided relative path
|
/// like `stdout_is()`, but expects the contents of the file at the provided relative path
|
||||||
pub fn stdout_is_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &Self {
|
pub fn stdout_is_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &Self {
|
||||||
let contents = read_scenario_fixture(&self.tmpd, file_rel_path);
|
let contents = read_scenario_fixture(&self.tmpd, file_rel_path);
|
||||||
self.stdout_is(String::from_utf8(contents).unwrap())
|
self.stdout_is(String::from_utf8(contents).unwrap())
|
||||||
|
@ -295,7 +294,7 @@ impl CmdResult {
|
||||||
self.stdout_is_bytes(contents)
|
self.stdout_is_bytes(contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// like stdout_is_fixture(...), but replaces the data in fixture file based on values provided in template_vars
|
/// like `stdout_is_fixture()`, but replaces the data in fixture file based on values provided in `template_vars`
|
||||||
/// command output
|
/// command output
|
||||||
pub fn stdout_is_templated_fixture<T: AsRef<OsStr>>(
|
pub fn stdout_is_templated_fixture<T: AsRef<OsStr>>(
|
||||||
&self,
|
&self,
|
||||||
|
@ -329,7 +328,7 @@ impl CmdResult {
|
||||||
|
|
||||||
/// asserts that the command resulted in stderr stream output that equals the
|
/// asserts that the command resulted in stderr stream output that equals the
|
||||||
/// passed in value, when both are trimmed of trailing whitespace
|
/// passed in value, when both are trimmed of trailing whitespace
|
||||||
/// stderr_only is a better choice unless stdout may or will be non-empty
|
/// `stderr_only` is a better choice unless stdout may or will be non-empty
|
||||||
pub fn stderr_is<T: AsRef<str>>(&self, msg: T) -> &Self {
|
pub fn stderr_is<T: AsRef<str>>(&self, msg: T) -> &Self {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
self.stderr_str().trim_end(),
|
self.stderr_str().trim_end(),
|
||||||
|
@ -345,7 +344,7 @@ impl CmdResult {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like stdout_is_fixture, but for stderr
|
/// Like `stdout_is_fixture`, but for stderr
|
||||||
pub fn stderr_is_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &Self {
|
pub fn stderr_is_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &Self {
|
||||||
let contents = read_scenario_fixture(&self.tmpd, file_rel_path);
|
let contents = read_scenario_fixture(&self.tmpd, file_rel_path);
|
||||||
self.stderr_is(String::from_utf8(contents).unwrap())
|
self.stderr_is(String::from_utf8(contents).unwrap())
|
||||||
|
@ -367,7 +366,7 @@ impl CmdResult {
|
||||||
self.no_stderr().stdout_is_bytes(msg)
|
self.no_stderr().stdout_is_bytes(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// like stdout_only(...), but expects the contents of the file at the provided relative path
|
/// like `stdout_only()`, but expects the contents of the file at the provided relative path
|
||||||
pub fn stdout_only_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &Self {
|
pub fn stdout_only_fixture<T: AsRef<OsStr>>(&self, file_rel_path: T) -> &Self {
|
||||||
let contents = read_scenario_fixture(&self.tmpd, file_rel_path);
|
let contents = read_scenario_fixture(&self.tmpd, file_rel_path);
|
||||||
self.stdout_only_bytes(contents)
|
self.stdout_only_bytes(contents)
|
||||||
|
@ -399,8 +398,8 @@ impl CmdResult {
|
||||||
/// 1. the command resulted in stderr stream output that equals the
|
/// 1. the command resulted in stderr stream output that equals the
|
||||||
/// the following format when both are trimmed of trailing whitespace
|
/// the following format when both are trimmed of trailing whitespace
|
||||||
/// `"{util_name}: {msg}\nTry '{bin_path} {util_name} --help' for more information."`
|
/// `"{util_name}: {msg}\nTry '{bin_path} {util_name} --help' for more information."`
|
||||||
/// This the expected format when a UUsageError is returned or when show_error! is called
|
/// This the expected format when a `UUsageError` is returned or when `show_error!` is called
|
||||||
/// `msg` should be the same as the one provided to UUsageError::new or show_error!
|
/// `msg` should be the same as the one provided to `UUsageError::new` or `show_error!`
|
||||||
///
|
///
|
||||||
/// 2. the command resulted in empty (zero-length) stdout stream output
|
/// 2. the command resulted in empty (zero-length) stdout stream output
|
||||||
pub fn usage_error<T: AsRef<str>>(&self, msg: T) -> &Self {
|
pub fn usage_error<T: AsRef<str>>(&self, msg: T) -> &Self {
|
||||||
|
@ -448,16 +447,20 @@ impl CmdResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stdout_matches(&self, regex: ®ex::Regex) -> &Self {
|
pub fn stdout_matches(&self, regex: ®ex::Regex) -> &Self {
|
||||||
if !regex.is_match(self.stdout_str().trim()) {
|
assert!(
|
||||||
panic!("Stdout does not match regex:\n{}", self.stdout_str());
|
regex.is_match(self.stdout_str().trim()),
|
||||||
}
|
"Stdout does not match regex:\n{}",
|
||||||
|
self.stdout_str()
|
||||||
|
);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stdout_does_not_match(&self, regex: ®ex::Regex) -> &Self {
|
pub fn stdout_does_not_match(&self, regex: ®ex::Regex) -> &Self {
|
||||||
if regex.is_match(self.stdout_str().trim()) {
|
assert!(
|
||||||
panic!("Stdout matches regex:\n{}", self.stdout_str());
|
!regex.is_match(self.stdout_str().trim()),
|
||||||
}
|
"Stdout matches regex:\n{}",
|
||||||
|
self.stdout_str()
|
||||||
|
);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -552,7 +555,7 @@ impl AtPath {
|
||||||
let mut f = self.open(name);
|
let mut f = self.open(name);
|
||||||
let mut contents = String::new();
|
let mut contents = String::new();
|
||||||
f.read_to_string(&mut contents)
|
f.read_to_string(&mut contents)
|
||||||
.unwrap_or_else(|e| panic!("Couldn't read {}: {}", name, e));
|
.unwrap_or_else(|e| panic!("Couldn't read {name}: {e}"));
|
||||||
contents
|
contents
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -560,20 +563,20 @@ impl AtPath {
|
||||||
let mut f = self.open(name);
|
let mut f = self.open(name);
|
||||||
let mut contents = Vec::new();
|
let mut contents = Vec::new();
|
||||||
f.read_to_end(&mut contents)
|
f.read_to_end(&mut contents)
|
||||||
.unwrap_or_else(|e| panic!("Couldn't read {}: {}", name, e));
|
.unwrap_or_else(|e| panic!("Couldn't read {name}: {e}"));
|
||||||
contents
|
contents
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(&self, name: &str, contents: &str) {
|
pub fn write(&self, name: &str, contents: &str) {
|
||||||
log_info("write(default)", self.plus_as_string(name));
|
log_info("write(default)", self.plus_as_string(name));
|
||||||
std::fs::write(self.plus(name), contents)
|
std::fs::write(self.plus(name), contents)
|
||||||
.unwrap_or_else(|e| panic!("Couldn't write {}: {}", name, e));
|
.unwrap_or_else(|e| panic!("Couldn't write {name}: {e}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_bytes(&self, name: &str, contents: &[u8]) {
|
pub fn write_bytes(&self, name: &str, contents: &[u8]) {
|
||||||
log_info("write(default)", self.plus_as_string(name));
|
log_info("write(default)", self.plus_as_string(name));
|
||||||
std::fs::write(self.plus(name), contents)
|
std::fs::write(self.plus(name), contents)
|
||||||
.unwrap_or_else(|e| panic!("Couldn't write {}: {}", name, e));
|
.unwrap_or_else(|e| panic!("Couldn't write {name}: {e}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn append(&self, name: &str, contents: &str) {
|
pub fn append(&self, name: &str, contents: &str) {
|
||||||
|
@ -585,7 +588,7 @@ impl AtPath {
|
||||||
.open(self.plus(name))
|
.open(self.plus(name))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
f.write_all(contents.as_bytes())
|
f.write_all(contents.as_bytes())
|
||||||
.unwrap_or_else(|e| panic!("Couldn't write(append) {}: {}", name, e));
|
.unwrap_or_else(|e| panic!("Couldn't write(append) {name}: {e}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn append_bytes(&self, name: &str, contents: &[u8]) {
|
pub fn append_bytes(&self, name: &str, contents: &[u8]) {
|
||||||
|
@ -597,7 +600,7 @@ impl AtPath {
|
||||||
.open(self.plus(name))
|
.open(self.plus(name))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
f.write_all(contents)
|
f.write_all(contents)
|
||||||
.unwrap_or_else(|e| panic!("Couldn't write(append) to {}: {}", name, e));
|
.unwrap_or_else(|e| panic!("Couldn't write(append) to {name}: {e}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn truncate(&self, name: &str, contents: &str) {
|
pub fn truncate(&self, name: &str, contents: &str) {
|
||||||
|
@ -609,30 +612,29 @@ impl AtPath {
|
||||||
.open(self.plus(name))
|
.open(self.plus(name))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
f.write_all(contents.as_bytes())
|
f.write_all(contents.as_bytes())
|
||||||
.unwrap_or_else(|e| panic!("Couldn't write(truncate) {}: {}", name, e));
|
.unwrap_or_else(|e| panic!("Couldn't write(truncate) {name}: {e}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rename(&self, source: &str, target: &str) {
|
pub fn rename(&self, source: &str, target: &str) {
|
||||||
let source = self.plus(source);
|
let source = self.plus(source);
|
||||||
let target = self.plus(target);
|
let target = self.plus(target);
|
||||||
log_info("rename", format!("{:?} {:?}", source, target));
|
log_info("rename", format!("{source:?} {target:?}"));
|
||||||
std::fs::rename(&source, &target)
|
std::fs::rename(&source, &target)
|
||||||
.unwrap_or_else(|e| panic!("Couldn't rename {:?} -> {:?}: {}", source, target, e));
|
.unwrap_or_else(|e| panic!("Couldn't rename {source:?} -> {target:?}: {e}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&self, source: &str) {
|
pub fn remove(&self, source: &str) {
|
||||||
let source = self.plus(source);
|
let source = self.plus(source);
|
||||||
log_info("remove", format!("{:?}", source));
|
log_info("remove", format!("{source:?}"));
|
||||||
std::fs::remove_file(&source)
|
std::fs::remove_file(&source).unwrap_or_else(|e| panic!("Couldn't remove {source:?}: {e}"));
|
||||||
.unwrap_or_else(|e| panic!("Couldn't remove {:?}: {}", source, e));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy(&self, source: &str, target: &str) {
|
pub fn copy(&self, source: &str, target: &str) {
|
||||||
let source = self.plus(source);
|
let source = self.plus(source);
|
||||||
let target = self.plus(target);
|
let target = self.plus(target);
|
||||||
log_info("copy", format!("{:?} {:?}", source, target));
|
log_info("copy", format!("{source:?} {target:?}"));
|
||||||
std::fs::copy(&source, &target)
|
std::fs::copy(&source, &target)
|
||||||
.unwrap_or_else(|e| panic!("Couldn't copy {:?} -> {:?}: {}", source, target, e));
|
.unwrap_or_else(|e| panic!("Couldn't copy {source:?} -> {target:?}: {e}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rmdir(&self, dir: &str) {
|
pub fn rmdir(&self, dir: &str) {
|
||||||
|
@ -959,7 +961,7 @@ impl UCommand {
|
||||||
env_clear: bool,
|
env_clear: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let bin_path = bin_path.as_ref();
|
let bin_path = bin_path.as_ref();
|
||||||
let util_name = util_name.as_ref().map(|un| un.as_ref());
|
let util_name = util_name.as_ref().map(std::convert::AsRef::as_ref);
|
||||||
|
|
||||||
let mut ucmd = Self {
|
let mut ucmd = Self {
|
||||||
tmpd: None,
|
tmpd: None,
|
||||||
|
@ -1079,7 +1081,7 @@ impl UCommand {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// like pipe_in(...), but uses the contents of the file at the provided relative path as the piped in data
|
/// like `pipe_in()`, but uses the contents of the file at the provided relative path as the piped in data
|
||||||
pub fn pipe_in_fixture<S: AsRef<OsStr>>(&mut self, file_rel_path: S) -> &mut Self {
|
pub fn pipe_in_fixture<S: AsRef<OsStr>>(&mut self, file_rel_path: S) -> &mut Self {
|
||||||
let contents = read_scenario_fixture(&self.tmpd, file_rel_path);
|
let contents = read_scenario_fixture(&self.tmpd, file_rel_path);
|
||||||
self.pipe_in(contents)
|
self.pipe_in(contents)
|
||||||
|
@ -1192,7 +1194,7 @@ impl UCommand {
|
||||||
|
|
||||||
/// Spawns the command, feeding the passed in stdin, waits for the result
|
/// Spawns the command, feeding the passed in stdin, waits for the result
|
||||||
/// and returns a command result.
|
/// and returns a command result.
|
||||||
/// It is recommended that, instead of this, you use a combination of pipe_in()
|
/// It is recommended that, instead of this, you use a combination of `pipe_in()`
|
||||||
/// with succeeds() or fails()
|
/// with succeeds() or fails()
|
||||||
pub fn run_piped_stdin<T: Into<Vec<u8>>>(&mut self, input: T) -> CmdResult {
|
pub fn run_piped_stdin<T: Into<Vec<u8>>>(&mut self, input: T) -> CmdResult {
|
||||||
self.pipe_in(input).run()
|
self.pipe_in(input).run()
|
||||||
|
@ -1216,7 +1218,7 @@ impl UCommand {
|
||||||
|
|
||||||
pub fn get_full_fixture_path(&self, file_rel_path: &str) -> String {
|
pub fn get_full_fixture_path(&self, file_rel_path: &str) -> String {
|
||||||
let tmpdir_path = self.tmpd.as_ref().unwrap().path();
|
let tmpdir_path = self.tmpd.as_ref().unwrap().path();
|
||||||
format!("{}/{}", tmpdir_path.to_str().unwrap(), file_rel_path)
|
format!("{}/{file_rel_path}", tmpdir_path.to_str().unwrap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1229,7 +1231,7 @@ struct CapturedOutput {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CapturedOutput {
|
impl CapturedOutput {
|
||||||
/// Creates a new instance of CapturedOutput
|
/// Creates a new instance of `CapturedOutput`
|
||||||
fn new(output: tempfile::NamedTempFile) -> Self {
|
fn new(output: tempfile::NamedTempFile) -> Self {
|
||||||
Self {
|
Self {
|
||||||
current_file: output.reopen().unwrap(),
|
current_file: output.reopen().unwrap(),
|
||||||
|
@ -1411,7 +1413,7 @@ impl<'a> UChildAssertion<'a> {
|
||||||
self.uchild.stderr_all()
|
self.uchild.stderr_all()
|
||||||
),
|
),
|
||||||
Ok(None) => {}
|
Ok(None) => {}
|
||||||
Err(error) => panic!("Assertion failed with error '{:?}'", error),
|
Err(error) => panic!("Assertion failed with error '{error:?}'"),
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
|
@ -1430,7 +1432,7 @@ impl<'a> UChildAssertion<'a> {
|
||||||
self.uchild.stdout_all(),
|
self.uchild.stdout_all(),
|
||||||
self.uchild.stderr_all()),
|
self.uchild.stderr_all()),
|
||||||
Ok(_) => {},
|
Ok(_) => {},
|
||||||
Err(error) => panic!("Assertion failed with error '{:?}'", error),
|
Err(error) => panic!("Assertion failed with error '{error:?}'"),
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
|
@ -1780,7 +1782,7 @@ impl UChild {
|
||||||
match writer.write_all(&content).and_then(|_| writer.flush()) {
|
match writer.write_all(&content).and_then(|_| writer.flush()) {
|
||||||
Err(error) if !ignore_stdin_write_error => Err(io::Error::new(
|
Err(error) if !ignore_stdin_write_error => Err(io::Error::new(
|
||||||
io::ErrorKind::Other,
|
io::ErrorKind::Other,
|
||||||
format!("failed to write to stdin of child: {}", error),
|
format!("failed to write to stdin of child: {error}"),
|
||||||
)),
|
)),
|
||||||
Ok(_) | Err(_) => Ok(()),
|
Ok(_) | Err(_) => Ok(()),
|
||||||
}
|
}
|
||||||
|
@ -1832,7 +1834,7 @@ impl UChild {
|
||||||
match stdin.write_all(&data.into()).and_then(|_| stdin.flush()) {
|
match stdin.write_all(&data.into()).and_then(|_| stdin.flush()) {
|
||||||
Err(error) if !self.ignore_stdin_write_error => Err(io::Error::new(
|
Err(error) if !self.ignore_stdin_write_error => Err(io::Error::new(
|
||||||
io::ErrorKind::Other,
|
io::ErrorKind::Other,
|
||||||
format!("failed to write to stdin of child: {}", error),
|
format!("failed to write to stdin of child: {error}"),
|
||||||
)),
|
)),
|
||||||
Ok(_) | Err(_) => Ok(()),
|
Ok(_) | Err(_) => Ok(()),
|
||||||
}
|
}
|
||||||
|
@ -1894,7 +1896,7 @@ pub fn whoami() -> String {
|
||||||
std::env::var("USER")
|
std::env::var("USER")
|
||||||
.or_else(|_| std::env::var("USERNAME"))
|
.or_else(|_| std::env::var("USERNAME"))
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| {
|
||||||
println!("{}: {}, using \"nobody\" instead", UUTILS_WARNING, e);
|
println!("{UUTILS_WARNING}: {e}, using \"nobody\" instead");
|
||||||
"nobody".to_string()
|
"nobody".to_string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1965,33 +1967,33 @@ pub fn check_coreutil_version(
|
||||||
// id (GNU coreutils) 8.32.162-4eda
|
// id (GNU coreutils) 8.32.162-4eda
|
||||||
|
|
||||||
let util_name = &host_name_for(util_name);
|
let util_name = &host_name_for(util_name);
|
||||||
log_info("run", format!("{} --version", util_name));
|
log_info("run", format!("{util_name} --version"));
|
||||||
let version_check = match Command::new(util_name.as_ref())
|
let version_check = match Command::new(util_name.as_ref())
|
||||||
.env("LC_ALL", "C")
|
.env("LC_ALL", "C")
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
.output()
|
.output()
|
||||||
{
|
{
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => return Err(format!("{}: '{}' {}", UUTILS_WARNING, util_name, e)),
|
Err(e) => return Err(format!("{UUTILS_WARNING}: '{util_name}' {e}")),
|
||||||
};
|
};
|
||||||
std::str::from_utf8(&version_check.stdout).unwrap()
|
std::str::from_utf8(&version_check.stdout).unwrap()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.first()
|
.first()
|
||||||
.map_or_else(
|
.map_or_else(
|
||||||
|| Err(format!("{}: unexpected output format for reference coreutil: '{} --version'", UUTILS_WARNING, util_name)),
|
|| Err(format!("{UUTILS_WARNING}: unexpected output format for reference coreutil: '{util_name} --version'")),
|
||||||
|s| {
|
|s| {
|
||||||
if s.contains(&format!("(GNU coreutils) {}", version_expected)) {
|
if s.contains(&format!("(GNU coreutils) {version_expected}")) {
|
||||||
Ok(format!("{}: {}", UUTILS_INFO, s))
|
Ok(format!("{UUTILS_INFO}: {s}"))
|
||||||
} else if s.contains("(GNU coreutils)") {
|
} else if s.contains("(GNU coreutils)") {
|
||||||
let version_found = parse_coreutil_version(s);
|
let version_found = parse_coreutil_version(s);
|
||||||
let version_expected = version_expected.parse::<f32>().unwrap_or_default();
|
let version_expected = version_expected.parse::<f32>().unwrap_or_default();
|
||||||
if version_found > version_expected {
|
if version_found > version_expected {
|
||||||
Ok(format!("{}: version for the reference coreutil '{}' is higher than expected; expected: {}, found: {}", UUTILS_INFO, util_name, version_expected, version_found))
|
Ok(format!("{UUTILS_INFO}: version for the reference coreutil '{util_name}' is higher than expected; expected: {version_expected}, found: {version_found}"))
|
||||||
} else {
|
} else {
|
||||||
Err(format!("{}: version for the reference coreutil '{}' does not match; expected: {}, found: {}", UUTILS_WARNING, util_name, version_expected, version_found)) }
|
Err(format!("{UUTILS_WARNING}: version for the reference coreutil '{util_name}' does not match; expected: {version_expected}, found: {version_found}")) }
|
||||||
} else {
|
} else {
|
||||||
Err(format!("{}: no coreutils version string found for reference coreutils '{} --version'", UUTILS_WARNING, util_name))
|
Err(format!("{UUTILS_WARNING}: no coreutils version string found for reference coreutils '{util_name} --version'"))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -2125,10 +2127,10 @@ pub fn run_ucmd_as_root(
|
||||||
Err("Cannot run non-interactive sudo".to_string())
|
Err("Cannot run non-interactive sudo".to_string())
|
||||||
}
|
}
|
||||||
Ok(_output) => Err("\"sudo whoami\" didn't return \"root\"".to_string()),
|
Ok(_output) => Err("\"sudo whoami\" didn't return \"root\"".to_string()),
|
||||||
Err(e) => Err(format!("{}: {}", UUTILS_WARNING, e)),
|
Err(e) => Err(format!("{UUTILS_WARNING}: {e}")),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(format!("{}: {}", UUTILS_INFO, "cannot run inside CI"))
|
Err(format!("{UUTILS_INFO}: {}", "cannot run inside CI"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2548,9 +2550,18 @@ mod tests {
|
||||||
fn test_uchild_when_run_no_wait_with_a_non_blocking_util() {
|
fn test_uchild_when_run_no_wait_with_a_non_blocking_util() {
|
||||||
let ts = TestScenario::new("echo");
|
let ts = TestScenario::new("echo");
|
||||||
let mut child = ts.ucmd().arg("hello world").run_no_wait();
|
let mut child = ts.ucmd().arg("hello world").run_no_wait();
|
||||||
child.delay(500);
|
|
||||||
|
|
||||||
// check `child.is_alive()` is working
|
// check `child.is_alive()` and `child.delay()` is working
|
||||||
|
let mut trials = 10;
|
||||||
|
while child.is_alive() {
|
||||||
|
if trials <= 0 {
|
||||||
|
panic!("Assertion failed: child process is still alive.")
|
||||||
|
}
|
||||||
|
|
||||||
|
child.delay(500);
|
||||||
|
trials -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
assert!(!child.is_alive());
|
assert!(!child.is_alive());
|
||||||
|
|
||||||
// check `child.is_not_alive()` is working
|
// check `child.is_not_alive()` is working
|
||||||
|
@ -2599,9 +2610,9 @@ mod tests {
|
||||||
at.touch("a/empty");
|
at.touch("a/empty");
|
||||||
|
|
||||||
#[cfg(target_vendor = "apple")]
|
#[cfg(target_vendor = "apple")]
|
||||||
let delay: u64 = 1000;
|
let delay: u64 = 2000;
|
||||||
#[cfg(not(target_vendor = "apple"))]
|
#[cfg(not(target_vendor = "apple"))]
|
||||||
let delay: u64 = 500;
|
let delay: u64 = 1000;
|
||||||
|
|
||||||
let yes = if cfg!(windows) { "y\r\n" } else { "y\n" };
|
let yes = if cfg!(windows) { "y\r\n" } else { "y\n" };
|
||||||
|
|
||||||
|
@ -2641,29 +2652,10 @@ mod tests {
|
||||||
.with_exact_output(44, 0)
|
.with_exact_output(44, 0)
|
||||||
.stdout_only(expected);
|
.stdout_only(expected);
|
||||||
|
|
||||||
#[cfg(windows)]
|
let expected = "removed directory 'a'\n";
|
||||||
let expected = "rm: descend into directory 'a'? \
|
|
||||||
rm: remove regular empty file 'a\\empty'? \
|
|
||||||
removed 'a\\empty'\n\
|
|
||||||
rm: remove directory 'a'? \
|
|
||||||
removed directory 'a'\n";
|
|
||||||
#[cfg(unix)]
|
|
||||||
let expected = "rm: descend into directory 'a'? \
|
|
||||||
rm: remove regular empty file 'a/empty'? \
|
|
||||||
removed 'a/empty'\n\
|
|
||||||
rm: remove directory 'a'? \
|
|
||||||
removed directory 'a'\n";
|
|
||||||
|
|
||||||
child.write_in(yes);
|
child.write_in(yes);
|
||||||
child
|
child.wait().unwrap().stdout_only(expected).success();
|
||||||
.delay(delay)
|
|
||||||
.kill()
|
|
||||||
.make_assertion()
|
|
||||||
.is_not_alive()
|
|
||||||
.with_all_output()
|
|
||||||
.stdout_only(expected);
|
|
||||||
|
|
||||||
child.wait().unwrap().no_stdout().no_stderr().success();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tail")]
|
#[cfg(feature = "tail")]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue