From: WanBingjiang Date: Fri, 27 Feb 2026 09:08:39 +0000 (+0800) Subject: rename: add --copy option to copy instead of rename X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d021e24bc185cca05cf81a257648ec5d2d94f0e4;p=thirdparty%2Futil-linux.git rename: add --copy option to copy instead of rename 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 --- diff --git a/bash-completion/rename b/bash-completion/rename index 8fb9b7c9b0..19cca5f3c6 100644 --- a/bash-completion/rename +++ b/bash-completion/rename @@ -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 ;; diff --git a/misc-utils/rename.1.adoc b/misc-utils/rename.1.adoc index fc7df4f1c2..9353b697f9 100644 --- a/misc-utils/rename.1.adoc +++ b/misc-utils/rename.1.adoc @@ -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. diff --git a/misc-utils/rename.c b/misc-utils/rename.c index 629c488409..19d5a13516 100644 --- a/misc-utils/rename.c +++ b/misc-utils/rename.c @@ -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 index 0000000000..364bd85871 --- /dev/null +++ b/tests/expected/rename/copy @@ -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 index 0000000000..97f71017fc --- /dev/null +++ b/tests/expected/rename/copy.err @@ -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 index 0000000000..a81870f15a --- /dev/null +++ b/tests/ts/rename/copy @@ -0,0 +1,72 @@ +#!/bin/bash + +# +# Copyright (C) 2026 WanBingjiang +# +# 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