]> git.ipfire.org Git - thirdparty/util-linux.git/commitdiff
rename: add --copy option to copy instead of rename
authorWanBingjiang <wanbingjiang@webray.com.cn>
Fri, 27 Feb 2026 09:08:39 +0000 (17:08 +0800)
committerWanBingjiang <wanbingjiang@webray.com.cn>
Tue, 28 Apr 2026 06:28:33 +0000 (14:28 +0800)
Regular files: copy content with ul_copy_file(), preserve permissions.
Symlinks: create new link with same target. Add tests and man page.

Addresses: util-linux#3887

bash-completion/rename
misc-utils/rename.1.adoc
misc-utils/rename.c
tests/expected/rename/copy [new file with mode: 0644]
tests/expected/rename/copy.err [new file with mode: 0644]
tests/ts/rename/copy [new file with mode: 0755]

index 8fb9b7c9b0cf17013e6f7c22dab4a1bf680bb204..19cca5f3c633de9d949cce33dee9e124d825d85f 100644 (file)
@@ -11,7 +11,7 @@ _rename_module()
        esac
        case $cur in
                -*)
-                       OPTS="--verbose --symlink --help --version --no-act --all --last --no-overwrite --interactive"
+                       OPTS="--verbose --symlink --copy --help --version --no-act --all --last --no-overwrite --interactive"
                        COMPREPLY=( $(compgen -W "${OPTS[*]}" -- $cur) )
                        return 0
                        ;;
index fc7df4f1c2374a151f44ad083f344747a09cbce7..9353b697f9a0d52f8403b68de0455a83756bdcaf 100644 (file)
@@ -31,6 +31,9 @@ rename - rename files
 *-s*, *--symlink*::
 Do not rename a symlink but change where it points.
 
+*-c*, *--copy*::
+Copy files instead of renaming them. The original files remain unchanged. For regular files, the content is copied and permission bits (*S_IRWXU*|*S_IRWXG*|*S_IRWXO*) are preserved, but ownership, timestamps, and extended attributes are not. For symbolic links, a new link with the same target is created.
+
 *-v*, *--verbose*::
 Show which files were renamed, if any.
 
index 629c488409d714e01b16fe0c7df6afdf6e799426..19d5a1351662344f132044510b1ce2b26a9737b6 100644 (file)
@@ -246,6 +246,116 @@ static int do_file(char *from, char *to, char *s, int verbose, int noact,
        return ret;
 }
 
