]> git.ipfire.org Git - thirdparty/coreutils.git/commitdiff
cp,mv: add --update=none-fail to fail if existing files
authorPádraig Brady <P@draigBrady.com>
Mon, 5 Feb 2024 15:55:07 +0000 (15:55 +0000)
committerPádraig Brady <P@draigBrady.com>
Mon, 26 Feb 2024 17:33:02 +0000 (17:33 +0000)
* src/cp.c (main): Add support for --update=none-fail to provide the
functionality of diagnosing files in the destination,
and exiting with failure status.
(usage): Mark -n as deprecated.
* src/mv.c: Likewise.
* src/copy.h: Add UPDATE_NONE_FAIL definition.
* src/system.h (emit_update_parameters_note): Add --update=none-fail
description.
* doc/coreutils.texi (cp invocation): Likewise.
Also mention why -n is deprecated.
* tests/mv/update.sh: Add a test case, including precedence
with -n and other --update options.
* tests/cp/cp-i.sh: Verify that --backup and --update=none{,-fail}
are mutually exclusive.
* tests/mv/mv-n.sh: Likewise.
* NEWS: Mention the new feature.
Addresses https://bugs.gnu.org/62572

NEWS
doc/coreutils.texi
src/copy.h
src/cp.c
src/mv.c
src/system.h
tests/cp/cp-i.sh
tests/mv/mv-n.sh
tests/mv/update.sh

diff --git a/NEWS b/NEWS
index c328a4d6e1a5392748e8f27e065d00fc3689cf27..8b1856cbd814e1854305c7cb3d60139110ab1354 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -64,6 +64,11 @@ GNU coreutils NEWS                                    -*- outline -*-
   cp now accepts the --keep-directory-symlink option (like tar), to preserve
   and follow exisiting symlinks to directories in the destination.
 
+  cp and mv now accept the --update=none-fail option, which is similar
+  to the --no-clobber option, except that existing files are diagnosed,
+  and the command exits with failure status if existing files.
+  The -n,--no-clobber option is best avoided due to platform differences.
+
   od now supports printing IEEE half precision floating point with -t fH,
   or brain 16 bit floating point with -t fB, where supported by the compiler.
 
index 7b7e8bcb00fa5b4f59e0a1aa3c8518b2f7edf9f0..9e3aa6c1c5463052a1ea969ee04a0504045fe3b8 100644 (file)
@@ -9060,7 +9060,10 @@ a regular file in the destination tree.
 Do not overwrite an existing file; silently skip instead.
 This option overrides a previous @option{-i} option.
 This option is mutually exclusive with @option{-b} or @option{--backup} option.
-See also the @option{--update} option.
+This option is deprecated due to having a different exit status from
+other platforms.  See also the @option{--update} option which will
+give more control over how to deal with existing files in the destination,
+and over the exit status in particular.
 
 @item -P
 @itemx --no-dereference
@@ -9334,6 +9337,10 @@ and results in all existing files in the destination being replaced.
 This is like the deprecated @option{--no-clobber} option, where no files in the
 destination are replaced, and also skipping a file does not induce a failure.
 
+@item none-fail
+This is similar to @samp{none}, in that no files in the destination
+are replaced, but any skipped files are diagnosed and induce a failure.
+
 @item older
 This is the default operation when @option{--update} is specified, and results
 in files being replaced if they're older than the corresponding source file.
index caf8755f95e5b040bd13e3065597eceb6f8a0b0e..dfa9435b38d0dbee708c3faf4e22712e944cf114 100644 (file)
@@ -68,6 +68,9 @@ enum Update_type
 
   /* Leave existing files.  */
   UPDATE_NONE,
+
+  /* Leave existing files, but exit failure if existing files.  */
+  UPDATE_NONE_FAIL,
 };
 
 /* This type is used to help mv (via copy.c) distinguish these cases.  */
index 36ae4fb66fd2d7a22c23bc0cb6a4a1db98c0a0f8..91c635af878a7908232f0af49d398f5ed3e2a892 100644 (file)
--- a/src/cp.c
+++ b/src/cp.c
@@ -123,7 +123,7 @@ static struct option const long_opts[] =
   {"force", no_argument, nullptr, 'f'},
   {"interactive", no_argument, nullptr, 'i'},
   {"link", no_argument, nullptr, 'l'},
-  {"no-clobber", no_argument, nullptr, 'n'},
+  {"no-clobber", no_argument, nullptr, 'n'},   /* Deprecated.  */
   {"no-dereference", no_argument, nullptr, 'P'},
   {"no-preserve", required_argument, nullptr, NO_PRESERVE_ATTRIBUTES_OPTION},
   {"no-target-directory", no_argument, nullptr, 'T'},
