commit: 57a2fea900fc5e056629101639d0045aea4881d4
parent d0d1c4fafa929ff4b4f5ae435947c727d8b8fdb6
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Sat, 13 Dec 2025 17:17:30 +0100
cmd/mv: add support for -T option
Diffstat:
3 files changed, 50 insertions(+), 4 deletions(-)
diff --git a/cmd/mv.1 b/cmd/mv.1
@@ -10,7 +10,7 @@
.Sh SYNOPSIS
.Nm
.Op Fl f Ns | Ns Fl i Ns | Ns Fl n
-.Op Fl v
+.Op Fl Tv
.Ar source
.Ar destfile
.Nm
@@ -75,6 +75,8 @@ and
options.
.It Fl t Ar destdir
Set the destination directory.
+.It Fl T
+Always treat destination as a file.
.It Fl v
Verbose, write which action has been done.
.El
@@ -88,7 +90,8 @@ specification.
.Pp
The
.Fl n ,
-.Fl t Ar destdir
+.Fl t Ar destdir ,
+.Fl T ,
and
.Fl v
options are extensions.
diff --git a/cmd/mv.c b/cmd/mv.c
@@ -43,6 +43,7 @@
const char *argv0 = "mv";
bool no_clob = false, force = false, interact = false, verbose = false;
+bool opt_T = false;
static int stdin_tty = 0;
@@ -347,6 +348,13 @@ do_renameat(struct named_fd srcdir,
break;
case EISDIR:
case ENOTDIR:
+ if(opt_T)
+ {
+ fprintf(
+ stderr, "mv: error: Failed moving '%s' to '%s': %s\n", src, dest, strerror(errno));
+ return -1;
+ }
+
if(destdir.fd != AT_FDCWD)
{
fprintf(
@@ -428,15 +436,16 @@ main(int argc, char *argv[])
{"interactive", no_argument, NULL, 'i'},
{"no-clobber", no_argument, NULL, 'n'},
{"target-directory", required_argument, NULL, 't'},
+ {"no-target-directory", no_argument, NULL, 'T'},
{"verbose", no_argument, NULL, 'v'},
{0, 0, 0, 0},
};
// clang-format on
// Need + as first character to get POSIX-style option parsing
- for(int c = -1; (c = getopt_long(argc, argv, "+:fint:v", opts, NULL)) != -1;)
+ for(int c = -1; (c = getopt_long(argc, argv, "+:fint:Tv", opts, NULL)) != -1;)
#else
- for(int c = -1; (c = getopt_nolong(argc, argv, ":fint:v")) != -1;)
+ for(int c = -1; (c = getopt_nolong(argc, argv, ":fint:Tv")) != -1;)
#endif
{
switch(c)
@@ -470,6 +479,9 @@ main(int argc, char *argv[])
return 1;
}
break;
+ case 'T':
+ opt_T = true;
+ break;
case 'v':
verbose = true;
break;
@@ -502,6 +514,21 @@ main(int argc, char *argv[])
stdin_tty = isatty(STDIN_FILENO);
if(!stdin_tty) errno = 0;
+ if(opt_T)
+ {
+ if(destdir.fd != AT_FDCWD)
+ {
+ fprintf(stderr, "mv: error: Cannot pass both -t and -T\n");
+ return 1;
+ }
+
+ if(argc != 2)
+ {
+ fprintf(stderr, "mv: error: Option -T passed but got %d arguments instead of 2\n", argc);
+ return 1;
+ }
+ }
+
if(destdir.fd == AT_FDCWD)
{
if(argc <= 1)
@@ -510,6 +537,14 @@ main(int argc, char *argv[])
return 1;
}
+ if(opt_T)
+ {
+ int ret = do_renameat(srcdir, argv[0], destdir, argv[1]);
+
+ consent_finish();
+ return ret < 0 ? 1 : 0;
+ }
+
struct stat dest_status;
int ret_stat = fstatat(destdir.fd, argv[1], &dest_status, 0);
if(
diff --git a/test-cmd/mv.t b/test-cmd/mv.t
@@ -150,6 +150,14 @@ Works with non-resolving source symlinks
$ test -L symlink_enoent.done
$ rm symlink_enoent.done
+Option -T
+ $ mkdir opt_T.d
+ $ touch opt_T
+ $ mv -T opt_T opt_T.d
+ mv: error: Failed moving 'opt_T' to 'opt_T.d': Is a directory
+ [1]
+ $ rm -fr opt_T opt_T.d
+
No files should be left
$ find .
.