Skip to content

Commit 68c91c1

Browse files
authored
install: implement the --no-target-directory option (#7867)
* implement --no-target-directory option * add test for --no-target-directory
1 parent 8ec5fe1 commit 68c91c1

File tree

2 files changed

+182
-6
lines changed

2 files changed

+182
-6
lines changed

src/uu/install/src/install.rs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ pub struct Behavior {
5050
strip_program: String,
5151
create_leading: bool,
5252
target_dir: Option<String>,
53+
no_target_dir: bool,
5354
}
5455

5556
#[derive(Error, Debug)]
@@ -104,6 +105,9 @@ enum InstallError {
104105

105106
#[error("'{0}' and '{1}' are the same file")]
106107
SameFile(PathBuf, PathBuf),
108+
109+
#[error("extra operand {}\n{}", .0.quote(), .1.quote())]
110+
ExtraOperand(String, String),
107111
}
108112

109113
impl UError for InstallError {
@@ -279,11 +283,10 @@ pub fn uu_app() -> Command {
279283
.value_hint(clap::ValueHint::DirPath),
280284
)
281285
.arg(
282-
// TODO implement flag
283286
Arg::new(OPT_NO_TARGET_DIRECTORY)
284287
.short('T')
285288
.long(OPT_NO_TARGET_DIRECTORY)
286-
.help("(unimplemented) treat DEST as a normal file")
289+
.help("treat DEST as a normal file")
287290
.action(ArgAction::SetTrue),
288291
)
289292
.arg(
@@ -328,9 +331,7 @@ pub fn uu_app() -> Command {
328331
///
329332
///
330333
fn check_unimplemented(matches: &ArgMatches) -> UResult<()> {
331-
if matches.get_flag(OPT_NO_TARGET_DIRECTORY) {
332-
Err(InstallError::Unimplemented(String::from("--no-target-directory, -T")).into())
333-
} else if matches.get_flag(OPT_PRESERVE_CONTEXT) {
334+
if matches.get_flag(OPT_PRESERVE_CONTEXT) {
334335
Err(InstallError::Unimplemented(String::from("--preserve-context, -P")).into())
335336
} else if matches.get_flag(OPT_CONTEXT) {
336337
Err(InstallError::Unimplemented(String::from("--context, -Z")).into())
@@ -368,6 +369,11 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
368369

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

372378
let preserve_timestamps = matches.get_flag(OPT_PRESERVE_TIMESTAMPS);
373379
let compare = matches.get_flag(OPT_COMPARE);
@@ -430,6 +436,7 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
430436
),
431437
create_leading: matches.get_flag(OPT_CREATE_LEADING),
432438
target_dir,
439+
no_target_dir,
433440
})
434441
}
435442

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

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

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

613+
if b.no_target_dir && target.exists() {
614+
return Err(
615+
InstallError::OverrideDirectoryFailed(target.clone(), source.clone()).into(),
616+
);
617+
}
618+
619+
if is_potential_directory_path(&target) {
620+
return copy_files_into_dir(sources, &target, b);
621+
}
622+
603623
if target.is_file() || is_new_file_path(&target) {
604624
copy(source, &target, b)
605625
} else {

tests/by-util/test_install.rs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1808,3 +1808,159 @@ fn test_install_symlink_same_file() {
18081808
"'{target_dir}/{file}' and '{target_link}/{file}' are the same file"
18091809
));
18101810
}
1811+
1812+
#[test]
1813+
fn test_install_no_target_directory_failing_cannot_overwrite() {
1814+
let scene = TestScenario::new(util_name!());
1815+
let at = &scene.fixtures;
1816+
let file = "file";
1817+
let dir = "dir";
1818+
1819+
at.touch(file);
1820+
at.mkdir(dir);
1821+
scene
1822+
.ucmd()
1823+
.arg("-T")
1824+
.arg(file)
1825+
.arg(dir)
1826+
.fails()
1827+
.stderr_contains("cannot overwrite directory 'dir' with non-directory");
1828+
1829+
assert!(!at.dir_exists("dir/file"));
1830+
}
1831+
1832+
#[test]
1833+
fn test_install_no_target_directory_failing_omitting_directory() {
1834+
let scene = TestScenario::new(util_name!());
1835+
let at = &scene.fixtures;
1836+
let dir1 = "dir1";
1837+
let dir2 = "dir2";
1838+
1839+
at.mkdir(dir1);
1840+
at.mkdir(dir2);
1841+
scene
1842+
.ucmd()
1843+
.arg("-T")
1844+
.arg(dir1)
1845+
.arg(dir2)
1846+
.fails()
1847+
.stderr_contains("omitting directory 'dir1'");
1848+
}
1849+
1850+
#[test]
1851+
fn test_install_no_target_directory_creating_leading_dirs_with_single_source_and_target_dir() {
1852+
let scene = TestScenario::new(util_name!());
1853+
let at = &scene.fixtures;
1854+
1855+
let source1 = "file";
1856+
let target_dir = "missing_target_dir/";
1857+
1858+
at.touch(source1);
1859+
1860+
// installing a single file into a missing directory will fail, when -D is used w/o -t parameter
1861+
scene
1862+
.ucmd()
1863+
.arg("-TD")
1864+
.arg(source1)
1865+
.arg(at.plus(target_dir))
1866+
.fails()
1867+
.stderr_contains("missing_target_dir/' is not a directory");
1868+
1869+
assert!(!at.dir_exists(target_dir));
1870+
}
1871+
1872+
#[test]
1873+
fn test_install_no_target_directory_failing_combine_with_target_directory() {
1874+
let scene = TestScenario::new(util_name!());
1875+
let at = &scene.fixtures;
1876+
let file = "file";
1877+
let dir1 = "dir1";
1878+
1879+
at.touch(file);
1880+
at.mkdir(dir1);
1881+
scene
1882+
.ucmd()
1883+
.arg("-T")
1884+
.arg(file)
1885+
.arg("-t")
1886+
.arg(dir1)
1887+
.fails()
1888+
.stderr_contains(
1889+
"Options --target-directory and --no-target-directory are mutually exclusive",
1890+
);
1891+
}
1892+
1893+
#[test]
1894+
fn test_install_no_target_directory_failing_usage_with_target_directory() {
1895+
let scene = TestScenario::new(util_name!());
1896+
let at = &scene.fixtures;
1897+
let file = "file";
1898+
1899+
at.touch(file);
1900+
scene
1901+
.ucmd()
1902+
.arg("-T")
1903+
.arg(file)
1904+
.arg("-t")
1905+
.fails()
1906+
.stderr_contains(
1907+
"a value is required for '--target-directory <DIRECTORY>' but none was supplied",
1908+
)
1909+
.stderr_contains("For more information, try '--help'");
1910+
}
1911+
1912+
#[test]
1913+
fn test_install_no_target_multiple_sources_and_target_dir() {
1914+
let scene = TestScenario::new(util_name!());
1915+
let at = &scene.fixtures;
1916+
1917+
let file1 = "file1";
1918+
let file2 = "file2";
1919+
let dir1 = "dir1";
1920+
let dir2 = "dir2";
1921+
1922+
at.touch(file1);
1923+
at.touch(file2);
1924+
at.mkdir(dir1);
1925+
at.mkdir(dir2);
1926+
1927+
// installing multiple files into a missing directory will fail, when -D is used w/o -t parameter
1928+
scene
1929+
.ucmd()
1930+
.arg("-T")
1931+
.arg(file1)
1932+
.arg(file2)
1933+
.arg(dir1)
1934+
.fails()
1935+
.stderr_contains("extra operand 'dir1'")
1936+
.stderr_contains("[OPTION]... [FILE]...");
1937+
1938+
scene
1939+
.ucmd()
1940+
.arg("-T")
1941+
.arg(file1)
1942+
.arg(file2)
1943+
.arg(dir1)
1944+
.arg(dir2)
1945+
.fails()
1946+
.stderr_contains("extra operand 'dir1'")
1947+
.stderr_contains("[OPTION]... [FILE]...");
1948+
}
1949+
1950+
#[test]
1951+
fn test_install_no_target_basic() {
1952+
let (at, mut ucmd) = at_and_ucmd!();
1953+
let file = "file";
1954+
let dir = "dir";
1955+
1956+
at.touch(file);
1957+
at.mkdir(dir);
1958+
ucmd.arg("-T")
1959+
.arg(file)
1960+
.arg(format!("{dir}/{file}"))
1961+
.succeeds()
1962+
.no_stderr();
1963+
1964+
assert!(at.file_exists(file));
1965+
assert!(at.file_exists(format!("{dir}/{file}")));
1966+
}

0 commit comments

Comments
 (0)