aboutsummaryrefslogtreecommitdiff
path: root/bin/cp
diff options
context:
space:
mode:
Diffstat (limited to 'bin/cp')
-rw-r--r--bin/cp/cp.151
-rw-r--r--bin/cp/cp.c90
-rw-r--r--bin/cp/extern.h2
-rwxr-xr-xbin/cp/tests/cp_test.sh195
-rw-r--r--bin/cp/utils.c4
5 files changed, 275 insertions, 67 deletions
diff --git a/bin/cp/cp.1 b/bin/cp/cp.1
index 2856391a029e..5231fa72621c 100644
--- a/bin/cp/cp.1
+++ b/bin/cp/cp.1
@@ -29,7 +29,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
-.Dd March 28, 2024
+.Dd July 9, 2025
.Dt CP 1
.Os
.Sh NAME
@@ -84,16 +84,16 @@ If the
.Fl R
option is specified, symbolic links on the command line are followed.
(Symbolic links encountered in the tree traversal are not followed.)
-.It Fl L
+.It Fl L , Fl -dereference
If the
.Fl R
option is specified, all symbolic links are followed.
-.It Fl P
+.It Fl P , Fl -no-dereference
No symbolic links are followed.
This is the default if the
.Fl R
option is specified.
-.It Fl R
+.It Fl R , Fl -recursive
If
.Ar source_file
designates a directory,
@@ -121,11 +121,11 @@ If you need to preserve hard links, consider using
or
.Xr pax 1
instead.
-.It Fl a
+.It Fl a , Fl -archive
Archive mode.
Same as
.Fl RpP .
-.It Fl f
+.It Fl f , Fl -force
For each existing destination pathname, remove it and
create a new file, without prompting for confirmation
regardless of its permissions.
@@ -136,10 +136,8 @@ option overrides any previous
or
.Fl n
options.)
-.It Fl i
-Cause
-.Nm
-to write a prompt to the standard error output before copying a file
+.It Fl i , Fl -interactive
+Write a prompt to the standard error output before copying a file
that would overwrite an existing file.
If the response from the standard input begins with the character
.Sq Li y
@@ -153,13 +151,13 @@ option overrides any previous
or
.Fl n
options.)
-.It Fl l
+.It Fl l , Fl -link
Create hard links to regular files in a hierarchy instead of copying.
.It Fl N
When used with
.Fl p ,
suppress copying file flags.
-.It Fl n
+.It Fl n , Fl -no-clobber
Do not overwrite an existing file.
(The
.Fl n
@@ -169,9 +167,7 @@ or
.Fl i
options.)
.It Fl p
-Cause
-.Nm
-to preserve the following attributes of each source
+Preserve the following attributes of each source
file in the copy: modification time, access time,
file flags, file mode, ACL, user ID, and group ID, as allowed by permissions.
.Pp
@@ -188,14 +184,25 @@ If the source file has both its set-user-ID and set-group-ID bits on,
and either the user ID or group ID cannot be preserved, neither
the set-user-ID nor set-group-ID bits are preserved in the copy's
permissions.
-.It Fl s
-Create symbolic links to regular files in a hierarchy instead of copying.
-.It Fl v
-Cause
+.It Fl -sort
+Visit and traverse sources in (non-localized) lexicographical order.
+Normally,
.Nm
-to be verbose, showing files as they are copied.
-.It Fl x
-File system mount points are not traversed.
+visits the sources in the order they were listed on the command line,
+and if recursing, traverses their contents in whichever order they
+were returned in by the kernel, which may be the order in which they
+were created, lexicographical order, or something else entirely.
+With
+.Fl -sort ,
+the sources are both visited and traversed in lexicographical order.
+This is mostly useful for testing.
+.It Fl s , Fl -symbolic-link
+Create symbolic links to regular files in a hierarchy instead of copying.
+.It Fl v , Fl -verbose
+Be verbose, showing both the source and destination path of each file
+as is copied.
+.It Fl x , Fl -one-file-system
+Do not traverse file system mount points.
.El
.Pp
For each destination file that already exists, its contents are
diff --git a/bin/cp/cp.c b/bin/cp/cp.c
index 7e97715c3ef4..38fe65399d06 100644
--- a/bin/cp/cp.c
+++ b/bin/cp/cp.c
@@ -55,6 +55,7 @@
#include <errno.h>
#include <fcntl.h>
#include <fts.h>
+#include <getopt.h>
#include <limits.h>
#include <signal.h>
#include <stdbool.h>
@@ -69,8 +70,8 @@ static char dot[] = ".";
#define END(buf) (buf + sizeof(buf))
PATH_T to = { .dir = -1, .end = to.path };
-int Nflag, fflag, iflag, lflag, nflag, pflag, sflag, vflag;
-static int Hflag, Lflag, Pflag, Rflag, rflag;
+bool Nflag, fflag, iflag, lflag, nflag, pflag, sflag, vflag;
+static bool Hflag, Lflag, Pflag, Rflag, rflag, Sflag;
volatile sig_atomic_t info;
enum op { FILE_TO_FILE, FILE_TO_DIR, DIR_TO_DNE };
@@ -78,6 +79,27 @@ enum op { FILE_TO_FILE, FILE_TO_DIR, DIR_TO_DNE };
static int copy(char *[], enum op, int, struct stat *);
static void siginfo(int __unused);
+enum {
+ SORT_OPT = CHAR_MAX,
+};
+
+static const struct option long_opts[] =
+{
+ { "archive", no_argument, NULL, 'a' },
+ { "force", no_argument, NULL, 'f' },
+ { "interactive", no_argument, NULL, 'i' },
+ { "dereference", no_argument, NULL, 'L' },
+ { "link", no_argument, NULL, 'l' },
+ { "no-clobber", no_argument, NULL, 'n' },
+ { "no-dereference", no_argument, NULL, 'P' },
+ { "recursive", no_argument, NULL, 'R' },
+ { "symbolic-link", no_argument, NULL, 's' },
+ { "verbose", no_argument, NULL, 'v' },
+ { "one-file-system", no_argument, NULL, 'x' },
+ { "sort", no_argument, NULL, SORT_OPT },
+ { 0 }
+};
+
int
main(int argc, char *argv[])
{
@@ -88,63 +110,67 @@ main(int argc, char *argv[])
bool have_trailing_slash = false;
fts_options = FTS_NOCHDIR | FTS_PHYSICAL;
- while ((ch = getopt(argc, argv, "HLPRafilNnprsvx")) != -1)
+ while ((ch = getopt_long(argc, argv, "+HLPRafilNnprsvx", long_opts,
+ NULL)) != -1)
switch (ch) {
case 'H':
- Hflag = 1;
- Lflag = Pflag = 0;
+ Hflag = true;
+ Lflag = Pflag = false;
break;
case 'L':
- Lflag = 1;
- Hflag = Pflag = 0;
+ Lflag = true;
+ Hflag = Pflag = false;
break;
case 'P':
- Pflag = 1;
- Hflag = Lflag = 0;
+ Pflag = true;
+ Hflag = Lflag = false;
break;
case 'R':
- Rflag = 1;
+ Rflag = true;
break;
case 'a':
- pflag = 1;
- Rflag = 1;
- Pflag = 1;
- Hflag = Lflag = 0;
+ pflag = true;
+ Rflag = true;
+ Pflag = true;
+ Hflag = Lflag = false;
break;
case 'f':
- fflag = 1;
- iflag = nflag = 0;
+ fflag = true;
+ iflag = nflag = false;
break;
case 'i':
- iflag = 1;
- fflag = nflag = 0;
+ iflag = true;
+ fflag = nflag = false;
break;
case 'l':
- lflag = 1;
+ lflag = true;
break;
case 'N':
- Nflag = 1;
+ Nflag = true;
break;
case 'n':
- nflag = 1;
- fflag = iflag = 0;
+ nflag = true;
+ fflag = iflag = false;
break;
case 'p':
- pflag = 1;
+ pflag = true;
break;
case 'r':
- rflag = Lflag = 1;
- Hflag = Pflag = 0;
+ rflag = Lflag = true;
+ Hflag = Pflag = false;
break;
case 's':
- sflag = 1;
+ sflag = true;
break;
case 'v':
- vflag = 1;
+ vflag = true;
break;
case 'x':
fts_options |= FTS_XDEV;
break;
+ case SORT_OPT:
+ Sflag = true;
+ break;
default:
usage();
}
@@ -159,7 +185,7 @@ main(int argc, char *argv[])
if (lflag && sflag)
errx(1, "the -l and -s options may not be specified together");
if (rflag)
- Rflag = 1;
+ Rflag = true;
if (Rflag) {
if (Hflag)
fts_options |= FTS_COMFOLLOW;
@@ -263,6 +289,12 @@ main(int argc, char *argv[])
}
static int
+ftscmp(const FTSENT * const *a, const FTSENT * const *b)
+{
+ return (strcmp((*a)->fts_name, (*b)->fts_name));
+}
+
+static int
copy(char *argv[], enum op type, int fts_options, struct stat *root_stat)
{
char rootname[NAME_MAX];
@@ -305,7 +337,7 @@ copy(char *argv[], enum op type, int fts_options, struct stat *root_stat)
}
level = FTS_ROOTLEVEL;
- if ((ftsp = fts_open(argv, fts_options, NULL)) == NULL)
+ if ((ftsp = fts_open(argv, fts_options, Sflag ? ftscmp : NULL)) == NULL)
err(1, "fts_open");
for (badcp = rval = 0;
(curr = fts_read(ftsp)) != NULL;
diff --git a/bin/cp/extern.h b/bin/cp/extern.h
index c0c524756980..683e6e5f289f 100644
--- a/bin/cp/extern.h
+++ b/bin/cp/extern.h
@@ -37,7 +37,7 @@ typedef struct {
} PATH_T;
extern PATH_T to;
-extern int Nflag, fflag, iflag, lflag, nflag, pflag, sflag, vflag;
+extern bool Nflag, fflag, iflag, lflag, nflag, pflag, sflag, vflag;
extern volatile sig_atomic_t info;
__BEGIN_DECLS
diff --git a/bin/cp/tests/cp_test.sh b/bin/cp/tests/cp_test.sh
index 1d2cd4292459..999993bfad67 100755
--- a/bin/cp/tests/cp_test.sh
+++ b/bin/cp/tests/cp_test.sh
@@ -34,6 +34,10 @@ check_size()
}
atf_test_case basic
+basic_head()
+{
+ atf_set "descr" "Copy a file"
+}
basic_body()
{
echo "foo" > bar
@@ -43,18 +47,26 @@ basic_body()
}
atf_test_case basic_symlink
+basic_symlink_head()
+{
+ atf_set "descr" "Copy a symlink to a file"
+}
basic_symlink_body()
{
echo "foo" > bar
ln -s bar baz
atf_check cp baz foo
- atf_check test '!' -L foo
+ atf_check test ! -L foo
atf_check cmp foo bar
}
atf_test_case chrdev
+chrdev_head()
+{
+ atf_set "descr" "Copy a character device"
+}
chrdev_body()
{
echo "foo" > bar
@@ -69,6 +81,10 @@ chrdev_body()
}
atf_test_case hardlink
+hardlink_head()
+{
+ atf_set "descr" "Create a hard link to a file"
+}
hardlink_body()
{
echo "foo" >foo
@@ -78,6 +94,11 @@ hardlink_body()
}
atf_test_case hardlink_exists
+hardlink_exists_head()
+{
+ atf_set "descr" "Attempt to create a hard link to a file, " \
+ "but the destination already exists"
+}
hardlink_exists_body()
{
echo "foo" >foo
@@ -88,6 +109,11 @@ hardlink_exists_body()
}
atf_test_case hardlink_exists_force
+hardlink_exists_force_head()
+{
+ atf_set "descr" "Force creation of a hard link to a file " \
+ "when the destination already exists"
+}
hardlink_exists_force_body()
{
echo "foo" >foo
@@ -98,9 +124,12 @@ hardlink_exists_force_body()
}
atf_test_case matching_srctgt
+matching_srctgt_head()
+{
+ atf_set "descr" "Avoid infinite loop when copying a directory to itself"
+}
matching_srctgt_body()
{
-
# PR235438: `cp -R foo foo` would previously infinitely recurse and
# eventually error out.
mkdir foo
@@ -110,13 +139,17 @@ matching_srctgt_body()
atf_check cp -R foo foo
atf_check -o inline:"qux\n" cat foo/foo/bar
atf_check -o inline:"qux\n" cat foo/foo/zoo
- atf_check -e not-empty -s not-exit:0 stat foo/foo/foo
+ atf_check test ! -e foo/foo/foo
}
atf_test_case matching_srctgt_contained
+matching_srctgt_contained_head()
+{
+ atf_set "descr" "Avoid infinite loop when copying a directory " \
+ "into an existing subdirectory of itself"
+}
matching_srctgt_contained_body()
{
-
# Let's do the same thing, except we'll try to recursively copy foo into
# one of its subdirectories.
mkdir foo
@@ -142,9 +175,13 @@ matching_srctgt_contained_body()
}
atf_test_case matching_srctgt_link
+matching_srctgt_link_head()
+{
+ atf_set "descr" "Avoid infinite loop when recursively copying a " \
+ "symlink to a directory into the directory it links to"
+}
matching_srctgt_link_body()
{
-
mkdir foo
echo "qux" > foo/bar
cp foo/bar foo/zoo
@@ -156,9 +193,13 @@ matching_srctgt_link_body()
}
atf_test_case matching_srctgt_nonexistent
+matching_srctgt_nonexistent_head()
+{
+ atf_set "descr" "Avoid infinite loop when recursively copying a " \
+ "directory into a new subdirectory of itself"
+}
matching_srctgt_nonexistent_body()
{
-
# We'll copy foo to a nonexistent subdirectory; ideally, we would
# skip just the directory and end up with a layout like;
#
@@ -180,6 +221,10 @@ matching_srctgt_nonexistent_body()
}
atf_test_case pflag_acls
+pflag_acls_head()
+{
+ atf_set "descr" "Verify that -p preserves access control lists"
+}
pflag_acls_body()
{
mkdir dir
@@ -216,6 +261,10 @@ pflag_acls_body()
}
atf_test_case pflag_flags
+pflag_flags_head()
+{
+ atf_set "descr" "Verify that -p preserves file flags"
+}
pflag_flags_body()
{
mkdir dir
@@ -263,6 +312,11 @@ recursive_link_setup()
}
atf_test_case recursive_link_dflt
+recursive_link_dflt_head()
+{
+ atf_set "descr" "Copy a directory containing a subdirectory and a " \
+ "symlink to that subdirectory"
+}
recursive_link_dflt_body()
{
recursive_link_setup
@@ -270,9 +324,15 @@ recursive_link_dflt_body()
# -P is the default, so this should work and preserve the link.
atf_check cp -R foo foo-mirror
atf_check test -L foo-mirror/foo/baz
+ atf_check test -d foo-mirror/foo/baz
}
atf_test_case recursive_link_Hflag
+recursive_link_Hflag_head()
+{
+ atf_set "descr" "Copy a directory containing a subdirectory and a " \
+ "symlink to that subdirectory"
+}
recursive_link_Hflag_body()
{
recursive_link_setup
@@ -281,22 +341,32 @@ recursive_link_Hflag_body()
# link.
atf_check cp -RH foo foo-mirror
atf_check test -L foo-mirror/foo/baz
+ atf_check test -d foo-mirror/foo/baz
}
atf_test_case recursive_link_Lflag
+recursive_link_Lflag_head()
+{
+ atf_set "descr" "Copy a directory containing a subdirectory and a " \
+ "symlink to that subdirectory"
+}
recursive_link_Lflag_body()
{
recursive_link_setup -L
# -L will work, but foo/baz ends up expanded to a directory.
- atf_check test -d foo-mirror/foo/baz -a \
- '(' ! -L foo-mirror/foo/baz ')'
+ atf_check test ! -L foo-mirror/foo/baz
+ atf_check test -d foo-mirror/foo/baz
atf_check cp -RL foo foo-mirror
- atf_check test -d foo-mirror/foo/baz -a \
- '(' ! -L foo-mirror/foo/baz ')'
+ atf_check test ! -L foo-mirror/foo/baz
+ atf_check test -d foo-mirror/foo/baz
}
atf_test_case samefile
+samefile_head()
+{
+ atf_set "descr" "Copy a file to itself"
+}
samefile_body()
{
echo "foo" >foo
@@ -324,6 +394,10 @@ files_are_equal()
}
atf_test_case sparse_leading_hole
+sparse_leading_hole_head()
+{
+ atf_set "descr" "Copy a sparse file stat starts with a hole"
+}
sparse_leading_hole_body()
{
# A 16-megabyte hole followed by one megabyte of data
@@ -337,6 +411,10 @@ sparse_leading_hole_body()
}
atf_test_case sparse_multiple_holes
+sparse_multiple_hole_head()
+{
+ atf_set "descr" "Copy a sparse file with multiple holes"
+}
sparse_multiple_holes_body()
{
# Three one-megabyte blocks of data preceded, separated, and
@@ -356,6 +434,10 @@ sparse_multiple_holes_body()
}
atf_test_case sparse_only_hole
+sparse_only_hole_head()
+{
+ atf_set "descr" "Copy a sparse file consisting entirely of a hole"
+}
sparse_only_hole_body()
{
# A 16-megabyte hole
@@ -368,6 +450,10 @@ sparse_only_hole_body()
}
atf_test_case sparse_to_dev
+sparse_to_dev_head()
+{
+ atf_set "descr" "Copy a sparse file to a device"
+}
sparse_to_dev_body()
{
# Three one-megabyte blocks of data preceded, separated, and
@@ -385,6 +471,10 @@ sparse_to_dev_body()
}
atf_test_case sparse_trailing_hole
+sparse_trailing_hole_head()
+{
+ atf_set "descr" "Copy a sparse file that ends with a hole"
+}
sparse_trailing_hole_body()
{
# One megabyte of data followed by a 16-megabyte hole
@@ -398,16 +488,24 @@ sparse_trailing_hole_body()
}
atf_test_case standalone_Pflag
+standalone_Pflag_head()
+{
+ atf_set "descr" "Test -P without -R"
+}
standalone_Pflag_body()
{
echo "foo" > bar
ln -s bar foo
atf_check cp -P foo baz
- atf_check -o inline:'Symbolic Link\n' stat -f %SHT baz
+ atf_check test -L baz
}
atf_test_case symlink
+symlink_head()
+{
+ atf_set "descr" "Create a symbolic link to a file"
+}
symlink_body()
{
echo "foo" >foo
@@ -417,6 +515,11 @@ symlink_body()
}
atf_test_case symlink_exists
+symlink_exists_head()
+{
+ atf_set "descr" "Attempt to create a symbolic link to a file, " \
+ "but the destination already exists"
+}
symlink_exists_body()
{
echo "foo" >foo
@@ -426,6 +529,11 @@ symlink_exists_body()
}
atf_test_case symlink_exists_force
+symlink_exists_force_head()
+{
+ atf_set "descr" "Force creation of a symbolic link to a file " \
+ "when the destination already exists"
+}
symlink_exists_force_body()
{
echo "foo" >foo
@@ -436,6 +544,10 @@ symlink_exists_force_body()
}
atf_test_case directory_to_symlink
+directory_to_symlink_head()
+{
+ atf_set "descr" "Attempt to copy a directory to a symlink"
+}
directory_to_symlink_body()
{
mkdir -p foo
@@ -449,6 +561,10 @@ directory_to_symlink_body()
}
atf_test_case overwrite_directory
+overwrite_directory_head()
+{
+ atf_set "descr" "Attempt to overwrite a directory with a file"
+}
overwrite_directory_body()
{
mkdir -p foo/bar/baz
@@ -465,6 +581,10 @@ overwrite_directory_body()
}
atf_test_case to_dir_dne
+to_dir_dne_head()
+{
+ atf_set "descr" "Copy a directory to a nonexistent directory"
+}
to_dir_dne_body()
{
mkdir dir
@@ -476,6 +596,10 @@ to_dir_dne_body()
}
atf_test_case to_nondir
+to_dir_dne_head()
+{
+ atf_set "descr" "Copy one or more files to a non-directory"
+}
to_nondir_body()
{
echo "foo" >foo
@@ -490,6 +614,10 @@ to_nondir_body()
}
atf_test_case to_deadlink
+to_deadlink_head()
+{
+ atf_set "descr" "Copy a file to a dead symbolic link"
+}
to_deadlink_body()
{
echo "foo" >foo
@@ -499,6 +627,10 @@ to_deadlink_body()
}
atf_test_case to_deadlink_append
+to_deadlink_append_head()
+{
+ atf_set "descr" "Copy multiple files to a dead symbolic link"
+}
to_deadlink_append_body()
{
echo "foo" >foo
@@ -517,6 +649,10 @@ to_deadlink_append_body()
}
atf_test_case to_dirlink
+to_dirlink_head()
+{
+ atf_set "descr" "Copy things to a symbolic link to a directory"
+}
to_dirlink_body()
{
mkdir src dir
@@ -542,6 +678,11 @@ to_dirlink_body()
}
atf_test_case to_deaddirlink
+to_deaddirlink_head()
+{
+ atf_set "descr" "Copy things to a symbolic link to a nonexistent " \
+ "directory"
+}
to_deaddirlink_body()
{
mkdir src
@@ -572,6 +713,11 @@ to_deaddirlink_body()
}
atf_test_case to_link_outside
+to_link_outside_head()
+{
+ atf_set "descr" "Recursively copy a directory containing a symbolic " \
+ "link that points to somewhere outside the source directory"
+}
to_link_outside_body()
{
mkdir dir dst dst/dir
@@ -584,6 +730,11 @@ to_link_outside_body()
}
atf_test_case dstmode
+dstmode_head()
+{
+ atf_set "descr" "Verify that directories are created with the " \
+ "correct permissions"
+}
dstmode_body()
{
mkdir -m 0755 dir
@@ -646,6 +797,7 @@ atf_test_case unrdir
unrdir_head()
{
atf_set "descr" "Test handling of unreadable directories"
+ atf_set "require.user" "unprivileged"
}
unrdir_body()
{
@@ -657,7 +809,7 @@ unrdir_body()
atf_check \
-s exit:1 \
-e match:"^cp: src/b: Permission denied" \
- cp -R src dst
+ cp -R --sort src dst
atf_check test -d dst/a
atf_check cmp src/a/f dst/a/f
atf_check test -d dst/b
@@ -670,6 +822,7 @@ atf_test_case unrfile
unrfile_head()
{
atf_set "descr" "Test handling of unreadable files"
+ atf_set "require.user" "unprivileged"
}
unrfile_body()
{
@@ -681,13 +834,28 @@ unrfile_body()
atf_check \
-s exit:1 \
-e match:"^cp: src/b: Permission denied" \
- cp -R src dst
+ cp -R --sort src dst
atf_check test -d dst
atf_check cmp src/a dst/a
atf_check test ! -e dst/b
atf_check cmp src/c dst/c
}
+atf_test_case nopermute
+nopermute_head()
+{
+ atf_set descr "Check that getopt_long does not permute options"
+}
+nopermute_body()
+{
+ mkdir src dst
+ atf_check \
+ -s exit:1 \
+ -e match:'cp: -p: No such file' \
+ cp -R src -p dst
+ atf_check test -d dst/src
+}
+
atf_init_test_cases()
{
atf_add_test_case basic
@@ -729,4 +897,5 @@ atf_init_test_cases()
atf_add_test_case dirloop
atf_add_test_case unrdir
atf_add_test_case unrfile
+ atf_add_test_case nopermute
}
diff --git a/bin/cp/utils.c b/bin/cp/utils.c
index cfc0f0f12603..2036056ada68 100644
--- a/bin/cp/utils.c
+++ b/bin/cp/utils.c
@@ -105,7 +105,7 @@ copy_file(const FTSENT *entp, bool dne, bool beneath)
ssize_t wcount;
off_t wtotal;
int ch, checkch, from_fd, rval, to_fd;
- int use_copy_file_range = 1;
+ bool use_copy_file_range = true;
fs = entp->fts_statp;
from_fd = to_fd = -1;
@@ -210,7 +210,7 @@ copy_file(const FTSENT *entp, bool dne, bool beneath)
to_fd, NULL, SSIZE_MAX, 0);
if (wcount < 0 && errno == EINVAL) {
/* probably a non-seekable descriptor */
- use_copy_file_range = 0;
+ use_copy_file_range = false;
}
}
if (!use_copy_file_range) {