mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-29 03:57:44 +00:00
Merge branch 'master' into ls/special-chars-in-double-quotes
This commit is contained in:
commit
499f10ca1f
19 changed files with 1205 additions and 474 deletions
3
.github/workflows/CICD.yml
vendored
3
.github/workflows/CICD.yml
vendored
|
@ -14,7 +14,6 @@ env:
|
||||||
PROJECT_DESC: "Core universal (cross-platform) utilities"
|
PROJECT_DESC: "Core universal (cross-platform) utilities"
|
||||||
PROJECT_AUTH: "uutils"
|
PROJECT_AUTH: "uutils"
|
||||||
RUST_MIN_SRV: "1.47.0" ## MSRV v1.47.0
|
RUST_MIN_SRV: "1.47.0" ## MSRV v1.47.0
|
||||||
RUST_COV_SRV: "2021-05-06" ## (~v1.52.0) supported rust version for code coverage; (date required/used by 'coverage') ## !maint: refactor when code coverage support is included in the stable channel
|
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
@ -606,7 +605,7 @@ jobs:
|
||||||
## VARs setup
|
## VARs setup
|
||||||
outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; }
|
outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; }
|
||||||
# toolchain
|
# toolchain
|
||||||
TOOLCHAIN="nightly-${{ env.RUST_COV_SRV }}" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support
|
TOOLCHAIN="nightly" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support
|
||||||
# * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files
|
# * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files
|
||||||
case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac;
|
case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac;
|
||||||
# * use requested TOOLCHAIN if specified
|
# * use requested TOOLCHAIN if specified
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
AFAICT
|
||||||
arity
|
arity
|
||||||
autogenerate
|
autogenerate
|
||||||
autogenerated
|
autogenerated
|
||||||
|
|
|
@ -8,6 +8,7 @@ csh
|
||||||
globstar
|
globstar
|
||||||
inotify
|
inotify
|
||||||
localtime
|
localtime
|
||||||
|
mksh
|
||||||
mountinfo
|
mountinfo
|
||||||
mountpoint
|
mountpoint
|
||||||
mtab
|
mtab
|
||||||
|
|
|
@ -11,46 +11,16 @@
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
pub use uucore::entries;
|
pub use uucore::entries;
|
||||||
use uucore::error::{FromIo, UResult, USimpleError};
|
use uucore::error::{FromIo, UResult, USimpleError};
|
||||||
use uucore::perms::{
|
use uucore::perms::{chown_base, options, IfFrom};
|
||||||
ChownExecutor, IfFrom, Verbosity, VerbosityLevel, FTS_COMFOLLOW, FTS_LOGICAL, FTS_PHYSICAL,
|
|
||||||
};
|
|
||||||
|
|
||||||
use clap::{App, Arg};
|
use clap::{App, Arg, ArgMatches};
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::os::unix::fs::MetadataExt;
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
|
||||||
use uucore::InvalidEncodingHandling;
|
|
||||||
|
|
||||||
static ABOUT: &str = "Change the group of each FILE to GROUP.";
|
static ABOUT: &str = "Change the group of each FILE to GROUP.";
|
||||||
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
pub mod options {
|
|
||||||
pub mod verbosity {
|
|
||||||
pub static CHANGES: &str = "changes";
|
|
||||||
pub static QUIET: &str = "quiet";
|
|
||||||
pub static SILENT: &str = "silent";
|
|
||||||
pub static VERBOSE: &str = "verbose";
|
|
||||||
}
|
|
||||||
pub mod preserve_root {
|
|
||||||
pub static PRESERVE: &str = "preserve-root";
|
|
||||||
pub static NO_PRESERVE: &str = "no-preserve-root";
|
|
||||||
}
|
|
||||||
pub mod dereference {
|
|
||||||
pub static DEREFERENCE: &str = "dereference";
|
|
||||||
pub static NO_DEREFERENCE: &str = "no-dereference";
|
|
||||||
}
|
|
||||||
pub static RECURSIVE: &str = "recursive";
|
|
||||||
pub mod traverse {
|
|
||||||
pub static TRAVERSE: &str = "H";
|
|
||||||
pub static NO_TRAVERSE: &str = "P";
|
|
||||||
pub static EVERY: &str = "L";
|
|
||||||
}
|
|
||||||
pub static REFERENCE: &str = "reference";
|
|
||||||
pub static ARG_GROUP: &str = "GROUP";
|
|
||||||
pub static ARG_FILES: &str = "FILE";
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_usage() -> String {
|
fn get_usage() -> String {
|
||||||
format!(
|
format!(
|
||||||
"{0} [OPTION]... GROUP FILE...\n {0} [OPTION]... --reference=RFILE FILE...",
|
"{0} [OPTION]... GROUP FILE...\n {0} [OPTION]... --reference=RFILE FILE...",
|
||||||
|
@ -58,101 +28,7 @@ fn get_usage() -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uucore_procs::gen_uumain]
|
fn parse_gid_and_uid(matches: &ArgMatches) -> UResult<(Option<u32>, Option<u32>, IfFrom)> {
|
||||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|
||||||
let args = args
|
|
||||||
.collect_str(InvalidEncodingHandling::ConvertLossy)
|
|
||||||
.accept_any();
|
|
||||||
|
|
||||||
let usage = get_usage();
|
|
||||||
|
|
||||||
let mut app = uu_app().usage(&usage[..]);
|
|
||||||
|
|
||||||
// we change the positional args based on whether
|
|
||||||
// --reference was used.
|
|
||||||
let mut reference = false;
|
|
||||||
let mut help = false;
|
|
||||||
// stop processing options on --
|
|
||||||
for arg in args.iter().take_while(|s| *s != "--") {
|
|
||||||
if arg.starts_with("--reference=") || arg == "--reference" {
|
|
||||||
reference = true;
|
|
||||||
} else if arg == "--help" {
|
|
||||||
// we stop processing once we see --help,
|
|
||||||
// as it doesn't matter if we've seen reference or not
|
|
||||||
help = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if help || !reference {
|
|
||||||
// add both positional arguments
|
|
||||||
app = app.arg(
|
|
||||||
Arg::with_name(options::ARG_GROUP)
|
|
||||||
.value_name(options::ARG_GROUP)
|
|
||||||
.required(true)
|
|
||||||
.takes_value(true)
|
|
||||||
.multiple(false),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
app = app.arg(
|
|
||||||
Arg::with_name(options::ARG_FILES)
|
|
||||||
.value_name(options::ARG_FILES)
|
|
||||||
.multiple(true)
|
|
||||||
.takes_value(true)
|
|
||||||
.required(true)
|
|
||||||
.min_values(1),
|
|
||||||
);
|
|
||||||
|
|
||||||
let matches = app.get_matches_from(args);
|
|
||||||
|
|
||||||
/* Get the list of files */
|
|
||||||
let files: Vec<String> = matches
|
|
||||||
.values_of(options::ARG_FILES)
|
|
||||||
.map(|v| v.map(ToString::to_string).collect())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let preserve_root = matches.is_present(options::preserve_root::PRESERVE);
|
|
||||||
|
|
||||||
let mut derefer = if matches.is_present(options::dereference::DEREFERENCE) {
|
|
||||||
1
|
|
||||||
} else if matches.is_present(options::dereference::NO_DEREFERENCE) {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
-1
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut bit_flag = if matches.is_present(options::traverse::TRAVERSE) {
|
|
||||||
FTS_COMFOLLOW | FTS_PHYSICAL
|
|
||||||
} else if matches.is_present(options::traverse::EVERY) {
|
|
||||||
FTS_LOGICAL
|
|
||||||
} else {
|
|
||||||
FTS_PHYSICAL
|
|
||||||
};
|
|
||||||
|
|
||||||
let recursive = matches.is_present(options::RECURSIVE);
|
|
||||||
if recursive {
|
|
||||||
if bit_flag == FTS_PHYSICAL {
|
|
||||||
if derefer == 1 {
|
|
||||||
return Err(USimpleError::new(1, "-R --dereference requires -H or -L"));
|
|
||||||
}
|
|
||||||
derefer = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bit_flag = FTS_PHYSICAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
let verbosity_level = if matches.is_present(options::verbosity::CHANGES) {
|
|
||||||
VerbosityLevel::Changes
|
|
||||||
} else if matches.is_present(options::verbosity::SILENT)
|
|
||||||
|| matches.is_present(options::verbosity::QUIET)
|
|
||||||
{
|
|
||||||
VerbosityLevel::Silent
|
|
||||||
} else if matches.is_present(options::verbosity::VERBOSE) {
|
|
||||||
VerbosityLevel::Verbose
|
|
||||||
} else {
|
|
||||||
VerbosityLevel::Normal
|
|
||||||
};
|
|
||||||
|
|
||||||
let dest_gid = if let Some(file) = matches.value_of(options::REFERENCE) {
|
let dest_gid = if let Some(file) = matches.value_of(options::REFERENCE) {
|
||||||
fs::metadata(&file)
|
fs::metadata(&file)
|
||||||
.map(|meta| Some(meta.gid()))
|
.map(|meta| Some(meta.gid()))
|
||||||
|
@ -168,22 +44,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Ok((dest_gid, None, IfFrom::All))
|
||||||
|
}
|
||||||
|
|
||||||
let executor = ChownExecutor {
|
#[uucore_procs::gen_uumain]
|
||||||
bit_flag,
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
dest_gid,
|
let usage = get_usage();
|
||||||
verbosity: Verbosity {
|
|
||||||
groups_only: true,
|
chown_base(
|
||||||
level: verbosity_level,
|
uu_app().usage(&usage[..]),
|
||||||
},
|
args,
|
||||||
recursive,
|
options::ARG_GROUP,
|
||||||
dereference: derefer != 0,
|
parse_gid_and_uid,
|
||||||
preserve_root,
|
true,
|
||||||
files,
|
)
|
||||||
filter: IfFrom::All,
|
|
||||||
dest_uid: None,
|
|
||||||
};
|
|
||||||
executor.exec()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uu_app() -> App<'static, 'static> {
|
pub fn uu_app() -> App<'static, 'static> {
|
||||||
|
|
|
@ -10,49 +10,17 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
pub use uucore::entries::{self, Group, Locate, Passwd};
|
pub use uucore::entries::{self, Group, Locate, Passwd};
|
||||||
use uucore::perms::{
|
use uucore::perms::{chown_base, options, IfFrom};
|
||||||
ChownExecutor, IfFrom, Verbosity, VerbosityLevel, FTS_COMFOLLOW, FTS_LOGICAL, FTS_PHYSICAL,
|
|
||||||
};
|
|
||||||
|
|
||||||
use uucore::error::{FromIo, UResult, USimpleError};
|
use uucore::error::{FromIo, UResult, USimpleError};
|
||||||
|
|
||||||
use clap::{crate_version, App, Arg};
|
use clap::{crate_version, App, Arg, ArgMatches};
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::os::unix::fs::MetadataExt;
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
|
||||||
use uucore::InvalidEncodingHandling;
|
|
||||||
|
|
||||||
static ABOUT: &str = "change file owner and group";
|
static ABOUT: &str = "change file owner and group";
|
||||||
|
|
||||||
pub mod options {
|
|
||||||
pub mod verbosity {
|
|
||||||
pub static CHANGES: &str = "changes";
|
|
||||||
pub static QUIET: &str = "quiet";
|
|
||||||
pub static SILENT: &str = "silent";
|
|
||||||
pub static VERBOSE: &str = "verbose";
|
|
||||||
}
|
|
||||||
pub mod preserve_root {
|
|
||||||
pub static PRESERVE: &str = "preserve-root";
|
|
||||||
pub static NO_PRESERVE: &str = "no-preserve-root";
|
|
||||||
}
|
|
||||||
pub mod dereference {
|
|
||||||
pub static DEREFERENCE: &str = "dereference";
|
|
||||||
pub static NO_DEREFERENCE: &str = "no-dereference";
|
|
||||||
}
|
|
||||||
pub static FROM: &str = "from";
|
|
||||||
pub static RECURSIVE: &str = "recursive";
|
|
||||||
pub mod traverse {
|
|
||||||
pub static TRAVERSE: &str = "H";
|
|
||||||
pub static NO_TRAVERSE: &str = "P";
|
|
||||||
pub static EVERY: &str = "L";
|
|
||||||
}
|
|
||||||
pub static REFERENCE: &str = "reference";
|
|
||||||
}
|
|
||||||
|
|
||||||
static ARG_OWNER: &str = "owner";
|
|
||||||
static ARG_FILES: &str = "files";
|
|
||||||
|
|
||||||
fn get_usage() -> String {
|
fn get_usage() -> String {
|
||||||
format!(
|
format!(
|
||||||
"{0} [OPTION]... [OWNER][:[GROUP]] FILE...\n{0} [OPTION]... --reference=RFILE FILE...",
|
"{0} [OPTION]... [OWNER][:[GROUP]] FILE...\n{0} [OPTION]... --reference=RFILE FILE...",
|
||||||
|
@ -60,65 +28,7 @@ fn get_usage() -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uucore_procs::gen_uumain]
|
fn parse_gid_uid_and_filter(matches: &ArgMatches) -> UResult<(Option<u32>, Option<u32>, IfFrom)> {
|
||||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|
||||||
let args = args
|
|
||||||
.collect_str(InvalidEncodingHandling::Ignore)
|
|
||||||
.accept_any();
|
|
||||||
|
|
||||||
let usage = get_usage();
|
|
||||||
|
|
||||||
let matches = uu_app().usage(&usage[..]).get_matches_from(args);
|
|
||||||
|
|
||||||
/* First arg is the owner/group */
|
|
||||||
let owner = matches.value_of(ARG_OWNER).unwrap();
|
|
||||||
|
|
||||||
/* Then the list of files */
|
|
||||||
let files: Vec<String> = matches
|
|
||||||
.values_of(ARG_FILES)
|
|
||||||
.map(|v| v.map(ToString::to_string).collect())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let preserve_root = matches.is_present(options::preserve_root::PRESERVE);
|
|
||||||
|
|
||||||
let mut derefer = if matches.is_present(options::dereference::NO_DEREFERENCE) {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut bit_flag = if matches.is_present(options::traverse::TRAVERSE) {
|
|
||||||
FTS_COMFOLLOW | FTS_PHYSICAL
|
|
||||||
} else if matches.is_present(options::traverse::EVERY) {
|
|
||||||
FTS_LOGICAL
|
|
||||||
} else {
|
|
||||||
FTS_PHYSICAL
|
|
||||||
};
|
|
||||||
|
|
||||||
let recursive = matches.is_present(options::RECURSIVE);
|
|
||||||
if recursive {
|
|
||||||
if bit_flag == FTS_PHYSICAL {
|
|
||||||
if derefer == 1 {
|
|
||||||
return Err(USimpleError::new(1, "-R --dereference requires -H or -L"));
|
|
||||||
}
|
|
||||||
derefer = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bit_flag = FTS_PHYSICAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
let verbosity = if matches.is_present(options::verbosity::CHANGES) {
|
|
||||||
VerbosityLevel::Changes
|
|
||||||
} else if matches.is_present(options::verbosity::SILENT)
|
|
||||||
|| matches.is_present(options::verbosity::QUIET)
|
|
||||||
{
|
|
||||||
VerbosityLevel::Silent
|
|
||||||
} else if matches.is_present(options::verbosity::VERBOSE) {
|
|
||||||
VerbosityLevel::Verbose
|
|
||||||
} else {
|
|
||||||
VerbosityLevel::Normal
|
|
||||||
};
|
|
||||||
|
|
||||||
let filter = if let Some(spec) = matches.value_of(options::FROM) {
|
let filter = if let Some(spec) = matches.value_of(options::FROM) {
|
||||||
match parse_spec(spec)? {
|
match parse_spec(spec)? {
|
||||||
(Some(uid), None) => IfFrom::User(uid),
|
(Some(uid), None) => IfFrom::User(uid),
|
||||||
|
@ -138,25 +48,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
dest_gid = Some(meta.gid());
|
dest_gid = Some(meta.gid());
|
||||||
dest_uid = Some(meta.uid());
|
dest_uid = Some(meta.uid());
|
||||||
} else {
|
} else {
|
||||||
let (u, g) = parse_spec(owner)?;
|
let (u, g) = parse_spec(matches.value_of(options::ARG_OWNER).unwrap())?;
|
||||||
dest_uid = u;
|
dest_uid = u;
|
||||||
dest_gid = g;
|
dest_gid = g;
|
||||||
}
|
}
|
||||||
let executor = ChownExecutor {
|
Ok((dest_gid, dest_uid, filter))
|
||||||
bit_flag,
|
}
|
||||||
dest_uid,
|
|
||||||
dest_gid,
|
#[uucore_procs::gen_uumain]
|
||||||
verbosity: Verbosity {
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
groups_only: false,
|
let usage = get_usage();
|
||||||
level: verbosity,
|
|
||||||
},
|
chown_base(
|
||||||
recursive,
|
uu_app().usage(&usage[..]),
|
||||||
dereference: derefer != 0,
|
args,
|
||||||
filter,
|
options::ARG_OWNER,
|
||||||
preserve_root,
|
parse_gid_uid_and_filter,
|
||||||
files,
|
false,
|
||||||
};
|
)
|
||||||
executor.exec()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uu_app() -> App<'static, 'static> {
|
pub fn uu_app() -> App<'static, 'static> {
|
||||||
|
@ -169,22 +78,31 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
.long(options::verbosity::CHANGES)
|
.long(options::verbosity::CHANGES)
|
||||||
.help("like verbose but report only when a change is made"),
|
.help("like verbose but report only when a change is made"),
|
||||||
)
|
)
|
||||||
.arg(Arg::with_name(options::dereference::DEREFERENCE).long(options::dereference::DEREFERENCE).help(
|
.arg(
|
||||||
"affect the referent of each symbolic link (this is the default), rather than the symbolic link itself",
|
Arg::with_name(options::dereference::DEREFERENCE)
|
||||||
))
|
.long(options::dereference::DEREFERENCE)
|
||||||
|
.help(
|
||||||
|
"affect the referent of each symbolic link (this is the default), \
|
||||||
|
rather than the symbolic link itself",
|
||||||
|
),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::dereference::NO_DEREFERENCE)
|
Arg::with_name(options::dereference::NO_DEREFERENCE)
|
||||||
.short("h")
|
.short("h")
|
||||||
.long(options::dereference::NO_DEREFERENCE)
|
.long(options::dereference::NO_DEREFERENCE)
|
||||||
.help(
|
.help(
|
||||||
"affect symbolic links instead of any referenced file (useful only on systems that can change the ownership of a symlink)",
|
"affect symbolic links instead of any referenced file \
|
||||||
|
(useful only on systems that can change the ownership of a symlink)",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::FROM)
|
Arg::with_name(options::FROM)
|
||||||
.long(options::FROM)
|
.long(options::FROM)
|
||||||
.help(
|
.help(
|
||||||
"change the owner and/or group of each file only if its current owner and/or group match those specified here. Either may be omitted, in which case a match is not required for the omitted attribute",
|
"change the owner and/or group of each file only if its \
|
||||||
|
current owner and/or group match those specified here. \
|
||||||
|
Either may be omitted, in which case a match is not required \
|
||||||
|
for the omitted attribute",
|
||||||
)
|
)
|
||||||
.value_name("CURRENT_OWNER:CURRENT_GROUP"),
|
.value_name("CURRENT_OWNER:CURRENT_GROUP"),
|
||||||
)
|
)
|
||||||
|
@ -216,7 +134,11 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
.value_name("RFILE")
|
.value_name("RFILE")
|
||||||
.min_values(1),
|
.min_values(1),
|
||||||
)
|
)
|
||||||
.arg(Arg::with_name(options::verbosity::SILENT).short("f").long(options::verbosity::SILENT))
|
.arg(
|
||||||
|
Arg::with_name(options::verbosity::SILENT)
|
||||||
|
.short("f")
|
||||||
|
.long(options::verbosity::SILENT),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::traverse::TRAVERSE)
|
Arg::with_name(options::traverse::TRAVERSE)
|
||||||
.short(options::traverse::TRAVERSE)
|
.short(options::traverse::TRAVERSE)
|
||||||
|
@ -238,21 +160,9 @@ pub fn uu_app() -> App<'static, 'static> {
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::verbosity::VERBOSE)
|
Arg::with_name(options::verbosity::VERBOSE)
|
||||||
.long(options::verbosity::VERBOSE)
|
.long(options::verbosity::VERBOSE)
|
||||||
|
.short("v")
|
||||||
.help("output a diagnostic for every file processed"),
|
.help("output a diagnostic for every file processed"),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::with_name(ARG_OWNER)
|
|
||||||
.multiple(false)
|
|
||||||
.takes_value(true)
|
|
||||||
.required(true),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name(ARG_FILES)
|
|
||||||
.multiple(true)
|
|
||||||
.takes_value(true)
|
|
||||||
.required(true)
|
|
||||||
.min_values(1),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_spec(spec: &str) -> UResult<(Option<u32>, Option<u32>)> {
|
fn parse_spec(spec: &str) -> UResult<(Option<u32>, Option<u32>)> {
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
|
||||||
use clap::{crate_version, App, Arg};
|
use clap::{crate_version, App, Arg};
|
||||||
use std::cmp::{min, Ordering};
|
use std::cmp::Ordering;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{stdin, BufRead, BufReader, Lines, Stdin};
|
use std::io::{stdin, BufRead, BufReader, Lines, Stdin};
|
||||||
|
|
||||||
|
@ -102,17 +102,12 @@ impl<'a> Repr<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print each field except the one at the index.
|
/// Print each field except the one at the index.
|
||||||
fn print_fields(&self, line: &Line, index: usize, max_fields: Option<usize>) {
|
fn print_fields(&self, line: &Line, index: usize) {
|
||||||
for i in 0..min(max_fields.unwrap_or(usize::max_value()), line.fields.len()) {
|
for i in 0..line.fields.len() {
|
||||||
if i != index {
|
if i != index {
|
||||||
print!("{}{}", self.separator, line.fields[i]);
|
print!("{}{}", self.separator, line.fields[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(n) = max_fields {
|
|
||||||
for _ in line.fields.len()..n {
|
|
||||||
print!("{}", self.separator)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print each field or the empty filler if the field is not set.
|
/// Print each field or the empty filler if the field is not set.
|
||||||
|
@ -233,7 +228,6 @@ struct State<'a> {
|
||||||
print_unpaired: bool,
|
print_unpaired: bool,
|
||||||
lines: Lines<Box<dyn BufRead + 'a>>,
|
lines: Lines<Box<dyn BufRead + 'a>>,
|
||||||
seq: Vec<Line>,
|
seq: Vec<Line>,
|
||||||
max_fields: Option<usize>,
|
|
||||||
line_num: usize,
|
line_num: usize,
|
||||||
has_failed: bool,
|
has_failed: bool,
|
||||||
}
|
}
|
||||||
|
@ -262,7 +256,6 @@ impl<'a> State<'a> {
|
||||||
print_unpaired,
|
print_unpaired,
|
||||||
lines: f.lines(),
|
lines: f.lines(),
|
||||||
seq: Vec::new(),
|
seq: Vec::new(),
|
||||||
max_fields: None,
|
|
||||||
line_num: 0,
|
line_num: 0,
|
||||||
has_failed: false,
|
has_failed: false,
|
||||||
}
|
}
|
||||||
|
@ -329,8 +322,8 @@ impl<'a> State<'a> {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
repr.print_field(key);
|
repr.print_field(key);
|
||||||
repr.print_fields(line1, self.key, self.max_fields);
|
repr.print_fields(line1, self.key);
|
||||||
repr.print_fields(line2, other.key, other.max_fields);
|
repr.print_fields(line2, other.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
|
@ -361,14 +354,15 @@ impl<'a> State<'a> {
|
||||||
!self.seq.is_empty()
|
!self.seq.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(&mut self, read_sep: Sep, autoformat: bool) {
|
fn initialize(&mut self, read_sep: Sep, autoformat: bool) -> usize {
|
||||||
if let Some(line) = self.read_line(read_sep) {
|
if let Some(line) = self.read_line(read_sep) {
|
||||||
if autoformat {
|
|
||||||
self.max_fields = Some(line.fields.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.seq.push(line);
|
self.seq.push(line);
|
||||||
|
|
||||||
|
if autoformat {
|
||||||
|
return self.seq[0].fields.len();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finalize(&mut self, input: &Input, repr: &Repr) {
|
fn finalize(&mut self, input: &Input, repr: &Repr) {
|
||||||
|
@ -431,7 +425,7 @@ impl<'a> State<'a> {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
repr.print_field(line.get_field(self.key));
|
repr.print_field(line.get_field(self.key));
|
||||||
repr.print_fields(line, self.key, self.max_fields);
|
repr.print_fields(line, self.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
|
@ -512,7 +506,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
crash!(1, "both files cannot be standard input");
|
crash!(1, "both files cannot be standard input");
|
||||||
}
|
}
|
||||||
|
|
||||||
exec(file1, file2, &settings)
|
exec(file1, file2, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uu_app() -> App<'static, 'static> {
|
pub fn uu_app() -> App<'static, 'static> {
|
||||||
|
@ -622,7 +616,7 @@ FILENUM is 1 or 2, corresponding to FILE1 or FILE2",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exec(file1: &str, file2: &str, settings: &Settings) -> i32 {
|
fn exec(file1: &str, file2: &str, settings: Settings) -> i32 {
|
||||||
let stdin = stdin();
|
let stdin = stdin();
|
||||||
|
|
||||||
let mut state1 = State::new(
|
let mut state1 = State::new(
|
||||||
|
@ -647,18 +641,34 @@ fn exec(file1: &str, file2: &str, settings: &Settings) -> i32 {
|
||||||
settings.check_order,
|
settings.check_order,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let format = if settings.autoformat {
|
||||||
|
let mut format = vec![Spec::Key];
|
||||||
|
let mut initialize = |state: &mut State| {
|
||||||
|
let max_fields = state.initialize(settings.separator, settings.autoformat);
|
||||||
|
for i in 0..max_fields {
|
||||||
|
if i != state.key {
|
||||||
|
format.push(Spec::Field(state.file_num, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
initialize(&mut state1);
|
||||||
|
initialize(&mut state2);
|
||||||
|
format
|
||||||
|
} else {
|
||||||
|
state1.initialize(settings.separator, settings.autoformat);
|
||||||
|
state2.initialize(settings.separator, settings.autoformat);
|
||||||
|
settings.format
|
||||||
|
};
|
||||||
|
|
||||||
let repr = Repr::new(
|
let repr = Repr::new(
|
||||||
match settings.separator {
|
match settings.separator {
|
||||||
Sep::Char(sep) => sep,
|
Sep::Char(sep) => sep,
|
||||||
_ => ' ',
|
_ => ' ',
|
||||||
},
|
},
|
||||||
&settings.format,
|
&format,
|
||||||
&settings.empty,
|
&settings.empty,
|
||||||
);
|
);
|
||||||
|
|
||||||
state1.initialize(settings.separator, settings.autoformat);
|
|
||||||
state2.initialize(settings.separator, settings.autoformat);
|
|
||||||
|
|
||||||
if settings.headers {
|
if settings.headers {
|
||||||
state1.print_headers(&state2, &repr);
|
state1.print_headers(&state2, &repr);
|
||||||
state1.reset_read_line(&input);
|
state1.reset_read_line(&input);
|
||||||
|
|
|
@ -21,6 +21,7 @@ use lscolors::LsColors;
|
||||||
use number_prefix::NumberPrefix;
|
use number_prefix::NumberPrefix;
|
||||||
use once_cell::unsync::OnceCell;
|
use once_cell::unsync::OnceCell;
|
||||||
use quoting_style::{escape_name, QuotingStyle};
|
use quoting_style::{escape_name, QuotingStyle};
|
||||||
|
use std::ffi::OsString;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::os::windows::fs::MetadataExt;
|
use std::os::windows::fs::MetadataExt;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -248,7 +249,7 @@ struct LongFormat {
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
#[allow(clippy::cognitive_complexity)]
|
#[allow(clippy::cognitive_complexity)]
|
||||||
fn from(options: clap::ArgMatches) -> UResult<Config> {
|
fn from(options: &clap::ArgMatches) -> UResult<Config> {
|
||||||
let (mut format, opt) = if let Some(format_) = options.value_of(options::FORMAT) {
|
let (mut format, opt) = if let Some(format_) = options.value_of(options::FORMAT) {
|
||||||
(
|
(
|
||||||
match format_ {
|
match format_ {
|
||||||
|
@ -428,11 +429,10 @@ impl Config {
|
||||||
#[allow(clippy::needless_bool)]
|
#[allow(clippy::needless_bool)]
|
||||||
let show_control = if options.is_present(options::HIDE_CONTROL_CHARS) {
|
let show_control = if options.is_present(options::HIDE_CONTROL_CHARS) {
|
||||||
false
|
false
|
||||||
} else if options.is_present(options::SHOW_CONTROL_CHARS) || atty::is(atty::Stream::Stdout)
|
} else if options.is_present(options::SHOW_CONTROL_CHARS) {
|
||||||
{
|
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
!atty::is(atty::Stream::Stdout)
|
||||||
};
|
};
|
||||||
|
|
||||||
let quoting_style = if let Some(style) = options.value_of(options::QUOTING_STYLE) {
|
let quoting_style = if let Some(style) = options.value_of(options::QUOTING_STYLE) {
|
||||||
|
@ -599,22 +599,19 @@ impl Config {
|
||||||
|
|
||||||
#[uucore_procs::gen_uumain]
|
#[uucore_procs::gen_uumain]
|
||||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
let args = args
|
|
||||||
.collect_str(InvalidEncodingHandling::Ignore)
|
|
||||||
.accept_any();
|
|
||||||
|
|
||||||
let usage = usage();
|
let usage = usage();
|
||||||
|
|
||||||
let app = uu_app().usage(&usage[..]);
|
let app = uu_app().usage(&usage[..]);
|
||||||
|
|
||||||
let matches = app.get_matches_from(args);
|
let matches = app.get_matches_from(args);
|
||||||
|
|
||||||
|
let config = Config::from(&matches)?;
|
||||||
let locs = matches
|
let locs = matches
|
||||||
.values_of(options::PATHS)
|
.values_of_os(options::PATHS)
|
||||||
.map(|v| v.map(ToString::to_string).collect())
|
.map(|v| v.map(Path::new).collect())
|
||||||
.unwrap_or_else(|| vec![String::from(".")]);
|
.unwrap_or_else(|| vec![Path::new(".")]);
|
||||||
|
|
||||||
list(locs, Config::from(matches)?)
|
list(locs, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uu_app() -> App<'static, 'static> {
|
pub fn uu_app() -> App<'static, 'static> {
|
||||||
|
@ -1177,7 +1174,7 @@ struct PathData {
|
||||||
md: OnceCell<Option<Metadata>>,
|
md: OnceCell<Option<Metadata>>,
|
||||||
ft: OnceCell<Option<FileType>>,
|
ft: OnceCell<Option<FileType>>,
|
||||||
// Name of the file - will be empty for . or ..
|
// Name of the file - will be empty for . or ..
|
||||||
display_name: String,
|
display_name: OsString,
|
||||||
// PathBuf that all above data corresponds to
|
// PathBuf that all above data corresponds to
|
||||||
p_buf: PathBuf,
|
p_buf: PathBuf,
|
||||||
must_dereference: bool,
|
must_dereference: bool,
|
||||||
|
@ -1187,7 +1184,7 @@ impl PathData {
|
||||||
fn new(
|
fn new(
|
||||||
p_buf: PathBuf,
|
p_buf: PathBuf,
|
||||||
file_type: Option<std::io::Result<FileType>>,
|
file_type: Option<std::io::Result<FileType>>,
|
||||||
file_name: Option<String>,
|
file_name: Option<OsString>,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
command_line: bool,
|
command_line: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -1195,16 +1192,13 @@ impl PathData {
|
||||||
// For '..', the filename is None
|
// For '..', the filename is None
|
||||||
let display_name = if let Some(name) = file_name {
|
let display_name = if let Some(name) = file_name {
|
||||||
name
|
name
|
||||||
|
} else if command_line {
|
||||||
|
p_buf.clone().into()
|
||||||
} else {
|
} else {
|
||||||
let display_os_str = if command_line {
|
p_buf
|
||||||
p_buf.as_os_str()
|
.file_name()
|
||||||
} else {
|
.unwrap_or_else(|| p_buf.iter().next_back().unwrap())
|
||||||
p_buf
|
.to_owned()
|
||||||
.file_name()
|
|
||||||
.unwrap_or_else(|| p_buf.iter().next_back().unwrap())
|
|
||||||
};
|
|
||||||
|
|
||||||
display_os_str.to_string_lossy().into_owned()
|
|
||||||
};
|
};
|
||||||
let must_dereference = match &config.dereference {
|
let must_dereference = match &config.dereference {
|
||||||
Dereference::All => true,
|
Dereference::All => true,
|
||||||
|
@ -1249,14 +1243,14 @@ impl PathData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list(locs: Vec<String>, config: Config) -> UResult<()> {
|
fn list(locs: Vec<&Path>, config: Config) -> UResult<()> {
|
||||||
let mut files = Vec::<PathData>::new();
|
let mut files = Vec::<PathData>::new();
|
||||||
let mut dirs = Vec::<PathData>::new();
|
let mut dirs = Vec::<PathData>::new();
|
||||||
|
|
||||||
let mut out = BufWriter::new(stdout());
|
let mut out = BufWriter::new(stdout());
|
||||||
|
|
||||||
for loc in &locs {
|
for loc in &locs {
|
||||||
let p = PathBuf::from(&loc);
|
let p = PathBuf::from(loc);
|
||||||
let path_data = PathData::new(p, None, None, &config, true);
|
let path_data = PathData::new(p, None, None, &config, true);
|
||||||
|
|
||||||
if path_data.md().is_none() {
|
if path_data.md().is_none() {
|
||||||
|
@ -1286,6 +1280,7 @@ fn list(locs: Vec<String>, config: Config) -> UResult<()> {
|
||||||
sort_entries(&mut dirs, &config);
|
sort_entries(&mut dirs, &config);
|
||||||
for dir in dirs {
|
for dir in dirs {
|
||||||
if locs.len() > 1 || config.recursive {
|
if locs.len() > 1 || config.recursive {
|
||||||
|
// FIXME: This should use the quoting style and propagate errors
|
||||||
let _ = writeln!(out, "\n{}:", dir.p_buf.display());
|
let _ = writeln!(out, "\n{}:", dir.p_buf.display());
|
||||||
}
|
}
|
||||||
enter_directory(&dir, &config, &mut out);
|
enter_directory(&dir, &config, &mut out);
|
||||||
|
@ -1394,14 +1389,17 @@ fn get_metadata(entry: &Path, dereference: bool) -> std::io::Result<Metadata> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize) {
|
fn display_dir_entry_size(entry: &PathData, config: &Config) -> (usize, usize, usize, usize) {
|
||||||
|
// TODO: Cache/memoize the display_* results so we don't have to recalculate them.
|
||||||
if let Some(md) = entry.md() {
|
if let Some(md) = entry.md() {
|
||||||
(
|
(
|
||||||
display_symlink_count(md).len(),
|
display_symlink_count(md).len(),
|
||||||
|
display_uname(md, config).len(),
|
||||||
|
display_group(md, config).len(),
|
||||||
display_size_or_rdev(md, config).len(),
|
display_size_or_rdev(md, config).len(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(0, 0)
|
(0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1409,15 +1407,28 @@ fn pad_left(string: String, count: usize) -> String {
|
||||||
format!("{:>width$}", string, width = count)
|
format!("{:>width$}", string, width = count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pad_right(string: String, count: usize) -> String {
|
||||||
|
format!("{:<width$}", string, width = count)
|
||||||
|
}
|
||||||
|
|
||||||
fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout>) {
|
fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout>) {
|
||||||
if config.format == Format::Long {
|
if config.format == Format::Long {
|
||||||
let (mut max_links, mut max_width) = (1, 1);
|
let (
|
||||||
|
mut longest_link_count_len,
|
||||||
|
mut longest_uname_len,
|
||||||
|
mut longest_group_len,
|
||||||
|
mut longest_size_len,
|
||||||
|
) = (1, 1, 1, 1);
|
||||||
let mut total_size = 0;
|
let mut total_size = 0;
|
||||||
|
|
||||||
for item in items {
|
for item in items {
|
||||||
let (links, width) = display_dir_entry_size(item, config);
|
let (link_count_len, uname_len, group_len, size_len) =
|
||||||
max_links = links.max(max_links);
|
display_dir_entry_size(item, config);
|
||||||
max_width = width.max(max_width);
|
longest_link_count_len = link_count_len.max(longest_link_count_len);
|
||||||
|
longest_size_len = size_len.max(longest_size_len);
|
||||||
|
longest_uname_len = uname_len.max(longest_uname_len);
|
||||||
|
longest_group_len = group_len.max(longest_group_len);
|
||||||
|
longest_size_len = size_len.max(longest_size_len);
|
||||||
total_size += item.md().map_or(0, |md| get_block_size(md, config));
|
total_size += item.md().map_or(0, |md| get_block_size(md, config));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1426,7 +1437,15 @@ fn display_items(items: &[PathData], config: &Config, out: &mut BufWriter<Stdout
|
||||||
}
|
}
|
||||||
|
|
||||||
for item in items {
|
for item in items {
|
||||||
display_item_long(item, max_links, max_width, config, out);
|
display_item_long(
|
||||||
|
item,
|
||||||
|
longest_link_count_len,
|
||||||
|
longest_uname_len,
|
||||||
|
longest_group_len,
|
||||||
|
longest_size_len,
|
||||||
|
config,
|
||||||
|
out,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let names = items.iter().filter_map(|i| display_file_name(i, config));
|
let names = items.iter().filter_map(|i| display_file_name(i, config));
|
||||||
|
@ -1531,10 +1550,48 @@ fn display_grid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This writes to the BufWriter out a single string of the output of `ls -l`.
|
||||||
|
///
|
||||||
|
/// It writes the following keys, in order:
|
||||||
|
/// * `inode` ([`get_inode`], config-optional)
|
||||||
|
/// * `permissions` ([`display_permissions`])
|
||||||
|
/// * `symlink_count` ([`display_symlink_count`])
|
||||||
|
/// * `owner` ([`display_uname`], config-optional)
|
||||||
|
/// * `group` ([`display_group`], config-optional)
|
||||||
|
/// * `author` ([`display_uname`], config-optional)
|
||||||
|
/// * `size / rdev` ([`display_size_or_rdev`])
|
||||||
|
/// * `system_time` ([`get_system_time`])
|
||||||
|
/// * `file_name` ([`display_file_name`])
|
||||||
|
///
|
||||||
|
/// This function needs to display information in columns:
|
||||||
|
/// * permissions and system_time are already guaranteed to be pre-formatted in fixed length.
|
||||||
|
/// * file_name is the last column and is left-aligned.
|
||||||
|
/// * Everything else needs to be padded using [`pad_left`].
|
||||||
|
///
|
||||||
|
/// That's why we have the parameters:
|
||||||
|
/// ```txt
|
||||||
|
/// max_links: usize,
|
||||||
|
/// longest_uname_len: usize,
|
||||||
|
/// longest_group_len: usize,
|
||||||
|
/// max_size: usize,
|
||||||
|
/// ```
|
||||||
|
/// that decide the maximum possible character count of each field.
|
||||||
|
///
|
||||||
|
/// [`get_inode`]: ls::get_inode
|
||||||
|
/// [`display_permissions`]: ls::display_permissions
|
||||||
|
/// [`display_symlink_count`]: ls::display_symlink_count
|
||||||
|
/// [`display_uname`]: ls::display_uname
|
||||||
|
/// [`display_group`]: ls::display_group
|
||||||
|
/// [`display_size_or_rdev`]: ls::display_size_or_rdev
|
||||||
|
/// [`get_system_time`]: ls::get_system_time
|
||||||
|
/// [`display_file_name`]: ls::display_file_name
|
||||||
|
/// [`pad_left`]: ls::pad_left
|
||||||
fn display_item_long(
|
fn display_item_long(
|
||||||
item: &PathData,
|
item: &PathData,
|
||||||
max_links: usize,
|
longest_link_count_len: usize,
|
||||||
max_size: usize,
|
longest_uname_len: usize,
|
||||||
|
longest_group_len: usize,
|
||||||
|
longest_size_len: usize,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
out: &mut BufWriter<Stdout>,
|
out: &mut BufWriter<Stdout>,
|
||||||
) {
|
) {
|
||||||
|
@ -1557,27 +1614,39 @@ fn display_item_long(
|
||||||
out,
|
out,
|
||||||
"{} {}",
|
"{} {}",
|
||||||
display_permissions(md, true),
|
display_permissions(md, true),
|
||||||
pad_left(display_symlink_count(md), max_links),
|
pad_left(display_symlink_count(md), longest_link_count_len),
|
||||||
);
|
);
|
||||||
|
|
||||||
if config.long.owner {
|
if config.long.owner {
|
||||||
let _ = write!(out, " {}", display_uname(md, config));
|
let _ = write!(
|
||||||
|
out,
|
||||||
|
" {}",
|
||||||
|
pad_right(display_uname(md, config), longest_uname_len)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.long.group {
|
if config.long.group {
|
||||||
let _ = write!(out, " {}", display_group(md, config));
|
let _ = write!(
|
||||||
|
out,
|
||||||
|
" {}",
|
||||||
|
pad_right(display_group(md, config), longest_group_len)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Author is only different from owner on GNU/Hurd, so we reuse
|
// Author is only different from owner on GNU/Hurd, so we reuse
|
||||||
// the owner, since GNU/Hurd is not currently supported by Rust.
|
// the owner, since GNU/Hurd is not currently supported by Rust.
|
||||||
if config.long.author {
|
if config.long.author {
|
||||||
let _ = write!(out, " {}", display_uname(md, config));
|
let _ = write!(
|
||||||
|
out,
|
||||||
|
" {}",
|
||||||
|
pad_right(display_uname(md, config), longest_uname_len)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
out,
|
out,
|
||||||
" {} {} {}",
|
" {} {} {}",
|
||||||
pad_left(display_size_or_rdev(md, config), max_size),
|
pad_left(display_size_or_rdev(md, config), longest_size_len),
|
||||||
display_date(md, config),
|
display_date(md, config),
|
||||||
// unwrap is fine because it fails when metadata is not available
|
// unwrap is fine because it fails when metadata is not available
|
||||||
// but we already know that it is because it's checked at the
|
// but we already know that it is because it's checked at the
|
||||||
|
@ -1597,7 +1666,6 @@ fn get_inode(metadata: &Metadata) -> String {
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use uucore::entries;
|
use uucore::entries;
|
||||||
use uucore::InvalidEncodingHandling;
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn cached_uid2usr(uid: u32) -> String {
|
fn cached_uid2usr(uid: u32) -> String {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::char::from_digit;
|
use std::char::from_digit;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
|
||||||
// These are characters with special meaning in the shell (e.g. bash).
|
// These are characters with special meaning in the shell (e.g. bash).
|
||||||
// The first const contains characters that only have a special meaning when they appear at the beginning of a name.
|
// The first const contains characters that only have a special meaning when they appear at the beginning of a name.
|
||||||
|
@ -255,19 +256,21 @@ fn shell_with_escape(name: &str, quotes: Quotes) -> (String, bool) {
|
||||||
(escaped_str, must_quote)
|
(escaped_str, must_quote)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn escape_name(name: &str, style: &QuotingStyle) -> String {
|
pub(super) fn escape_name(name: &OsStr, style: &QuotingStyle) -> String {
|
||||||
match style {
|
match style {
|
||||||
QuotingStyle::Literal { show_control } => {
|
QuotingStyle::Literal { show_control } => {
|
||||||
if !show_control {
|
if !show_control {
|
||||||
name.chars()
|
name.to_string_lossy()
|
||||||
|
.chars()
|
||||||
.flat_map(|c| EscapedChar::new_literal(c).hide_control())
|
.flat_map(|c| EscapedChar::new_literal(c).hide_control())
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
name.into()
|
name.to_string_lossy().into_owned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QuotingStyle::C { quotes } => {
|
QuotingStyle::C { quotes } => {
|
||||||
let escaped_str: String = name
|
let escaped_str: String = name
|
||||||
|
.to_string_lossy()
|
||||||
.chars()
|
.chars()
|
||||||
.flat_map(|c| EscapedChar::new_c(c, *quotes))
|
.flat_map(|c| EscapedChar::new_c(c, *quotes))
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -283,6 +286,7 @@ pub(super) fn escape_name(name: &str, style: &QuotingStyle) -> String {
|
||||||
always_quote,
|
always_quote,
|
||||||
show_control,
|
show_control,
|
||||||
} => {
|
} => {
|
||||||
|
let name = name.to_string_lossy();
|
||||||
let (quotes, must_quote) = if name.contains(&['"', '`', '$', '\\'][..]) {
|
let (quotes, must_quote) = if name.contains(&['"', '`', '$', '\\'][..]) {
|
||||||
(Quotes::Single, true)
|
(Quotes::Single, true)
|
||||||
} else if name.contains('\'') {
|
} else if name.contains('\'') {
|
||||||
|
@ -294,9 +298,9 @@ pub(super) fn escape_name(name: &str, style: &QuotingStyle) -> String {
|
||||||
};
|
};
|
||||||
|
|
||||||
let (escaped_str, contains_quote_chars) = if *escape {
|
let (escaped_str, contains_quote_chars) = if *escape {
|
||||||
shell_with_escape(name, quotes)
|
shell_with_escape(&name, quotes)
|
||||||
} else {
|
} else {
|
||||||
shell_without_escape(name, quotes, *show_control)
|
shell_without_escape(&name, quotes, *show_control)
|
||||||
};
|
};
|
||||||
|
|
||||||
match (must_quote | contains_quote_chars, quotes) {
|
match (must_quote | contains_quote_chars, quotes) {
|
||||||
|
@ -362,7 +366,7 @@ mod tests {
|
||||||
fn check_names(name: &str, map: Vec<(&str, &str)>) {
|
fn check_names(name: &str, map: Vec<(&str, &str)>) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
map.iter()
|
map.iter()
|
||||||
.map(|(_, style)| escape_name(name, &get_style(style)))
|
.map(|(_, style)| escape_name(name.as_ref(), &get_style(style)))
|
||||||
.collect::<Vec<String>>(),
|
.collect::<Vec<String>>(),
|
||||||
map.iter()
|
map.iter()
|
||||||
.map(|(correct, _)| correct.to_string())
|
.map(|(correct, _)| correct.to_string())
|
||||||
|
|
|
@ -10,9 +10,10 @@ extern crate uucore;
|
||||||
|
|
||||||
use clap::{crate_version, App, Arg};
|
use clap::{crate_version, App, Arg};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::io::{self, Write};
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use uucore::display::println_verbatim;
|
||||||
use uucore::error::{FromIo, UResult};
|
use uucore::error::{FromIo, UResult};
|
||||||
|
|
||||||
static ABOUT: &str = "Display the full filename of the current working directory.";
|
static ABOUT: &str = "Display the full filename of the current working directory.";
|
||||||
|
@ -57,6 +58,7 @@ fn logical_path() -> io::Result<PathBuf> {
|
||||||
// POSIX: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pwd.html
|
// POSIX: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pwd.html
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
|
use std::path::Path;
|
||||||
fn looks_reasonable(path: &Path) -> bool {
|
fn looks_reasonable(path: &Path) -> bool {
|
||||||
// First, check if it's an absolute path.
|
// First, check if it's an absolute path.
|
||||||
if !path.has_root() {
|
if !path.has_root() {
|
||||||
|
@ -148,30 +150,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.unwrap_or(cwd);
|
.unwrap_or(cwd);
|
||||||
|
|
||||||
print_path(&cwd).map_err_context(|| "failed to print current directory".to_owned())?;
|
println_verbatim(&cwd).map_err_context(|| "failed to print current directory".to_owned())?;
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_path(path: &Path) -> io::Result<()> {
|
|
||||||
let stdout = io::stdout();
|
|
||||||
let mut stdout = stdout.lock();
|
|
||||||
|
|
||||||
// On Unix we print non-lossily.
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
stdout.write_all(path.as_os_str().as_bytes())?;
|
|
||||||
stdout.write_all(b"\n")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// On other platforms we potentially mangle it.
|
|
||||||
// There might be some clever way to do it correctly on Windows, but
|
|
||||||
// invalid unicode in filenames is rare there.
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
{
|
|
||||||
writeln!(stdout, "{}", path.display())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ use clap::{crate_version, App, Arg};
|
||||||
use std::fs::{read_dir, remove_dir};
|
use std::fs::{read_dir, remove_dir};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use uucore::display::Quotable;
|
||||||
use uucore::error::{set_exit_code, strip_errno, UResult};
|
use uucore::error::{set_exit_code, strip_errno, UResult};
|
||||||
use uucore::util_name;
|
use uucore::util_name;
|
||||||
|
|
||||||
|
@ -77,27 +78,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
Ok(path.metadata()?.file_type().is_dir())
|
Ok(path.metadata()?.file_type().is_dir())
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = path.as_os_str().as_bytes();
|
let bytes = path.as_os_str().as_bytes();
|
||||||
if error.raw_os_error() == Some(libc::ENOTDIR) && path.ends_with(b"/") {
|
if error.raw_os_error() == Some(libc::ENOTDIR) && bytes.ends_with(b"/") {
|
||||||
// Strip the trailing slash or .symlink_metadata() will follow the symlink
|
// Strip the trailing slash or .symlink_metadata() will follow the symlink
|
||||||
let path: &Path = OsStr::from_bytes(&path[..path.len() - 1]).as_ref();
|
let no_slash: &Path = OsStr::from_bytes(&bytes[..bytes.len() - 1]).as_ref();
|
||||||
if is_symlink(path).unwrap_or(false)
|
if is_symlink(no_slash).unwrap_or(false)
|
||||||
&& points_to_directory(path).unwrap_or(true)
|
&& points_to_directory(no_slash).unwrap_or(true)
|
||||||
{
|
{
|
||||||
show_error!(
|
show_error!(
|
||||||
"failed to remove '{}/': Symbolic link not followed",
|
"failed to remove {}: Symbolic link not followed",
|
||||||
path.display()
|
path.quote()
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
show_error!(
|
show_error!("failed to remove {}: {}", path.quote(), strip_errno(&error));
|
||||||
"failed to remove '{}': {}",
|
|
||||||
path.display(),
|
|
||||||
strip_errno(&error)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +122,7 @@ fn remove(mut path: &Path, opts: Opts) -> Result<(), Error<'_>> {
|
||||||
|
|
||||||
fn remove_single(path: &Path, opts: Opts) -> Result<(), Error<'_>> {
|
fn remove_single(path: &Path, opts: Opts) -> Result<(), Error<'_>> {
|
||||||
if opts.verbose {
|
if opts.verbose {
|
||||||
println!("{}: removing directory, '{}'", util_name(), path.display());
|
println!("{}: removing directory, {}", util_name(), path.quote());
|
||||||
}
|
}
|
||||||
remove_dir(path).map_err(|error| Error { error, path })
|
remove_dir(path).map_err(|error| Error { error, path })
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,8 @@ use std::fs::{self, File};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use uucore::display::{Quotable, Quoted};
|
||||||
|
|
||||||
/// The minimum character width for formatting counts when reading from stdin.
|
/// The minimum character width for formatting counts when reading from stdin.
|
||||||
const MINIMUM_WIDTH: usize = 7;
|
const MINIMUM_WIDTH: usize = 7;
|
||||||
|
|
||||||
|
@ -122,10 +124,10 @@ impl Input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path_display(&self) -> std::path::Display<'_> {
|
fn path_display(&self) -> Quoted<'_> {
|
||||||
match self {
|
match self {
|
||||||
Input::Path(path) => path.display(),
|
Input::Path(path) => path.maybe_quote(),
|
||||||
Input::Stdin(_) => Path::display("'standard input'".as_ref()),
|
Input::Stdin(_) => "standard input".maybe_quote(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -448,7 +450,10 @@ fn wc(inputs: Vec<Input>, settings: &Settings) -> Result<(), u32> {
|
||||||
if let Err(err) = print_stats(settings, &result, max_width) {
|
if let Err(err) = print_stats(settings, &result, max_width) {
|
||||||
show_warning!(
|
show_warning!(
|
||||||
"failed to print result for {}: {}",
|
"failed to print result for {}: {}",
|
||||||
result.title.unwrap_or_else(|| "<stdin>".as_ref()).display(),
|
result
|
||||||
|
.title
|
||||||
|
.unwrap_or_else(|| "<stdin>".as_ref())
|
||||||
|
.maybe_quote(),
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
failure = true;
|
failure = true;
|
||||||
|
@ -526,7 +531,7 @@ fn print_stats(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(title) = result.title {
|
if let Some(title) = result.title {
|
||||||
writeln!(stdout_lock, " {}", title.display())?;
|
writeln!(stdout_lock, " {}", title.maybe_quote())?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(stdout_lock)?;
|
writeln!(stdout_lock)?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,16 @@
|
||||||
|
|
||||||
//! Common functions to manage permissions
|
//! Common functions to manage permissions
|
||||||
|
|
||||||
|
use crate::error::strip_errno;
|
||||||
use crate::error::UResult;
|
use crate::error::UResult;
|
||||||
|
use crate::error::USimpleError;
|
||||||
pub use crate::features::entries;
|
pub use crate::features::entries;
|
||||||
use crate::fs::resolve_relative_path;
|
use crate::fs::resolve_relative_path;
|
||||||
use crate::show_error;
|
use crate::show_error;
|
||||||
use libc::{self, gid_t, lchown, uid_t};
|
use clap::App;
|
||||||
|
use clap::Arg;
|
||||||
|
use clap::ArgMatches;
|
||||||
|
use libc::{self, gid_t, uid_t};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use std::io::Error as IOError;
|
use std::io::Error as IOError;
|
||||||
|
@ -44,7 +49,7 @@ fn chown<P: AsRef<Path>>(path: P, uid: uid_t, gid: gid_t, follow: bool) -> IORes
|
||||||
if follow {
|
if follow {
|
||||||
libc::chown(s.as_ptr(), uid, gid)
|
libc::chown(s.as_ptr(), uid, gid)
|
||||||
} else {
|
} else {
|
||||||
lchown(s.as_ptr(), uid, gid)
|
libc::lchown(s.as_ptr(), uid, gid)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if ret == 0 {
|
if ret == 0 {
|
||||||
|
@ -160,31 +165,26 @@ pub enum IfFrom {
|
||||||
UserGroup(u32, u32),
|
UserGroup(u32, u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
pub enum TraverseSymlinks {
|
||||||
|
None,
|
||||||
|
First,
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ChownExecutor {
|
pub struct ChownExecutor {
|
||||||
pub dest_uid: Option<u32>,
|
pub dest_uid: Option<u32>,
|
||||||
pub dest_gid: Option<u32>,
|
pub dest_gid: Option<u32>,
|
||||||
pub bit_flag: u8,
|
pub traverse_symlinks: TraverseSymlinks,
|
||||||
pub verbosity: Verbosity,
|
pub verbosity: Verbosity,
|
||||||
pub filter: IfFrom,
|
pub filter: IfFrom,
|
||||||
pub files: Vec<String>,
|
pub files: Vec<String>,
|
||||||
pub recursive: bool,
|
pub recursive: bool,
|
||||||
pub preserve_root: bool,
|
pub preserve_root: bool,
|
||||||
|
// Must be true if traverse_symlinks is not None
|
||||||
pub dereference: bool,
|
pub dereference: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! unwrap {
|
|
||||||
($m:expr, $e:ident, $err:block) => {
|
|
||||||
match $m {
|
|
||||||
Ok(meta) => meta,
|
|
||||||
Err($e) => $err,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const FTS_COMFOLLOW: u8 = 1;
|
|
||||||
pub const FTS_PHYSICAL: u8 = 1 << 1;
|
|
||||||
pub const FTS_LOGICAL: u8 = 1 << 2;
|
|
||||||
|
|
||||||
impl ChownExecutor {
|
impl ChownExecutor {
|
||||||
pub fn exec(&self) -> UResult<()> {
|
pub fn exec(&self) -> UResult<()> {
|
||||||
let mut ret = 0;
|
let mut ret = 0;
|
||||||
|
@ -198,9 +198,8 @@ impl ChownExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn traverse<P: AsRef<Path>>(&self, root: P) -> i32 {
|
fn traverse<P: AsRef<Path>>(&self, root: P) -> i32 {
|
||||||
let follow_arg = self.dereference || self.bit_flag != FTS_PHYSICAL;
|
|
||||||
let path = root.as_ref();
|
let path = root.as_ref();
|
||||||
let meta = match self.obtain_meta(path, follow_arg) {
|
let meta = match self.obtain_meta(path, self.dereference) {
|
||||||
Some(m) => m,
|
Some(m) => m,
|
||||||
_ => return 1,
|
_ => return 1,
|
||||||
};
|
};
|
||||||
|
@ -212,7 +211,7 @@ impl ChownExecutor {
|
||||||
// (argument is symlink && should follow argument && resolved to be '/')
|
// (argument is symlink && should follow argument && resolved to be '/')
|
||||||
// )
|
// )
|
||||||
if self.recursive && self.preserve_root {
|
if self.recursive && self.preserve_root {
|
||||||
let may_exist = if follow_arg {
|
let may_exist = if self.dereference {
|
||||||
path.canonicalize().ok()
|
path.canonicalize().ok()
|
||||||
} else {
|
} else {
|
||||||
let real = resolve_relative_path(path);
|
let real = resolve_relative_path(path);
|
||||||
|
@ -238,7 +237,7 @@ impl ChownExecutor {
|
||||||
&meta,
|
&meta,
|
||||||
self.dest_uid,
|
self.dest_uid,
|
||||||
self.dest_gid,
|
self.dest_gid,
|
||||||
follow_arg,
|
self.dereference,
|
||||||
self.verbosity.clone(),
|
self.verbosity.clone(),
|
||||||
) {
|
) {
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
|
@ -266,20 +265,55 @@ impl ChownExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dive_into<P: AsRef<Path>>(&self, root: P) -> i32 {
|
fn dive_into<P: AsRef<Path>>(&self, root: P) -> i32 {
|
||||||
let mut ret = 0;
|
|
||||||
let root = root.as_ref();
|
let root = root.as_ref();
|
||||||
let follow = self.dereference || self.bit_flag & FTS_LOGICAL != 0;
|
|
||||||
for entry in WalkDir::new(root).follow_links(follow).min_depth(1) {
|
// walkdir always dereferences the root directory, so we have to check it ourselves
|
||||||
let entry = unwrap!(entry, e, {
|
// TODO: replace with `root.is_symlink()` once it is stable
|
||||||
ret = 1;
|
if self.traverse_symlinks == TraverseSymlinks::None
|
||||||
show_error!("{}", e);
|
&& std::fs::symlink_metadata(root)
|
||||||
continue;
|
.map(|m| m.file_type().is_symlink())
|
||||||
});
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ret = 0;
|
||||||
|
let mut iterator = WalkDir::new(root)
|
||||||
|
.follow_links(self.traverse_symlinks == TraverseSymlinks::All)
|
||||||
|
.min_depth(1)
|
||||||
|
.into_iter();
|
||||||
|
// We can't use a for loop because we need to manipulate the iterator inside the loop.
|
||||||
|
while let Some(entry) = iterator.next() {
|
||||||
|
let entry = match entry {
|
||||||
|
Err(e) => {
|
||||||
|
ret = 1;
|
||||||
|
if let Some(path) = e.path() {
|
||||||
|
show_error!(
|
||||||
|
"cannot access '{}': {}",
|
||||||
|
path.display(),
|
||||||
|
if let Some(error) = e.io_error() {
|
||||||
|
strip_errno(error)
|
||||||
|
} else {
|
||||||
|
"Too many levels of symbolic links".into()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
show_error!("{}", e)
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Ok(entry) => entry,
|
||||||
|
};
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
let meta = match self.obtain_meta(path, follow) {
|
let meta = match self.obtain_meta(path, self.dereference) {
|
||||||
Some(m) => m,
|
Some(m) => m,
|
||||||
_ => {
|
_ => {
|
||||||
ret = 1;
|
ret = 1;
|
||||||
|
if entry.file_type().is_dir() {
|
||||||
|
// Instruct walkdir to skip this directory to avoid getting another error
|
||||||
|
// when walkdir tries to query the children of this directory.
|
||||||
|
iterator.skip_current_dir();
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -293,7 +327,7 @@ impl ChownExecutor {
|
||||||
&meta,
|
&meta,
|
||||||
self.dest_uid,
|
self.dest_uid,
|
||||||
self.dest_gid,
|
self.dest_gid,
|
||||||
follow,
|
self.dereference,
|
||||||
self.verbosity.clone(),
|
self.verbosity.clone(),
|
||||||
) {
|
) {
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
|
@ -316,23 +350,25 @@ impl ChownExecutor {
|
||||||
fn obtain_meta<P: AsRef<Path>>(&self, path: P, follow: bool) -> Option<Metadata> {
|
fn obtain_meta<P: AsRef<Path>>(&self, path: P, follow: bool) -> Option<Metadata> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let meta = if follow {
|
let meta = if follow {
|
||||||
unwrap!(path.metadata(), e, {
|
path.metadata()
|
||||||
match self.verbosity.level {
|
|
||||||
VerbosityLevel::Silent => (),
|
|
||||||
_ => show_error!("cannot access '{}': {}", path.display(), e),
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
unwrap!(path.symlink_metadata(), e, {
|
path.symlink_metadata()
|
||||||
|
};
|
||||||
|
match meta {
|
||||||
|
Err(e) => {
|
||||||
match self.verbosity.level {
|
match self.verbosity.level {
|
||||||
VerbosityLevel::Silent => (),
|
VerbosityLevel::Silent => (),
|
||||||
_ => show_error!("cannot dereference '{}': {}", path.display(), e),
|
_ => show_error!(
|
||||||
|
"cannot {} '{}': {}",
|
||||||
|
if follow { "dereference" } else { "access" },
|
||||||
|
path.display(),
|
||||||
|
strip_errno(&e)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
return None;
|
None
|
||||||
})
|
}
|
||||||
};
|
Ok(meta) => Some(meta),
|
||||||
Some(meta)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -345,3 +381,148 @@ impl ChownExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod options {
|
||||||
|
pub mod verbosity {
|
||||||
|
pub const CHANGES: &str = "changes";
|
||||||
|
pub const QUIET: &str = "quiet";
|
||||||
|
pub const SILENT: &str = "silent";
|
||||||
|
pub const VERBOSE: &str = "verbose";
|
||||||
|
}
|
||||||
|
pub mod preserve_root {
|
||||||
|
pub const PRESERVE: &str = "preserve-root";
|
||||||
|
pub const NO_PRESERVE: &str = "no-preserve-root";
|
||||||
|
}
|
||||||
|
pub mod dereference {
|
||||||
|
pub const DEREFERENCE: &str = "dereference";
|
||||||
|
pub const NO_DEREFERENCE: &str = "no-dereference";
|
||||||
|
}
|
||||||
|
pub const FROM: &str = "from";
|
||||||
|
pub const RECURSIVE: &str = "recursive";
|
||||||
|
pub mod traverse {
|
||||||
|
pub const TRAVERSE: &str = "H";
|
||||||
|
pub const NO_TRAVERSE: &str = "P";
|
||||||
|
pub const EVERY: &str = "L";
|
||||||
|
}
|
||||||
|
pub const REFERENCE: &str = "reference";
|
||||||
|
pub const ARG_OWNER: &str = "OWNER";
|
||||||
|
pub const ARG_GROUP: &str = "GROUP";
|
||||||
|
pub const ARG_FILES: &str = "FILE";
|
||||||
|
}
|
||||||
|
|
||||||
|
type GidUidFilterParser<'a> = fn(&ArgMatches<'a>) -> UResult<(Option<u32>, Option<u32>, IfFrom)>;
|
||||||
|
|
||||||
|
/// Base implementation for `chgrp` and `chown`.
|
||||||
|
///
|
||||||
|
/// An argument called `add_arg_if_not_reference` will be added to `app` if
|
||||||
|
/// `args` does not contain the `--reference` option.
|
||||||
|
/// `parse_gid_uid_and_filter` will be called to obtain the target gid and uid, and the filter,
|
||||||
|
/// from `ArgMatches`.
|
||||||
|
/// `groups_only` determines whether verbose output will only mention the group.
|
||||||
|
pub fn chown_base<'a>(
|
||||||
|
mut app: App<'a, 'a>,
|
||||||
|
args: impl crate::Args,
|
||||||
|
add_arg_if_not_reference: &'a str,
|
||||||
|
parse_gid_uid_and_filter: GidUidFilterParser<'a>,
|
||||||
|
groups_only: bool,
|
||||||
|
) -> UResult<()> {
|
||||||
|
let args: Vec<_> = args.collect();
|
||||||
|
let mut reference = false;
|
||||||
|
let mut help = false;
|
||||||
|
// stop processing options on --
|
||||||
|
for arg in args.iter().take_while(|s| *s != "--") {
|
||||||
|
if arg.to_string_lossy().starts_with("--reference=") || arg == "--reference" {
|
||||||
|
reference = true;
|
||||||
|
} else if arg == "--help" {
|
||||||
|
// we stop processing once we see --help,
|
||||||
|
// as it doesn't matter if we've seen reference or not
|
||||||
|
help = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if help || !reference {
|
||||||
|
// add both positional arguments
|
||||||
|
// arg_group is only required if
|
||||||
|
app = app.arg(
|
||||||
|
Arg::with_name(add_arg_if_not_reference)
|
||||||
|
.value_name(add_arg_if_not_reference)
|
||||||
|
.required(true)
|
||||||
|
.takes_value(true)
|
||||||
|
.multiple(false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
app = app.arg(
|
||||||
|
Arg::with_name(options::ARG_FILES)
|
||||||
|
.value_name(options::ARG_FILES)
|
||||||
|
.multiple(true)
|
||||||
|
.takes_value(true)
|
||||||
|
.required(true)
|
||||||
|
.min_values(1),
|
||||||
|
);
|
||||||
|
let matches = app.get_matches_from(args);
|
||||||
|
|
||||||
|
let files: Vec<String> = matches
|
||||||
|
.values_of(options::ARG_FILES)
|
||||||
|
.map(|v| v.map(ToString::to_string).collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let preserve_root = matches.is_present(options::preserve_root::PRESERVE);
|
||||||
|
|
||||||
|
let mut dereference = if matches.is_present(options::dereference::DEREFERENCE) {
|
||||||
|
Some(true)
|
||||||
|
} else if matches.is_present(options::dereference::NO_DEREFERENCE) {
|
||||||
|
Some(false)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut traverse_symlinks = if matches.is_present(options::traverse::TRAVERSE) {
|
||||||
|
TraverseSymlinks::First
|
||||||
|
} else if matches.is_present(options::traverse::EVERY) {
|
||||||
|
TraverseSymlinks::All
|
||||||
|
} else {
|
||||||
|
TraverseSymlinks::None
|
||||||
|
};
|
||||||
|
|
||||||
|
let recursive = matches.is_present(options::RECURSIVE);
|
||||||
|
if recursive {
|
||||||
|
if traverse_symlinks == TraverseSymlinks::None {
|
||||||
|
if dereference == Some(true) {
|
||||||
|
return Err(USimpleError::new(1, "-R --dereference requires -H or -L"));
|
||||||
|
}
|
||||||
|
dereference = Some(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
traverse_symlinks = TraverseSymlinks::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let verbosity_level = if matches.is_present(options::verbosity::CHANGES) {
|
||||||
|
VerbosityLevel::Changes
|
||||||
|
} else if matches.is_present(options::verbosity::SILENT)
|
||||||
|
|| matches.is_present(options::verbosity::QUIET)
|
||||||
|
{
|
||||||
|
VerbosityLevel::Silent
|
||||||
|
} else if matches.is_present(options::verbosity::VERBOSE) {
|
||||||
|
VerbosityLevel::Verbose
|
||||||
|
} else {
|
||||||
|
VerbosityLevel::Normal
|
||||||
|
};
|
||||||
|
let (dest_gid, dest_uid, filter) = parse_gid_uid_and_filter(&matches)?;
|
||||||
|
|
||||||
|
let executor = ChownExecutor {
|
||||||
|
traverse_symlinks,
|
||||||
|
dest_gid,
|
||||||
|
dest_uid,
|
||||||
|
verbosity: Verbosity {
|
||||||
|
groups_only,
|
||||||
|
level: verbosity_level,
|
||||||
|
},
|
||||||
|
recursive,
|
||||||
|
dereference: dereference.unwrap_or(true),
|
||||||
|
preserve_root,
|
||||||
|
files,
|
||||||
|
filter,
|
||||||
|
};
|
||||||
|
executor.exec()
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ mod parser; // string parsing modules
|
||||||
// * cross-platform modules
|
// * cross-platform modules
|
||||||
pub use crate::mods::backup_control;
|
pub use crate::mods::backup_control;
|
||||||
pub use crate::mods::coreopts;
|
pub use crate::mods::coreopts;
|
||||||
|
pub use crate::mods::display;
|
||||||
pub use crate::mods::error;
|
pub use crate::mods::error;
|
||||||
pub use crate::mods::os;
|
pub use crate::mods::os;
|
||||||
pub use crate::mods::panic;
|
pub use crate::mods::panic;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
pub mod backup_control;
|
pub mod backup_control;
|
||||||
pub mod coreopts;
|
pub mod coreopts;
|
||||||
|
pub mod display;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod os;
|
pub mod os;
|
||||||
pub mod panic;
|
pub mod panic;
|
||||||
|
|
534
src/uucore/src/lib/mods/display.rs
Normal file
534
src/uucore/src/lib/mods/display.rs
Normal file
|
@ -0,0 +1,534 @@
|
||||||
|
/// Utilities for printing paths, with special attention paid to special
|
||||||
|
/// characters and invalid unicode.
|
||||||
|
///
|
||||||
|
/// For displaying paths in informational messages use `Quotable::quote`. This
|
||||||
|
/// will wrap quotes around the filename and add the necessary escapes to make
|
||||||
|
/// it copy/paste-able into a shell.
|
||||||
|
///
|
||||||
|
/// For writing raw paths to stdout when the output should not be quoted or escaped,
|
||||||
|
/// use `println_verbatim`. This will preserve invalid unicode.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use std::path::Path;
|
||||||
|
/// use uucore::display::{Quotable, println_verbatim};
|
||||||
|
///
|
||||||
|
/// let path = Path::new("foo/bar.baz");
|
||||||
|
///
|
||||||
|
/// println!("Found file {}", path.quote()); // Prints "Found file 'foo/bar.baz'"
|
||||||
|
/// println_verbatim(path)?; // Prints "foo/bar.baz"
|
||||||
|
/// # Ok::<(), std::io::Error>(())
|
||||||
|
/// ```
|
||||||
|
// spell-checker:ignore Fbar
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
#[cfg(any(unix, target_os = "wasi", windows))]
|
||||||
|
use std::fmt::Write as FmtWrite;
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::io::{self, Write as IoWrite};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
#[cfg(target_os = "wasi")]
|
||||||
|
use std::os::wasi::ffi::OsStrExt;
|
||||||
|
#[cfg(any(unix, target_os = "wasi"))]
|
||||||
|
use std::str::from_utf8;
|
||||||
|
|
||||||
|
/// An extension trait for displaying filenames to users.
|
||||||
|
pub trait Quotable {
|
||||||
|
/// Returns an object that implements [`Display`] for printing filenames with
|
||||||
|
/// proper quoting and escaping for the platform.
|
||||||
|
///
|
||||||
|
/// On Unix this corresponds to sh/bash syntax, on Windows Powershell syntax
|
||||||
|
/// is used.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use std::path::Path;
|
||||||
|
/// use uucore::display::Quotable;
|
||||||
|
///
|
||||||
|
/// let path = Path::new("foo/bar.baz");
|
||||||
|
///
|
||||||
|
/// println!("Found file {}", path.quote()); // Prints "Found file 'foo/bar.baz'"
|
||||||
|
/// ```
|
||||||
|
fn quote(&self) -> Quoted<'_>;
|
||||||
|
|
||||||
|
/// Like `quote()`, but don't actually add quotes unless necessary because of
|
||||||
|
/// whitespace or special characters.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use std::path::Path;
|
||||||
|
/// use uucore::display::Quotable;
|
||||||
|
/// use uucore::show_error;
|
||||||
|
///
|
||||||
|
/// let foo = Path::new("foo/bar.baz");
|
||||||
|
/// let bar = Path::new("foo bar");
|
||||||
|
///
|
||||||
|
/// show_error!("{}: Not found", foo.maybe_quote()); // Prints "util: foo/bar.baz: Not found"
|
||||||
|
/// show_error!("{}: Not found", bar.maybe_quote()); // Prints "util: 'foo bar': Not found"
|
||||||
|
/// ```
|
||||||
|
fn maybe_quote(&self) -> Quoted<'_> {
|
||||||
|
let mut quoted = self.quote();
|
||||||
|
quoted.force_quote = false;
|
||||||
|
quoted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_as_ref {
|
||||||
|
($type: ty) => {
|
||||||
|
impl Quotable for $type {
|
||||||
|
fn quote(&self) -> Quoted<'_> {
|
||||||
|
Quoted::new(self.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_as_ref!(&'_ str);
|
||||||
|
impl_as_ref!(String);
|
||||||
|
impl_as_ref!(&'_ std::path::Path);
|
||||||
|
impl_as_ref!(std::path::PathBuf);
|
||||||
|
impl_as_ref!(std::path::Component<'_>);
|
||||||
|
impl_as_ref!(std::path::Components<'_>);
|
||||||
|
impl_as_ref!(std::path::Iter<'_>);
|
||||||
|
impl_as_ref!(&'_ std::ffi::OsStr);
|
||||||
|
impl_as_ref!(std::ffi::OsString);
|
||||||
|
|
||||||
|
// Cow<'_, str> does not implement AsRef<OsStr> and this is unlikely to be fixed
|
||||||
|
// for backward compatibility reasons. Otherwise we'd use a blanket impl.
|
||||||
|
impl Quotable for Cow<'_, str> {
|
||||||
|
fn quote(&self) -> Quoted<'_> {
|
||||||
|
let text: &str = self.as_ref();
|
||||||
|
Quoted::new(text.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around [`OsStr`] for printing paths with quoting and escaping applied.
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct Quoted<'a> {
|
||||||
|
text: &'a OsStr,
|
||||||
|
force_quote: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Quoted<'a> {
|
||||||
|
fn new(text: &'a OsStr) -> Self {
|
||||||
|
Quoted {
|
||||||
|
text,
|
||||||
|
force_quote: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Quoted<'_> {
|
||||||
|
#[cfg(any(windows, unix, target_os = "wasi"))]
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
// On Unix we emulate sh syntax. On Windows Powershell.
|
||||||
|
// They're just similar enough to share some code.
|
||||||
|
|
||||||
|
/// Characters with special meaning outside quotes.
|
||||||
|
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02
|
||||||
|
// I don't know why % is in there, and GNU doesn't quote it either.
|
||||||
|
// {} were used in a version elsewhere but seem unnecessary, GNU doesn't
|
||||||
|
// quote them. They're used in function definitions but not in a way we
|
||||||
|
// have to worry about.
|
||||||
|
#[cfg(any(unix, target_os = "wasi"))]
|
||||||
|
const SPECIAL_SHELL_CHARS: &[u8] = b"|&;<>()$`\\\"'*?[]=";
|
||||||
|
// FIXME: I'm not a PowerShell wizard and don't know if this is correct.
|
||||||
|
// I just copied the Unix version, removed \, and added ,{} based on
|
||||||
|
// experimentation.
|
||||||
|
// I have noticed that ~?*[] only get expanded in some contexts, so watch
|
||||||
|
// out for that if doing your own tests.
|
||||||
|
// Get-ChildItem seems unwilling to quote anything so it doesn't help.
|
||||||
|
// There's the additional wrinkle that Windows has stricter requirements
|
||||||
|
// for filenames: I've been testing using a Linux build of PowerShell, but
|
||||||
|
// this code doesn't even compile on Linux.
|
||||||
|
#[cfg(windows)]
|
||||||
|
const SPECIAL_SHELL_CHARS: &[u8] = b"|&;<>()$`\"'*?[]=,{}";
|
||||||
|
|
||||||
|
/// Characters with a special meaning at the beginning of a name.
|
||||||
|
// ~ expands a home directory.
|
||||||
|
// # starts a comment.
|
||||||
|
// ! is a common extension for expanding the shell history.
|
||||||
|
#[cfg(any(unix, target_os = "wasi"))]
|
||||||
|
const SPECIAL_SHELL_CHARS_START: &[char] = &['~', '#', '!'];
|
||||||
|
// Same deal as before, this is possibly incomplete.
|
||||||
|
// A single stand-alone exclamation mark seems to have some special meaning.
|
||||||
|
#[cfg(windows)]
|
||||||
|
const SPECIAL_SHELL_CHARS_START: &[char] = &['~', '#', '@', '!'];
|
||||||
|
|
||||||
|
/// Characters that are interpreted specially in a double-quoted string.
|
||||||
|
#[cfg(any(unix, target_os = "wasi"))]
|
||||||
|
const DOUBLE_UNSAFE: &[u8] = &[b'"', b'`', b'$', b'\\'];
|
||||||
|
#[cfg(windows)]
|
||||||
|
const DOUBLE_UNSAFE: &[u8] = &[b'"', b'`', b'$'];
|
||||||
|
|
||||||
|
let text = match self.text.to_str() {
|
||||||
|
None => return write_escaped(f, self.text),
|
||||||
|
Some(text) => text,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut is_single_safe = true;
|
||||||
|
let mut is_double_safe = true;
|
||||||
|
let mut requires_quote = self.force_quote;
|
||||||
|
|
||||||
|
if let Some(first) = text.chars().next() {
|
||||||
|
if SPECIAL_SHELL_CHARS_START.contains(&first) {
|
||||||
|
requires_quote = true;
|
||||||
|
}
|
||||||
|
// Unlike in Unix, quoting an argument may stop it
|
||||||
|
// from being recognized as an option. I like that very much.
|
||||||
|
// But we don't want to quote "-" because that's a common
|
||||||
|
// special argument and PowerShell doesn't mind it.
|
||||||
|
#[cfg(windows)]
|
||||||
|
if first == '-' && text.len() > 1 {
|
||||||
|
requires_quote = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Empty string
|
||||||
|
requires_quote = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ch in text.chars() {
|
||||||
|
if ch.is_ascii() {
|
||||||
|
let ch = ch as u8;
|
||||||
|
if ch == b'\'' {
|
||||||
|
is_single_safe = false;
|
||||||
|
}
|
||||||
|
if DOUBLE_UNSAFE.contains(&ch) {
|
||||||
|
is_double_safe = false;
|
||||||
|
}
|
||||||
|
if !requires_quote && SPECIAL_SHELL_CHARS.contains(&ch) {
|
||||||
|
requires_quote = true;
|
||||||
|
}
|
||||||
|
if ch.is_ascii_control() {
|
||||||
|
return write_escaped(f, self.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !requires_quote && ch.is_whitespace() {
|
||||||
|
// This includes unicode whitespace.
|
||||||
|
// We maybe don't have to escape it, we don't escape other lookalike
|
||||||
|
// characters either, but it's confusing if it goes unquoted.
|
||||||
|
requires_quote = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !requires_quote {
|
||||||
|
return f.write_str(text);
|
||||||
|
} else if is_single_safe {
|
||||||
|
return write_simple(f, text, '\'');
|
||||||
|
} else if is_double_safe {
|
||||||
|
return write_simple(f, text, '\"');
|
||||||
|
} else {
|
||||||
|
return write_single_escaped(f, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_simple(f: &mut Formatter<'_>, text: &str, quote: char) -> fmt::Result {
|
||||||
|
f.write_char(quote)?;
|
||||||
|
f.write_str(text)?;
|
||||||
|
f.write_char(quote)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(unix, target_os = "wasi"))]
|
||||||
|
fn write_single_escaped(f: &mut Formatter<'_>, text: &str) -> fmt::Result {
|
||||||
|
let mut iter = text.split('\'');
|
||||||
|
if let Some(chunk) = iter.next() {
|
||||||
|
if !chunk.is_empty() {
|
||||||
|
write_simple(f, chunk, '\'')?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for chunk in iter {
|
||||||
|
f.write_str("\\'")?;
|
||||||
|
if !chunk.is_empty() {
|
||||||
|
write_simple(f, chunk, '\'')?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write using the syntax described here:
|
||||||
|
/// https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
|
||||||
|
///
|
||||||
|
/// Supported by these shells:
|
||||||
|
/// - bash
|
||||||
|
/// - zsh
|
||||||
|
/// - busybox sh
|
||||||
|
/// - mksh
|
||||||
|
///
|
||||||
|
/// Not supported by these:
|
||||||
|
/// - fish
|
||||||
|
/// - dash
|
||||||
|
/// - tcsh
|
||||||
|
#[cfg(any(unix, target_os = "wasi"))]
|
||||||
|
fn write_escaped(f: &mut Formatter<'_>, text: &OsStr) -> fmt::Result {
|
||||||
|
f.write_str("$'")?;
|
||||||
|
for chunk in from_utf8_iter(text.as_bytes()) {
|
||||||
|
match chunk {
|
||||||
|
Ok(chunk) => {
|
||||||
|
for ch in chunk.chars() {
|
||||||
|
match ch {
|
||||||
|
'\n' => f.write_str("\\n")?,
|
||||||
|
'\t' => f.write_str("\\t")?,
|
||||||
|
'\r' => f.write_str("\\r")?,
|
||||||
|
// We could do \b, \f, \v, etc., but those are
|
||||||
|
// rare enough to be confusing.
|
||||||
|
// \0 doesn't work consistently because of the
|
||||||
|
// octal \nnn syntax, and null bytes can't appear
|
||||||
|
// in filenames anyway.
|
||||||
|
ch if ch.is_ascii_control() => write!(f, "\\x{:02X}", ch as u8)?,
|
||||||
|
'\\' | '\'' => {
|
||||||
|
// '?' and '"' can also be escaped this way
|
||||||
|
// but AFAICT there's no reason to do so
|
||||||
|
f.write_char('\\')?;
|
||||||
|
f.write_char(ch)?;
|
||||||
|
}
|
||||||
|
ch => {
|
||||||
|
f.write_char(ch)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(unit) => write!(f, "\\x{:02X}", unit)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.write_char('\'')?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn write_single_escaped(f: &mut Formatter<'_>, text: &str) -> fmt::Result {
|
||||||
|
// Quotes in Powershell can be escaped by doubling them
|
||||||
|
f.write_char('\'')?;
|
||||||
|
let mut iter = text.split('\'');
|
||||||
|
if let Some(chunk) = iter.next() {
|
||||||
|
f.write_str(chunk)?;
|
||||||
|
}
|
||||||
|
for chunk in iter {
|
||||||
|
f.write_str("''")?;
|
||||||
|
f.write_str(chunk)?;
|
||||||
|
}
|
||||||
|
f.write_char('\'')?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn write_escaped(f: &mut Formatter<'_>, text: &OsStr) -> fmt::Result {
|
||||||
|
// ` takes the role of \ since \ is already used as the path separator.
|
||||||
|
// Things are UTF-16-oriented, so we escape code units as "`u{1234}".
|
||||||
|
use std::char::decode_utf16;
|
||||||
|
use std::os::windows::ffi::OsStrExt;
|
||||||
|
|
||||||
|
f.write_char('"')?;
|
||||||
|
for ch in decode_utf16(text.encode_wide()) {
|
||||||
|
match ch {
|
||||||
|
Ok(ch) => match ch {
|
||||||
|
'\0' => f.write_str("`0")?,
|
||||||
|
'\r' => f.write_str("`r")?,
|
||||||
|
'\n' => f.write_str("`n")?,
|
||||||
|
'\t' => f.write_str("`t")?,
|
||||||
|
ch if ch.is_ascii_control() => write!(f, "`u{{{:04X}}}", ch as u8)?,
|
||||||
|
'`' => f.write_str("``")?,
|
||||||
|
'$' => f.write_str("`$")?,
|
||||||
|
'"' => f.write_str("\"\"")?,
|
||||||
|
ch => f.write_char(ch)?,
|
||||||
|
},
|
||||||
|
Err(err) => write!(f, "`u{{{:04X}}}", err.unpaired_surrogate())?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.write_char('"')?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(unix, target_os = "wasi", windows)))]
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
// As a fallback, we use Rust's own escaping rules.
|
||||||
|
// This is reasonably sane and very easy to implement.
|
||||||
|
// We use single quotes because that's hardcoded in a lot of tests.
|
||||||
|
let text = self.text.to_string_lossy();
|
||||||
|
if self.force_quote || !text.chars().all(|ch| ch.is_alphanumeric() || ch == '.') {
|
||||||
|
write!(f, "'{}'", text.escape_debug())
|
||||||
|
} else {
|
||||||
|
f.write_str(&text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(unix, target_os = "wasi"))]
|
||||||
|
fn from_utf8_iter(mut bytes: &[u8]) -> impl Iterator<Item = Result<&str, u8>> {
|
||||||
|
std::iter::from_fn(move || {
|
||||||
|
if bytes.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match from_utf8(bytes) {
|
||||||
|
Ok(text) => {
|
||||||
|
bytes = &[];
|
||||||
|
Some(Ok(text))
|
||||||
|
}
|
||||||
|
Err(err) if err.valid_up_to() == 0 => {
|
||||||
|
let res = bytes[0];
|
||||||
|
bytes = &bytes[1..];
|
||||||
|
Some(Err(res))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let (valid, rest) = bytes.split_at(err.valid_up_to());
|
||||||
|
bytes = rest;
|
||||||
|
Some(Ok(from_utf8(valid).unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print a path (or `OsStr`-like object) directly to stdout, with a trailing newline,
|
||||||
|
/// without losing any information if its encoding is invalid.
|
||||||
|
///
|
||||||
|
/// This function is appropriate for commands where printing paths is the point and the
|
||||||
|
/// output is likely to be captured, like `pwd` and `basename`. For informational output
|
||||||
|
/// use `Quotable::quote`.
|
||||||
|
///
|
||||||
|
/// FIXME: This is lossy on Windows. It could probably be implemented using some low-level
|
||||||
|
/// API that takes UTF-16, without going through io::Write. This is not a big priority
|
||||||
|
/// because broken filenames are much rarer on Windows than on Unix.
|
||||||
|
pub fn println_verbatim<S: AsRef<OsStr>>(text: S) -> io::Result<()> {
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let mut stdout = stdout.lock();
|
||||||
|
#[cfg(any(unix, target_os = "wasi"))]
|
||||||
|
{
|
||||||
|
stdout.write_all(text.as_ref().as_bytes())?;
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
}
|
||||||
|
#[cfg(not(any(unix, target_os = "wasi")))]
|
||||||
|
{
|
||||||
|
writeln!(stdout, "{}", std::path::Path::new(text.as_ref()).display())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn verify_quote(cases: &[(impl Quotable, &str)]) {
|
||||||
|
for (case, expected) in cases {
|
||||||
|
assert_eq!(case.quote().to_string(), *expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_maybe(cases: &[(impl Quotable, &str)]) {
|
||||||
|
for (case, expected) in cases {
|
||||||
|
assert_eq!(case.maybe_quote().to_string(), *expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This should hold on any platform, or else other tests fail.
|
||||||
|
#[test]
|
||||||
|
fn test_basic() {
|
||||||
|
verify_quote(&[
|
||||||
|
("foo", "'foo'"),
|
||||||
|
("", "''"),
|
||||||
|
("foo/bar.baz", "'foo/bar.baz'"),
|
||||||
|
]);
|
||||||
|
verify_maybe(&[
|
||||||
|
("foo", "foo"),
|
||||||
|
("", "''"),
|
||||||
|
("foo bar", "'foo bar'"),
|
||||||
|
("$foo", "'$foo'"),
|
||||||
|
("-", "-"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(unix, target_os = "wasi", windows))]
|
||||||
|
#[test]
|
||||||
|
fn test_common() {
|
||||||
|
verify_maybe(&[
|
||||||
|
("a#b", "a#b"),
|
||||||
|
("#ab", "'#ab'"),
|
||||||
|
("a~b", "a~b"),
|
||||||
|
("!", "'!'"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(unix, target_os = "wasi"))]
|
||||||
|
#[test]
|
||||||
|
fn test_unix() {
|
||||||
|
verify_quote(&[
|
||||||
|
("can't", r#""can't""#),
|
||||||
|
(r#"can'"t"#, r#"'can'\''"t'"#),
|
||||||
|
(r#"can'$t"#, r#"'can'\''$t'"#),
|
||||||
|
("foo\nb\ta\r\\\0`r", r#"$'foo\nb\ta\r\\\x00`r'"#),
|
||||||
|
("foo\x02", r#"$'foo\x02'"#),
|
||||||
|
(r#"'$''"#, r#"\''$'\'\'"#),
|
||||||
|
]);
|
||||||
|
verify_quote(&[(OsStr::from_bytes(b"foo\xFF"), r#"$'foo\xFF'"#)]);
|
||||||
|
verify_maybe(&[
|
||||||
|
("-x", "-x"),
|
||||||
|
("a,b", "a,b"),
|
||||||
|
("a\\b", "'a\\b'"),
|
||||||
|
("}", ("}")),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[test]
|
||||||
|
fn test_windows() {
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::os::windows::ffi::OsStringExt;
|
||||||
|
verify_quote(&[
|
||||||
|
(r#"foo\bar"#, r#"'foo\bar'"#),
|
||||||
|
("can't", r#""can't""#),
|
||||||
|
(r#"can'"t"#, r#"'can''"t'"#),
|
||||||
|
(r#"can'$t"#, r#"'can''$t'"#),
|
||||||
|
("foo\nb\ta\r\\\0`r", r#""foo`nb`ta`r\`0``r""#),
|
||||||
|
("foo\x02", r#""foo`u{0002}""#),
|
||||||
|
(r#"'$''"#, r#"'''$'''''"#),
|
||||||
|
]);
|
||||||
|
verify_quote(&[(
|
||||||
|
OsString::from_wide(&[b'x' as u16, 0xD800]),
|
||||||
|
r#""x`u{D800}""#,
|
||||||
|
)]);
|
||||||
|
verify_maybe(&[
|
||||||
|
("-x", "'-x'"),
|
||||||
|
("a,b", "'a,b'"),
|
||||||
|
("a\\b", "a\\b"),
|
||||||
|
("}", "'}'"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(unix, target_os = "wasi"))]
|
||||||
|
#[test]
|
||||||
|
fn test_utf8_iter() {
|
||||||
|
type ByteStr = &'static [u8];
|
||||||
|
type Chunk = Result<&'static str, u8>;
|
||||||
|
const CASES: &[(ByteStr, &[Chunk])] = &[
|
||||||
|
(b"", &[]),
|
||||||
|
(b"hello", &[Ok("hello")]),
|
||||||
|
// Immediately invalid
|
||||||
|
(b"\xFF", &[Err(b'\xFF')]),
|
||||||
|
// Incomplete UTF-8
|
||||||
|
(b"\xC2", &[Err(b'\xC2')]),
|
||||||
|
(b"\xF4\x8F", &[Err(b'\xF4'), Err(b'\x8F')]),
|
||||||
|
(b"\xFF\xFF", &[Err(b'\xFF'), Err(b'\xFF')]),
|
||||||
|
(b"hello\xC2", &[Ok("hello"), Err(b'\xC2')]),
|
||||||
|
(b"\xFFhello", &[Err(b'\xFF'), Ok("hello")]),
|
||||||
|
(b"\xFF\xC2hello", &[Err(b'\xFF'), Err(b'\xC2'), Ok("hello")]),
|
||||||
|
(b"foo\xFFbar", &[Ok("foo"), Err(b'\xFF'), Ok("bar")]),
|
||||||
|
(
|
||||||
|
b"foo\xF4\x8Fbar",
|
||||||
|
&[Ok("foo"), Err(b'\xF4'), Err(b'\x8F'), Ok("bar")],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
b"foo\xFF\xC2bar",
|
||||||
|
&[Ok("foo"), Err(b'\xFF'), Err(b'\xC2'), Ok("bar")],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for &(case, expected) in CASES {
|
||||||
|
assert_eq!(
|
||||||
|
from_utf8_iter(case).collect::<Vec<_>>().as_slice(),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -368,7 +368,7 @@ impl UIoError {
|
||||||
pub fn new<S: Into<String>>(kind: std::io::ErrorKind, context: S) -> Box<dyn UError> {
|
pub fn new<S: Into<String>>(kind: std::io::ErrorKind, context: S) -> Box<dyn UError> {
|
||||||
Box::new(Self {
|
Box::new(Self {
|
||||||
context: context.into(),
|
context: context.into(),
|
||||||
inner: std::io::Error::new(kind, ""),
|
inner: kind.into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -380,32 +380,36 @@ impl Error for UIoError {}
|
||||||
impl Display for UIoError {
|
impl Display for UIoError {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
use std::io::ErrorKind::*;
|
use std::io::ErrorKind::*;
|
||||||
write!(
|
|
||||||
f,
|
let message;
|
||||||
"{}: {}",
|
let message = match self.inner.kind() {
|
||||||
self.context,
|
NotFound => "No such file or directory",
|
||||||
match self.inner.kind() {
|
PermissionDenied => "Permission denied",
|
||||||
NotFound => "No such file or directory",
|
ConnectionRefused => "Connection refused",
|
||||||
PermissionDenied => "Permission denied",
|
ConnectionReset => "Connection reset",
|
||||||
ConnectionRefused => "Connection refused",
|
ConnectionAborted => "Connection aborted",
|
||||||
ConnectionReset => "Connection reset",
|
NotConnected => "Not connected",
|
||||||
ConnectionAborted => "Connection aborted",
|
AddrInUse => "Address in use",
|
||||||
NotConnected => "Not connected",
|
AddrNotAvailable => "Address not available",
|
||||||
AddrInUse => "Address in use",
|
BrokenPipe => "Broken pipe",
|
||||||
AddrNotAvailable => "Address not available",
|
AlreadyExists => "Already exists",
|
||||||
BrokenPipe => "Broken pipe",
|
WouldBlock => "Would block",
|
||||||
AlreadyExists => "Already exists",
|
InvalidInput => "Invalid input",
|
||||||
WouldBlock => "Would block",
|
InvalidData => "Invalid data",
|
||||||
InvalidInput => "Invalid input",
|
TimedOut => "Timed out",
|
||||||
InvalidData => "Invalid data",
|
WriteZero => "Write zero",
|
||||||
TimedOut => "Timed out",
|
Interrupted => "Interrupted",
|
||||||
WriteZero => "Write zero",
|
Other => "Other",
|
||||||
Interrupted => "Interrupted",
|
UnexpectedEof => "Unexpected end of file",
|
||||||
Other => "Other",
|
_ => {
|
||||||
UnexpectedEof => "Unexpected end of file",
|
// TODO: using `strip_errno()` causes the error message
|
||||||
_ => panic!("Unexpected io error: {}", self.inner),
|
// to not be capitalized. When the new error variants (https://github.com/rust-lang/rust/issues/86442)
|
||||||
},
|
// are stabilized, we should add them to the match statement.
|
||||||
)
|
message = strip_errno(&self.inner);
|
||||||
|
&message
|
||||||
|
}
|
||||||
|
};
|
||||||
|
write!(f, "{}: {}", self.context, message,)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -230,7 +230,7 @@ fn test_big_h() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(not(target_vendor = "apple"))]
|
||||||
fn basic_succeeds() {
|
fn basic_succeeds() {
|
||||||
let (at, mut ucmd) = at_and_ucmd!();
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
let one_group = nix::unistd::getgroups().unwrap();
|
let one_group = nix::unistd::getgroups().unwrap();
|
||||||
|
@ -251,3 +251,105 @@ fn test_no_change() {
|
||||||
at.touch("file");
|
at.touch("file");
|
||||||
ucmd.arg("").arg(at.plus("file")).succeeds();
|
ucmd.arg("").arg(at.plus("file")).succeeds();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(target_vendor = "apple"))]
|
||||||
|
fn test_permission_denied() {
|
||||||
|
use std::os::unix::prelude::PermissionsExt;
|
||||||
|
|
||||||
|
if let Some(group) = nix::unistd::getgroups().unwrap().first() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
at.mkdir("dir");
|
||||||
|
at.touch("dir/file");
|
||||||
|
std::fs::set_permissions(at.plus("dir"), PermissionsExt::from_mode(0o0000)).unwrap();
|
||||||
|
ucmd.arg("-R")
|
||||||
|
.arg(group.as_raw().to_string())
|
||||||
|
.arg("dir")
|
||||||
|
.fails()
|
||||||
|
.stderr_only("chgrp: cannot access 'dir': Permission denied");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(target_vendor = "apple"))]
|
||||||
|
fn test_subdir_permission_denied() {
|
||||||
|
use std::os::unix::prelude::PermissionsExt;
|
||||||
|
|
||||||
|
if let Some(group) = nix::unistd::getgroups().unwrap().first() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
at.mkdir("dir");
|
||||||
|
at.mkdir("dir/subdir");
|
||||||
|
at.touch("dir/subdir/file");
|
||||||
|
std::fs::set_permissions(at.plus("dir/subdir"), PermissionsExt::from_mode(0o0000)).unwrap();
|
||||||
|
ucmd.arg("-R")
|
||||||
|
.arg(group.as_raw().to_string())
|
||||||
|
.arg("dir")
|
||||||
|
.fails()
|
||||||
|
.stderr_only("chgrp: cannot access 'dir/subdir': Permission denied");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(target_vendor = "apple"))]
|
||||||
|
fn test_traverse_symlinks() {
|
||||||
|
use std::os::unix::prelude::MetadataExt;
|
||||||
|
let groups = nix::unistd::getgroups().unwrap();
|
||||||
|
if groups.len() < 2 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (first_group, second_group) = (groups[0], groups[1]);
|
||||||
|
|
||||||
|
for &(args, traverse_first, traverse_second) in &[
|
||||||
|
(&[][..] as &[&str], false, false),
|
||||||
|
(&["-H"][..], true, false),
|
||||||
|
(&["-P"][..], false, false),
|
||||||
|
(&["-L"][..], true, true),
|
||||||
|
] {
|
||||||
|
let scenario = TestScenario::new("chgrp");
|
||||||
|
|
||||||
|
let (at, mut ucmd) = (scenario.fixtures.clone(), scenario.ucmd());
|
||||||
|
|
||||||
|
at.mkdir("dir");
|
||||||
|
at.mkdir("dir2");
|
||||||
|
at.touch("dir2/file");
|
||||||
|
at.mkdir("dir3");
|
||||||
|
at.touch("dir3/file");
|
||||||
|
at.symlink_dir("dir2", "dir/dir2_ln");
|
||||||
|
at.symlink_dir("dir3", "dir3_ln");
|
||||||
|
|
||||||
|
scenario
|
||||||
|
.ccmd("chgrp")
|
||||||
|
.arg(first_group.to_string())
|
||||||
|
.arg("dir2/file")
|
||||||
|
.arg("dir3/file")
|
||||||
|
.succeeds();
|
||||||
|
|
||||||
|
assert!(at.plus("dir2/file").metadata().unwrap().gid() == first_group.as_raw());
|
||||||
|
assert!(at.plus("dir3/file").metadata().unwrap().gid() == first_group.as_raw());
|
||||||
|
|
||||||
|
ucmd.arg("-R")
|
||||||
|
.args(args)
|
||||||
|
.arg(second_group.to_string())
|
||||||
|
.arg("dir")
|
||||||
|
.arg("dir3_ln")
|
||||||
|
.succeeds()
|
||||||
|
.no_stderr();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
at.plus("dir2/file").metadata().unwrap().gid(),
|
||||||
|
if traverse_second {
|
||||||
|
second_group.as_raw()
|
||||||
|
} else {
|
||||||
|
first_group.as_raw()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
at.plus("dir3/file").metadata().unwrap().gid(),
|
||||||
|
if traverse_first {
|
||||||
|
second_group.as_raw()
|
||||||
|
} else {
|
||||||
|
first_group.as_raw()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -227,6 +227,19 @@ fn autoformat() {
|
||||||
.pipe_in("1 x y z\n2 p")
|
.pipe_in("1 x y z\n2 p")
|
||||||
.succeeds()
|
.succeeds()
|
||||||
.stdout_only("1 x y z a\n2 p b\n");
|
.stdout_only("1 x y z a\n2 p b\n");
|
||||||
|
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("-")
|
||||||
|
.arg("fields_2.txt")
|
||||||
|
.arg("-a")
|
||||||
|
.arg("1")
|
||||||
|
.arg("-o")
|
||||||
|
.arg("auto")
|
||||||
|
.arg("-e")
|
||||||
|
.arg(".")
|
||||||
|
.pipe_in("1 x y z\n2 p\n99 a b\n")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only("1 x y z a\n2 p . . b\n99 a b . .\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -333,6 +333,63 @@ fn test_ls_long() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ls_long_format() {
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let last;
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let _guard = UMASK_MUTEX.lock();
|
||||||
|
last = unsafe { umask(0) };
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
umask(0o002);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let scene = TestScenario::new(util_name!());
|
||||||
|
let at = &scene.fixtures;
|
||||||
|
at.mkdir(&at.plus_as_string("test-long-dir"));
|
||||||
|
at.touch(&at.plus_as_string("test-long-dir/test-long-file"));
|
||||||
|
at.mkdir(&at.plus_as_string("test-long-dir/test-long-dir"));
|
||||||
|
|
||||||
|
for arg in &["-l", "--long", "--format=long", "--format=verbose"] {
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
let result = scene.ucmd().arg(arg).arg("test-long-dir").succeeds();
|
||||||
|
// Assuming sane username do not have spaces within them.
|
||||||
|
// A line of the output should be:
|
||||||
|
// One of the characters -bcCdDlMnpPsStTx?
|
||||||
|
// rwx, with - for missing permissions, thrice.
|
||||||
|
// A number, preceded by column whitespace, and followed by a single space.
|
||||||
|
// A username, currently [^ ], followed by column whitespace, twice (or thrice for Hurd).
|
||||||
|
// A number, followed by a single space.
|
||||||
|
// A month, followed by a single space.
|
||||||
|
// A day, preceded by column whitespace, and followed by a single space.
|
||||||
|
// Either a year or a time, currently [0-9:]+, preceded by column whitespace,
|
||||||
|
// and followed by a single space.
|
||||||
|
// Whatever comes after is irrelevant to this specific test.
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
result.stdout_matches(&Regex::new(
|
||||||
|
r"\n[-bcCdDlMnpPsStTx?]([r-][w-][xt-]){3} +\d+ [^ ]+ +[^ ]+( +[^ ]+)? +\d+ [A-Z][a-z]{2} {0,2}\d{0,2} {0,2}[0-9:]+ "
|
||||||
|
).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
let result = scene.ucmd().arg("-lan").arg("test-long-dir").succeeds();
|
||||||
|
// This checks for the line with the .. entry. The uname and group should be digits.
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
result.stdout_matches(&Regex::new(
|
||||||
|
r"\nd([r-][w-][xt-]){3} +\d+ \d+ +\d+( +\d+)? +\d+ [A-Z][a-z]{2} {0,2}\d{0,2} {0,2}[0-9:]+ \.\."
|
||||||
|
).unwrap());
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
unsafe {
|
||||||
|
umask(last);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ls_long_total_size() {
|
fn test_ls_long_total_size() {
|
||||||
let scene = TestScenario::new(util_name!());
|
let scene = TestScenario::new(util_name!());
|
||||||
|
@ -1361,6 +1418,7 @@ fn test_ls_quoting_style() {
|
||||||
// Default is shell-escape
|
// Default is shell-escape
|
||||||
scene
|
scene
|
||||||
.ucmd()
|
.ucmd()
|
||||||
|
.arg("--hide-control-chars")
|
||||||
.arg("one\ntwo")
|
.arg("one\ntwo")
|
||||||
.succeeds()
|
.succeeds()
|
||||||
.stdout_only("'one'$'\\n''two'\n");
|
.stdout_only("'one'$'\\n''two'\n");
|
||||||
|
@ -1382,23 +1440,8 @@ fn test_ls_quoting_style() {
|
||||||
] {
|
] {
|
||||||
scene
|
scene
|
||||||
.ucmd()
|
.ucmd()
|
||||||
.arg(arg)
|
|
||||||
.arg("one\ntwo")
|
|
||||||
.succeeds()
|
|
||||||
.stdout_only(format!("{}\n", correct));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (arg, correct) in &[
|
|
||||||
("--quoting-style=literal", "one?two"),
|
|
||||||
("-N", "one?two"),
|
|
||||||
("--literal", "one?two"),
|
|
||||||
("--quoting-style=shell", "one?two"),
|
|
||||||
("--quoting-style=shell-always", "'one?two'"),
|
|
||||||
] {
|
|
||||||
scene
|
|
||||||
.ucmd()
|
|
||||||
.arg(arg)
|
|
||||||
.arg("--hide-control-chars")
|
.arg("--hide-control-chars")
|
||||||
|
.arg(arg)
|
||||||
.arg("one\ntwo")
|
.arg("one\ntwo")
|
||||||
.succeeds()
|
.succeeds()
|
||||||
.stdout_only(format!("{}\n", correct));
|
.stdout_only(format!("{}\n", correct));
|
||||||
|
@ -1408,7 +1451,7 @@ fn test_ls_quoting_style() {
|
||||||
("--quoting-style=literal", "one\ntwo"),
|
("--quoting-style=literal", "one\ntwo"),
|
||||||
("-N", "one\ntwo"),
|
("-N", "one\ntwo"),
|
||||||
("--literal", "one\ntwo"),
|
("--literal", "one\ntwo"),
|
||||||
("--quoting-style=shell", "one\ntwo"),
|
("--quoting-style=shell", "one\ntwo"), // FIXME: GNU ls quotes this case
|
||||||
("--quoting-style=shell-always", "'one\ntwo'"),
|
("--quoting-style=shell-always", "'one\ntwo'"),
|
||||||
] {
|
] {
|
||||||
scene
|
scene
|
||||||
|
@ -1435,6 +1478,7 @@ fn test_ls_quoting_style() {
|
||||||
] {
|
] {
|
||||||
scene
|
scene
|
||||||
.ucmd()
|
.ucmd()
|
||||||
|
.arg("--hide-control-chars")
|
||||||
.arg(arg)
|
.arg(arg)
|
||||||
.arg("one\\two")
|
.arg("one\\two")
|
||||||
.succeeds()
|
.succeeds()
|
||||||
|
@ -1450,6 +1494,7 @@ fn test_ls_quoting_style() {
|
||||||
] {
|
] {
|
||||||
scene
|
scene
|
||||||
.ucmd()
|
.ucmd()
|
||||||
|
.arg("--hide-control-chars")
|
||||||
.arg(arg)
|
.arg(arg)
|
||||||
.arg("one\n&two")
|
.arg("one\n&two")
|
||||||
.succeeds()
|
.succeeds()
|
||||||
|
@ -1480,6 +1525,7 @@ fn test_ls_quoting_style() {
|
||||||
] {
|
] {
|
||||||
scene
|
scene
|
||||||
.ucmd()
|
.ucmd()
|
||||||
|
.arg("--hide-control-chars")
|
||||||
.arg(arg)
|
.arg(arg)
|
||||||
.arg("one two")
|
.arg("one two")
|
||||||
.succeeds()
|
.succeeds()
|
||||||
|
@ -1503,6 +1549,7 @@ fn test_ls_quoting_style() {
|
||||||
] {
|
] {
|
||||||
scene
|
scene
|
||||||
.ucmd()
|
.ucmd()
|
||||||
|
.arg("--hide-control-chars")
|
||||||
.arg(arg)
|
.arg(arg)
|
||||||
.arg("one")
|
.arg("one")
|
||||||
.succeeds()
|
.succeeds()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue