logo

utils-std

Collection of commonly available Unix tools
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:

Acmd/mv.197+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/mv.c322+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcoreutils.txt2+-
Mlsb_commands.txt2+-
Mposix_utilities.txt2+-
Atest-cmd/mv.t140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 . + .