/* mv -- move or rename files Copyright (C) 86, 89, 90, 91, 95, 96, 1997 Free Software Foundation, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ /* Options: -f, --force Assume a 'y' answer to all questions it would normally ask, and not ask the questions. -i, --interactive Require confirmation from the user before performing any move that would destroy an existing file. -u, --update Do not move a nondirectory that has an existing destination with the same or newer modification time. -v, --verbose List the name of each file as it is moved, and the name it is moved to. -b, --backup -S, --suffix -V, --version-control Backup file creation. Written by Mike Parker and David MacKenzie */ #ifdef _AIX #pragma alloca #endif #include #include #include #include #include "system.h" #include "path-concat.h" #include "backupfile.h" #include "error.h" #ifdef HAVE_LCHOWN # define chown(PATH, OWNER, GROUP) lchown(PATH, OWNER, GROUP) #endif char *base_name (); enum backup_type get_version (); int isdir (); int yesno (); int safe_read (); int full_write (); int euidaccess (); /* The name this program was run with. */ char *program_name; /* If nonzero, query the user before overwriting files. */ static int interactive; /* If nonzero, do not query the user before overwriting unwritable files. */ static int override_mode; /* If nonzero, do not move a nondirectory that has an existing destination with the same or newer modification time. */ static int update = 0; /* If nonzero, list each file as it is moved. */ static int verbose; /* If nonzero, stdin is a tty. */ static int stdin_tty; /* If nonzero, display usage information and exit. */ static int show_help; /* If nonzero, print the version on standard output and exit. */ static int show_version; static struct option const long_options[] = { {"backup", no_argument, NULL, 'b'}, {"force", no_argument, NULL, 'f'}, {"interactive", no_argument, NULL, 'i'}, {"suffix", required_argument, NULL, 'S'}, {"update", no_argument, &update, 1}, {"verbose", no_argument, &verbose, 1}, {"version-control", required_argument, NULL, 'V'}, {"help", no_argument, &show_help, 1}, {"version", no_argument, &show_version, 1}, {NULL, 0, NULL, 0} }; /* If PATH is an existing directory, return nonzero, else 0. */ static int is_real_dir (const char *path) { struct stat stats; return lstat (path, &stats) == 0 && S_ISDIR (stats.st_mode); } /* Apply as many of the file attributes (the struct stat fields: st_atime, st_mtime, st_uid, st_gid, st_mode) of ATTR to FILE as possible. Return non-zero if any operation failed; return zero otherwise. */ static int apply_attributes (const char *file, const struct stat *attr) { struct utimbuf tv; mode_t mode = attr->st_mode; int fail = 0; /* Try to apply the modtime and access time. */ tv.actime = attr->st_atime; tv.modtime = attr->st_mtime; if (utime (file, &tv)) { error (0, errno, "%s: unable to restore file times", file); fail = 1; } /* chown would turn off set[ug]id bits for non-root, so do the chown before the chmod. */ /* Try to apply group ID and owner ID. */ if (chown (file, attr->st_uid, attr->st_gid)) { error (0, errno, "%s: unable to restore owner and group IDs", file); /* If the owner and group cannot be preserved, then mask off any setgid and setuid bits. */ mode &= (~(S_ISUID | S_ISGID)); fail = 1; } /* Try to apply file mode. */ if (chmod (file, mode & 07777)) { error (0, errno, "%s: unable to restore file mode", file); fail = 1; } return fail; } /* Copy regular file SOURCE onto file DEST. SOURCE_STATS must be the result of calling lstat on SOURCE. Return 1 if an error occurred, 0 if successful. */ static int copy_reg (const char *source, const char *dest, const struct stat *source_stats) { int ifd; int ofd; char buf[1024 * 8]; int len; /* Number of bytes read into `buf'. */ if (!S_ISREG (source_stats->st_mode)) { error (0, 0, _("cannot move `%s' across filesystems: Not a regular file"), source); return 1; } if (unlink (dest) && errno != ENOENT) { error (0, errno, _("cannot remove `%s'"), dest); return 1; } ifd = open (source, O_RDONLY, 0); if (ifd < 0) { error (0, errno, "%s", source); return 1; } ofd = open (dest, O_WRONLY | O_CREAT | O_TRUNC, 0600); if (ofd < 0) { error (0, errno, "%s", dest); close (ifd); return 1; } while ((len = safe_read (ifd, buf, sizeof (buf))) > 0) { if (full_write (ofd, buf, len) < 0) { error (0, errno, "%s", dest); close (ifd); close (ofd); unlink (dest); return 1; } } if (len < 0) { error (0, errno, "%s", source); close (ifd); close (ofd); unlink (dest); return 1; } if (close (ifd) < 0) { error (0, errno, "%s", source); close (ofd); return 1; } if (close (ofd) < 0) { error (0, errno, "%s", dest); return 1; } /* Try to apply the attributes of SOURCE to DEST. Each failure gets a diagnostic, but POSIX requires that failure to preserve attributes not change mv's exit status. */ apply_attributes (dest, source_stats); return 0; } /* Move SOURCE onto DEST. Handles cross-filesystem moves. If SOURCE is a directory, DEST must not exist. Return 0 if successful, non-zero if an error occurred. */ static int do_move (const char *source, const char *dest) { char *dest_backup = NULL; struct stat source_stats; struct stat dest_stats; int fail; if (lstat (source, &source_stats) != 0) { error (0, errno, "%s", source); return 1; } if (lstat (dest, &dest_stats) == 0) { if (source_stats.st_dev == dest_stats.st_dev && source_stats.st_ino == dest_stats.st_ino) { error (0, 0, _("`%s' and `%s' are the same file"), source, dest); return 1; } if (S_ISDIR (dest_stats.st_mode)) { error (0, 0, _("%s: cannot overwrite directory"), dest); return 1; } if (!S_ISDIR (source_stats.st_mode) && update && source_stats.st_mtime <= dest_stats.st_mtime) return 0; if (!override_mode && (interactive || stdin_tty) && euidaccess (dest, W_OK)) { fprintf (stderr, _("%s: replace `%s', overriding mode %04o? "), program_name, dest, (unsigned int) (dest_stats.st_mode & 07777)); if (!yesno ()) return 0; } else if (interactive) { fprintf (stderr, _("%s: replace `%s'? "), program_name, dest); if (!yesno ()) return 0; } if (backup_type != none) { char *tmp_backup = find_backup_file_name (dest); if (tmp_backup == NULL) error (1, 0, _("virtual memory exhausted")); dest_backup = (char *) alloca (strlen (tmp_backup) + 1); strcpy (dest_backup, tmp_backup); free (tmp_backup); if (rename (dest, dest_backup)) { if (errno != ENOENT) { error (0, errno, _("cannot backup `%s'"), dest); return 1; } else dest_backup = NULL; } } } else if (errno != ENOENT) { error (0, errno, "%s", dest); return 1; } if (verbose) printf ("%s -> %s\n", source, dest); /* Always try rename first. */ fail = rename (source, dest); if (fail) { /* This may mean SOURCE and DEST are on different devices. It may also (conceivably) mean that even though they are on the same device, rename isn't implemented for that device. E.g., (from Joel N. Weber), [...] there might someday be cases where you can't rename but you can copy where the device name is the same, especially on Hurd. Consider an ftpfs with a primitive ftp server that supports uploading, downloading and deleting, but not renaming. Also, note that comparing device numbers is not a reliable check for `can-rename'. Some systems can be set up so that files from many different physical devices all have the same st_dev field. This is a feature of some NFS mounting configurations. Try copying-then-removing SOURCE instead. This function used to resort to copying only when rename failed and set errno to EXDEV. */ fail = copy_reg (source, dest, &source_stats); if (fail) { /* Restore original destination file DEST if made a backup. */ if (dest_backup && rename (dest_backup, dest)) error (0, errno, _("cannot un-backup `%s'"), dest); } else { fail = unlink (source); if (fail) error (0, errno, _("cannot remove `%s'"), source); } } return fail; } static int strip_trailing_slashes_2 (char *path) { char *end_p = path + strlen (path) - 1; char *slash = end_p; while (slash > path && *slash == '/') *slash-- = '\0'; return slash < end_p; } /* Move file SOURCE onto DEST. Handles the case when DEST is a directory. DEST_IS_DIR must be nonzero when DEST is a directory or a symlink to a directory and zero otherwise. Return 0 if successful, non-zero if an error occurred. */ static int movefile (char *source, char *dest, int dest_is_dir) { int dest_had_trailing_slash = strip_trailing_slashes_2 (dest); int fail; /* In addition to when DEST is a directory, if DEST has a trailing slash and neither SOURCE nor DEST is a directory, presume the target is DEST/`basename source`. This converts `mv x y/' to `mv x y/x'. This change means that the command `mv any file/' will now fail rather than performing the move. The case when SOURCE is a directory and DEST is not is properly diagnosed by do_move. */ if (dest_is_dir || (dest_had_trailing_slash && !is_real_dir (source))) { /* DEST is a directory; build full target filename. */ char *src_basename; char *new_dest; /* Remove trailing slashes before taking base_name. Otherwise, base_name ("a/") returns "". */ strip_trailing_slashes_2 (source); src_basename = base_name (source); new_dest = path_concat (dest, src_basename, NULL); if (new_dest == NULL) error (1, 0, _("virtual memory exhausted")); fail = do_move (source, new_dest); free (new_dest); } else { fail = do_move (source, dest); } return fail; } static void usage (int status) { if (status != 0) fprintf (stderr, _("Try `%s --help' for more information.\n"), program_name); else { printf (_("\ Usage: %s [OPTION]... SOURCE DEST\n\ or: %s [OPTION]... SOURCE... DIRECTORY\n\ "), program_name, program_name); printf (_("\ Rename SOURCE to DEST, or move SOURCE(s) to DIRECTORY.\n\ \n\ -b, --backup make backup before removal\n\ -f, --force remove existing destinations, never prompt\n\ -i, --interactive prompt before overwrite\n\ -S, --suffix=SUFFIX override the usual backup suffix\n\ -u, --update move only older or brand new files\n\ -v, --verbose explain what is being done\n\ -V, --version-control=WORD override the usual version control\n\ --help display this help and exit\n\ --version output version information and exit\n\ \n\ ")); printf (_("\ The backup suffix is ~, unless set with SIMPLE_BACKUP_SUFFIX. The\n\ version control may be set with VERSION_CONTROL, values are:\n\ \n\ t, numbered make numbered backups\n\ nil, existing numbered if numbered backups exist, simple otherwise\n\ never, simple always make simple backups\n\ ")); puts (_("\nReport bugs to .")); } exit (status); } int main (int argc, char **argv) { int c; int errors; int make_backups = 0; int dest_is_dir; char *version; program_name = argv[0]; setlocale (LC_ALL, ""); bindtextdomain (PACKAGE, LOCALEDIR); textdomain (PACKAGE); version = getenv ("SIMPLE_BACKUP_SUFFIX"); if (version) simple_backup_suffix = version; version = getenv ("VERSION_CONTROL"); interactive = override_mode = verbose = update = 0; errors = 0; while ((c = getopt_long (argc, argv, "bfiuvS:V:", long_options, NULL)) != -1) { switch (c) { case 0: break; case 'b': make_backups = 1; break; case 'f': interactive = 0; override_mode = 1; break; case 'i': interactive = 1; override_mode = 0; break; case 'u': update = 1; break; case 'v': verbose = 1; break; case 'S': simple_backup_suffix = optarg; break; case 'V': version = optarg; break; default: usage (1); } } if (show_version) { printf ("mv (%s) %s\n", GNU_PACKAGE, VERSION); exit (0); } if (show_help) usage (0); if (argc < optind + 2) { error (0, 0, "%s", (argc == optind ? _("missing file arguments") : _("missing file argument"))); usage (1); } if (make_backups) backup_type = get_version (version); stdin_tty = isatty (STDIN_FILENO); dest_is_dir = isdir (argv[argc - 1]); if (argc > optind + 2 && !dest_is_dir) error (1, 0, _("when moving multiple files, last argument must be a directory")); /* Move each arg but the last onto the last. */ for (; optind < argc - 1; ++optind) errors |= movefile (argv[optind], argv[argc - 1], dest_is_dir); exit (errors); }