From 57c812cc3e17ecf5df887029221fe3f2d0cd7ea0 Mon Sep 17 00:00:00 2001 From: Paul Eggert Date: Sat, 29 Jan 2022 11:40:17 -0800 Subject: [PATCH] mv: when installing to dir use dir-relative names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the destination for mv is a directory, use functions like openat to access the destination files, when such functions are available. This should be more efficient and should avoid some race conditions. Likewise for 'install'. * src/cp.c (must_be_working_directory, target_directory_operand) (target_dirfd_valid): Move from here ... * src/system.h: ... to here, so that install and mv can use them. Make them inline so GCC doesn’t complain. * src/install.c (lchown) [HAVE_LCHOWN]: Remove; no longer needed. (need_copy, copy_file, change_attributes, change_timestamps) (install_file_in_file, install_file_in_dir): New args for directory-relative names. All uses changed. Continue to pass full names as needed, for diagnostics and for lower-level functions that do not support directory-relative names. (install_file_in_dir): Update *TARGET_DIRFD as needed. (main): Handle target-directory in the new, cp-like way. * src/mv.c (remove_trailing_slashes): Remove static var; now local. (do_move): New args for directory-relative names. All uses changed. Continue to pass full names as needed, for diagnostics and for lower-level functions that do not support directory-relative names. (movefile): Remove; no longer needed. (main): Handle target-directory in the new, cp-like way. * tests/install/basic-1.sh: * tests/mv/diag.sh: Adjust to match new diagnostic wording. --- NEWS | 2 +- src/cp.c | 57 --------------- src/install.c | 150 ++++++++++++++++++++------------------- src/mv.c | 143 ++++++++++++++++--------------------- src/system.h | 57 +++++++++++++++ tests/install/basic-1.sh | 2 +- tests/mv/diag.sh | 4 +- 7 files changed, 202 insertions(+), 213 deletions(-) diff --git a/NEWS b/NEWS index ebcd5cb2f..711f7811a 100644 --- a/NEWS +++ b/NEWS @@ -52,7 +52,7 @@ GNU coreutils NEWS -*- outline -*- ** Improvements - cp now uses openat and similar syscalls when copying to a directory. + cp, mv, and install now use openat-like syscalls when copying to a directory. This avoids some race conditions and should be more efficient. On macOS, cp creates a copy-on-write clone if source and destination diff --git a/src/cp.c b/src/cp.c index 3bcc0d681..d680eb01d 100644 --- a/src/cp.c +++ b/src/cp.c @@ -564,63 +564,6 @@ make_dir_parents_private (char const *const_dir, size_t src_offset, return true; } -/* Must F designate the working directory? */ - -ATTRIBUTE_PURE static bool -must_be_working_directory (char const *f) -{ - /* Return true for ".", "./.", ".///./", etc. */ - while (*f++ == '.') - { - if (*f != '/') - return !*f; - while (*++f == '/') - continue; - if (!*f) - return true; - } - return false; -} - -/* Return a file descriptor open to FILE, for use in openat. - As an optimization, return AT_FDCWD if FILE must be the working directory. - Fail if FILE is not a directory. - On failure return a negative value; this is -1 unless AT_FDCWD == -1. */ - -static int -target_directory_operand (char const *file) -{ - if (must_be_working_directory (file)) - return AT_FDCWD; - - int fd = open (file, O_PATHSEARCH | O_DIRECTORY); - - if (!O_DIRECTORY && 0 <= fd) - { - /* On old systems like Solaris 10 that do not support O_DIRECTORY, - check by hand whether FILE is a directory. */ - struct stat st; - int err; - if (fstat (fd, &st) != 0 ? (err = errno, true) - : !S_ISDIR (st.st_mode) && (err = ENOTDIR, true)) - { - close (fd); - errno = err; - fd = -1; - } - } - - return fd - (AT_FDCWD == -1 && fd < 0); -} - -/* Return true if FD represents success for target_directory_operand. */ - -static bool -target_dirfd_valid (int fd) -{ - return fd != -1 - (AT_FDCWD == -1); -} - /* Scan the arguments, and copy each by calling copy. Return true if successful. */ diff --git a/src/install.c b/src/install.c index b157f59d2..a27a5343b 100644 --- a/src/install.c +++ b/src/install.c @@ -61,10 +61,6 @@ static bool use_default_selinux_context = true; # define endpwent() ((void) 0) #endif -#if ! HAVE_LCHOWN -# define lchown(name, uid, gid) chown (name, uid, gid) -#endif - /* The user name that will own the files, or NULL to make the owner the current user ID. */ static char *owner_name; @@ -165,9 +161,11 @@ extra_mode (mode_t input) return !! (input & ~ mask); } -/* Return true if copy of file SRC_NAME to file DEST_NAME is necessary. */ +/* Return true if copy of file SRC_NAME to file DEST_NAME aka + DEST_DIRFD+DEST_RELNAME is necessary. */ static bool need_copy (char const *src_name, char const *dest_name, + int dest_dirfd, char const *dest_relname, const struct cp_options *x) { struct stat src_sb, dest_sb; @@ -181,7 +179,7 @@ need_copy (char const *src_name, char const *dest_name, if (lstat (src_name, &src_sb) != 0) return true; - if (lstat (dest_name, &dest_sb) != 0) + if (lstatat (dest_dirfd, dest_relname, &dest_sb) != 0) return true; if (!S_ISREG (src_sb.st_mode) || !S_ISREG (dest_sb.st_mode) @@ -241,7 +239,7 @@ need_copy (char const *src_name, char const *dest_name, if (src_fd < 0) return true; - dest_fd = open (dest_name, O_RDONLY | O_BINARY); + dest_fd = openat (dest_dirfd, dest_relname, O_RDONLY | O_BINARY); if (dest_fd < 0) { close (src_fd); @@ -353,28 +351,6 @@ setdefaultfilecon (char const *file) freecon (scontext); } -/* FILE is the last operand of this command. Return true if FILE is a - directory. But report an error there is a problem accessing FILE, - or if FILE does not exist but would have to refer to an existing - directory if it referred to anything at all. */ - -static bool -target_directory_operand (char const *file) -{ - char const *b = last_component (file); - size_t blen = strlen (b); - bool looks_like_a_dir = (blen == 0 || ISSLASH (b[blen - 1])); - struct stat st; - int err = (stat (file, &st) == 0 ? 0 : errno); - bool is_a_dir = !err && S_ISDIR (st.st_mode); - if (err && err != ENOENT) - die (EXIT_FAILURE, err, _("failed to access %s"), quoteaf (file)); - if (is_a_dir < looks_like_a_dir) - die (EXIT_FAILURE, err, _("target %s is not a directory"), - quoteaf (file)); - return is_a_dir; -} - /* Report that directory DIR was made, if OPTIONS requests this. */ static void announce_mkdir (char const *dir, void *options) @@ -431,15 +407,16 @@ process_dir (char *dir, struct savewd *wd, void *options) return ret; } -/* Copy file FROM onto file TO, creating TO if necessary. - Return true if successful. */ +/* Copy file FROM onto file TO aka TO_DIRFD+TO_RELNAME, creating TO if + necessary. Return true if successful. */ static bool -copy_file (char const *from, char const *to, const struct cp_options *x) +copy_file (char const *from, char const *to, + int to_dirfd, char const *to_relname, const struct cp_options *x) { bool copy_into_self; - if (copy_only_if_needed && !need_copy (from, to, x)) + if (copy_only_if_needed && !need_copy (from, to, to_dirfd, to_relname, x)) return true; /* Allow installing from non-regular files like /dev/null. @@ -448,14 +425,14 @@ copy_file (char const *from, char const *to, const struct cp_options *x) However, since !x->recursive, the call to "copy" will fail if FROM is a directory. */ - return copy (from, to, AT_FDCWD, to, 0, x, ©_into_self, NULL); + return copy (from, to, to_dirfd, to_relname, 0, x, ©_into_self, NULL); } -/* Set the attributes of file or directory NAME. +/* Set the attributes of file or directory NAME aka DIRFD+RELNAME. Return true if successful. */ static bool -change_attributes (char const *name) +change_attributes (char const *name, int dirfd, char const *relname) { bool ok = false; /* chown must precede chmod because on some systems, @@ -471,9 +448,9 @@ change_attributes (char const *name) want to know. */ if (! (owner_id == (uid_t) -1 && group_id == (gid_t) -1) - && lchown (name, owner_id, group_id) != 0) + && lchownat (dirfd, relname, owner_id, group_id) != 0) error (0, errno, _("cannot change ownership of %s"), quoteaf (name)); - else if (chmod (name, mode) != 0) + else if (chmodat (dirfd, relname, mode) != 0) error (0, errno, _("cannot change permissions of %s"), quoteaf (name)); else ok = true; @@ -484,17 +461,18 @@ change_attributes (char const *name) return ok; } -/* Set the timestamps of file DEST to match those of SRC_SB. +/* Set the timestamps of file DEST aka DIRFD+RELNAME to match those of SRC_SB. Return true if successful. */ static bool -change_timestamps (struct stat const *src_sb, char const *dest) +change_timestamps (struct stat const *src_sb, char const *dest, + int dirfd, char const *relname) { struct timespec timespec[2]; timespec[0] = get_stat_atime (src_sb); timespec[1] = get_stat_mtime (src_sb); - if (utimens (dest, timespec)) + if (utimensat (dirfd, relname, timespec, 0)) { error (0, errno, _("cannot set timestamps for %s"), quoteaf (dest)); return false; @@ -653,12 +631,13 @@ In the 4th form, create all components of the given DIRECTORY(ies).\n\ exit (status); } -/* Copy file FROM onto file TO and give TO the appropriate - attributes. +/* Copy file FROM onto file TO aka TO_DIRFD+TO_RELNAME and give TO the + appropriate attributes. X gives the command options. Return true if successful. */ static bool install_file_in_file (char const *from, char const *to, + int to_dirfd, char const *to_relname, const struct cp_options *x) { struct stat from_sb; @@ -667,19 +646,19 @@ install_file_in_file (char const *from, char const *to, error (0, errno, _("cannot stat %s"), quoteaf (from)); return false; } - if (! copy_file (from, to, x)) + if (! copy_file (from, to, to_dirfd, to_relname, x)) return false; if (strip_files) if (! strip (to)) { - if (unlink (to) != 0) /* Cleanup. */ + if (unlinkat (to_dirfd, to_relname, 0) != 0) /* Cleanup. */ die (EXIT_FAILURE, errno, _("cannot unlink %s"), quoteaf (to)); return false; } if (x->preserve_timestamps && (strip_files || ! S_ISREG (from_sb.st_mode)) - && ! change_timestamps (&from_sb, to)) + && ! change_timestamps (&from_sb, to, to_dirfd, to_relname)) return false; - return change_attributes (to); + return change_attributes (to, to_dirfd, to_relname); } /* Create any missing parent directories of TO, @@ -731,7 +710,7 @@ install_file_in_file_parents (char const *from, char *to, const struct cp_options *x) { return (mkancesdirs_safe_wd (from, to, (struct cp_options *)x, false) - && install_file_in_file (from, to, x)); + && install_file_in_file (from, to, AT_FDCWD, to, x)); } /* Copy file FROM into directory TO_DIR, keeping its same name, @@ -740,16 +719,39 @@ install_file_in_file_parents (char const *from, char *to, static bool install_file_in_dir (char const *from, char const *to_dir, - const struct cp_options *x, bool mkdir_and_install) + const struct cp_options *x, bool mkdir_and_install, + int *target_dirfd) { char const *from_base = last_component (from); - char *to = file_name_concat (to_dir, from_base, NULL); + char *to_relname; + char *to = file_name_concat (to_dir, from_base, &to_relname); bool ret = true; - if (mkdir_and_install) - ret = mkancesdirs_safe_wd (from, to, (struct cp_options *)x, true); + if (!target_dirfd_valid (*target_dirfd) + && (ret = mkdir_and_install) + && (ret = mkancesdirs_safe_wd (from, to, (struct cp_options *) x, true))) + { + int fd = open (to_dir, O_PATHSEARCH | O_DIRECTORY); + if (fd < 0) + { + error (0, errno, _("cannot open %s"), quoteaf (to)); + ret = false; + } + else + *target_dirfd = fd; + } + + if (ret) + { + int to_dirfd = *target_dirfd; + if (!target_dirfd_valid (to_dirfd)) + { + to_dirfd = AT_FDCWD; + to_relname = to; + } + ret = install_file_in_file (from, to, to_dirfd, to_relname, x); + } - ret = ret && install_file_in_file (from, to, x); free (to); return ret; } @@ -899,18 +901,6 @@ main (int argc, char **argv) die (EXIT_FAILURE, 0, _("target directory not allowed when installing a directory")); - if (target_directory) - { - struct stat st; - bool stat_success = stat (target_directory, &st) == 0 ? true : false; - if (! mkdir_and_install && ! stat_success) - die (EXIT_FAILURE, errno, _("failed to access %s"), - quoteaf (target_directory)); - if (stat_success && ! S_ISDIR (st.st_mode)) - die (EXIT_FAILURE, 0, _("target %s is not a directory"), - quoteaf (target_directory)); - } - x.backup_type = (make_backups ? xget_version (_("backup type"), version_control_string) @@ -939,6 +929,7 @@ main (int argc, char **argv) usage (EXIT_FAILURE); } + int target_dirfd = AT_FDCWD; if (no_target_directory) { if (target_directory) @@ -951,13 +942,26 @@ main (int argc, char **argv) usage (EXIT_FAILURE); } } - else if (! (dir_arg || target_directory)) + else if (target_directory) { - if (2 <= n_files && target_directory_operand (file[n_files - 1])) - target_directory = file[--n_files]; + target_dirfd = target_directory_operand (target_directory); + if (! (target_dirfd_valid (target_dirfd) + || (mkdir_and_install && errno == ENOENT))) + die (EXIT_FAILURE, errno, _("failed to access %s"), + quoteaf (target_directory)); + } + else if (!dir_arg) + { + char const *lastfile = file[n_files - 1]; + int fd = target_directory_operand (lastfile); + if (target_dirfd_valid (fd)) + { + target_dirfd = fd; + target_directory = lastfile; + n_files--; + } else if (2 < n_files) - die (EXIT_FAILURE, 0, _("target %s is not a directory"), - quoteaf (file[n_files - 1])); + die (EXIT_FAILURE, errno, _("target %s"), quoteaf (lastfile)); } if (specified_mode) @@ -1006,7 +1010,8 @@ main (int argc, char **argv) { if (! (mkdir_and_install ? install_file_in_file_parents (file[0], file[1], &x) - : install_file_in_file (file[0], file[1], &x))) + : install_file_in_file (file[0], file[1], AT_FDCWD, + file[1], &x))) exit_status = EXIT_FAILURE; } else @@ -1015,7 +1020,8 @@ main (int argc, char **argv) dest_info_init (&x); for (i = 0; i < n_files; i++) if (! install_file_in_dir (file[i], target_directory, &x, - i == 0 && mkdir_and_install)) + i == 0 && mkdir_and_install, + &target_dirfd)) exit_status = EXIT_FAILURE; #ifdef lint dest_info_free (&x); diff --git a/src/mv.c b/src/mv.c index b5eb169f3..fcf32cd43 100644 --- a/src/mv.c +++ b/src/mv.c @@ -50,9 +50,6 @@ enum STRIP_TRAILING_SLASHES_OPTION = CHAR_MAX + 1 }; -/* Remove any trailing slashes from each SOURCE argument. */ -static bool remove_trailing_slashes; - static struct option const long_options[] = { {"backup", optional_argument, NULL, 'b'}, @@ -146,31 +143,18 @@ cp_option_init (struct cp_options *x) x->src_info = NULL; } -/* FILE is the last operand of this command. Return true if FILE is a - directory. But report an error if there is a problem accessing FILE, other - than nonexistence (errno == ENOENT). */ - -static bool -target_directory_operand (char const *file) -{ - struct stat st; - int err = (stat (file, &st) == 0 ? 0 : errno); - bool is_a_dir = !err && S_ISDIR (st.st_mode); - if (err && err != ENOENT) - die (EXIT_FAILURE, err, _("failed to access %s"), quoteaf (file)); - return is_a_dir; -} - -/* Move SOURCE onto DEST. Handles cross-file-system moves. +/* Move SOURCE onto DEST aka DEST_DIRFD+DEST_RELNAME. + Handle cross-file-system moves. If SOURCE is a directory, DEST must not exist. Return true if successful. */ static bool -do_move (char const *source, char const *dest, const struct cp_options *x) +do_move (char const *source, char const *dest, + int dest_dirfd, char const *dest_relname, const struct cp_options *x) { bool copy_into_self; bool rename_succeeded; - bool ok = copy (source, dest, AT_FDCWD, dest, 0, x, + bool ok = copy (source, dest, dest_dirfd, dest_relname, 0, x, ©_into_self, &rename_succeeded); if (ok) @@ -246,43 +230,6 @@ do_move (char const *source, char const *dest, const struct cp_options *x) return ok; } -/* Move file SOURCE onto DEST. Handles the case when DEST is a directory. - Treat DEST as a directory if DEST_IS_DIR. - Return true if successful. */ - -static bool -movefile (char *source, char *dest, bool dest_is_dir, - const struct cp_options *x) -{ - bool ok; - - /* This code was introduced to handle the ambiguity in the semantics - of mv that is induced by the varying semantics of the rename function. - Some systems (e.g., GNU/Linux) have a rename function that honors a - trailing slash, while others (like Solaris 5,6,7) have a rename - function that ignores a trailing slash. I believe the GNU/Linux - rename semantics are POSIX and susv2 compliant. */ - - if (remove_trailing_slashes) - strip_trailing_slashes (source); - - if (dest_is_dir) - { - /* Treat DEST as a directory; build the full filename. */ - char const *src_basename = last_component (source); - char *new_dest = file_name_concat (dest, src_basename, NULL); - strip_trailing_slashes (new_dest); - ok = do_move (source, new_dest, x); - free (new_dest); - } - else - { - ok = do_move (source, dest, x); - } - - return ok; -} - void usage (int status) { @@ -343,7 +290,8 @@ main (int argc, char **argv) char const *backup_suffix = NULL; char *version_control_string = NULL; struct cp_options x; - char *target_directory = NULL; + bool remove_trailing_slashes = false; + char const *target_directory = NULL; bool no_target_directory = false; int n_files; char **file; @@ -387,16 +335,6 @@ main (int argc, char **argv) case 't': if (target_directory) die (EXIT_FAILURE, 0, _("multiple target directories specified")); - else - { - struct stat st; - if (stat (optarg, &st) != 0) - die (EXIT_FAILURE, errno, _("failed to access %s"), - quoteaf (optarg)); - if (! S_ISDIR (st.st_mode)) - die (EXIT_FAILURE, 0, _("target %s is not a directory"), - quoteaf (optarg)); - } target_directory = optarg; break; case 'T': @@ -443,6 +381,7 @@ main (int argc, char **argv) usage (EXIT_FAILURE); } + int target_dirfd = AT_FDCWD; if (no_target_directory) { if (target_directory) @@ -455,23 +394,60 @@ main (int argc, char **argv) usage (EXIT_FAILURE); } } - else if (!target_directory) + else if (target_directory) { - assert (2 <= n_files); + target_dirfd = target_directory_operand (target_directory); + if (! target_dirfd_valid (target_dirfd)) + die (EXIT_FAILURE, errno, _("target directory %s"), + quoteaf (target_directory)); + } + else + { + char const *lastfile = file[n_files - 1]; if (n_files == 2) - x.rename_errno = (renameatu (AT_FDCWD, file[0], AT_FDCWD, file[1], + x.rename_errno = (renameatu (AT_FDCWD, file[0], AT_FDCWD, lastfile, RENAME_NOREPLACE) ? errno : 0); - if (x.rename_errno != 0 && target_directory_operand (file[n_files - 1])) + if (x.rename_errno != 0) { - x.rename_errno = -1; - target_directory = file[--n_files]; + int fd = target_directory_operand (lastfile); + if (target_dirfd_valid (fd)) + { + x.rename_errno = -1; + target_dirfd = fd; + target_directory = lastfile; + n_files--; + } + else + { + /* The last operand LASTFILE cannot be opened as a directory. + If there are more than two operands, report an error. + + Also, report an error if LASTFILE is known to be a directory + even though it could not be opened, which can happen if + opening failed with EACCES on a platform lacking O_PATH. + In this case use stat to test whether LASTFILE is a + directory, in case opening a non-directory with (O_SEARCH + | O_DIRECTORY) failed with EACCES not ENOTDIR. */ + int err = errno; + struct stat st; + if (2 < n_files + || (O_PATHSEARCH == O_SEARCH && err == EACCES + && stat (lastfile, &st) == 0 && S_ISDIR (st.st_mode))) + die (EXIT_FAILURE, err, _("target %s"), quoteaf (lastfile)); + } } - else if (2 < n_files) - die (EXIT_FAILURE, 0, _("target %s is not a directory"), - quoteaf (file[n_files - 1])); } + /* Handle the ambiguity in the semantics of mv induced by the + varying semantics of the rename function. POSIX-compatible + systems (e.g., GNU/Linux) have a rename function that honors a + trailing slash in the source, while others (Solaris 9, FreeBSD + 7.2) have a rename function that ignores it. */ + if (remove_trailing_slashes) + for (int i = 0; i < n_files; i++) + strip_trailing_slashes (file[i]); + if (x.interactive == I_ALWAYS_NO) x.update = false; @@ -502,7 +478,14 @@ main (int argc, char **argv) for (int i = 0; i < n_files; ++i) { x.last_file = i + 1 == n_files; - ok &= movefile (file[i], target_directory, true, &x); + char const *source = file[i]; + char const *source_basename = last_component (source); + char *dest_relname; + char *dest = file_name_concat (target_directory, source_basename, + &dest_relname); + strip_trailing_slashes (dest_relname); + ok &= do_move (source, dest, target_dirfd, dest_relname, &x); + free (dest); } #ifdef lint @@ -512,7 +495,7 @@ main (int argc, char **argv) else { x.last_file = true; - ok = movefile (file[0], file[1], false, &x); + ok = do_move (file[0], file[1], AT_FDCWD, file[1], &x); } return ok ? EXIT_SUCCESS : EXIT_FAILURE; diff --git a/src/system.h b/src/system.h index 6f9ebbc7c..9f10579dc 100644 --- a/src/system.h +++ b/src/system.h @@ -107,6 +107,63 @@ enum { O_PATHSEARCH = O_PATH }; enum { O_PATHSEARCH = O_SEARCH }; #endif +/* Must F designate the working directory? */ + +ATTRIBUTE_PURE static inline bool +must_be_working_directory (char const *f) +{ + /* Return true for ".", "./.", ".///./", etc. */ + while (*f++ == '.') + { + if (*f != '/') + return !*f; + while (*++f == '/') + continue; + if (!*f) + return true; + } + return false; +} + +/* Return a file descriptor open to FILE, for use in openat. + As an optimization, return AT_FDCWD if FILE must be the working directory. + Fail if FILE is not a directory. + On failure return a negative value; this is -1 unless AT_FDCWD == -1. */ + +static inline int +target_directory_operand (char const *file) +{ + if (must_be_working_directory (file)) + return AT_FDCWD; + + int fd = open (file, O_PATHSEARCH | O_DIRECTORY); + + if (!O_DIRECTORY && 0 <= fd) + { + /* On old systems like Solaris 10 that do not support O_DIRECTORY, + check by hand whether FILE is a directory. */ + struct stat st; + int err; + if (fstat (fd, &st) != 0 ? (err = errno, true) + : !S_ISDIR (st.st_mode) && (err = ENOTDIR, true)) + { + close (fd); + errno = err; + fd = -1; + } + } + + return fd - (AT_FDCWD == -1 && fd < 0); +} + +/* Return true if FD represents success for target_directory_operand. */ + +static inline bool +target_dirfd_valid (int fd) +{ + return fd != -1 - (AT_FDCWD == -1); +} + #include #ifndef _D_EXACT_NAMLEN # define _D_EXACT_NAMLEN(dp) strlen ((dp)->d_name) diff --git a/tests/install/basic-1.sh b/tests/install/basic-1.sh index 83bec639b..690d591e5 100755 --- a/tests/install/basic-1.sh +++ b/tests/install/basic-1.sh @@ -131,7 +131,7 @@ EOF touch sub4/file_exists || framework_failure_ ginstall -t sub4/file_exists -Dv file >out 2>&1 && fail=1 compare - out <<\EOF || fail=1 -ginstall: target 'sub4/file_exists' is not a directory +ginstall: failed to access 'sub4/file_exists': Not a directory EOF # Ensure that -D with an already existing directory for -t's option argument diff --git a/tests/mv/diag.sh b/tests/mv/diag.sh index 92410699f..c0a558548 100755 --- a/tests/mv/diag.sh +++ b/tests/mv/diag.sh @@ -39,8 +39,8 @@ mv: missing file operand Try 'mv --help' for more information. mv: missing destination file operand after 'no-file' Try 'mv --help' for more information. -mv: target 'f1' is not a directory -mv: target 'f2' is not a directory +mv: target 'f1': Not a directory +mv: target directory 'f2': Not a directory EOF compare exp out || fail=1 -- 2.32.0