commit: d29e712c7db44455f002fe27e1d83610a25c70ec
parent 4cc3fb4141ed19f0502385cfecd4047c86ca3bd3
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Wed, 31 Jul 2024 02:59:14 +0200
Revert "cmd/mv: cleanout for now, misses directory handling"
This reverts commit 2eecdbaad34600bb6004ba646fd530121d174cda.
Diffstat:
6 files changed, 562 insertions(+), 3 deletions(-)
diff --git a/cmd/mv.1 b/cmd/mv.1
@@ -0,0 +1,97 @@
+.\" utils-std: Collection of commonly available Unix tools
+.\" Copyright 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+.\" SPDX-License-Identifier: MPL-2.0
+.Dd 2024-05-10
+.Dt MV 1
+.Os
+.Sh NAME
+.Nm mv
+.Nd move and rename files
+.Sh SYNOPSIS
+.Nm
+.Op Fl f Ns | Ns Fl i Ns | Ns Fl n
+.Op Fl v
+.Ar source
+.Ar destfile
+.Nm
+.Op Fl f Ns | Ns Fl i Ns | Ns Fl n
+.Op Fl v
+.Ar source...
+.Ar destdir
+.Nm
+.Op Fl f Ns | Ns Fl i Ns | Ns Fl n
+.Op Fl v
+.Fl t Ar destdir
+.Ar source...
+.Sh DESCRIPTION
+In the first form,
+.Nm
+moves each given
+.Ar source
+to
+.Ar destfile .
+This form is assumed when
+.Ar destfile
+does not refers to an existing directory, or a symlink pointing to one.
+Additionally, in this case a trailing slash and
+.Ar source
+not referring to a directory results in an error.
+.Pp
+In the second and third form,
+.Nm
+moves each given
+.Ar source
+into
+.Ar destdir
+with appending the
+.Ar source
+basename to
+.Ar destdir
+to create the full destination path.
+.Sh OPTIONS
+.Bl -tag -width _f
+.It Fl f
+Force, do not ask before overwriting to the destination path.
+Overrides previously set
+.Fl i
+and
+.Fl n
+options.
+.It Fl i
+Interactive, causes
+.Nm
+to ask before overwriting a file.
+Overrides previously set
+.Fl f
+and
+.Fl n
+options.
+.It Fl n
+No-clobber, never overwrite.
+Overrides previously set
+.Fl f
+and
+.Fl i
+options.
+.It Fl t Ar destdir
+Set the destination directory.
+.It Fl v
+Verbose, write which action has been done.
+.El
+.Sh STANDARDS
+The
+.Nm
+utility is expected to be
+.St -p1003.2
+compatible.
+The
+.Fl n ,
+.Fl t Ar destdir
+and
+.Fl v
+options are extensions.
+.Sh HISTORY
+A
+.Nm
+command appeared in
+.At v1 .
diff --git a/cmd/mv.c b/cmd/mv.c
@@ -0,0 +1,322 @@
+// utils-std: Collection of commonly available Unix tools
+// SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+// SPDX-License-Identifier: MPL-2.0
+
+#define _POSIX_C_SOURCE 200809L
+#define _GNU_SOURCE // copy_file_range
+#define _FILE_OFFSET_BITS 64
+
+#include "../lib/consent.h"
+#include "../lib/fs.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h> // open
+#include <libgen.h> // basename
+#include <limits.h> // PATH_MAX
+#include <locale.h> // setlocale
+#include <stdbool.h>
+#include <stdint.h> // SIZE_MAX
+#include <stdio.h> // fprintf, rename
+#include <string.h> // strcmp
+#include <sys/stat.h> // stat, S_ISDIR
+#include <unistd.h> // getopt
+
+// Workaround against GNU glibc
+// https://sourceware.org/bugzilla/show_bug.cgi?id=18228
+#if defined(__linux__) && !defined(O_SEARCH)
+// As defined in musl
+#define O_SEARCH O_PATH
+#endif
+
+char *argv0 = "mv";
+
+bool no_clob = false, force = false, interact = false, verbose = false;
+
+static int stdin_tty = 0;
+
+struct named_fd
+{
+ int fd;
+ char *name;
+};
+
+static int
+copy_unlink(const char *restrict src, const char *restrict dest)
+{
+ int in = open(src, O_RDONLY | O_NOCTTY);
+ if(in < 0)
+ {
+ fprintf(stderr, "mv: Failed opening '%s': %s\n", src, strerror(errno));
+ errno = 0;
+ return -1;
+ }
+
+ int out = open(dest, O_WRONLY | O_CREAT | O_NOCTTY);
+ if(out < 0)
+ {
+ fprintf(stderr, "mv: Failed opening '%s': %s\n", dest, strerror(errno));
+ errno = 0;
+ return -1;
+ }
+
+ if(auto_file_copy(in, out, SIZE_MAX, 0) < 0) return -1;
+
+ return unlink(src);
+}
+
+static int
+do_renameat(const char *restrict src, struct named_fd destdir, const char *restrict dest)
+{
+ if(destdir.fd == AT_FDCWD && strcmp(src, dest) == 0)
+ {
+ fprintf(stderr, "mv: Error, passed to both source and destination: '%s'\n", src);
+ return -1;
+ }
+
+ errno = 0;
+
+ struct stat dest_status;
+ int ret = fstatat(destdir.fd, dest, &dest_status, 0);
+ if(ret < 0 && errno != ENOENT)
+ {
+ fprintf(stderr,
+ "mv: Failed getting status for destination file '%s/%s': %s\n",
+ destdir.name,
+ dest,
+ strerror(errno));
+ return -1;
+ }
+ errno = 0;
+
+ if(ret == 0)
+ {
+ struct stat src_status;
+ if(fstatat(AT_FDCWD, src, &src_status, 0) < 0)
+ {
+ fprintf(stderr, "mv: Failed getting status for source file '%s': %s\n", src, strerror(errno));
+ return -1;
+ }
+
+ if(dest_status.st_ino == src_status.st_ino && dest_status.st_dev == src_status.st_dev) return 0;
+
+ if(no_clob)
+ {
+ fprintf(stderr, "mv: Destination file '%s/%s' already exists\n", destdir.name, dest);
+ return -1;
+ }
+
+ if(!force)
+ {
+ if(interact)
+ {
+ if(!consentf(
+ "mv: Destination file '%s/%s' already exists, overwrite? [yN] ", destdir.name, dest))
+ return 0;
+ }
+ else if(stdin_tty)
+ {
+ if(faccessat(destdir.fd, dest, W_OK, 0) == 0)
+ {
+ if(!consentf("mv: No write permissions for destination file '%s/%s', overwrite? [yN] ",
+ destdir.name,
+ dest))
+ return 0;
+ }
+ else
+ {
+ errno = 0;
+ }
+ }
+ }
+ }
+
+ assert(errno == 0);
+ if(renameat(AT_FDCWD, src, destdir.fd, dest) < 0)
+ {
+ switch(errno)
+ {
+ case EXDEV:
+ errno = 0;
+ if(copy_unlink(src, dest) < 0) return -1;
+ break;
+ case EISDIR:
+ case ENOTDIR:
+ if(destdir.fd != AT_FDCWD)
+ {
+ fprintf(stderr, "mv: Failed moving '%s' into '%s': %s\n", src, dest, strerror(errno));
+ return -1;
+ }
+
+ int tmp_destdir = open(dest, O_SEARCH | O_DIRECTORY);
+ if(tmp_destdir < 0)
+ {
+ fprintf(
+ stderr, "mv: Failed opening destination directory '%s': %s\n", dest, strerror(errno));
+ return -1;
+ }
+
+ if(renameat(AT_FDCWD, src, tmp_destdir, src) < 0)
+ {
+ fprintf(
+ stderr, "mv: Failed moving '%s' into directory '%s': %s\n", src, dest, strerror(errno));
+ return -1;
+ }
+
+ if(close(tmp_destdir) < 0)
+ {
+ fprintf(stderr, "mv: Failed closing directory '%s': %s\n", dest, strerror(errno));
+ return -1;
+ }
+ break;
+ default:
+ fprintf(stderr,
+ "mv: Failed moving '%s' to '%s/%s': %s\n",
+ src,
+ destdir.name,
+ dest,
+ strerror(errno));
+ return -1;
+ }
+ }
+
+ if(verbose) fprintf(stderr, "mv: renamed '%s' -> '%s/%s'\n", src, destdir.name, dest);
+
+ return 0;
+}
+
+static void
+usage()
+{
+ fprintf(stderr, "Usage: mv [-f|-i|-n] [-v] source dest\n");
+ fprintf(stderr, " mv [-f|-i|-n] [-v] source... destdir\n");
+ fprintf(stderr, " mv [-f|-i|-n] [-v] -t destdir source...\n");
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct named_fd destdir = {
+ .fd = AT_FDCWD,
+ .name = ".",
+ };
+
+ int c = -1;
+ while((c = getopt(argc, argv, ":fint:v")) != -1)
+ {
+ switch(c)
+ {
+ case 'f':
+ force = true;
+ interact = false;
+ no_clob = false;
+ break;
+ case 'i':
+ force = false;
+ interact = true;
+ no_clob = false;
+ break;
+ case 'n':
+ force = false;
+ interact = false;
+ no_clob = true;
+ break;
+ case 't':
+ destdir.name = optarg;
+ destdir.fd = open(optarg, O_SEARCH | O_DIRECTORY);
+
+ if(destdir.fd < 0)
+ {
+ fprintf(
+ stderr, "mv: Failed opening destination directory '%s': %s\n", optarg, strerror(errno));
+ return 1;
+ }
+ break;
+ case 'v':
+ verbose = true;
+ break;
+ case ':':
+ fprintf(stderr, "mv: Error: Missing operand for option: '-%c'\n", optopt);
+ usage();
+ return 1;
+ case '?':
+ fprintf(stderr, "mv: Error: Unrecognised option: '-%c'\n", optopt);
+ usage();
+ return 1;
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ errno = 0;
+ setlocale(LC_ALL, "");
+ if(errno != 0)
+ {
+ fprintf(stderr, "%s: Warning: Failed to initialize locales: %s\n", argv0, strerror(errno));
+ errno = 0;
+ }
+
+ consent_init();
+
+ stdin_tty = isatty(STDIN_FILENO);
+ if(!stdin_tty) errno = 0;
+
+ if(destdir.fd == AT_FDCWD)
+ {
+ if(argc <= 1)
+ {
+ fprintf(stderr, "mv: Not enough operands, %d given, expect >= 2\n", argc);
+ return 1;
+ }
+
+ struct stat dest_status;
+ int ret_stat = fstatat(destdir.fd, argv[1], &dest_status, 0);
+ if(argc == 2 && (errno == ENOENT || (ret_stat == 0 && !S_ISDIR(dest_status.st_mode))))
+ {
+ int ret = do_renameat(argv[0], destdir, argv[1]);
+
+ consent_finish();
+ return ret < 0 ? 1 : 0;
+ }
+ errno = 0;
+
+ argc--;
+ destdir.name = argv[argc];
+ destdir.fd = open(destdir.name, O_SEARCH | O_DIRECTORY);
+ if(destdir.fd < 0)
+ {
+ fprintf(stderr,
+ "mv: Failed opening destination directory '%s': %s\n",
+ destdir.name,
+ strerror(errno));
+
+ consent_finish();
+ return 1;
+ }
+ }
+
+ for(int i = 0; i < argc; i++)
+ {
+ char arg[PATH_MAX] = "";
+ strcpy(arg, argv[i]);
+
+ char *filename = basename(arg);
+
+ if(do_renameat(argv[i], destdir, filename) < 0)
+ {
+ consent_finish();
+ return 1;
+ }
+ }
+
+ consent_finish();
+
+ if(close(destdir.fd) < 0)
+ {
+ fprintf(stderr, "mv: Failed closing directory '%s': %s\n", destdir.name, strerror(errno));
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/coreutils.txt b/coreutils.txt
@@ -47,7 +47,7 @@ mkdir: Done
mkfifo: Done
mknod: Done
mktemp: Done
-mv
+mv: Done
nice: Done
nl: No, use sed
nohup: Done
diff --git a/lsb_commands.txt b/lsb_commands.txt
@@ -84,7 +84,7 @@ mktemp: Done
more: No
mount: out of scope
msgfmt: out of scope
-mv
+mv: Done
newgrp: out of scope
nice: Done
nl: No, use sed
diff --git a/posix_utilities.txt b/posix_utilities.txt
@@ -85,7 +85,7 @@ mkdir: done
mkfifo: done
more: no POSIX2_UPE
msgfmt: no, gettext
-mv
+mv: done
newgrp
ngettext: no, gettext
nice: done
diff --git a/test-cmd/mv.t b/test-cmd/mv.t
@@ -0,0 +1,140 @@
+#!/usr/bin/env cram
+# SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+# SPDX-License-Identifier: MPL-2.0
+
+ $ export PATH="$TESTDIR/../cmd:$PATH"
+
+ $ test "$(command -v mv)" = "$TESTDIR/../cmd/mv"
+
+POSIX, non-directory target with a trailing slash is an error
+ $ touch nondir file
+ $ mv file nondir/
+ mv: Failed opening destination directory 'nondir/': Not a directory
+ [1]
+ $ test -e file
+ $ test -e nondir
+ $ mv enoent nondir/
+ mv: Failed opening destination directory 'nondir/': Not a directory
+ [1]
+ $ rm nondir file
+
+POSIX mv(1) step 1a, no -f option, no -i option
+ $ mkdir -m -w 1a_no-f_no-write
+ $ touch src
+ $ printf '\n' | mv src 1a_no-f_no-write/dest
+ mv: Failed moving 'src' to './1a_no-f_no-write/dest': Permission denied
+ [1]
+ $ test -e src
+ $ printf 'n\n' | mv src 1a_no-f_no-write/dest
+ mv: Failed moving 'src' to './1a_no-f_no-write/dest': Permission denied
+ [1]
+ $ test -e src
+ $ printf 'y\n' | mv src 1a_no-f_no-write/dest
+ mv: Failed moving 'src' to './1a_no-f_no-write/dest': Permission denied
+ [1]
+ $ test ! -e src
+ [1]
+ $ rm -fr 1a_no-f_no-write
+
+POSIX mv(1) step 1b, no -f option, -i passed
+ $ mkdir 1a_no-f_write
+ $ touch src 1a_no-f_write/dest
+ $ printf '\n' | mv -i src 1a_no-f_write/dest
+ mv: Destination file './1a_no-f_write/dest' already exists, overwrite? [yN]
+ mv: Got empty response, considering it false
+ $ test -e src
+ $ test -d 1a_no-f_write
+ $ printf 'n\n' | mv -i src 1a_no-f_write/dest
+ mv: Destination file './1a_no-f_write/dest' already exists, overwrite? [yN] n
+ $ test -f src
+ $ test -d 1a_no-f_write
+ $ printf 'y\n' | mv -i src 1a_no-f_write/dest
+ mv: Destination file './1a_no-f_write/dest' already exists, overwrite? [yN] y
+ $ test ! -e src
+ $ test -d 1a_no-f_write
+ $ rm -fr 1a_no-f_write
+
+POSIX mv(1) step 2, same file
+ $ touch same
+ $ mv same same
+ mv: Error, passed to both source and destination: 'same'
+ [1]
+ $ test -e same
+ $ ln same Same
+ $ mv same Same
+ $ test -e same
+ $ test -e Same
+ $ ln -s same same-s
+ $ mv same same-s
+ $ test -e same
+ $ test -e Same
+ $ test -e same-s
+ $ mv Same same-s
+ $ test -e same
+ $ test -e Same
+ $ test -e same-s
+ $ rm same Same same-s
+
+Where destination is an existing directory
+ $ mkdir destdir
+ $ touch foo
+ $ mv foo destdir
+ $ test ! -e foo
+ $ test -d destdir
+ $ test -f destdir/foo
+ $ rm -r destdir
+
+ $ mkdir destdir_trail/
+ $ touch foo_trail
+ $ mv foo_trail destdir_trail/
+ $ test ! -e foo_trail
+ $ test -d destdir_trail
+ $ test -f destdir_trail/foo_trail
+ $ rm -r destdir_trail
+
+Where destination is an existing file
+ $ touch foo_file destfile
+ $ mv foo_file destfile
+ $ test ! -e foo_file
+ $ test -f destfile
+ $ rm destfile
+
+ $ touch foo_file_i destfile_i
+ $ printf 'y\n' | mv -i foo_file_i destfile_i
+ mv: Destination file './destfile_i' already exists, overwrite? [yN] y
+ $ test ! -e foo_file_i
+ $ test -f destfile_i
+ $ rm destfile_i
+
+ $ touch foo_file_trail destfile_trail
+ $ mv foo_file_trail destfile_trail/
+ mv: Failed opening destination directory 'destfile_trail/': Not a directory
+ [1]
+ $ test -f foo_file_trail
+ $ test -f destfile_trail
+ $ rm foo_file_trail destfile_trail
+
+Verbose (non-standard)
+ $ touch foo
+ $ mv -v foo bar
+ mv: renamed 'foo' -> './bar'
+ $ test ! -e foo
+ $ test -e bar
+ $ rm bar
+
+Last component used for destination filename
+ $ mkdir -p src_last/dir dest_last
+ $ touch src_last/dir/file
+ $ mv src_last/dir/file dest_last/
+ $ test -f dest_last/file
+ $ test ! -e dest_last/dir/file
+ $ test ! -e src_last/dir/file
+ $ mv src_last/dir dest_last
+ $ test -d dest_last/dir
+ $ test ! -e src_last/dir
+ $ test -f dest_last/file
+ $ rm -r src_last dest_last
+
+No files should be left
+ $ find .
+ .