mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-27 19:17:43 +00:00
chgrp: replace getopts with clap (#2118)
This commit is contained in:
parent
a017c1b589
commit
cff75f242a
9 changed files with 266 additions and 79 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1772,6 +1772,7 @@ dependencies = [
|
||||||
name = "uu_chgrp"
|
name = "uu_chgrp"
|
||||||
version = "0.0.6"
|
version = "0.0.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"clap",
|
||||||
"uucore",
|
"uucore",
|
||||||
"uucore_procs",
|
"uucore_procs",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
|
|
@ -15,6 +15,7 @@ edition = "2018"
|
||||||
path = "src/chgrp.rs"
|
path = "src/chgrp.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
clap = "2.33"
|
||||||
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "fs", "perms"] }
|
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "fs", "perms"] }
|
||||||
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
|
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
|
||||||
walkdir = "2.2"
|
walkdir = "2.2"
|
||||||
|
|
|
@ -14,6 +14,8 @@ use uucore::fs::resolve_relative_path;
|
||||||
use uucore::libc::gid_t;
|
use uucore::libc::gid_t;
|
||||||
use uucore::perms::{wrap_chgrp, Verbosity};
|
use uucore::perms::{wrap_chgrp, Verbosity};
|
||||||
|
|
||||||
|
use clap::{App, Arg};
|
||||||
|
|
||||||
extern crate walkdir;
|
extern crate walkdir;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
@ -24,76 +26,194 @@ use std::os::unix::fs::MetadataExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use uucore::InvalidEncodingHandling;
|
use uucore::InvalidEncodingHandling;
|
||||||
|
|
||||||
static SYNTAX: &str =
|
static ABOUT: &str = "Change the group of each FILE to GROUP.";
|
||||||
"chgrp [OPTION]... GROUP FILE...\n or : chgrp [OPTION]... --reference=RFILE FILE...";
|
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
static SUMMARY: &str = "Change the group of each FILE to 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 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";
|
||||||
|
}
|
||||||
|
|
||||||
const FTS_COMFOLLOW: u8 = 1;
|
const FTS_COMFOLLOW: u8 = 1;
|
||||||
const FTS_PHYSICAL: u8 = 1 << 1;
|
const FTS_PHYSICAL: u8 = 1 << 1;
|
||||||
const FTS_LOGICAL: u8 = 1 << 2;
|
const FTS_LOGICAL: u8 = 1 << 2;
|
||||||
|
|
||||||
|
fn get_usage() -> String {
|
||||||
|
format!(
|
||||||
|
"{0} [OPTION]... GROUP FILE...\n {0} [OPTION]... --reference=RFILE FILE...",
|
||||||
|
executable!()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn uumain(args: impl uucore::Args) -> i32 {
|
pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
let args = args
|
let args = args
|
||||||
.collect_str(InvalidEncodingHandling::ConvertLossy)
|
.collect_str(InvalidEncodingHandling::ConvertLossy)
|
||||||
.accept_any();
|
.accept_any();
|
||||||
|
|
||||||
let mut opts = app!(SYNTAX, SUMMARY, "");
|
let usage = get_usage();
|
||||||
opts.optflag("c",
|
|
||||||
"changes",
|
|
||||||
"like verbose but report only when a change is made")
|
|
||||||
.optflag("f", "silent", "")
|
|
||||||
.optflag("", "quiet", "suppress most error messages")
|
|
||||||
.optflag("v",
|
|
||||||
"verbose",
|
|
||||||
"output a diagnostic for every file processed")
|
|
||||||
.optflag("", "dereference", "affect the referent of each symbolic link (this is the default), rather than the symbolic link itself")
|
|
||||||
.optflag("h", "no-dereference", "affect symbolic links instead of any referenced file (useful only on systems that can change the ownership of a symlink)")
|
|
||||||
.optflag("",
|
|
||||||
"no-preserve-root",
|
|
||||||
"do not treat '/' specially (the default)")
|
|
||||||
.optflag("", "preserve-root", "fail to operate recursively on '/'")
|
|
||||||
.optopt("",
|
|
||||||
"reference",
|
|
||||||
"use RFILE's owner and group rather than specifying OWNER:GROUP values",
|
|
||||||
"RFILE")
|
|
||||||
.optflag("R",
|
|
||||||
"recursive",
|
|
||||||
"operate on files and directories recursively")
|
|
||||||
.optflag("H",
|
|
||||||
"",
|
|
||||||
"if a command line argument is a symbolic link to a directory, traverse it")
|
|
||||||
.optflag("L",
|
|
||||||
"",
|
|
||||||
"traverse every symbolic link to a directory encountered")
|
|
||||||
.optflag("P", "", "do not traverse any symbolic links (default)");
|
|
||||||
|
|
||||||
let mut bit_flag = FTS_PHYSICAL;
|
let mut app = App::new(executable!())
|
||||||
let mut preserve_root = false;
|
.version(VERSION)
|
||||||
let mut derefer = -1;
|
.about(ABOUT)
|
||||||
let flags: &[char] = &['H', 'L', 'P'];
|
.usage(&usage[..])
|
||||||
for opt in &args {
|
.arg(
|
||||||
match opt.as_str() {
|
Arg::with_name(options::verbosity::CHANGES)
|
||||||
// If more than one is specified, only the final one takes effect.
|
.short("c")
|
||||||
s if s.contains(flags) => {
|
.long(options::verbosity::CHANGES)
|
||||||
if let Some(idx) = s.rfind(flags) {
|
.help("like verbose but report only when a change is made"),
|
||||||
match s.chars().nth(idx).unwrap() {
|
)
|
||||||
'H' => bit_flag = FTS_COMFOLLOW | FTS_PHYSICAL,
|
.arg(
|
||||||
'L' => bit_flag = FTS_LOGICAL,
|
Arg::with_name(options::verbosity::SILENT)
|
||||||
'P' => bit_flag = FTS_PHYSICAL,
|
.short("f")
|
||||||
_ => (),
|
.long(options::verbosity::SILENT),
|
||||||
}
|
)
|
||||||
}
|
.arg(
|
||||||
}
|
Arg::with_name(options::verbosity::QUIET)
|
||||||
"--no-preserve-root" => preserve_root = false,
|
.long(options::verbosity::QUIET)
|
||||||
"--preserve-root" => preserve_root = true,
|
.help("suppress most error messages"),
|
||||||
"--dereference" => derefer = 1,
|
)
|
||||||
"--no-dereference" => derefer = 0,
|
.arg(
|
||||||
_ => (),
|
Arg::with_name(options::verbosity::VERBOSE)
|
||||||
|
.short("v")
|
||||||
|
.long(options::verbosity::VERBOSE)
|
||||||
|
.help("output a diagnostic for every file processed"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(options::dereference::DEREFERENCE)
|
||||||
|
.long(options::dereference::DEREFERENCE),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(options::dereference::NO_DEREFERENCE)
|
||||||
|
.short("h")
|
||||||
|
.long(options::dereference::NO_DEREFERENCE)
|
||||||
|
.help(
|
||||||
|
"affect symbolic links instead of any referenced file (useful only on systems that can change the ownership of a symlink)",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(options::preserve_root::PRESERVE)
|
||||||
|
.long(options::preserve_root::PRESERVE)
|
||||||
|
.help("fail to operate recursively on '/'"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(options::preserve_root::NO_PRESERVE)
|
||||||
|
.long(options::preserve_root::NO_PRESERVE)
|
||||||
|
.help("do not treat '/' specially (the default)"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(options::REFERENCE)
|
||||||
|
.long(options::REFERENCE)
|
||||||
|
.value_name("RFILE")
|
||||||
|
.help("use RFILE's group rather than specifying GROUP values")
|
||||||
|
.takes_value(true)
|
||||||
|
.multiple(false),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(options::RECURSIVE)
|
||||||
|
.short("R")
|
||||||
|
.long(options::RECURSIVE)
|
||||||
|
.help("operate on files and directories recursively"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(options::traverse::TRAVERSE)
|
||||||
|
.short(options::traverse::TRAVERSE)
|
||||||
|
.help("if a command line argument is a symbolic link to a directory, traverse it"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(options::traverse::NO_TRAVERSE)
|
||||||
|
.short(options::traverse::NO_TRAVERSE)
|
||||||
|
.help("do not traverse any symbolic links (default)")
|
||||||
|
.overrides_with_all(&[options::traverse::TRAVERSE, options::traverse::EVERY]),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(options::traverse::EVERY)
|
||||||
|
.short(options::traverse::EVERY)
|
||||||
|
.help("traverse every symbolic link to a directory encountered"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let matches = opts.parse(args);
|
if help || !reference {
|
||||||
let recursive = matches.opt_present("recursive");
|
// 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 recursive {
|
||||||
if bit_flag == FTS_PHYSICAL {
|
if bit_flag == FTS_PHYSICAL {
|
||||||
if derefer == 1 {
|
if derefer == 1 {
|
||||||
|
@ -106,27 +226,20 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
bit_flag = FTS_PHYSICAL;
|
bit_flag = FTS_PHYSICAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
let verbosity = if matches.opt_present("changes") {
|
let verbosity = if matches.is_present(options::verbosity::CHANGES) {
|
||||||
Verbosity::Changes
|
Verbosity::Changes
|
||||||
} else if matches.opt_present("silent") || matches.opt_present("quiet") {
|
} else if matches.is_present(options::verbosity::SILENT)
|
||||||
|
|| matches.is_present(options::verbosity::QUIET)
|
||||||
|
{
|
||||||
Verbosity::Silent
|
Verbosity::Silent
|
||||||
} else if matches.opt_present("verbose") {
|
} else if matches.is_present(options::verbosity::VERBOSE) {
|
||||||
Verbosity::Verbose
|
Verbosity::Verbose
|
||||||
} else {
|
} else {
|
||||||
Verbosity::Normal
|
Verbosity::Normal
|
||||||
};
|
};
|
||||||
|
|
||||||
if matches.free.is_empty() {
|
let dest_gid: u32;
|
||||||
show_usage_error!("missing operand");
|
if let Some(file) = matches.value_of(options::REFERENCE) {
|
||||||
return 1;
|
|
||||||
} else if matches.free.len() < 2 && !matches.opt_present("reference") {
|
|
||||||
show_usage_error!("missing operand after ‘{}’", matches.free[0]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dest_gid: gid_t;
|
|
||||||
let mut files;
|
|
||||||
if let Some(file) = matches.opt_str("reference") {
|
|
||||||
match fs::metadata(&file) {
|
match fs::metadata(&file) {
|
||||||
Ok(meta) => {
|
Ok(meta) => {
|
||||||
dest_gid = meta.gid();
|
dest_gid = meta.gid();
|
||||||
|
@ -136,19 +249,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
files = matches.free;
|
|
||||||
} else {
|
} else {
|
||||||
match entries::grp2gid(&matches.free[0]) {
|
let group = matches.value_of(options::ARG_GROUP).unwrap_or_default();
|
||||||
|
match entries::grp2gid(&group) {
|
||||||
Ok(g) => {
|
Ok(g) => {
|
||||||
dest_gid = g;
|
dest_gid = g;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
show_error!("invalid group: {}", matches.free[0].as_str());
|
show_error!("invalid group: {}", group);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
files = matches.free;
|
|
||||||
files.remove(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let executor = Chgrper {
|
let executor = Chgrper {
|
||||||
|
|
|
@ -92,7 +92,7 @@ pub fn wrap_chgrp<P: AsRef<Path>>(
|
||||||
out = format!(
|
out = format!(
|
||||||
"group of '{}' retained as {}",
|
"group of '{}' retained as {}",
|
||||||
path.display(),
|
path.display(),
|
||||||
entries::gid2grp(dest_gid).unwrap()
|
entries::gid2grp(dest_gid).unwrap_or_default()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,33 @@ fn test_invalid_option() {
|
||||||
|
|
||||||
static DIR: &str = "/tmp";
|
static DIR: &str = "/tmp";
|
||||||
|
|
||||||
|
// we should always get both arguments, regardless of whether --refernce was used
|
||||||
|
#[test]
|
||||||
|
fn test_help() {
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("--help")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_contains("ARGS:\n <GROUP> \n <FILE>... ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_help_ref() {
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("--help")
|
||||||
|
.arg("--reference=ref_file")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_contains("ARGS:\n <GROUP> \n <FILE>... ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ref_help() {
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("--reference=ref_file")
|
||||||
|
.arg("--help")
|
||||||
|
.succeeds()
|
||||||
|
.stdout_contains("ARGS:\n <GROUP> \n <FILE>... ");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_group() {
|
fn test_invalid_group() {
|
||||||
new_ucmd!()
|
new_ucmd!()
|
||||||
|
@ -121,9 +148,52 @@ fn test_reference() {
|
||||||
fn test_reference() {
|
fn test_reference() {
|
||||||
new_ucmd!()
|
new_ucmd!()
|
||||||
.arg("-v")
|
.arg("-v")
|
||||||
.arg("--reference=/etc/passwd")
|
.arg("--reference=ref_file")
|
||||||
.arg("/etc")
|
.arg("/etc")
|
||||||
.succeeds();
|
.fails()
|
||||||
|
// group name can differ, so just check the first part of the message
|
||||||
|
.stderr_contains("chgrp: changing group of '/etc': Operation not permitted (os error 1)\nfailed to change group of '/etc' from ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
|
||||||
|
fn test_reference_multi_no_equal() {
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("-v")
|
||||||
|
.arg("--reference")
|
||||||
|
.arg("ref_file")
|
||||||
|
.arg("file1")
|
||||||
|
.arg("file2")
|
||||||
|
.succeeds()
|
||||||
|
.stderr_contains("chgrp: group of 'file1' retained as ")
|
||||||
|
.stderr_contains("\nchgrp: group of 'file2' retained as ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
|
||||||
|
fn test_reference_last() {
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("-v")
|
||||||
|
.arg("file1")
|
||||||
|
.arg("file2")
|
||||||
|
.arg("file3")
|
||||||
|
.arg("--reference")
|
||||||
|
.arg("ref_file")
|
||||||
|
.succeeds()
|
||||||
|
.stderr_contains("chgrp: group of 'file1' retained as ")
|
||||||
|
.stderr_contains("\nchgrp: group of 'file2' retained as ")
|
||||||
|
.stderr_contains("\nchgrp: group of 'file3' retained as ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_missing_files() {
|
||||||
|
new_ucmd!()
|
||||||
|
.arg("-v")
|
||||||
|
.arg("groupname")
|
||||||
|
.fails()
|
||||||
|
.stderr_contains(
|
||||||
|
"error: The following required arguments were not provided:\n <FILE>...\n",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -135,7 +205,7 @@ fn test_big_p() {
|
||||||
.arg("bin")
|
.arg("bin")
|
||||||
.arg("/proc/self/cwd")
|
.arg("/proc/self/cwd")
|
||||||
.fails()
|
.fails()
|
||||||
.stderr_is(
|
.stderr_contains(
|
||||||
"chgrp: changing group of '/proc/self/cwd': Operation not permitted (os error 1)\n",
|
"chgrp: changing group of '/proc/self/cwd': Operation not permitted (os error 1)\n",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
1
tests/fixtures/chgrp/file1
vendored
Normal file
1
tests/fixtures/chgrp/file1
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
target file 1
|
1
tests/fixtures/chgrp/file2
vendored
Normal file
1
tests/fixtures/chgrp/file2
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
target file 2
|
1
tests/fixtures/chgrp/file3
vendored
Normal file
1
tests/fixtures/chgrp/file3
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
target file 3
|
1
tests/fixtures/chgrp/ref_file
vendored
Normal file
1
tests/fixtures/chgrp/ref_file
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Reference file
|
Loading…
Add table
Add a link
Reference in a new issue