logo

utils-std

Collection of commonly available Unix tools
commit: b2b8a0ef7ead2b993fdf6f268353b400395170f9
parent e18b5bdc80e3c59ac03dd1d6b7f682e3c9a01c4e
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date:   Thu, 11 Apr 2024 10:06:22 +0200

cmd/mv: new

Diffstat:

MMakefile4++++
Acmd/mv.185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/mv.c270+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest-cmd/mv.t86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 . + .