+/* Copy file/symlink instead of rename; same semantics as do_file. */
+static int do_copy(char *from, char *to, char *s, int verbose, int noact,
+                   int nooverwrite, int interactive)
+{
+       char *newname = NULL, *target = NULL;
+       int ret = 1, res;
+       int src_fd = -1, dst_fd = -1;
+       ssize_t ssz;
+       struct stat sb;
+
+       if (faccessat(AT_FDCWD, s, F_OK, AT_SYMLINK_NOFOLLOW) != 0 &&
+           errno != EINVAL) {
+               warn(_("%s: not accessible"), s);
+               return 2;
+       }
+
+       if (string_replace(from, to, s, &newname) != 0)
+               return 0;
+
+       if ((nooverwrite || interactive) && access(newname, F_OK) != 0)
+               nooverwrite = interactive = 0;
+
+       if (nooverwrite || (interactive && (noact || ask(newname) != 0))) {
+               if (verbose)
+                       printf(_("Skipping existing file: `%s'\n"), newname);
+               ret = 0;
+               goto done;
+       }
+
+       if (noact)
+               goto done;
+
+       src_fd = open(s, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
+       if (src_fd < 0 && errno != ELOOP) {
+               warn(_("%s: open failed"), s);
+               ret = 2;
+               goto done;
+       }
+
+       if (src_fd < 0) {
+               /* ELOOP: path is a symlink, O_NOFOLLOW caused open to fail */
+               if (lstat(s, &sb) == -1) {
+                       warn(_("stat of %s failed"), s);
+                       ret = 2;
+                       goto done;
+               }
+               if (!S_ISLNK(sb.st_mode)) {
+                       warnx(_("%s: cannot copy (unsupported file type)"), s);
+                       ret = 2;
+                       goto done;
+               }
+               target = xmalloc(sb.st_size + 1);
+               ssz = readlink(s, target, sb.st_size + 1);
+               if (ssz < 0) {
+                       warn(_("%s: readlink failed"), s);
+                       ret = 2;
+                       goto done;
+               }
+               target[ssz] = '\0';
+               if (unlink(newname) != 0 && errno != ENOENT) {
+                       warn(_("%s: unlink failed"), newname);
+                       ret = 2;
+                       goto done;
+               }
+               if (symlink(target, newname) != 0) {
+                       warn(_("%s: symlinking to %s failed"), s, newname);
+                       ret = 2;
+               }
+               goto done;
+       }
+
+       /* Regular file copy */
+       if (fstat(src_fd, &sb) == -1) {
+               warn(_("stat of %s failed"), s);
+               ret = 2;
+               goto done;
+       }
+       if (!S_ISREG(sb.st_mode)) {
+               warnx(_("%s: cannot copy (unsupported file type)"), s);
+               ret = 2;
+               goto done;
+       }
+
+       dst_fd = open(newname, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC,
+                     sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
+       if (dst_fd < 0) {
+               warn(_("%s: create failed"), newname);
+               ret = 2;
+               goto done;
+       }
+       res = ul_copy_file(src_fd, dst_fd);
+       if (res != 0) {
+               int num = errno;
+               unlink(newname);
+               errno = num;
+               warn(_("%s: copy to %s failed"), s, newname);
+               ret = 2;
+       }
+done:
+       if (verbose && (noact || ret == 1))
+               printf("`%s' -> `%s'\n", s, newname);
+       free(target);
+       free(newname);
+       if (src_fd >= 0)
+               close(src_fd);
+       if (dst_fd >= 0)
+               close(dst_fd);
+       return ret;
+}
+
 static void __attribute__((__noreturn__)) usage(void)
 {
        FILE *out = stdout;
@@ -260,6 +370,7 @@ static void __attribute__((__noreturn__)) usage(void)
        fputs(USAGE_OPTIONS, out);
        fputs(_(" -v, --verbose       explain what is being done\n"), out);
        fputs(_(" -s, --symlink       act on the target of symlinks\n"), out);
+       fputs(_(" -c, --copy          copy instead of rename\n"), out);
        fputs(_(" -n, --no-act        do not make any changes\n"), out);
        fputs(_(" -a, --all           replace all occurrences\n"), out);
        fputs(_(" -l, --last          replace only the last occurrence\n"), out);
@@ -289,10 +400,12 @@ int main(int argc, char **argv)
                {"no-overwrite", no_argument, NULL, 'o'},
                {"interactive", no_argument, NULL, 'i'},
                {"symlink", no_argument, NULL, 's'},
+               {"copy", no_argument, NULL, 'c'},
                {NULL, 0, NULL, 0}
        };
        static const ul_excl_t excl[] = {       /* rows and cols in ASCII order */
                { 'a','l' },
+               { 'c','s' },
                { 'i','o' },
                { 0 }
        };
@@ -303,7 +416,7 @@ int main(int argc, char **argv)
        textdomain(PACKAGE);
        close_stdout_atexit();
 
-       while ((c = getopt_long(argc, argv, "vsVhnaloi", longopts, NULL)) != -1) {
+       while ((c = getopt_long(argc, argv, "vsVhnaloic", longopts, NULL)) != -1) {
                err_exclusive_options(c, longopts, excl, excl_st);
                switch (c) {
                case 'n':
@@ -327,6 +440,9 @@ int main(int argc, char **argv)
                case 's':
                        do_rename = do_symlink;
                        break;
+               case 'c':
+                       do_rename = do_copy;
+                       break;
 
                case 'V':
                        print_version(EXIT_SUCCESS);
diff --git a/tests/expected/rename/copy b/tests/expected/rename/copy
new file mode 100644 (file)
index 0000000..364bd85
--- /dev/null
@@ -0,0 +1,3 @@
+`rename_copy_foo.1' -> `rename_copy_bar.1'
+`rename_copy_foo.2' -> `rename_copy_bar.2'
+Skipping existing file: `rename_copy_bar.3'
diff --git a/tests/expected/rename/copy.err b/tests/expected/rename/copy.err
new file mode 100644 (file)
index 0000000..97f7101
--- /dev/null
@@ -0,0 +1 @@
+rename: rename_copy_unsupported_foo: cannot copy (unsupported file type)
diff --git a/tests/ts/rename/copy b/tests/ts/rename/copy
new file mode 100755 (executable)
index 0000000..a81870f
--- /dev/null
@@ -0,0 +1,72 @@
+#!/bin/bash
+
+#
+# Copyright (C) 2026 WanBingjiang <wanbingjiang@webray.com.cn>
+#
+# This file is part of util-linux.
+#
+# This file 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 of the License, or
+# (at your option) any later version.
+#
+# This file 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.
+#
+TS_TOPDIR="${0%/*}/../.."
+TS_DESC="copy check"
+
+. "$TS_TOPDIR"/functions.sh
+ts_init "$*"
+
+ts_check_test_command "$TS_CMD_RENAME"
+ts_cd "$TS_OUTDIR"
+
+# Test 1: regular file copy
+echo "test content" > rename_copy_foo.1
+$TS_CMD_RENAME --copy --verbose foo bar rename_copy_foo.1 >> $TS_OUTPUT 2>> $TS_ERRLOG
+if [ ! -f rename_copy_foo.1 ]; then
+       echo "error: original file rename_copy_foo.1 is missing (should be preserved)" >> $TS_OUTPUT
+fi
+if [ ! -f rename_copy_bar.1 ]; then
+       echo "error: copy rename_copy_bar.1 is missing" >> $TS_OUTPUT
+elif [ "$(cat rename_copy_bar.1)" != "test content" ]; then
+       echo "error: copy content mismatch" >> $TS_OUTPUT
+fi
+rm -f rename_copy_foo.1 rename_copy_bar.1
+
+# Test 2: symlink copy
+touch rename_copy_target
+ln -s rename_copy_target rename_copy_foo.2
+$TS_CMD_RENAME --copy --verbose foo bar rename_copy_foo.2 >> $TS_OUTPUT 2>> $TS_ERRLOG
+where_orig="$(readlink rename_copy_foo.2)"
+where_new="$(readlink rename_copy_bar.2)"
+if [ "$where_orig" != "rename_copy_target" ]; then
+       echo "error: original symlink points to $where_orig" >> $TS_OUTPUT
+fi
+if [ "$where_new" != "rename_copy_target" ]; then
+       echo "error: new symlink points to $where_new" >> $TS_OUTPUT
+fi
+rm -f rename_copy_foo.2 rename_copy_bar.2 rename_copy_target
+
+# Test 3: unsupported file type (directory)
+mkdir rename_copy_unsupported_foo
+$TS_CMD_RENAME --copy --verbose foo bar rename_copy_unsupported_foo >> $TS_OUTPUT 2>> $TS_ERRLOG
+if [ -d rename_copy_unsupported_bar ]; then
+       echo "error: unsupported file type should not be copied" >> $TS_OUTPUT
+fi
+rmdir rename_copy_unsupported_foo 2>/dev/null
+rm -rf rename_copy_unsupported_bar 2>/dev/null
+
+# Test 4: --no-overwrite
+echo "original" > rename_copy_foo.3
+echo "existing" > rename_copy_bar.3
+$TS_CMD_RENAME --copy --no-overwrite --verbose foo bar rename_copy_foo.3 >> $TS_OUTPUT 2>> $TS_ERRLOG
+if [ "$(cat rename_copy_bar.3)" != "existing" ]; then
+       echo "error: existing file should not be overwritten" >> $TS_OUTPUT
+fi
+rm -f rename_copy_foo.3 rename_copy_bar.3
+
+ts_finalize