diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 11220cf85..be0095c9a 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -50,6 +50,7 @@ pub struct Behavior { strip_program: String, create_leading: bool, target_dir: Option, + no_target_dir: bool, } #[derive(Error, Debug)] @@ -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 { @@ -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( @@ -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()) @@ -368,6 +369,11 @@ fn behavior(matches: &ArgMatches) -> UResult { let backup_mode = backup_control::determine_backup_mode(matches)?; let target_dir = matches.get_one::(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); @@ -430,6 +436,7 @@ fn behavior(matches: &ArgMatches) -> UResult { ), create_leading: matches.get_flag(OPT_CREATE_LEADING), target_dir, + no_target_dir, }) } @@ -522,6 +529,9 @@ fn standard(mut paths: Vec, 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 { @@ -591,7 +601,7 @@ fn standard(mut paths: Vec, 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(); @@ -600,6 +610,16 @@ fn standard(mut paths: Vec, 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 { diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index 57ac74aff..fdb66639f 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -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 ' 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}"))); +}