1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

Merge pull request #1955 from jhscheer/chmod2clap

chmod: move from getopts to clap
This commit is contained in:
Sylvestre Ledru 2021-03-29 13:02:13 +02:00 committed by GitHub
commit acb57ecbd0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 179 additions and 88 deletions

View file

@ -15,6 +15,7 @@ edition = "2018"
path = "src/chmod.rs" path = "src/chmod.rs"
[dependencies] [dependencies]
clap = "2.33.3"
libc = "0.2.42" libc = "0.2.42"
uucore = { version=">=0.0.7", package="uucore", path="../../uucore", features=["fs", "mode"] } uucore = { version=">=0.0.7", package="uucore", path="../../uucore", features=["fs", "mode"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }

View file

@ -10,6 +10,7 @@
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use clap::{App, Arg};
use std::fs; use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::Path; use std::path::Path;
@ -18,115 +19,173 @@ use uucore::fs::display_permissions_unix;
use uucore::mode; use uucore::mode;
use walkdir::WalkDir; use walkdir::WalkDir;
const NAME: &str = "chmod"; static VERSION: &str = env!("CARGO_PKG_VERSION");
static SUMMARY: &str = "Change the mode of each FILE to MODE. static ABOUT: &str = "Change the mode of each FILE to MODE.
With --reference, change the mode of each FILE to that of RFILE."; With --reference, change the mode of each FILE to that of RFILE.";
static LONG_HELP: &str = "
Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'. mod options {
"; pub const CHANGES: &str = "changes";
pub const QUIET: &str = "quiet"; // visible_alias("silent")
pub const VERBOSE: &str = "verbose";
pub const NO_PRESERVE_ROOT: &str = "no-preserve-root";
pub const PRESERVE_ROOT: &str = "preserve-root";
pub const REFERENCE: &str = "RFILE";
pub const RECURSIVE: &str = "recursive";
pub const MODE: &str = "MODE";
pub const FILE: &str = "FILE";
}
fn get_usage() -> String {
format!(
"{0} [OPTION]... MODE[,MODE]... FILE...
or: {0} [OPTION]... OCTAL-MODE FILE...
or: {0} [OPTION]... --reference=RFILE FILE...",
executable!()
)
}
fn get_long_usage() -> String {
String::from("Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'.")
}
pub fn uumain(args: impl uucore::Args) -> i32 { pub fn uumain(args: impl uucore::Args) -> i32 {
let mut args = args.collect_str(); let mut args = args.collect_str();
let syntax = format!( // Before we can parse 'args' with clap (and previously getopts),
"[OPTION]... MODE[,MODE]... FILE... // a possible MODE prefix '-' needs to be removed (e.g. "chmod -x FILE").
{0} [OPTION]... OCTAL-MODE FILE... let mode_had_minus_prefix = strip_minus_from_mode(&mut args);
{0} [OPTION]... --reference=RFILE FILE...",
NAME
);
let mut opts = app!(&syntax, SUMMARY, LONG_HELP);
opts.optflag(
"c",
"changes",
"like verbose but report only when a change is made",
)
// TODO: support --silent (can be done using clap)
.optflag("f", "quiet", "suppress most error messages")
.optflag(
"v",
"verbose",
"output a diagnostic for every file processed",
)
.optflag(
"",
"no-preserve-root",
"do not treat '/' specially (the default)",
)
.optflag("", "preserve-root", "fail to operate recursively on '/'")
.optopt(
"",
"reference",
"use RFILE's mode instead of MODE values",
"RFILE",
)
.optflag("R", "recursive", "change files and directories recursively");
// sanitize input for - at beginning (e.g. chmod -x test_file). Remove let usage = get_usage();
// the option and save it for later, after parsing is finished. let after_help = get_long_usage();
let negative_option = sanitize_input(&mut args);
let mut matches = opts.parse(args); let matches = App::new(executable!())
if matches.free.is_empty() { .version(VERSION)
show_error!("missing an argument"); .about(ABOUT)
show_error!("for help, try '{} --help'", NAME); .usage(&usage[..])
return 1; .after_help(&after_help[..])
} else { .arg(
let changes = matches.opt_present("changes"); Arg::with_name(options::CHANGES)
let quiet = matches.opt_present("quiet"); .long(options::CHANGES)
let verbose = matches.opt_present("verbose"); .short("c")
let preserve_root = matches.opt_present("preserve-root"); .help("like verbose but report only when a change is made"),
let recursive = matches.opt_present("recursive"); )
let fmode = matches .arg(
.opt_str("reference") Arg::with_name(options::QUIET)
.long(options::QUIET)
.visible_alias("silent")
.short("f")
.help("suppress most error messages"),
)
.arg(
Arg::with_name(options::VERBOSE)
.long(options::VERBOSE)
.short("v")
.help("output a diagnostic for every file processed"),
)
.arg(
Arg::with_name(options::NO_PRESERVE_ROOT)
.long(options::NO_PRESERVE_ROOT)
.help("do not treat '/' specially (the default)"),
)
.arg(
Arg::with_name(options::PRESERVE_ROOT)
.long(options::PRESERVE_ROOT)
.help("fail to operate recursively on '/'"),
)
.arg(
Arg::with_name(options::RECURSIVE)
.long(options::RECURSIVE)
.short("R")
.help("change files and directories recursively"),
)
.arg(
Arg::with_name(options::REFERENCE)
.long("reference")
.takes_value(true)
.help("use RFILE's mode instead of MODE values"),
)
.arg(
Arg::with_name(options::MODE)
.required_unless(options::REFERENCE)
.takes_value(true),
// It would be nice if clap could parse with delimeter, e.g. "g-x,u+x",
// however .multiple(true) cannot be used here because FILE already needs that.
// Only one positional argument with .multiple(true) set is allowed per command
)
.arg(
Arg::with_name(options::FILE)
.required_unless(options::MODE)
.multiple(true),
)
.get_matches_from(args);
let changes = matches.is_present(options::CHANGES);
let quiet = matches.is_present(options::QUIET);
let verbose = matches.is_present(options::VERBOSE);
let preserve_root = matches.is_present(options::PRESERVE_ROOT);
let recursive = matches.is_present(options::RECURSIVE);
let fmode =
matches
.value_of(options::REFERENCE)
.and_then(|ref fref| match fs::metadata(fref) { .and_then(|ref fref| match fs::metadata(fref) {
Ok(meta) => Some(meta.mode()), Ok(meta) => Some(meta.mode()),
Err(err) => crash!(1, "cannot stat attributes of '{}': {}", fref, err), Err(err) => crash!(1, "cannot stat attributes of '{}': {}", fref, err),
}); });
let cmode = if fmode.is_none() { let modes = matches.value_of(options::MODE).unwrap(); // should always be Some because required
// If there was a negative option, now it's a good time to let mut cmode = if mode_had_minus_prefix {
// use it. // clap parsing is finished, now put prefix back
if negative_option.is_some() { Some(format!("-{}", modes))
negative_option } else {
} else { Some(modes.to_string())
Some(matches.free.remove(0)) };
} let mut files: Vec<String> = matches
} else { .values_of(options::FILE)
None .map(|v| v.map(ToString::to_string).collect())
}; .unwrap_or_default();
let chmoder = Chmoder { if fmode.is_some() {
changes, // "--reference" and MODE are mutually exclusive
quiet, // if "--reference" was used MODE needs to be interpreted as another FILE
verbose, // it wasn't possible to implement this behavior directly with clap
preserve_root, files.push(cmode.unwrap());
recursive, cmode = None;
fmode, }
cmode,
}; let chmoder = Chmoder {
match chmoder.chmod(matches.free) { changes,
Ok(()) => {} quiet,
Err(e) => return e, verbose,
} preserve_root,
recursive,
fmode,
cmode,
};
match chmoder.chmod(files) {
Ok(()) => {}
Err(e) => return e,
} }
0 0
} }
fn sanitize_input(args: &mut Vec<String>) -> Option<String> { // Iterate 'args' and delete the first occurrence
// of a prefix '-' if it's associated with MODE
// e.g. "chmod -v -xw -R FILE" -> "chmod -v xw -R FILE"
pub fn strip_minus_from_mode(args: &mut Vec<String>) -> bool {
for i in 0..args.len() { for i in 0..args.len() {
let first = args[i].chars().next().unwrap(); if args[i].starts_with("-") {
if first != '-' { if let Some(second) = args[i].chars().nth(1) {
continue; match second {
} 'r' | 'w' | 'x' | 'X' | 's' | 't' | 'u' | 'g' | 'o' | '0'..='7' => {
if let Some(second) = args[i].chars().nth(1) { // TODO: use strip_prefix() once minimum rust version reaches 1.45.0
match second { args[i] = args[i][1..args[i].len()].to_string();
'r' | 'w' | 'x' | 'X' | 's' | 't' | 'u' | 'g' | 'o' | '0'..='7' => { return true;
return Some(args.remove(i)); }
_ => {}
} }
_ => {}
} }
} }
} }
None false
} }
struct Chmoder { struct Chmoder {

View file

@ -4,6 +4,8 @@ use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::sync::Mutex; use std::sync::Mutex;
extern crate libc; extern crate libc;
use self::chmod::strip_minus_from_mode;
extern crate chmod;
use self::libc::umask; use self::libc::umask;
static TEST_FILE: &'static str = "file"; static TEST_FILE: &'static str = "file";
@ -374,3 +376,32 @@ fn test_chmod_symlink_non_existing_recursive() {
.stderr .stderr
.contains("neither symbolic link 'tmp/test-long.link' nor referent has been changed")); .contains("neither symbolic link 'tmp/test-long.link' nor referent has been changed"));
} }
#[test]
fn test_chmod_strip_minus_from_mode() {
let tests = vec![
// ( before, after )
("chmod -v -xw -R FILE", "chmod -v xw -R FILE"),
("chmod g=rwx FILE -c", "chmod g=rwx FILE -c"),
(
"chmod -c -R -w,o+w FILE --preserve-root",
"chmod -c -R w,o+w FILE --preserve-root",
),
("chmod -c -R +w FILE ", "chmod -c -R +w FILE "),
("chmod a=r,=xX FILE", "chmod a=r,=xX FILE"),
(
"chmod -v --reference RFILE -R FILE",
"chmod -v --reference RFILE -R FILE",
),
("chmod -Rvc -w-x FILE", "chmod -Rvc w-x FILE"),
("chmod 755 -v FILE", "chmod 755 -v FILE"),
("chmod -v +0004 FILE -R", "chmod -v +0004 FILE -R"),
("chmod -v -0007 FILE -R", "chmod -v 0007 FILE -R"),
];
for test in tests {
let mut args: Vec<String> = test.0.split(" ").map(|v| v.to_string()).collect();
let _mode_had_minus_prefix = strip_minus_from_mode(&mut args);
assert_eq!(test.1, args.join(" "));
}
}