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

refactor(ln): move to clap

This commit is contained in:
Sylvestre Ledru 2021-01-02 22:35:03 +01:00
parent 90722c1f3c
commit 7f1d47b77a
4 changed files with 160 additions and 114 deletions

1
Cargo.lock generated
View file

@ -1756,6 +1756,7 @@ dependencies = [
name = "uu_ln" name = "uu_ln"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"uucore 0.0.4", "uucore 0.0.4",
"uucore_procs 0.0.4", "uucore_procs 0.0.4",

View file

@ -15,6 +15,7 @@ edition = "2018"
path = "src/ln.rs" path = "src/ln.rs"
[dependencies] [dependencies]
clap = "2.33"
libc = "0.2.42" libc = "0.2.42"
uucore = { version=">=0.0.4", package="uucore", path="../../uucore" } uucore = { version=">=0.0.4", package="uucore", path="../../uucore" }
uucore_procs = { version=">=0.0.4", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.4", package="uucore_procs", path="../../uucore_procs" }

View file

@ -10,6 +10,8 @@
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use clap::{App, Arg};
use std::borrow::Cow; use std::borrow::Cow;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fs; use std::fs;
@ -22,19 +24,6 @@ use std::os::windows::fs::{symlink_dir, symlink_file};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use uucore::fs::{canonicalize, CanonicalizeMode}; use uucore::fs::{canonicalize, CanonicalizeMode};
static NAME: &str = "ln";
static SUMMARY: &str = "";
static LONG_HELP: &str = "
In the 1st form, create a link to TARGET with the name LINK_NAME.
In the 2nd form, create a link to TARGET in the current directory.
In the 3rd and 4th forms, create links to each TARGET in DIRECTORY.
Create hard links by default, symbolic links with --symbolic.
By default, each destination (name of new link) should not already exist.
When creating hard links, each TARGET must exist. Symbolic links
can hold arbitrary text; if later resolved, a relative link is
interpreted in relation to its parent directory.
";
pub struct Settings { pub struct Settings {
overwrite: OverwriteMode, overwrite: OverwriteMode,
backup: BackupMode, backup: BackupMode,
@ -61,143 +50,202 @@ pub enum BackupMode {
ExistingBackup, ExistingBackup,
} }
pub fn uumain(args: impl uucore::Args) -> i32 { fn get_usage() -> String {
let args = args.collect_str(); format!(
"{0} [OPTION]... [-T] TARGET LINK_executable!() (1st form)
{0} [OPTION]... TARGET (2nd form)
{0} [OPTION]... TARGET... DIRECTORY (3rd form)
{0} [OPTION]... -t DIRECTORY TARGET... (4th form)",
executable!()
)
}
let syntax = format!( fn get_long_usage() -> String {
"[OPTION]... [-T] TARGET LINK_NAME (1st form) String::from(
{0} [OPTION]... TARGET (2nd form) " In the 1st form, create a link to TARGET with the name LINK_executable!().
{0} [OPTION]... TARGET... DIRECTORY (3rd form) In the 2nd form, create a link to TARGET in the current directory.
{0} [OPTION]... -t DIRECTORY TARGET... (4th form)", In the 3rd and 4th forms, create links to each TARGET in DIRECTORY.
NAME Create hard links by default, symbolic links with --symbolic.
); By default, each destination (name of new link) should not already exist.
let matches = app!(&syntax, SUMMARY, LONG_HELP) When creating hard links, each TARGET must exist. Symbolic links
.optflag( can hold arbitrary text; if later resolved, a relative link is
"b", interpreted in relation to its parent directory.
"", ",
)
}
static ABOUT: &str = "change file owner and group";
static VERSION: &str = env!("CARGO_PKG_VERSION");
static OPT_B: &str = "b";
static OPT_BACKUP: &str = "backup";
static OPT_FORCE: &str = "force";
static OPT_INTERACTIVE: &str = "interactive";
static OPT_SYMBOLIC: &str = "symbolic";
static OPT_SUFFIX: &str = "suffix";
static OPT_TARGET_DIRECTORY: &str = "target-directory";
static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory";
static OPT_RELATIVE: &str = "relative";
static OPT_VERBOSE: &str = "verbose";
static ARG_FILES: &str = "files";
pub fn uumain(args: impl uucore::Args) -> i32 {
let usage = get_usage();
let long_usage = get_long_usage();
let matches = App::new(executable!())
.version(VERSION)
.about(ABOUT)
.usage(&usage[..])
.after_help(&long_usage[..])
.arg(Arg::with_name(OPT_B).short(OPT_B).help(
"make a backup of each file that would otherwise be overwritten or \ "make a backup of each file that would otherwise be overwritten or \
removed", removed",
) ))
.optflagopt( .arg(
"", Arg::with_name(OPT_BACKUP)
"backup", .long(OPT_BACKUP)
"make a backup of each file that would otherwise be overwritten \ .help(
"make a backup of each file that would otherwise be overwritten \
or removed", or removed",
"METHOD", )
.takes_value(true)
.possible_value("simple")
.possible_value("never")
.possible_value("numbered")
.possible_value("t")
.possible_value("existing")
.possible_value("nil")
.possible_value("none")
.possible_value("off")
.value_name("METHOD"),
) )
// TODO: opts.optflag("d", "directory", "allow users with appropriate privileges to attempt \ // TODO: opts.arg(
// Arg::with_name(("d", "directory", "allow users with appropriate privileges to attempt \
// to make hard links to directories"); // to make hard links to directories");
.optflag("f", "force", "remove existing destination files") .arg(
.optflag( Arg::with_name(OPT_FORCE)
"i", .short("f")
"interactive", .long(OPT_FORCE)
"prompt whether to remove existing destination files", .help("remove existing destination files"),
) )
// TODO: opts.optflag("L", "logical", "dereference TARGETs that are symbolic links"); .arg(
// TODO: opts.optflag("n", "no-dereference", "treat LINK_NAME as a normal file if it is a \ Arg::with_name(OPT_INTERACTIVE)
.short("i")
.long(OPT_INTERACTIVE)
.help("prompt whether to remove existing destination files"),
)
// TODO: opts.arg(
// Arg::with_name(("L", "logical", "dereference TARGETs that are symbolic links");
// TODO: opts.arg(
// Arg::with_name(("n", "no-dereference", "treat LINK_executable!() as a normal file if it is a \
// symbolic link to a directory"); // symbolic link to a directory");
// TODO: opts.optflag("P", "physical", "make hard links directly to symbolic links"); // TODO: opts.arg(
.optflag("s", "symbolic", "make symbolic links instead of hard links") // Arg::with_name(("P", "physical", "make hard links directly to symbolic links");
.optopt("S", "suffix", "override the usual backup suffix", "SUFFIX") .arg(
.optopt( Arg::with_name(OPT_SYMBOLIC)
"t", .short("s")
"target-directory", .long("symbolic")
"specify the DIRECTORY in which to create the links", .help("make symbolic links instead of hard links"),
"DIRECTORY",
) )
.optflag( .arg(
"T", Arg::with_name(OPT_SUFFIX)
"no-target-directory", .short("S")
"treat LINK_NAME as a normal file always", .long(OPT_SUFFIX)
.help("override the usual backup suffix")
.value_name("SUFFIX")
.takes_value(true),
) )
.optflag( .arg(
"r", Arg::with_name(OPT_TARGET_DIRECTORY)
"relative", .short("t")
"create symbolic links relative to link location", .long(OPT_TARGET_DIRECTORY)
.help("specify the DIRECTORY in which to create the links")
.value_name("DIRECTORY")
.conflicts_with(OPT_NO_TARGET_DIRECTORY),
) )
.optflag("v", "verbose", "print name of each linked file") .arg(
.parse(args); Arg::with_name(OPT_NO_TARGET_DIRECTORY)
.short("T")
.long(OPT_NO_TARGET_DIRECTORY)
.help("treat LINK_executable!() as a normal file always"),
)
.arg(
Arg::with_name(OPT_RELATIVE)
.short("r")
.long(OPT_RELATIVE)
.help("create symbolic links relative to link location"),
)
.arg(
Arg::with_name(OPT_VERBOSE)
.short("v")
.long(OPT_VERBOSE)
.help("print name of each linked file"),
)
.arg(
Arg::with_name(ARG_FILES)
.multiple(true)
.takes_value(true)
.required(true)
.min_values(1),
)
.get_matches_from(args);
let overwrite_mode = if matches.opt_present("force") { /* the list of files */
let paths: Vec<PathBuf> = matches
.values_of(ARG_FILES)
.unwrap()
.map(|path| PathBuf::from(path))
.collect();
let overwrite_mode = if matches.is_present(OPT_FORCE) {
OverwriteMode::Force OverwriteMode::Force
} else if matches.opt_present("interactive") { } else if matches.is_present(OPT_INTERACTIVE) {
OverwriteMode::Interactive OverwriteMode::Interactive
} else { } else {
OverwriteMode::NoClobber OverwriteMode::NoClobber
}; };
let backup_mode = if matches.opt_present("b") { let backup_mode = if matches.is_present(OPT_B) {
BackupMode::ExistingBackup BackupMode::ExistingBackup
} else if matches.opt_present("backup") { } else if matches.is_present(OPT_BACKUP) {
match matches.opt_str("backup") { match matches.value_of(OPT_BACKUP) {
None => BackupMode::ExistingBackup, None => BackupMode::ExistingBackup,
Some(mode) => match &mode[..] { Some(mode) => match &mode[..] {
"simple" | "never" => BackupMode::SimpleBackup, "simple" | "never" => BackupMode::SimpleBackup,
"numbered" | "t" => BackupMode::NumberedBackup, "numbered" | "t" => BackupMode::NumberedBackup,
"existing" | "nil" => BackupMode::ExistingBackup, "existing" | "nil" => BackupMode::ExistingBackup,
"none" | "off" => BackupMode::NoBackup, "none" | "off" => BackupMode::NoBackup,
x => { _ => panic!(), // cannot happen as it is managed by clap
show_error!(
"invalid argument '{}' for 'backup method'\n\
Try '{} --help' for more information.",
x,
NAME
);
return 1;
}
}, },
} }
} else { } else {
BackupMode::NoBackup BackupMode::NoBackup
}; };
let backup_suffix = if matches.opt_present("suffix") { let backup_suffix = if matches.is_present(OPT_SUFFIX) {
match matches.opt_str("suffix") { matches.value_of(OPT_SUFFIX).unwrap()
Some(x) => x,
None => {
show_error!(
"option '--suffix' requires an argument\n\
Try '{} --help' for more information.",
NAME
);
return 1;
}
}
} else { } else {
"~".to_owned() "~"
}; };
if matches.opt_present("T") && matches.opt_present("t") {
show_error!("cannot combine --target-directory (-t) and --no-target-directory (-T)");
return 1;
}
let settings = Settings { let settings = Settings {
overwrite: overwrite_mode, overwrite: overwrite_mode,
backup: backup_mode, backup: backup_mode,
suffix: backup_suffix, suffix: backup_suffix.to_string(),
symbolic: matches.opt_present("s"), symbolic: matches.is_present(OPT_SYMBOLIC),
relative: matches.opt_present("r"), relative: matches.is_present(OPT_RELATIVE),
target_dir: matches.opt_str("t"), target_dir: matches.value_of(OPT_TARGET_DIRECTORY).map(String::from),
no_target_dir: matches.opt_present("T"), no_target_dir: matches.is_present(OPT_NO_TARGET_DIRECTORY),
verbose: matches.opt_present("v"), verbose: matches.is_present(OPT_VERBOSE),
}; };
let string_to_path = |s: &String| PathBuf::from(s);
let paths: Vec<PathBuf> = matches.free.iter().map(string_to_path).collect();
exec(&paths[..], &settings) exec(&paths[..], &settings)
} }
fn exec(files: &[PathBuf], settings: &Settings) -> i32 { fn exec(files: &[PathBuf], settings: &Settings) -> i32 {
if files.is_empty() {
show_error!(
"missing file operand\nTry '{} --help' for more information.",
NAME
);
return 1;
}
// Handle cases where we create links in a directory first. // Handle cases where we create links in a directory first.
if let Some(ref name) = settings.target_dir { if let Some(ref name) = settings.target_dir {
// 4th form: a directory is specified by -t. // 4th form: a directory is specified by -t.
@ -228,7 +276,7 @@ fn exec(files: &[PathBuf], settings: &Settings) -> i32 {
show_error!( show_error!(
"extra operand '{}'\nTry '{} --help' for more information.", "extra operand '{}'\nTry '{} --help' for more information.",
files[2].display(), files[2].display(),
NAME executable!()
); );
return 1; return 1;
} }
@ -321,7 +369,7 @@ fn link(src: &PathBuf, dst: &PathBuf, settings: &Settings) -> Result<()> {
match settings.overwrite { match settings.overwrite {
OverwriteMode::NoClobber => {} OverwriteMode::NoClobber => {}
OverwriteMode::Interactive => { OverwriteMode::Interactive => {
print!("{}: overwrite '{}'? ", NAME, dst.display()); print!("{}: overwrite '{}'? ", executable!(), dst.display());
if !read_yes() { if !read_yes() {
return Ok(()); return Ok(());
} }

View file

@ -322,11 +322,7 @@ fn test_symlink_errors() {
// $ ln -T -t a b // $ ln -T -t a b
// ln: cannot combine --target-directory (-t) and --no-target-directory (-T) // ln: cannot combine --target-directory (-t) and --no-target-directory (-T)
ucmd.args(&["-T", "-t", dir, file_a, file_b]) ucmd.args(&["-T", "-t", dir, file_a, file_b])
.fails() .fails();
.stderr_is(
"ln: error: cannot combine --target-directory (-t) and --no-target-directory \
(-T)\n",
);
} }
#[test] #[test]