On 01/04/2023 00:29, Paul Eggert wrote:
On 2023-03-31 14:32, Pádraig Brady wrote:

Perhaps we should support:
    --no-clobber[={skip, fail (default)}]

so then users can at least easily change -n to --no-clobber=skip
to get the old behavior?

An alternative would be to augment the --update option to support:
    --update[={none, older (default)}]
where --update=none would be the equivalent of the old -n behavior.

The latter sounds a bit better but I suppose either would work. We could
generalize it a bit further, e.g.:

    --skip-diagnose[={yes,no}]
       Whether to diagnose a copying action being skipped.
    --skip-fail[={yes,no}]
       Whether exit status should be 1 when skipping a copying action.

Presumably similar options would apply to ln and mv.

All these extra options might be overkill, though.


Perhaps we should also diagnose files skipped in the -n fail case,
to make it easier for users to see what the issue is.

FreeBSD cp -n doesn't diagnose, and GNU cp -n has never diagnosed, so
it's probably better to leave sleeping dogs lie.

OK first stab at --update=none support is attached.

cheers,
Pádraig
From 1d0617258b4e9605e348679df3610c356c8377b0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <p...@draigbrady.com>
Date: Sat, 1 Apr 2023 16:27:52 +0100
Subject: [PATCH] cp,mv: add --update=none to always skip existing files

Add --update=none which is equivalent to the --no-clobber behavior
from before coreutils 9.2.  I.e. existing files are unconditionally
skipped, and them not being copied does not affect the exit status.

* src/copy.h [enum Update_type]: A new type to support parameters
to the --update command line option.
[enum Interactive]: Add I_ALWAYS_SKIP.
* src/copy.c: Treat I_ALWAYS_SKIP like I_ALWAYS_NO (-n),
except that we don't fail when skipping.
* src/cp.c (main): Parse --update arguments, ensuring that
-n takes precedence if specified.
(usage): Describe the new option.  Also allude that
-u is related in the -n description.
* src/mv.c: TODO.
* doc/coreutils.texi: TODO.
* NEWS: Mention the new feature.
Addresses https://bugs.gnu.org/62572
---
 NEWS       |  8 +++++++-
 src/copy.c | 15 ++++++++++-----
 src/copy.h | 16 +++++++++++++++-
 src/cp.c   | 44 ++++++++++++++++++++++++++++++++++++++++----
 4 files changed, 72 insertions(+), 11 deletions(-)

diff --git a/NEWS b/NEWS
index f53adab6f..8efd5a5f0 100644
--- a/NEWS
+++ b/NEWS
@@ -4,7 +4,7 @@ GNU coreutils NEWS                                    -*- 
outline -*-
 
 ** Bug fixes
 
-  cp --relink=auto (the default), mv, and install
+  cp --reflink=auto (the default), mv, and install
   will again fall back to a standard copy in more cases.
   Previously copies could fail with permission errors on
   more restricted systems like android or containers etc.
@@ -22,6 +22,12 @@ GNU coreutils NEWS                                    -*- 
outline -*-
   wc will now diagnose if any total counts have overflowed.
   [This bug was present in "the beginning".]
 
+** New features
+
+  cp and mv now support --update=none to always skip existing files
+  in the destination, while not impacting the exit status.
+  This is equivalent to the --no-clobber behavior from before v9.2.
+
 
 * Noteworthy changes in release 9.2 (2023-03-20) [stable]
 
