I have added tests and documentation for this.
Two questions:
1. I can’t run the tests on my machine, make -f tests/local.mk fails with
tests/local.mk:28: *** missing separator. Stop.
(GNU Make 4.4.1, obtained from nixpkgs unstable)
Also happens before my changes. What am I doing wrong?
2. Do I need to assign copyright to FSF for these changes?
—Anselm Schüler
On 20/02/2026 16:47, Anselm Schüler wrote:
Not sure if this is the right mailing list for this, the website was a
bit unclear on this.
This patch is not finished, it’s missing tests (if that is necessary)
and documentation, but I’d like to get feedback on whether this is a
welcome change.
The motivation behind this change is that I’d like to make an alias
for cp and mv that automatically passes -T to avoid accidentally using
the wrong behaviour, but would still like to be able to invoke that
behaviour explicitly. This change makes a -T flag combined with a -t
option behave as if the -T flag wasn’t passed.
—Anselm Schüler
From 07bac9ac7788529d370f45b754559b149d746453 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Anselm=20Sch=C3=BCler?= <[email protected]>
Date: Fri, 20 Feb 2026 16:34:14 +0100
Subject: [PATCH 1/2] mv, cp, install: allow -t together with -T
-T check is simply ignored if -t is present
* src/cp.c (do_copy):
* src/mv.c (main):
Allow -t to set a target directory even if -T is given, overriding it.
---
src/cp.c | 11 ++++-------
src/install.c | 11 ++++-------
src/mv.c | 11 ++++-------
3 files changed, 12 insertions(+), 21 deletions(-)
diff --git a/src/cp.c b/src/cp.c
index e17484b5d..133cc1793 100644
--- a/src/cp.c
+++ b/src/cp.c
@@ -160,7 +160,7 @@ usage (int status)
printf (_("\
Usage: %s [OPTION]... [-T] SOURCE DEST\n\
or: %s [OPTION]... SOURCE... DIRECTORY\n\
- or: %s [OPTION]... -t DIRECTORY SOURCE...\n\
+ or: %s [OPTION]... [-T] -t DIRECTORY SOURCE...\n\
"),
program_name, program_name, program_name);
fputs (_("\
@@ -281,7 +281,8 @@ Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.\n\
"));
oputs (_("\
-T, --no-target-directory\n\
- treat DEST as a normal file\n\
+ treat DEST as a normal file instead of as DIRECTORY\n\
+ (ignored if you use --target-directory explicitly)\n\
"));
oputs (_("\
--update[=UPDATE]\n\
@@ -685,12 +686,8 @@ do_copy (int n_files, char **file, char const *target_directory,
int target_dirfd = AT_FDCWD;
bool new_dst = false;
bool ok = true;
- if (no_target_directory)
+ if (no_target_directory && !target_directory)
{
- if (target_directory)
- error (EXIT_FAILURE, 0,
- _("cannot combine --target-directory (-t) "
- "and --no-target-directory (-T)"));
if (2 < n_files)
{
error (0, 0, _("extra operand %s"), quoteaf (file[2]));
diff --git a/src/install.c b/src/install.c
index 58ecfbc31..79e51a07d 100644
--- a/src/install.c
+++ b/src/install.c
@@ -591,7 +591,7 @@ usage (int status)
printf (_("\
Usage: %s [OPTION]... [-T] SOURCE DEST\n\
or: %s [OPTION]... SOURCE... DIRECTORY\n\
- or: %s [OPTION]... -t DIRECTORY SOURCE...\n\
+ or: %s [OPTION]... [-T] -t DIRECTORY SOURCE...\n\
or: %s [OPTION]... -d DIRECTORY...\n\
"),
program_name, program_name, program_name, program_name);
@@ -677,7 +677,8 @@ In the 4th form, create all components of the given DIRECTORY(ies).\n\
"));
oputs (_("\
-T, --no-target-directory\n\
- treat DEST as a normal file\n\
+ treat DEST as a normal file instead of as DIRECTORY\n\
+ (ignored if you use --target-directory explicitly)\n\
"));
oputs (_("\
-v, --verbose\n\
@@ -1003,12 +1004,8 @@ main (int argc, char **argv)
struct stat sb;
int target_dirfd = AT_FDCWD;
- if (no_target_directory)
+ if (no_target_directory && !target_directory)
{
- if (target_directory)
- error (EXIT_FAILURE, 0,
- _("cannot combine --target-directory (-t) "
- "and --no-target-directory (-T)"));
if (2 < n_files)
{
error (0, 0, _("extra operand %s"), quoteaf (file[2]));
diff --git a/src/mv.c b/src/mv.c
index cd6aab473..34c0d5019 100644
--- a/src/mv.c
+++ b/src/mv.c
@@ -253,7 +253,7 @@ usage (int status)
printf (_("\
Usage: %s [OPTION]... [-T] SOURCE DEST\n\
or: %s [OPTION]... SOURCE... DIRECTORY\n\
- or: %s [OPTION]... -t DIRECTORY SOURCE...\n\
+ or: %s [OPTION]... [-T] -t DIRECTORY SOURCE...\n\
"),
program_name, program_name, program_name);
fputs (_("\
@@ -311,7 +311,8 @@ If you specify more than one of -i, -f, -n, only the final one takes effect.\n\
"));
oputs (_("\
-T, --no-target-directory\n\
- treat DEST as a normal file\n\
+ treat DEST as a normal file instead of as DIRECTORY\n\
+ (ignored if you use --target-directory explicitly)\n\
"));
oputs (_("\
--update[=UPDATE]\n\
@@ -452,12 +453,8 @@ main (int argc, char **argv)
struct stat sb;
sb.st_mode = 0;
int target_dirfd = AT_FDCWD;
- if (no_target_directory)
+ if (no_target_directory && !target_directory)
{
- if (target_directory)
- error (EXIT_FAILURE, 0,
- _("cannot combine --target-directory (-t) "
- "and --no-target-directory (-T)"));
if (2 < n_files)
{
error (0, 0, _("extra operand %s"), quoteaf (file[2]));
--
2.52.0
From 8de2329dc9e4d57464b149b6dd995e16e22dadf9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Anselm=20Sch=C3=BCler?= <[email protected]>
Date: Fri, 20 Feb 2026 22:39:25 +0100
Subject: [PATCH 2/2] tests: add tests for cp, mv, install's --target-directory
(also with -T)
Add missing tests for --target-directory and also for its new interaction with --no-target-directory
* tests/cp/explicit-target-dir.sh:
* tests/cp/explicit-target-dir.sh:
* tests/cp/explicit-target-dir.sh:
Created to test --target-directory operation
---
tests/cp/explicit-target-dir.sh | 48 +++++++++++++++++++++++++
tests/install/explicit-target-dir.sh | 48 +++++++++++++++++++++++++
tests/local.mk | 3 ++
tests/mv/explicit-target-dir.sh | 52 ++++++++++++++++++++++++++++
4 files changed, 151 insertions(+)
create mode 100644 tests/cp/explicit-target-dir.sh
create mode 100644 tests/install/explicit-target-dir.sh
create mode 100644 tests/mv/explicit-target-dir.sh
diff --git a/tests/cp/explicit-target-dir.sh b/tests/cp/explicit-target-dir.sh
new file mode 100644
index 000000000..b764cd308
--- /dev/null
+++ b/tests/cp/explicit-target-dir.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+# ensure that --target-directory works
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ cp
+
+LS_FAILURE=2
+
+# with underscore = already in dest / overwrite
+touch f1_ f2 f3 f4 f5_ f6 f7 f8 f01 f02 f03_ f04_ || framework_failure_
+mkdir d01 d02_ || framework_failure_
+mkdir dest || framework_failure_
+touch dest/f1_ || framework_failure_
+touch dest/f5_ || framework_failure_
+touch dest/f03_ || framework_failure_
+touch dest/f04_ || framework_failure_
+mkdir dest/d02_ || framework_failure_
+mkdir dest/d02_/d || framework_failure_
+
+cp -f f1_ f2 -t dest || fail=1
+cp f3 f4 -t dest || fail=1
+cp -ft dest f5_ f6 || fail=1
+cp -t dest f7 f8 || fail=1
+cp f01 -t dest || fail=1
+cp -t dest f02 || fail=1
+cp -f f03_ -t dest || fail=1
+cp -ft dest f04_ || fail=1
+cp -t dest d01 || fail=1
+# nonempty dir cannot be overwritten
+returns_ 1 cp -t dest d02_ || fail=1
+
+returns_ 0 ls -d dest/f1_ dest/f2 || fail=1
+returns_ 0 ls -d dest/f3 dest/f4 || fail=1
+returns_ 0 ls -d dest/f5_ dest/f6 || fail=1
+returns_ 0 ls -d dest/f7 dest/f8 || fail=1
+returns_ 0 ls -d dest/f01 dest/f02 || fail=1
+returns_ 0 ls -d dest/f03_ dest/f04_ || fail=1
+returns_ 0 ls -d dest/d01 dest/d02_ || fail=1
+
+returns_ $LS_FAILURE ls -d /nonexistent || skip_ "/nonexistent exists"
+returns_ 1 cp -t /nonexistent dest || fail=1
+
+# also test combination with -T
+touch f11 f12
+cp -T -t dest f11 f12 || fail=1
+returns_ 0 ls -d dest/f11 dest/f12 || fail=1
+
+Exit $fail
diff --git a/tests/install/explicit-target-dir.sh b/tests/install/explicit-target-dir.sh
new file mode 100644
index 000000000..b764cd308
--- /dev/null
+++ b/tests/install/explicit-target-dir.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+# ensure that --target-directory works
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ cp
+
+LS_FAILURE=2
+
+# with underscore = already in dest / overwrite
+touch f1_ f2 f3 f4 f5_ f6 f7 f8 f01 f02 f03_ f04_ || framework_failure_
+mkdir d01 d02_ || framework_failure_
+mkdir dest || framework_failure_
+touch dest/f1_ || framework_failure_
+touch dest/f5_ || framework_failure_
+touch dest/f03_ || framework_failure_
+touch dest/f04_ || framework_failure_
+mkdir dest/d02_ || framework_failure_
+mkdir dest/d02_/d || framework_failure_
+
+cp -f f1_ f2 -t dest || fail=1
+cp f3 f4 -t dest || fail=1
+cp -ft dest f5_ f6 || fail=1
+cp -t dest f7 f8 || fail=1
+cp f01 -t dest || fail=1
+cp -t dest f02 || fail=1
+cp -f f03_ -t dest || fail=1
+cp -ft dest f04_ || fail=1
+cp -t dest d01 || fail=1
+# nonempty dir cannot be overwritten
+returns_ 1 cp -t dest d02_ || fail=1
+
+returns_ 0 ls -d dest/f1_ dest/f2 || fail=1
+returns_ 0 ls -d dest/f3 dest/f4 || fail=1
+returns_ 0 ls -d dest/f5_ dest/f6 || fail=1
+returns_ 0 ls -d dest/f7 dest/f8 || fail=1
+returns_ 0 ls -d dest/f01 dest/f02 || fail=1
+returns_ 0 ls -d dest/f03_ dest/f04_ || fail=1
+returns_ 0 ls -d dest/d01 dest/d02_ || fail=1
+
+returns_ $LS_FAILURE ls -d /nonexistent || skip_ "/nonexistent exists"
+returns_ 1 cp -t /nonexistent dest || fail=1
+
+# also test combination with -T
+touch f11 f12
+cp -T -t dest f11 f12 || fail=1
+returns_ 0 ls -d dest/f11 dest/f12 || fail=1
+
+Exit $fail
diff --git a/tests/local.mk b/tests/local.mk
index 5f9f3d19b..d478d62a8 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -545,6 +545,7 @@ all_tests = \
tests/cp/dir-vs-file.sh \
tests/cp/existing-perm-dir.sh \
tests/cp/existing-perm-race.sh \
+ tests/cp/explicit-target-dir.sh \
tests/cp/fail-perm.sh \
tests/cp/keep-directory-symlink.sh \
tests/cp/sparse-extents.sh \
@@ -652,6 +653,7 @@ all_tests = \
tests/install/basic-1.sh \
tests/install/create-leading.sh \
tests/install/d-slashdot.sh \
+ tests/install/explicit-target-dir.sh \
tests/install/install-C.sh \
tests/install/install-C-selinux.sh \
tests/install/install-Z-selinux.sh \
@@ -736,6 +738,7 @@ all_tests = \
tests/mv/dir-file.sh \
tests/mv/dir2dir.sh \
tests/mv/dup-source.sh \
+ tests/mv/explicit-target-dir.sh \
tests/mv/force.sh \
tests/mv/hard-2.sh \
tests/mv/hard-3.sh \
diff --git a/tests/mv/explicit-target-dir.sh b/tests/mv/explicit-target-dir.sh
new file mode 100644
index 000000000..238ae84c6
--- /dev/null
+++ b/tests/mv/explicit-target-dir.sh
@@ -0,0 +1,52 @@
+#!/bin/sh
+# ensure that --target-directory works
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ mv
+
+LS_FAILURE=2
+
+# with underscore = already in dest / overwrite
+touch f1_ f2 f3 f4 f5_ f6 f7 f8 f01 f02 f03_ f04_ || framework_failure_
+mkdir d01 d02_ || framework_failure_
+mkdir dest || framework_failure_
+touch dest/f1_ || framework_failure_
+touch dest/f5_ || framework_failure_
+touch dest/f03_ || framework_failure_
+touch dest/f04_ || framework_failure_
+mkdir dest/d02_ || framework_failure_
+mkdir dest/d02_/d || framework_failure_
+
+mv -f f1_ f2 -t dest || fail=1
+mv f3 f4 -t dest || fail=1
+mv -ft dest f5_ f6 || fail=1
+mv -t dest f7 f8 || fail=1
+mv f01 -t dest || fail=1
+mv -t dest f02 || fail=1
+mv -f f03_ -t dest || fail=1
+mv -ft dest f04_ || fail=1
+mv -t dest d01 || fail=1
+# nonempty dir cannot be overwritten
+returns_ 1 mv -t dest d02_ || fail=1
+
+returns_ $LS_FAILURE ls -d f1_ f2 f3 f4 f5_ f6 f7 f8 f01 f02 f03_ f04_ \
+ || fail=1
+returns_ $LS_FAILURE ls -d d01 d02_ || fail=1
+returns_ 0 ls -d dest/f1_ dest/f2 || fail=1
+returns_ 0 ls -d dest/f3 dest/f4 || fail=1
+returns_ 0 ls -d dest/f5_ dest/f6 || fail=1
+returns_ 0 ls -d dest/f7 dest/f8 || fail=1
+returns_ 0 ls -d dest/f01 dest/f02 || fail=1
+returns_ 0 ls -d dest/f03_ dest/f04_ || fail=1
+returns_ 0 ls -d dest/d01 dest/d02_ || fail=1
+
+returns_ $LS_FAILURE ls -d /nonexistent || skip_ "/nonexistent exists"
+returns_ 1 mv -t /nonexistent dest || fail=1
+
+# also test combination with -T
+touch f11 f12
+mv -T -t dest f11 f12 || fail=1
+returns_ $LS_FAILURE ls -d f11 f12 || fail=1
+returns_ 0 ls -d dest/f11 dest/f12 || fail=1
+
+Exit $fail
--
2.52.0