@@ -195,7 +195,7 @@ Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.\n\
   -L, --dereference            always follow symbolic links in SOURCE\n\
 "), stdout);
       fputs (_("\
-  -n, --no-clobber             silently skip existing files.\n\
+  -n, --no-clobber             (deprecated) silently skip exisiting files.\n\
                                  See also --update\n\
 "), stdout);
       fputs (_("\
@@ -228,8 +228,8 @@ Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.\n\
 "), stdout);
       fputs (_("\
   --update[=UPDATE]            control which existing files are updated;\n\
-                                 UPDATE={all,none,older(default)}.  See below\n\
-  -u                           equivalent to --update[=older]\n\
+                                 UPDATE={all,none,none-fail,older(default)}.\n\
+  -u                           equivalent to --update[=older].  See below\n\
 "), stdout);
       fputs (_("\
   -v, --verbose                explain what is being done\n\
@@ -1077,6 +1077,7 @@ main (int argc, char **argv)
         case 'n':
           x.interactive = I_ALWAYS_SKIP;
           no_clobber = true;
+          x.update = false;
           break;
 
         case 'P':
@@ -1140,13 +1141,12 @@ main (int argc, char **argv)
           break;
 
         case 'u':
-          if (optarg == nullptr)
-            x.update = true;
-          else if (! no_clobber)  /* -n takes precedence.  */
+          if (! no_clobber) /* -n > -u */
             {
-              enum Update_type update_opt;
-              update_opt = XARGMATCH ("--update", optarg,
-                                      update_type_string, update_type);
+              enum Update_type update_opt = UPDATE_OLDER;
+              if (optarg)
+                update_opt = XARGMATCH ("--update", optarg,
+                                        update_type_string, update_type);
               if (update_opt == UPDATE_ALL)
                 {
                   /* Default cp operation.  */
@@ -1158,6 +1158,11 @@ main (int argc, char **argv)
                   x.update = false;
                   x.interactive = I_ALWAYS_SKIP;
                 }
+              else if (update_opt == UPDATE_NONE_FAIL)
+                {
+                  x.update = false;
+                  x.interactive = I_ALWAYS_NO;
+                }
               else if (update_opt == UPDATE_OLDER)
                 {
                   x.update = true;
@@ -1227,13 +1232,12 @@ main (int argc, char **argv)
       usage (EXIT_FAILURE);
     }
 
-  if (x.interactive == I_ALWAYS_SKIP)
-    x.update = false;
-
-  if (make_backups && x.interactive == I_ALWAYS_SKIP)
+  if (make_backups
+      && (x.interactive == I_ALWAYS_SKIP
+          || x.interactive == I_ALWAYS_NO))
     {
       error (0, 0,
-             _("options --backup and --no-clobber are mutually exclusive"));
+             _("--backup is mutually exclusive with -n or --update=none-fail"));
       usage (EXIT_FAILURE);
     }
 
index 2a8c977a78163463f9387bda6461ca3c71e4adfd..9dc40fe3e82f8ffea68b726e10f909516525ac3f 100644 (file)
--- a/src/mv.c
+++ b/src/mv.c
@@ -54,11 +54,11 @@ enum
 
 static char const *const update_type_string[] =
 {
-  "all", "none", "older", nullptr
+  "all", "none", "none-fail", "older", nullptr
 };
 static enum Update_type const update_type[] =
 {
-  UPDATE_ALL, UPDATE_NONE, UPDATE_OLDER,
+  UPDATE_ALL, UPDATE_NONE, UPDATE_NONE_FAIL, UPDATE_OLDER,
 };
 ARGMATCH_VERIFY (update_type_string, update_type);
 
@@ -69,7 +69,7 @@ static struct option const long_options[] =
   {"debug", no_argument, nullptr, DEBUG_OPTION},
   {"force", no_argument, nullptr, 'f'},
   {"interactive", no_argument, nullptr, 'i'},
-  {"no-clobber", no_argument, nullptr, 'n'},
+  {"no-clobber", no_argument, nullptr, 'n'},   /* Deprecated.  */
   {"no-copy", no_argument, nullptr, NO_COPY_OPTION},
   {"no-target-directory", no_argument, nullptr, 'T'},
   {"strip-trailing-slashes", no_argument, nullptr,
@@ -290,8 +290,8 @@ If you specify more than one of -i, -f, -n, only the final one takes effect.\n\
 "), stdout);
       fputs (_("\
   --update[=UPDATE]            control which existing files are updated;\n\
-                                 UPDATE={all,none,older(default)}.  See below\n\
-  -u                           equivalent to --update[=older]\n\
+                                 UPDATE={all,none,none-fail,older(default)}.\n\
+  -u                           equivalent to --update[=older].  See below\n\
 "), stdout);
       fputs (_("\
   -v, --verbose                explain what is being done\n\
@@ -356,6 +356,7 @@ main (int argc, char **argv)
         case 'n':
           x.interactive = I_ALWAYS_SKIP;
           no_clobber = true;
+          x.update = false;
           break;
         case DEBUG_OPTION:
           x.debug = x.verbose = true;
@@ -375,13 +376,12 @@ main (int argc, char **argv)
           no_target_directory = true;
           break;
         case 'u':
-          if (optarg == nullptr)
-            x.update = true;
-          else if (! no_clobber)  /* -n takes precedence.  */
+          if (! no_clobber)
             {
-              enum Update_type update_opt;
-              update_opt = XARGMATCH ("--update", optarg,
-                                      update_type_string, update_type);
+              enum Update_type update_opt = UPDATE_OLDER;
+              if (optarg)
+                update_opt = XARGMATCH ("--update", optarg,
+                                        update_type_string, update_type);
               if (update_opt == UPDATE_ALL)
                 {
                   /* Default mv operation.  */
@@ -393,6 +393,11 @@ main (int argc, char **argv)
                   x.update = false;
                   x.interactive = I_ALWAYS_SKIP;
                 }
+              else if (update_opt == UPDATE_NONE_FAIL)
+                {
+                  x.update = false;
+                  x.interactive = I_ALWAYS_NO;
+                }
               else if (update_opt == UPDATE_OLDER)
                 {
                   x.update = true;
@@ -508,13 +513,12 @@ main (int argc, char **argv)
     for (int i = 0; i < n_files; i++)
       strip_trailing_slashes (file[i]);
 
-  if (x.interactive == I_ALWAYS_SKIP)
-    x.update = false;
-
-  if (make_backups && x.interactive == I_ALWAYS_SKIP)
+  if (make_backups
+      && (x.interactive == I_ALWAYS_SKIP
+          || x.interactive == I_ALWAYS_NO))
     {
       error (0, 0,
-             _("options --backup and --no-clobber are mutually exclusive"));
+             _("--backup is mutually exclusive with -n or --update=none-fail"));
       usage (EXIT_FAILURE);
     }
 
index d9e169c6ed52309db493c4bfef8509919ed64429..69aed6ea25e938300ecd04d99b869b77895a534b 100644 (file)
@@ -598,6 +598,8 @@ UPDATE controls which existing files in the destination are replaced.\n\
 and results in all existing files in the destination being replaced.\n\
 'none' is like the --no-clobber option, in that no files in the\n\
 destination are replaced, and skipped files do not induce a failure.\n\
+'none-fail' also ensures no files are replaced in the destination,\n\
+but any skipped files are diagnosed and induce a failure.\n\
 'older' is the default operation when --update is specified, and results\n\
 in files being replaced if they're older than the corresponding source file.\n\
 "), stdout);
index 02a992c3a0c50fdb6a977e4c5e36ce0d3f145dd5..f99f743dc7ba5cefbb09c02012c192dfbc8949bd 100755 (executable)
@@ -66,5 +66,8 @@ compare out7 out_empty || fail=1
 
 # options --backup and --no-clobber are mutually exclusive
 returns_ 1 cp -bn c d 2>/dev/null || fail=1
+# options --backup and --update=none{,-fail} are mutually exclusive
+returns_ 1 cp -b --update=none c d 2>/dev/null || fail=1
+returns_ 1 cp -b --update=none-fail c d 2>/dev/null || fail=1
 
 Exit $fail
index 9bd3866ccbb62bb7b34e0499401fbd83c095f152..627c97aee5cc0bb67ae2543388ac87c0240c2127 100755 (executable)
@@ -59,5 +59,8 @@ compare out5 out_empty || fail=1
 # options --backup and --no-clobber are mutually exclusive
 touch a || framework_failure_
 returns_ 1 mv -bn a b 2>/dev/null || fail=1
+# options --backup and --update=none{,-fail} are mutually exclusive
+returns_ 1 mv -b --update=none a b 2>/dev/null || fail=1
+returns_ 1 mv -b --update=none-fail a b 2>/dev/null || fail=1
 
 Exit $fail
index 049c7384a80cdc87991ce0b870b1f1b73fa10746..16435780369bcfbc3a232ff25d28202c83ef1a63 100755 (executable)
@@ -53,15 +53,20 @@ for update_option in '--update' '--update=older' '--update=all' \
 done
 
 # These should not perform the rename / copy
-for update_option in '--update=none' \
- '--update=all --update=none'; do
+for update_option in '--update=none' '--update=none-fail' \
+ '--update=all --update=none' \
+ '--update=all --no-clobber' \
+ '--no-clobber --update=all'; do
+
+  echo "$update_option" | grep 'fail' >/dev/null && ret=1 || ret=0
+
   test_reset
-  mv $update_option new old || fail=1
+  returns_ $ret mv $update_option new old || fail=1
   case "$(cat new)" in new) ;; *) fail=1 ;; esac
   case "$(cat old)" in old) ;; *) fail=1 ;; esac
 
   test_reset
-  cp $update_option new old || fail=1
+  returns_ $ret cp $update_option new old || fail=1
   case "$(cat new)" in new) ;; *) fail=1 ;; esac
   case "$(cat old)" in old) ;; *) fail=1 ;; esac
 done