diff --git a/src/copy.c b/src/copy.c
index a8aa14920..e7e14c150 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -2061,6 +2061,7 @@ abandon_move (const struct cp_options *x,
 {
   assert (x->move_mode);
   return (x->interactive == I_ALWAYS_NO
+          || x->interactive == I_ALWAYS_SKIP
           || ((x->interactive == I_ASK_USER
                || (x->interactive == I_UNSPECIFIED
                    && x->stdin_tty
@@ -2234,7 +2235,8 @@ copy_internal (char const *src_name, char const *dst_name,
 
   if (rename_errno == 0
       ? !x->last_file
-      : rename_errno != EEXIST || x->interactive != I_ALWAYS_NO)
+      : rename_errno != EEXIST
+        || (x->interactive != I_ALWAYS_NO && x->interactive != I_ALWAYS_SKIP))
     {
       char const *name = rename_errno == 0 ? dst_name : src_name;
       int dirfd = rename_errno == 0 ? dst_dirfd : AT_FDCWD;
@@ -2288,7 +2290,9 @@ copy_internal (char const *src_name, char const *dst_name,
 
   if (nonexistent_dst <= 0)
     {
-      if (! (rename_errno == EEXIST && x->interactive == I_ALWAYS_NO))
+      if (! (rename_errno == EEXIST
+             && (x->interactive == I_ALWAYS_NO
+                 || x->interactive == I_ALWAYS_SKIP)))
         {
           /* Regular files can be created by writing through symbolic
              links, but other files cannot.  So use stat on the
@@ -2330,7 +2334,7 @@ copy_internal (char const *src_name, char const *dst_name,
         {
           bool return_now = false;
 
-          if (x->interactive != I_ALWAYS_NO
+          if ((x->interactive != I_ALWAYS_NO && x->interactive != 
I_ALWAYS_SKIP)
               && ! same_file_ok (src_name, &src_sb, dst_dirfd, drelname,
                                  &dst_sb, x, &return_now))
             {
@@ -2400,17 +2404,18 @@ copy_internal (char const *src_name, char const 
*dst_name,
                      doesn't end up removing the source file.  */
                   if (rename_succeeded)
                     *rename_succeeded = true;
-                  return false;
+                  return x->interactive == I_ALWAYS_SKIP;
                 }
             }
           else
             {
               if (! S_ISDIR (src_mode)
                   && (x->interactive == I_ALWAYS_NO
+                      || x->interactive == I_ALWAYS_SKIP
                       || (x->interactive == I_ASK_USER
                           && ! overwrite_ok (x, dst_name, dst_dirfd,
                                              dst_relname, &dst_sb))))
-                return false;
+                return x->interactive == I_ALWAYS_SKIP;
             }
 
           if (return_now)
diff --git a/src/copy.h b/src/copy.h
index b02aa2bbb..f1778c7ed 100644
--- a/src/copy.h
+++ b/src/copy.h
@@ -57,11 +57,25 @@ enum Reflink_type
   REFLINK_ALWAYS
 };
 
+/* Control how existing destination files are updated.  */
+enum Update_type
+{
+  /* Always Overwrite.  */
+  UPDATE_OVERWRITE,
+
+  /* Update if dest older.  */
+  UPDATE_OLDER,
+
+  /* Leave existing files.  */
+  UPDATE_NONE,
+};
+
 /* This type is used to help mv (via copy.c) distinguish these cases.  */
 enum Interactive
 {
   I_ALWAYS_YES = 1,
-  I_ALWAYS_NO,
+  I_ALWAYS_NO,       /* Skip and fail.   */
+  I_ALWAYS_SKIP,     /* Skip and ignore. */
   I_ASK_USER,
   I_UNSPECIFIED
 };
diff --git a/src/cp.c b/src/cp.c
index 75ae7de47..010453f9f 100644
--- a/src/cp.c
+++ b/src/cp.c
@@ -102,6 +102,16 @@ static enum Reflink_type const reflink_type[] =
 };
 ARGMATCH_VERIFY (reflink_type_string, reflink_type);
 
+static char const *const update_type_string[] =
+{
+  "none", "older", NULL
+};
+static enum Update_type const update_type[] =
+{
+  UPDATE_NONE, UPDATE_OLDER,
+};
+ARGMATCH_VERIFY (update_type_string, update_type);
+
 static struct option const long_opts[] =
 {
   {"archive", no_argument, NULL, 'a'},
@@ -129,7 +139,7 @@ static struct option const long_opts[] =
   {"suffix", required_argument, NULL, 'S'},
   {"symbolic-link", no_argument, NULL, 's'},
   {"target-directory", required_argument, NULL, 't'},
-  {"update", no_argument, NULL, 'u'},
+  {"update", optional_argument, NULL, 'u'},
   {"verbose", no_argument, NULL, 'v'},
   {GETOPT_SELINUX_CONTEXT_OPTION_DECL},
   {GETOPT_HELP_OPTION_DECL},
@@ -183,7 +193,7 @@ Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.\n\
 "), stdout);
       fputs (_("\
   -n, --no-clobber             do not overwrite an existing file (overrides\n\
-                                 a previous -i option)\n\
+                                 a -u or previous -i option)\n\
   -P, --no-dereference         never follow symbolic links in SOURCE\n\
 "), stdout);
       fputs (_("\
@@ -212,9 +222,11 @@ Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.\n\
   -T, --no-target-directory    treat DEST as a normal file\n\
 "), stdout);
       fputs (_("\
-  -u, --update                 copy only when the SOURCE file is newer\n\
+  -u                           only update when the SOURCE file is newer\n\
                                  than the destination file or when the\n\
                                  destination file is missing\n\
+  --update[=UPDATE_MODE]       like -u, but support skipping existing files;\n\
+                                 UPDATE_MODE={none,older(default)}\n\
   -v, --verbose                explain what is being done\n\
   -x, --one-file-system        stay on this file system\n\
 "), stdout);
@@ -1103,7 +1115,24 @@ main (int argc, char **argv)
           break;
 
         case 'u':
-          x.update = true;
+          if (optarg == NULL)
+            x.update = true;
+          else if (x.interactive != I_ALWAYS_NO)  /* -n takes precedence.  */
+            {
+              enum Update_type update_opt;
+              update_opt = XARGMATCH ("--update", optarg,
+                                      update_type_string, update_type);
+              if (update_opt == UPDATE_NONE)
+                {
+                  x.update = false;
+                  x.interactive = I_ALWAYS_SKIP;
+                }
+              else if (update_opt == UPDATE_OLDER)
+                {
+                  x.update = true;
+                  x.interactive = I_UNSPECIFIED;
+                }
+            }
           break;
 
         case 'v':
@@ -1166,6 +1195,13 @@ main (int argc, char **argv)
       usage (EXIT_FAILURE);
     }
 
+  if (make_backups && x.interactive == I_ALWAYS_SKIP)
+    {
+      error (0, 0,
+             _("options --backup and --update=none are mutually exclusive"));
+      usage (EXIT_FAILURE);
+    }
+
   if (x.reflink_mode == REFLINK_ALWAYS && x.sparse_mode != SPARSE_AUTO)
     {
       error (0, 0, _("--reflink can be used only with --sparse=auto"));
-- 
2.26.2

Reply via email to