git: 2d6b33f801d5 - main - cp: Add an option to visit sources in order.

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Wed, 09 Jul 2025 17:09:55 UTC
The branch main has been updated by des:

URL: https://cgit.FreeBSD.org/src/commit/?id=2d6b33f801d5352b8e078db83f6c90f6fe8291bb

commit 2d6b33f801d5352b8e078db83f6c90f6fe8291bb
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2025-07-09 17:06:07 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2025-07-09 17:07:13 +0000

    cp: Add an option to visit sources in order.
    
    This adds a --sort option which makes cp pass a comparison function to
    FTS, ensuring that sources are visited and traversed in a predictable
    order.  This will help make certain test cases more reliable.
    
    Sponsored by:   Klara, Inc.
    Reviewed by:    kevans
    Differential Revision:  https://reviews.freebsd.org/D51214
---
 bin/cp/cp.1             | 12 ++++++++++++
 bin/cp/cp.c             | 14 ++++++++++++--
 bin/cp/tests/cp_test.sh |  4 ++--
 3 files changed, 26 insertions(+), 4 deletions(-)

diff --git a/bin/cp/cp.1 b/bin/cp/cp.1
index 6edc8e403acd..5231fa72621c 100644
--- a/bin/cp/cp.1
+++ b/bin/cp/cp.1
@@ -184,6 +184,18 @@ 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 -sort
+Visit and traverse sources in (non-localized) lexicographical order.
+Normally,
+.Nm
+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
diff --git a/bin/cp/cp.c b/bin/cp/cp.c
index a1b62084a790..38fe65399d06 100644
--- a/bin/cp/cp.c
+++ b/bin/cp/cp.c
@@ -71,7 +71,7 @@ static char dot[] = ".";
 #define END(buf) (buf + sizeof(buf))
 PATH_T to = { .dir = -1, .end = to.path };
 bool Nflag, fflag, iflag, lflag, nflag, pflag, sflag, vflag;
-static bool Hflag, Lflag, Pflag, Rflag, rflag;
+static bool Hflag, Lflag, Pflag, Rflag, rflag, Sflag;
 volatile sig_atomic_t info;
 
 enum op { FILE_TO_FILE, FILE_TO_DIR, DIR_TO_DNE };
@@ -96,6 +96,7 @@ static const struct option long_opts[] =
 	{ "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 }
 };
 
@@ -167,6 +168,9 @@ main(int argc, char *argv[])
 		case 'x':
 			fts_options |= FTS_XDEV;
 			break;
+		case SORT_OPT:
+			Sflag = true;
+			break;
 		default:
 			usage();
 		}
@@ -284,6 +288,12 @@ main(int argc, char *argv[])
 	    &to_stat)));
 }
 
+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)
 {
@@ -327,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/tests/cp_test.sh b/bin/cp/tests/cp_test.sh
index 6adbc45c5009..3c3dd309b9e4 100755
--- a/bin/cp/tests/cp_test.sh
+++ b/bin/cp/tests/cp_test.sh
@@ -657,7 +657,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
@@ -681,7 +681,7 @@ 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