mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
Merge branch 'main' into issue-5766
This commit is contained in:
commit
b309d64e78
14 changed files with 435 additions and 94 deletions
|
@ -17,7 +17,9 @@ use std::path::{Path, PathBuf, StripPrefixError};
|
||||||
use indicatif::ProgressBar;
|
use indicatif::ProgressBar;
|
||||||
use uucore::display::Quotable;
|
use uucore::display::Quotable;
|
||||||
use uucore::error::UIoError;
|
use uucore::error::UIoError;
|
||||||
use uucore::fs::{canonicalize, FileInformation, MissingHandling, ResolveMode};
|
use uucore::fs::{
|
||||||
|
canonicalize, path_ends_with_terminator, FileInformation, MissingHandling, ResolveMode,
|
||||||
|
};
|
||||||
use uucore::show;
|
use uucore::show;
|
||||||
use uucore::show_error;
|
use uucore::show_error;
|
||||||
use uucore::uio_error;
|
use uucore::uio_error;
|
||||||
|
@ -170,8 +172,15 @@ impl Entry {
|
||||||
let mut descendant =
|
let mut descendant =
|
||||||
get_local_to_root_parent(&source_absolute, context.root_parent.as_deref())?;
|
get_local_to_root_parent(&source_absolute, context.root_parent.as_deref())?;
|
||||||
if no_target_dir {
|
if no_target_dir {
|
||||||
|
let source_is_dir = direntry.path().is_dir();
|
||||||
|
if path_ends_with_terminator(context.target) && source_is_dir {
|
||||||
|
if let Err(e) = std::fs::create_dir_all(context.target) {
|
||||||
|
eprintln!("Failed to create directory: {}", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
descendant = descendant.strip_prefix(context.root)?.to_path_buf();
|
descendant = descendant.strip_prefix(context.root)?.to_path_buf();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let local_to_target = context.target.join(descendant);
|
let local_to_target = context.target.join(descendant);
|
||||||
let target_is_file = context.target.is_file();
|
let target_is_file = context.target.is_file();
|
||||||
|
|
|
@ -32,8 +32,8 @@ use platform::copy_on_write;
|
||||||
use uucore::display::Quotable;
|
use uucore::display::Quotable;
|
||||||
use uucore::error::{set_exit_code, UClapError, UError, UResult, UUsageError};
|
use uucore::error::{set_exit_code, UClapError, UError, UResult, UUsageError};
|
||||||
use uucore::fs::{
|
use uucore::fs::{
|
||||||
are_hardlinks_to_same_file, canonicalize, is_symlink_loop, paths_refer_to_same_file,
|
are_hardlinks_to_same_file, canonicalize, is_symlink_loop, path_ends_with_terminator,
|
||||||
FileInformation, MissingHandling, ResolveMode,
|
paths_refer_to_same_file, FileInformation, MissingHandling, ResolveMode,
|
||||||
};
|
};
|
||||||
use uucore::{backup_control, update_control};
|
use uucore::{backup_control, update_control};
|
||||||
// These are exposed for projects (e.g. nushell) that want to create an `Options` value, which
|
// These are exposed for projects (e.g. nushell) that want to create an `Options` value, which
|
||||||
|
@ -1994,6 +1994,10 @@ fn copy_helper(
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if path_ends_with_terminator(dest) && !dest.is_dir() {
|
||||||
|
return Err(Error::NotADirectory(dest.to_path_buf()));
|
||||||
|
}
|
||||||
|
|
||||||
if source.as_os_str() == "/dev/null" {
|
if source.as_os_str() == "/dev/null" {
|
||||||
/* workaround a limitation of fs::copy
|
/* workaround a limitation of fs::copy
|
||||||
* https://github.com/rust-lang/rust/issues/79390
|
* https://github.com/rust-lang/rust/issues/79390
|
||||||
|
|
|
@ -621,7 +621,52 @@ fn extract_hyperlink(options: &clap::ArgMatches) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Match the argument given to --quoting-style or the QUOTING_STYLE env variable.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `style`: the actual argument string
|
||||||
|
/// * `show_control` - A boolean value representing whether or not to show control characters.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * An option with None if the style string is invalid, or a `QuotingStyle` wrapped in `Some`.
|
||||||
|
fn match_quoting_style_name(style: &str, show_control: bool) -> Option<QuotingStyle> {
|
||||||
|
match style {
|
||||||
|
"literal" => Some(QuotingStyle::Literal { show_control }),
|
||||||
|
"shell" => Some(QuotingStyle::Shell {
|
||||||
|
escape: false,
|
||||||
|
always_quote: false,
|
||||||
|
show_control,
|
||||||
|
}),
|
||||||
|
"shell-always" => Some(QuotingStyle::Shell {
|
||||||
|
escape: false,
|
||||||
|
always_quote: true,
|
||||||
|
show_control,
|
||||||
|
}),
|
||||||
|
"shell-escape" => Some(QuotingStyle::Shell {
|
||||||
|
escape: true,
|
||||||
|
always_quote: false,
|
||||||
|
show_control,
|
||||||
|
}),
|
||||||
|
"shell-escape-always" => Some(QuotingStyle::Shell {
|
||||||
|
escape: true,
|
||||||
|
always_quote: true,
|
||||||
|
show_control,
|
||||||
|
}),
|
||||||
|
"c" => Some(QuotingStyle::C {
|
||||||
|
quotes: quoting_style::Quotes::Double,
|
||||||
|
}),
|
||||||
|
"escape" => Some(QuotingStyle::C {
|
||||||
|
quotes: quoting_style::Quotes::None,
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Extracts the quoting style to use based on the options provided.
|
/// Extracts the quoting style to use based on the options provided.
|
||||||
|
/// If no options are given, it looks if a default quoting style is provided
|
||||||
|
/// through the QUOTING_STYLE environment variable.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
|
@ -632,38 +677,12 @@ fn extract_hyperlink(options: &clap::ArgMatches) -> bool {
|
||||||
///
|
///
|
||||||
/// A QuotingStyle variant representing the quoting style to use.
|
/// A QuotingStyle variant representing the quoting style to use.
|
||||||
fn extract_quoting_style(options: &clap::ArgMatches, show_control: bool) -> QuotingStyle {
|
fn extract_quoting_style(options: &clap::ArgMatches, show_control: bool) -> QuotingStyle {
|
||||||
let opt_quoting_style = options.get_one::<String>(options::QUOTING_STYLE).cloned();
|
let opt_quoting_style = options.get_one::<String>(options::QUOTING_STYLE);
|
||||||
|
|
||||||
if let Some(style) = opt_quoting_style {
|
if let Some(style) = opt_quoting_style {
|
||||||
match style.as_str() {
|
match match_quoting_style_name(style, show_control) {
|
||||||
"literal" => QuotingStyle::Literal { show_control },
|
Some(qs) => qs,
|
||||||
"shell" => QuotingStyle::Shell {
|
None => unreachable!("Should have been caught by Clap"),
|
||||||
escape: false,
|
|
||||||
always_quote: false,
|
|
||||||
show_control,
|
|
||||||
},
|
|
||||||
"shell-always" => QuotingStyle::Shell {
|
|
||||||
escape: false,
|
|
||||||
always_quote: true,
|
|
||||||
show_control,
|
|
||||||
},
|
|
||||||
"shell-escape" => QuotingStyle::Shell {
|
|
||||||
escape: true,
|
|
||||||
always_quote: false,
|
|
||||||
show_control,
|
|
||||||
},
|
|
||||||
"shell-escape-always" => QuotingStyle::Shell {
|
|
||||||
escape: true,
|
|
||||||
always_quote: true,
|
|
||||||
show_control,
|
|
||||||
},
|
|
||||||
"c" => QuotingStyle::C {
|
|
||||||
quotes: quoting_style::Quotes::Double,
|
|
||||||
},
|
|
||||||
"escape" => QuotingStyle::C {
|
|
||||||
quotes: quoting_style::Quotes::None,
|
|
||||||
},
|
|
||||||
_ => unreachable!("Should have been caught by Clap"),
|
|
||||||
}
|
}
|
||||||
} else if options.get_flag(options::quoting::LITERAL) {
|
} else if options.get_flag(options::quoting::LITERAL) {
|
||||||
QuotingStyle::Literal { show_control }
|
QuotingStyle::Literal { show_control }
|
||||||
|
@ -675,17 +694,32 @@ fn extract_quoting_style(options: &clap::ArgMatches, show_control: bool) -> Quot
|
||||||
QuotingStyle::C {
|
QuotingStyle::C {
|
||||||
quotes: quoting_style::Quotes::Double,
|
quotes: quoting_style::Quotes::Double,
|
||||||
}
|
}
|
||||||
} else if options.get_flag(options::DIRED) || !std::io::stdout().is_terminal() {
|
} else if options.get_flag(options::DIRED) {
|
||||||
// By default, `ls` uses Literal quoting when
|
|
||||||
// writing to a non-terminal file descriptor
|
|
||||||
QuotingStyle::Literal { show_control }
|
QuotingStyle::Literal { show_control }
|
||||||
} else {
|
} else {
|
||||||
// TODO: use environment variable if available
|
// If set, the QUOTING_STYLE environment variable specifies a default style.
|
||||||
|
if let Ok(style) = std::env::var("QUOTING_STYLE") {
|
||||||
|
match match_quoting_style_name(style.as_str(), show_control) {
|
||||||
|
Some(qs) => return qs,
|
||||||
|
None => eprintln!(
|
||||||
|
"{}: Ignoring invalid value of environment variable QUOTING_STYLE: '{}'",
|
||||||
|
std::env::args().next().unwrap_or("ls".to_string()),
|
||||||
|
style
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default, `ls` uses Shell escape quoting style when writing to a terminal file
|
||||||
|
// descriptor and Literal otherwise.
|
||||||
|
if std::io::stdout().is_terminal() {
|
||||||
QuotingStyle::Shell {
|
QuotingStyle::Shell {
|
||||||
escape: true,
|
escape: true,
|
||||||
always_quote: false,
|
always_quote: false,
|
||||||
show_control,
|
show_control,
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
QuotingStyle::Literal { show_control }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,10 @@ use std::path::{Path, PathBuf};
|
||||||
use uucore::backup_control::{self, source_is_target_backup};
|
use uucore::backup_control::{self, source_is_target_backup};
|
||||||
use uucore::display::Quotable;
|
use uucore::display::Quotable;
|
||||||
use uucore::error::{set_exit_code, FromIo, UResult, USimpleError, UUsageError};
|
use uucore::error::{set_exit_code, FromIo, UResult, USimpleError, UUsageError};
|
||||||
use uucore::fs::{are_hardlinks_or_one_way_symlink_to_same_file, are_hardlinks_to_same_file};
|
use uucore::fs::{
|
||||||
|
are_hardlinks_or_one_way_symlink_to_same_file, are_hardlinks_to_same_file,
|
||||||
|
path_ends_with_terminator,
|
||||||
|
};
|
||||||
use uucore::update_control;
|
use uucore::update_control;
|
||||||
// These are exposed for projects (e.g. nushell) that want to create an `Options` value, which
|
// These are exposed for projects (e.g. nushell) that want to create an `Options` value, which
|
||||||
// requires these enums
|
// requires these enums
|
||||||
|
@ -104,25 +107,6 @@ static OPT_VERBOSE: &str = "verbose";
|
||||||
static OPT_PROGRESS: &str = "progress";
|
static OPT_PROGRESS: &str = "progress";
|
||||||
static ARG_FILES: &str = "files";
|
static ARG_FILES: &str = "files";
|
||||||
|
|
||||||
/// Returns true if the passed `path` ends with a path terminator.
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn path_ends_with_terminator(path: &Path) -> bool {
|
|
||||||
use std::os::unix::prelude::OsStrExt;
|
|
||||||
path.as_os_str()
|
|
||||||
.as_bytes()
|
|
||||||
.last()
|
|
||||||
.map_or(false, |&byte| byte == b'/' || byte == b'\\')
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
fn path_ends_with_terminator(path: &Path) -> bool {
|
|
||||||
use std::os::windows::prelude::OsStrExt;
|
|
||||||
path.as_os_str()
|
|
||||||
.encode_wide()
|
|
||||||
.last()
|
|
||||||
.map_or(false, |wide| wide == b'/'.into() || wide == b'\\'.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[uucore::main]
|
#[uucore::main]
|
||||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let mut app = uu_app();
|
let mut app = uu_app();
|
||||||
|
@ -335,9 +319,10 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()>
|
||||||
}
|
}
|
||||||
|
|
||||||
let target_is_dir = target.is_dir();
|
let target_is_dir = target.is_dir();
|
||||||
|
let source_is_dir = source.is_dir();
|
||||||
|
|
||||||
if path_ends_with_terminator(target)
|
if path_ends_with_terminator(target)
|
||||||
&& !target_is_dir
|
&& (!target_is_dir && !source_is_dir)
|
||||||
&& !opts.no_target_dir
|
&& !opts.no_target_dir
|
||||||
&& opts.update != UpdateMode::ReplaceIfOlder
|
&& opts.update != UpdateMode::ReplaceIfOlder
|
||||||
{
|
{
|
||||||
|
|
|
@ -28,10 +28,17 @@ pub mod options {
|
||||||
pub const FILE: &str = "file";
|
pub const FILE: &str = "file";
|
||||||
pub const ITERATIONS: &str = "iterations";
|
pub const ITERATIONS: &str = "iterations";
|
||||||
pub const SIZE: &str = "size";
|
pub const SIZE: &str = "size";
|
||||||
|
pub const WIPESYNC: &str = "u";
|
||||||
pub const REMOVE: &str = "remove";
|
pub const REMOVE: &str = "remove";
|
||||||
pub const VERBOSE: &str = "verbose";
|
pub const VERBOSE: &str = "verbose";
|
||||||
pub const EXACT: &str = "exact";
|
pub const EXACT: &str = "exact";
|
||||||
pub const ZERO: &str = "zero";
|
pub const ZERO: &str = "zero";
|
||||||
|
|
||||||
|
pub mod remove {
|
||||||
|
pub const UNLINK: &str = "unlink";
|
||||||
|
pub const WIPE: &str = "wipe";
|
||||||
|
pub const WIPESYNC: &str = "wipesync";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This block size seems to match GNU (2^16 = 65536)
|
// This block size seems to match GNU (2^16 = 65536)
|
||||||
|
@ -81,6 +88,14 @@ enum PassType {
|
||||||
Random,
|
Random,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Copy)]
|
||||||
|
enum RemoveMethod {
|
||||||
|
None, // Default method. Only obfuscate the file data
|
||||||
|
Unlink, // The same as 'None' + unlink the file
|
||||||
|
Wipe, // The same as 'Unlink' + obfuscate the file name before unlink
|
||||||
|
WipeSync, // The same as 'Wipe' sync the file name changes
|
||||||
|
}
|
||||||
|
|
||||||
/// Iterates over all possible filenames of a certain length using NAME_CHARSET as an alphabet
|
/// Iterates over all possible filenames of a certain length using NAME_CHARSET as an alphabet
|
||||||
struct FilenameIter {
|
struct FilenameIter {
|
||||||
// Store the indices of the letters of our filename in NAME_CHARSET
|
// Store the indices of the letters of our filename in NAME_CHARSET
|
||||||
|
@ -219,17 +234,25 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
None => unreachable!(),
|
None => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: implement --remove HOW
|
|
||||||
// The optional HOW parameter indicates how to remove a directory entry:
|
|
||||||
// - 'unlink' => use a standard unlink call.
|
|
||||||
// - 'wipe' => also first obfuscate bytes in the name.
|
|
||||||
// - 'wipesync' => also sync each obfuscated byte to disk.
|
|
||||||
// The default mode is 'wipesync', but note it can be expensive.
|
|
||||||
|
|
||||||
// TODO: implement --random-source
|
// TODO: implement --random-source
|
||||||
|
|
||||||
|
let remove_method = if matches.get_flag(options::WIPESYNC) {
|
||||||
|
RemoveMethod::WipeSync
|
||||||
|
} else if matches.contains_id(options::REMOVE) {
|
||||||
|
match matches
|
||||||
|
.get_one::<String>(options::REMOVE)
|
||||||
|
.map(AsRef::as_ref)
|
||||||
|
{
|
||||||
|
Some(options::remove::UNLINK) => RemoveMethod::Unlink,
|
||||||
|
Some(options::remove::WIPE) => RemoveMethod::Wipe,
|
||||||
|
Some(options::remove::WIPESYNC) => RemoveMethod::WipeSync,
|
||||||
|
_ => unreachable!("should be caught by clap"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RemoveMethod::None
|
||||||
|
};
|
||||||
|
|
||||||
let force = matches.get_flag(options::FORCE);
|
let force = matches.get_flag(options::FORCE);
|
||||||
let remove = matches.get_flag(options::REMOVE);
|
|
||||||
let size_arg = matches
|
let size_arg = matches
|
||||||
.get_one::<String>(options::SIZE)
|
.get_one::<String>(options::SIZE)
|
||||||
.map(|s| s.to_string());
|
.map(|s| s.to_string());
|
||||||
|
@ -240,7 +263,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
|
|
||||||
for path_str in matches.get_many::<String>(options::FILE).unwrap() {
|
for path_str in matches.get_many::<String>(options::FILE).unwrap() {
|
||||||
show_if_err!(wipe_file(
|
show_if_err!(wipe_file(
|
||||||
path_str, iterations, remove, size, exact, zero, verbose, force,
|
path_str,
|
||||||
|
iterations,
|
||||||
|
remove_method,
|
||||||
|
size,
|
||||||
|
exact,
|
||||||
|
zero,
|
||||||
|
verbose,
|
||||||
|
force,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -276,12 +306,26 @@ pub fn uu_app() -> Command {
|
||||||
.help("shred this many bytes (suffixes like K, M, G accepted)"),
|
.help("shred this many bytes (suffixes like K, M, G accepted)"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(options::REMOVE)
|
Arg::new(options::WIPESYNC)
|
||||||
.short('u')
|
.short('u')
|
||||||
.long(options::REMOVE)
|
.help("deallocate and remove file after overwriting")
|
||||||
.help("truncate and remove file after overwriting; See below")
|
|
||||||
.action(ArgAction::SetTrue),
|
.action(ArgAction::SetTrue),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(options::REMOVE)
|
||||||
|
.long(options::REMOVE)
|
||||||
|
.value_name("HOW")
|
||||||
|
.value_parser([
|
||||||
|
options::remove::UNLINK,
|
||||||
|
options::remove::WIPE,
|
||||||
|
options::remove::WIPESYNC,
|
||||||
|
])
|
||||||
|
.num_args(0..=1)
|
||||||
|
.require_equals(true)
|
||||||
|
.default_missing_value(options::remove::WIPESYNC)
|
||||||
|
.help("like -u but give control on HOW to delete; See below")
|
||||||
|
.action(ArgAction::Set),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(options::VERBOSE)
|
Arg::new(options::VERBOSE)
|
||||||
.long(options::VERBOSE)
|
.long(options::VERBOSE)
|
||||||
|
@ -340,7 +384,7 @@ fn pass_name(pass_type: &PassType) -> String {
|
||||||
fn wipe_file(
|
fn wipe_file(
|
||||||
path_str: &str,
|
path_str: &str,
|
||||||
n_passes: usize,
|
n_passes: usize,
|
||||||
remove: bool,
|
remove_method: RemoveMethod,
|
||||||
size: Option<u64>,
|
size: Option<u64>,
|
||||||
exact: bool,
|
exact: bool,
|
||||||
zero: bool,
|
zero: bool,
|
||||||
|
@ -457,8 +501,8 @@ fn wipe_file(
|
||||||
.map_err_context(|| format!("{}: File write pass failed", path.maybe_quote())));
|
.map_err_context(|| format!("{}: File write pass failed", path.maybe_quote())));
|
||||||
}
|
}
|
||||||
|
|
||||||
if remove {
|
if remove_method != RemoveMethod::None {
|
||||||
do_remove(path, path_str, verbose)
|
do_remove(path, path_str, verbose, remove_method)
|
||||||
.map_err_context(|| format!("{}: failed to remove file", path.maybe_quote()))?;
|
.map_err_context(|| format!("{}: failed to remove file", path.maybe_quote()))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -501,7 +545,7 @@ fn get_file_size(path: &Path) -> Result<u64, io::Error> {
|
||||||
|
|
||||||
// Repeatedly renames the file with strings of decreasing length (most likely all 0s)
|
// Repeatedly renames the file with strings of decreasing length (most likely all 0s)
|
||||||
// Return the path of the file after its last renaming or None if error
|
// Return the path of the file after its last renaming or None if error
|
||||||
fn wipe_name(orig_path: &Path, verbose: bool) -> Option<PathBuf> {
|
fn wipe_name(orig_path: &Path, verbose: bool, remove_method: RemoveMethod) -> Option<PathBuf> {
|
||||||
let file_name_len = orig_path.file_name().unwrap().to_str().unwrap().len();
|
let file_name_len = orig_path.file_name().unwrap().to_str().unwrap().len();
|
||||||
|
|
||||||
let mut last_path = PathBuf::from(orig_path);
|
let mut last_path = PathBuf::from(orig_path);
|
||||||
|
@ -526,12 +570,14 @@ fn wipe_name(orig_path: &Path, verbose: bool) -> Option<PathBuf> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if remove_method == RemoveMethod::WipeSync {
|
||||||
// Sync every file rename
|
// Sync every file rename
|
||||||
let new_file = OpenOptions::new()
|
let new_file = OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.open(new_path.clone())
|
.open(new_path.clone())
|
||||||
.expect("Failed to open renamed file for syncing");
|
.expect("Failed to open renamed file for syncing");
|
||||||
new_file.sync_all().expect("Failed to sync renamed file");
|
new_file.sync_all().expect("Failed to sync renamed file");
|
||||||
|
}
|
||||||
|
|
||||||
last_path = new_path;
|
last_path = new_path;
|
||||||
break;
|
break;
|
||||||
|
@ -552,12 +598,23 @@ fn wipe_name(orig_path: &Path, verbose: bool) -> Option<PathBuf> {
|
||||||
Some(last_path)
|
Some(last_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_remove(path: &Path, orig_filename: &str, verbose: bool) -> Result<(), io::Error> {
|
fn do_remove(
|
||||||
|
path: &Path,
|
||||||
|
orig_filename: &str,
|
||||||
|
verbose: bool,
|
||||||
|
remove_method: RemoveMethod,
|
||||||
|
) -> Result<(), io::Error> {
|
||||||
if verbose {
|
if verbose {
|
||||||
show_error!("{}: removing", orig_filename.maybe_quote());
|
show_error!("{}: removing", orig_filename.maybe_quote());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(rp) = wipe_name(path, verbose) {
|
let remove_path = if remove_method == RemoveMethod::Unlink {
|
||||||
|
Some(path.with_file_name(orig_filename))
|
||||||
|
} else {
|
||||||
|
wipe_name(path, verbose, remove_method)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(rp) = remove_path {
|
||||||
fs::remove_file(rp)?;
|
fs::remove_file(rp)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,8 +57,8 @@ pub enum FormatError {
|
||||||
IoError(std::io::Error),
|
IoError(std::io::Error),
|
||||||
NoMoreArguments,
|
NoMoreArguments,
|
||||||
InvalidArgument(FormatArgument),
|
InvalidArgument(FormatArgument),
|
||||||
TooManySpecs,
|
TooManySpecs(Vec<u8>),
|
||||||
NeedAtLeastOneSpec,
|
NeedAtLeastOneSpec(Vec<u8>),
|
||||||
WrongSpecType,
|
WrongSpecType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,9 +79,16 @@ impl Display for FormatError {
|
||||||
"%{}: invalid conversion specification",
|
"%{}: invalid conversion specification",
|
||||||
String::from_utf8_lossy(s)
|
String::from_utf8_lossy(s)
|
||||||
),
|
),
|
||||||
// TODO: The next two should print the spec as well
|
Self::TooManySpecs(s) => write!(
|
||||||
Self::TooManySpecs => write!(f, "format has too many % directives"),
|
f,
|
||||||
Self::NeedAtLeastOneSpec => write!(f, "format has no % directive"),
|
"format '{}' has too many % directives",
|
||||||
|
String::from_utf8_lossy(s)
|
||||||
|
),
|
||||||
|
Self::NeedAtLeastOneSpec(s) => write!(
|
||||||
|
f,
|
||||||
|
"format '{}' has no % directive",
|
||||||
|
String::from_utf8_lossy(s)
|
||||||
|
),
|
||||||
// TODO: Error message below needs some work
|
// TODO: Error message below needs some work
|
||||||
Self::WrongSpecType => write!(f, "wrong % directive type was given"),
|
Self::WrongSpecType => write!(f, "wrong % directive type was given"),
|
||||||
Self::IoError(_) => write!(f, "io error"),
|
Self::IoError(_) => write!(f, "io error"),
|
||||||
|
@ -303,7 +310,9 @@ impl<F: Formatter> Format<F> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(spec) = spec else {
|
let Some(spec) = spec else {
|
||||||
return Err(FormatError::NeedAtLeastOneSpec);
|
return Err(FormatError::NeedAtLeastOneSpec(
|
||||||
|
format_string.as_ref().to_vec(),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
let formatter = F::try_from_spec(spec)?;
|
let formatter = F::try_from_spec(spec)?;
|
||||||
|
@ -312,7 +321,7 @@ impl<F: Formatter> Format<F> {
|
||||||
for item in &mut iter {
|
for item in &mut iter {
|
||||||
match item? {
|
match item? {
|
||||||
FormatItem::Spec(_) => {
|
FormatItem::Spec(_) => {
|
||||||
return Err(FormatError::TooManySpecs);
|
return Err(FormatError::TooManySpecs(format_string.as_ref().to_vec()));
|
||||||
}
|
}
|
||||||
FormatItem::Char(c) => suffix.push(c),
|
FormatItem::Char(c) => suffix.push(c),
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,6 +121,7 @@ impl FileInformation {
|
||||||
not(target_arch = "aarch64"),
|
not(target_arch = "aarch64"),
|
||||||
not(target_arch = "riscv64"),
|
not(target_arch = "riscv64"),
|
||||||
not(target_arch = "loongarch64"),
|
not(target_arch = "loongarch64"),
|
||||||
|
not(target_arch = "sparc64"),
|
||||||
target_pointer_width = "64"
|
target_pointer_width = "64"
|
||||||
))]
|
))]
|
||||||
return self.0.st_nlink;
|
return self.0.st_nlink;
|
||||||
|
@ -137,6 +138,7 @@ impl FileInformation {
|
||||||
target_arch = "aarch64",
|
target_arch = "aarch64",
|
||||||
target_arch = "riscv64",
|
target_arch = "riscv64",
|
||||||
target_arch = "loongarch64",
|
target_arch = "loongarch64",
|
||||||
|
target_arch = "sparc64",
|
||||||
not(target_pointer_width = "64")
|
not(target_pointer_width = "64")
|
||||||
)
|
)
|
||||||
))]
|
))]
|
||||||
|
@ -714,6 +716,33 @@ pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Pat
|
||||||
source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev()
|
source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the passed `path` ends with a path terminator.
|
||||||
|
///
|
||||||
|
/// This function examines the last character of the path to determine
|
||||||
|
/// if it is a directory separator. It supports both Unix-style (`/`)
|
||||||
|
/// and Windows-style (`\`) separators.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `path` - A reference to the path to be checked.
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn path_ends_with_terminator(path: &Path) -> bool {
|
||||||
|
use std::os::unix::prelude::OsStrExt;
|
||||||
|
path.as_os_str()
|
||||||
|
.as_bytes()
|
||||||
|
.last()
|
||||||
|
.map_or(false, |&byte| byte == b'/' || byte == b'\\')
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn path_ends_with_terminator(path: &Path) -> bool {
|
||||||
|
use std::os::windows::prelude::OsStrExt;
|
||||||
|
path.as_os_str()
|
||||||
|
.encode_wide()
|
||||||
|
.last()
|
||||||
|
.map_or(false, |wide| wide == b'/'.into() || wide == b'\\'.into())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
// Note this useful idiom: importing names from outer (for mod tests) scope.
|
// Note this useful idiom: importing names from outer (for mod tests) scope.
|
||||||
|
@ -921,4 +950,24 @@ mod tests {
|
||||||
assert_eq!(get_file_display(S_IFSOCK | 0o600), 's');
|
assert_eq!(get_file_display(S_IFSOCK | 0o600), 's');
|
||||||
assert_eq!(get_file_display(0o777), '?');
|
assert_eq!(get_file_display(0o777), '?');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_ends_with_terminator() {
|
||||||
|
// Path ends with a forward slash
|
||||||
|
assert!(path_ends_with_terminator(Path::new("/some/path/")));
|
||||||
|
|
||||||
|
// Path ends with a backslash
|
||||||
|
assert!(path_ends_with_terminator(Path::new("C:\\some\\path\\")));
|
||||||
|
|
||||||
|
// Path does not end with a terminator
|
||||||
|
assert!(!path_ends_with_terminator(Path::new("/some/path")));
|
||||||
|
assert!(!path_ends_with_terminator(Path::new("C:\\some\\path")));
|
||||||
|
|
||||||
|
// Empty path
|
||||||
|
assert!(!path_ends_with_terminator(Path::new("")));
|
||||||
|
|
||||||
|
// Root path
|
||||||
|
assert!(path_ends_with_terminator(Path::new("/")));
|
||||||
|
assert!(path_ends_with_terminator(Path::new("C:\\")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,3 +5,6 @@ version = "0.0.23"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "A collection of functions to parse the markdown code of help files"
|
description = "A collection of functions to parse the markdown code of help files"
|
||||||
|
|
||||||
|
homepage = "https://github.com/uutils/coreutils"
|
||||||
|
repository = "https://github.com/uutils/coreutils/tree/main/src/uuhelp_parser"
|
||||||
|
|
|
@ -3681,3 +3681,23 @@ fn test_cp_seen_file() {
|
||||||
assert!(at.plus("c").join("f").exists());
|
assert!(at.plus("c").join("f").exists());
|
||||||
assert!(at.plus("c").join("f.~1~").exists());
|
assert!(at.plus("c").join("f.~1~").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_path_ends_with_terminator() {
|
||||||
|
let ts = TestScenario::new(util_name!());
|
||||||
|
let at = &ts.fixtures;
|
||||||
|
at.mkdir("a");
|
||||||
|
ts.ucmd().arg("-r").arg("-T").arg("a").arg("e/").succeeds();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_no_such() {
|
||||||
|
let ts = TestScenario::new(util_name!());
|
||||||
|
let at = &ts.fixtures;
|
||||||
|
at.touch("b");
|
||||||
|
ts.ucmd()
|
||||||
|
.arg("b")
|
||||||
|
.arg("no-such/")
|
||||||
|
.fails()
|
||||||
|
.stderr_is("cp: 'no-such/' is not a directory\n");
|
||||||
|
}
|
||||||
|
|
|
@ -2622,6 +2622,70 @@ fn test_ls_quoting_style() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ls_quoting_style_env_var_default() {
|
||||||
|
let scene = TestScenario::new(util_name!());
|
||||||
|
let at = &scene.fixtures;
|
||||||
|
at.touch(at.plus_as_string("foo-1"));
|
||||||
|
at.touch(at.plus_as_string("bar-2"));
|
||||||
|
|
||||||
|
// If no quoting style argument is provided, the QUOTING_STYLE environment variable
|
||||||
|
// shall be used.
|
||||||
|
|
||||||
|
let correct_c = "\"bar-2\"\n\"foo-1\"";
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.env("QUOTING_STYLE", "c")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only(format!("{correct_c}\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ls_quoting_style_arg_overrides_env_var() {
|
||||||
|
let scene = TestScenario::new(util_name!());
|
||||||
|
let at = &scene.fixtures;
|
||||||
|
at.touch(at.plus_as_string("foo-1"));
|
||||||
|
at.touch(at.plus_as_string("bar-2"));
|
||||||
|
|
||||||
|
// The quoting style given by the env variable should be
|
||||||
|
// overridden by any escape style provided by argument.
|
||||||
|
for (arg, correct) in [
|
||||||
|
("--quoting-style=literal", "foo-1"),
|
||||||
|
("-N", "foo-1"),
|
||||||
|
("--quoting-style=escape", "foo-1"),
|
||||||
|
("-b", "foo-1"),
|
||||||
|
("--quoting-style=shell-escape", "foo-1"),
|
||||||
|
("--quoting-style=shell-escape-always", "'foo-1'"),
|
||||||
|
("--quoting-style=shell", "foo-1"),
|
||||||
|
("--quoting-style=shell-always", "'foo-1'"),
|
||||||
|
] {
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.env("QUOTING_STYLE", "c")
|
||||||
|
.arg("--hide-control-chars")
|
||||||
|
.arg(arg)
|
||||||
|
.arg("foo-1")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only(format!("{correct}\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Another loop to check for the C quoting style that is used as a default above.
|
||||||
|
for (arg, correct) in [
|
||||||
|
("--quoting-style=c", "\"foo-1\""),
|
||||||
|
("-Q", "\"foo-1\""),
|
||||||
|
("--quote-name", "\"foo-1\""),
|
||||||
|
] {
|
||||||
|
scene
|
||||||
|
.ucmd()
|
||||||
|
.env("QUOTING_STYLE", "literal")
|
||||||
|
.arg("--hide-control-chars")
|
||||||
|
.arg(arg)
|
||||||
|
.arg("foo-1")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only(format!("{correct}\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ls_quoting_and_color() {
|
fn test_ls_quoting_and_color() {
|
||||||
let scene = TestScenario::new(util_name!());
|
let scene = TestScenario::new(util_name!());
|
||||||
|
|
|
@ -1556,6 +1556,19 @@ fn test_mv_dir_into_file_where_both_are_files() {
|
||||||
.stderr_contains("mv: cannot stat 'a/': Not a directory");
|
.stderr_contains("mv: cannot stat 'a/': Not a directory");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mv_dir_into_path_slash() {
|
||||||
|
let scene = TestScenario::new(util_name!());
|
||||||
|
let at = &scene.fixtures;
|
||||||
|
at.mkdir("a");
|
||||||
|
scene.ucmd().arg("a").arg("e/").succeeds();
|
||||||
|
assert!(at.dir_exists("e"));
|
||||||
|
at.mkdir("b");
|
||||||
|
at.mkdir("f");
|
||||||
|
scene.ucmd().arg("b").arg("f/").succeeds();
|
||||||
|
assert!(at.dir_exists("f/b"));
|
||||||
|
}
|
||||||
|
|
||||||
// Todo:
|
// Todo:
|
||||||
|
|
||||||
// $ at.touch a b
|
// $ at.touch a b
|
||||||
|
|
|
@ -786,3 +786,17 @@ fn test_default_g_precision() {
|
||||||
.succeeds()
|
.succeeds()
|
||||||
.stdout_only("000001e+06\n");
|
.stdout_only("000001e+06\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_format() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-f", "%%g", "1"])
|
||||||
|
.fails()
|
||||||
|
.no_stdout()
|
||||||
|
.stderr_contains("format '%%g' has no % directive");
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-f", "%g%g", "1"])
|
||||||
|
.fails()
|
||||||
|
.no_stdout()
|
||||||
|
.stderr_contains("format '%g%g' has too many % directives");
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
//
|
//
|
||||||
// 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.
|
||||||
|
|
||||||
|
// spell-checker:ignore wipesync
|
||||||
|
|
||||||
use crate::common::util::TestScenario;
|
use crate::common::util::TestScenario;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -9,8 +12,82 @@ fn test_invalid_arg() {
|
||||||
new_ucmd!().arg("--definitely-invalid").fails().code_is(1);
|
new_ucmd!().arg("--definitely-invalid").fails().code_is(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_remove_arg() {
|
||||||
|
new_ucmd!().arg("--remove=unknown").fails().code_is(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shred() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
let file = "test_shred";
|
||||||
|
let file_original_content = "test_shred file content";
|
||||||
|
|
||||||
|
at.write(file, file_original_content);
|
||||||
|
|
||||||
|
ucmd.arg(file).succeeds();
|
||||||
|
|
||||||
|
// File exists
|
||||||
|
assert!(at.file_exists(file));
|
||||||
|
// File is obfuscated
|
||||||
|
assert!(at.read_bytes(file) != file_original_content.as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_shred_remove() {
|
fn test_shred_remove() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
let file = "test_shred_remove";
|
||||||
|
at.touch(file);
|
||||||
|
|
||||||
|
ucmd.arg("--remove").arg(file).succeeds();
|
||||||
|
|
||||||
|
// File was deleted
|
||||||
|
assert!(!at.file_exists(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shred_remove_unlink() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
let file = "test_shred_remove_unlink";
|
||||||
|
at.touch(file);
|
||||||
|
|
||||||
|
ucmd.arg("--remove=unlink").arg(file).succeeds();
|
||||||
|
|
||||||
|
// File was deleted
|
||||||
|
assert!(!at.file_exists(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shred_remove_wipe() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
let file = "test_shred_remove_wipe";
|
||||||
|
at.touch(file);
|
||||||
|
|
||||||
|
ucmd.arg("--remove=wipe").arg(file).succeeds();
|
||||||
|
|
||||||
|
// File was deleted
|
||||||
|
assert!(!at.file_exists(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shred_remove_wipesync() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
let file = "test_shred_remove_wipesync";
|
||||||
|
at.touch(file);
|
||||||
|
|
||||||
|
ucmd.arg("--remove=wipesync").arg(file).succeeds();
|
||||||
|
|
||||||
|
// File was deleted
|
||||||
|
assert!(!at.file_exists(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shred_u() {
|
||||||
let scene = TestScenario::new(util_name!());
|
let scene = TestScenario::new(util_name!());
|
||||||
let at = &scene.fixtures;
|
let at = &scene.fixtures;
|
||||||
|
|
||||||
|
|
|
@ -206,6 +206,9 @@ sed -i "s|cp: target directory 'symlink': Permission denied|cp: 'symlink' is not
|
||||||
# to transform an ERROR into FAIL
|
# to transform an ERROR into FAIL
|
||||||
sed -i 's|xargs mkdir )|xargs mkdir -p )|' tests/cp/link-heap.sh
|
sed -i 's|xargs mkdir )|xargs mkdir -p )|' tests/cp/link-heap.sh
|
||||||
|
|
||||||
|
# Our message is a bit better
|
||||||
|
sed -i "s|cannot create regular file 'no-such/': Not a directory|'no-such/' is not a directory|" tests/mv/trailing-slash.sh
|
||||||
|
|
||||||
sed -i 's|cp |/usr/bin/cp |' tests/mv/hard-2.sh
|
sed -i 's|cp |/usr/bin/cp |' tests/mv/hard-2.sh
|
||||||
sed -i 's|paste |/usr/bin/paste |' tests/od/od-endian.sh
|
sed -i 's|paste |/usr/bin/paste |' tests/od/od-endian.sh
|
||||||
sed -i 's|timeout |'"${SYSTEM_TIMEOUT}"' |' tests/tail/follow-stdin.sh
|
sed -i 's|timeout |'"${SYSTEM_TIMEOUT}"' |' tests/tail/follow-stdin.sh
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue