Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions src/uu/install/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub struct Behavior {
strip_program: String,
create_leading: bool,
target_dir: Option<String>,
no_target_dir: bool,
}

#[derive(Error, Debug)]
Expand Down Expand Up @@ -104,6 +105,9 @@ enum InstallError {

#[error("'{0}' and '{1}' are the same file")]
SameFile(PathBuf, PathBuf),

#[error("extra operand {}\n{}", .0.quote(), .1.quote())]
ExtraOperand(String, String),
}

impl UError for InstallError {
Expand Down Expand Up @@ -279,11 +283,10 @@ pub fn uu_app() -> Command {
.value_hint(clap::ValueHint::DirPath),
)
.arg(
// TODO implement flag
Arg::new(OPT_NO_TARGET_DIRECTORY)
.short('T')
.long(OPT_NO_TARGET_DIRECTORY)
.help("(unimplemented) treat DEST as a normal file")
.help("treat DEST as a normal file")
.action(ArgAction::SetTrue),
)
.arg(
Expand Down Expand Up @@ -328,9 +331,7 @@ pub fn uu_app() -> Command {
///
///
fn check_unimplemented(matches: &ArgMatches) -> UResult<()> {
if matches.get_flag(OPT_NO_TARGET_DIRECTORY) {
Err(InstallError::Unimplemented(String::from("--no-target-directory, -T")).into())
} else if matches.get_flag(OPT_PRESERVE_CONTEXT) {
if matches.get_flag(OPT_PRESERVE_CONTEXT) {
Err(InstallError::Unimplemented(String::from("--preserve-context, -P")).into())
} else if matches.get_flag(OPT_CONTEXT) {
Err(InstallError::Unimplemented(String::from("--context, -Z")).into())
Expand Down Expand Up @@ -368,6 +369,11 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {

let backup_mode = backup_control::determine_backup_mode(matches)?;
let target_dir = matches.get_one::<String>(OPT_TARGET_DIRECTORY).cloned();
let no_target_dir = matches.get_flag(OPT_NO_TARGET_DIRECTORY);
if target_dir.is_some() && no_target_dir {
show_error!("Options --target-directory and --no-target-directory are mutually exclusive");
return Err(1.into());
}

let preserve_timestamps = matches.get_flag(OPT_PRESERVE_TIMESTAMPS);
let compare = matches.get_flag(OPT_COMPARE);
Expand Down Expand Up @@ -430,6 +436,7 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
),
create_leading: matches.get_flag(OPT_CREATE_LEADING),
target_dir,
no_target_dir,
})
}

Expand Down Expand Up @@ -522,6 +529,9 @@ fn standard(mut paths: Vec<String>, b: &Behavior) -> UResult<()> {
if paths.is_empty() {
return Err(UUsageError::new(1, "missing file operand"));
}
if b.no_target_dir && paths.len() > 2 {
return Err(InstallError::ExtraOperand(paths[2].clone(), format_usage(USAGE)).into());
}

// get the target from either "-t foo" param or from the last given paths argument
let target: PathBuf = if let Some(path) = &b.target_dir {
Expand Down Expand Up @@ -591,7 +601,7 @@ fn standard(mut paths: Vec<String>, b: &Behavior) -> UResult<()> {
}
}

if sources.len() > 1 || is_potential_directory_path(&target) {
if sources.len() > 1 {
copy_files_into_dir(sources, &target, b)
} else {
let source = sources.first().unwrap();
Expand All @@ -600,6 +610,16 @@ fn standard(mut paths: Vec<String>, b: &Behavior) -> UResult<()> {
return Err(InstallError::OmittingDirectory(source.clone()).into());
}

if b.no_target_dir && target.exists() {
return Err(
InstallError::OverrideDirectoryFailed(target.clone(), source.clone()).into(),
);
}

if is_potential_directory_path(&target) {
return copy_files_into_dir(sources, &target, b);
}

if target.is_file() || is_new_file_path(&target) {
copy(source, &target, b)
} else {
Expand Down
156 changes: 156 additions & 0 deletions tests/by-util/test_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1808,3 +1808,159 @@ fn test_install_symlink_same_file() {
"'{target_dir}/{file}' and '{target_link}/{file}' are the same file"
));
}

#[test]
fn test_install_no_target_directory_failing_cannot_overwrite() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let file = "file";
let dir = "dir";

at.touch(file);
at.mkdir(dir);
scene
.ucmd()
.arg("-T")
.arg(file)
.arg(dir)
.fails()
.stderr_contains("cannot overwrite directory 'dir' with non-directory");

assert!(!at.dir_exists("dir/file"));
}

#[test]
fn test_install_no_target_directory_failing_omitting_directory() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let dir1 = "dir1";
let dir2 = "dir2";

at.mkdir(dir1);
at.mkdir(dir2);
scene
.ucmd()
.arg("-T")
.arg(dir1)
.arg(dir2)
.fails()
.stderr_contains("omitting directory 'dir1'");
}

#[test]
fn test_install_no_target_directory_creating_leading_dirs_with_single_source_and_target_dir() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

let source1 = "file";
let target_dir = "missing_target_dir/";

at.touch(source1);

// installing a single file into a missing directory will fail, when -D is used w/o -t parameter
scene
.ucmd()
.arg("-TD")
.arg(source1)
.arg(at.plus(target_dir))
.fails()
.stderr_contains("missing_target_dir/' is not a directory");

assert!(!at.dir_exists(target_dir));
}

#[test]
fn test_install_no_target_directory_failing_combine_with_target_directory() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let file = "file";
let dir1 = "dir1";

at.touch(file);
at.mkdir(dir1);
scene
.ucmd()
.arg("-T")
.arg(file)
.arg("-t")
.arg(dir1)
.fails()
.stderr_contains(
"Options --target-directory and --no-target-directory are mutually exclusive",
);
}

#[test]
fn test_install_no_target_directory_failing_usage_with_target_directory() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let file = "file";

at.touch(file);
scene
.ucmd()
.arg("-T")
.arg(file)
.arg("-t")
.fails()
.stderr_contains(
"a value is required for '--target-directory <DIRECTORY>' but none was supplied",
)
.stderr_contains("For more information, try '--help'");
}

#[test]
fn test_install_no_target_multiple_sources_and_target_dir() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

let file1 = "file1";
let file2 = "file2";
let dir1 = "dir1";
let dir2 = "dir2";

at.touch(file1);
at.touch(file2);
at.mkdir(dir1);
at.mkdir(dir2);

// installing multiple files into a missing directory will fail, when -D is used w/o -t parameter
scene
.ucmd()
.arg("-T")
.arg(file1)
.arg(file2)
.arg(dir1)
.fails()
.stderr_contains("extra operand 'dir1'")
.stderr_contains("[OPTION]... [FILE]...");

scene
.ucmd()
.arg("-T")
.arg(file1)
.arg(file2)
.arg(dir1)
.arg(dir2)
.fails()
.stderr_contains("extra operand 'dir1'")
.stderr_contains("[OPTION]... [FILE]...");
}

#[test]
fn test_install_no_target_basic() {
let (at, mut ucmd) = at_and_ucmd!();
let file = "file";
let dir = "dir";

at.touch(file);
at.mkdir(dir);
ucmd.arg("-T")
.arg(file)
.arg(format!("{dir}/{file}"))
.succeeds()
.no_stderr();

assert!(at.file_exists(file));
assert!(at.file_exists(format!("{dir}/{file}")));
}
Loading