commit: b2b8a0ef7ead2b993fdf6f268353b400395170f9
parent e18b5bdc80e3c59ac03dd1d6b7f682e3c9a01c4e
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Thu, 11 Apr 2024 10:06:22 +0200
cmd/mv: new
Diffstat:
M | Makefile | 4 | ++++ |
A | cmd/mv.1 | 85 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | cmd/mv.c | 270 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | test-cmd/mv.t | 86 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4 files changed, 445 insertions(+), 0 deletions(-)
diff --git a/Makefile b/Makefile
@@ -153,3 +153,7 @@ cmd/expr: cmd/expr.tab.c Makefile
cmd/install: cmd/install.c lib/mode.c lib/user_group_parse.c lib/user_group_parse.h lib/fs.c lib/fs.h lib/mkdir.c lib/mkdir.h Makefile
rm -f ${<:=.gcov} ${@:=.gcda} ${@:=.gcno}
$(CC) -std=c99 $(CFLAGS) -o $@ cmd/install.c lib/mode.c lib/user_group_parse.c lib/fs.c lib/mkdir.c $(LDFLAGS) $(LDSTATIC)
+
+cmd/mv: cmd/mv.c lib/consent.c lib/consent.h lib/fs.c lib/fs.h Makefile
+ rm -f ${<:=.gcov} ${@:=.gcda} ${@:=.gcno}
+ $(CC) -std=c99 $(CFLAGS) -o $@ cmd/mv.c lib/consent.c lib/fs.c $(LDFLAGS) $(LDSTATIC)
diff --git a/cmd/mv.1 b/cmd/mv.1
@@ -0,0 +1,85 @@
+.\" 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
+.Ar source
+.Ar dest
+.Nm
+.Op Fl f Ns | Ns Fl i Ns | Ns Fl n
+.Ar source...
+.Ar destdir
+.Nm
+.Op Fl f Ns | Ns Fl i Ns | Ns Fl n
+.Fl t Ar destdir
+.Ar source...
+.Sh DESCRIPTION
+In the first form
+.Nm
+moves each given
+.Ar source
+to
+.Ar dest .
+.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.
+.El
+.Sh STANDARDS
+The
+.Nm
+utility is expected to be
+.St -p1003.2
+compatible.
+The
+.Fl n
+and
+.Fl t Ar destdir
+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,270 @@
+// 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 <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
+
+char *argv0 = "mv";
+
+bool no_clob = false, force = false;
+
+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;
+ }
+
+ int ret = faccessat(destdir.fd, dest, F_OK, 0);
+ if(ret == 0)
+ {
+ struct stat dest_status;
+ if(fstatat(destdir.fd, dest, &dest_status, 0) < 0)
+ {
+ fprintf(stderr,
+ "mv: Failed getting status for destination file '%s/%s': %s\n",
+ destdir.name,
+ dest,
+ strerror(errno));
+ return -1;
+ }
+
+ 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)
+ {
+ fprintf(stderr,
+ "mv: Error: Source '%s' and destination '%s/%s' are the same file\n",
+ src,
+ destdir.name,
+ dest);
+ return -1;
+ }
+
+ if(no_clob)
+ {
+ fprintf(stderr, "mv: Destination file '%s/%s' already exists\n", destdir.name, dest);
+ return -1;
+ }
+
+ if(!force && !consentf("mv: Destination file '%s/%s' already exists, 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;
+ }
+ }
+
+ return 0;
+}
+
+static void
+usage()
+{
+ fprintf(stderr, "Usage: mv [-f|-i|-n] source dest\n");
+ fprintf(stderr, " mv [-f|-i|-n] source... destdir\n");
+ fprintf(stderr, " mv [-f|-i|-n] -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:")) != -1)
+ {
+ switch(c)
+ {
+ case 'f':
+ force = true;
+ no_clob = false;
+ break;
+ case 'i':
+ force = false;
+ no_clob = false;
+ break;
+ case 'n':
+ force = 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 ':':
+ 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;
+
+ consent_init();
+
+ if(destdir.fd == AT_FDCWD)
+ {
+ if(argc <= 1)
+ {
+ fprintf(stderr, "mv: Not enough operands, %d given, expect >= 2\n", argc);
+ return 1;
+ }
+ else if(argc == 2)
+ {
+ int ret = do_renameat(argv[0], destdir, argv[1]);
+
+ consent_finish();
+ return ret < 0 ? 1 : 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++)
+ if(do_renameat(argv[i], destdir, argv[i]) < 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/test-cmd/mv.t b/test-cmd/mv.t
@@ -0,0 +1,86 @@
+#!/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 moving 'enoent' to './nondir/': No such file or 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
+ mv: Error: Source 'same' and destination './Same' are the same file
+ [1]
+ $ test -e same
+ $ test -e Same
+ $ ln -s same same-s
+ $ mv same same-s
+ mv: Error: Source 'same' and destination './same-s' are the same file
+ [1]
+ $ test -e same
+ $ test -e Same
+ $ test -e same-s
+ $ mv Same same-s
+ mv: Error: Source 'Same' and destination './same-s' are the same file
+ [1]
+ $ test -e same
+ $ test -e Same
+ $ test -e same-s
+ $ rm same Same same-s
+
+No files should be left
+ $ find .
+ .