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;
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);
{"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 }
};
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':
case 's':
do_rename = do_symlink;
break;
+ case 'c':
+ do_rename = do_copy;
+ break;
case 'V':
print_version(EXIT_SUCCESS);
--- /dev/null
+#!/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