git: 22fba3a9d641 - main - find: add -xattr and -xttrname
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Thu, 09 Apr 2026 02:41:22 UTC
The branch main has been updated by kevans:
URL: https://cgit.FreeBSD.org/src/commit/?id=22fba3a9d64140d80a9e2093cfc02c9c503b2e19
commit 22fba3a9d64140d80a9e2093cfc02c9c503b2e19
Author: Kyle Evans <kevans@FreeBSD.org>
AuthorDate: 2026-04-09 02:41:12 +0000
Commit: Kyle Evans <kevans@FreeBSD.org>
CommitDate: 2026-04-09 02:41:12 +0000
find: add -xattr and -xttrname
We use -xattr in our openrsync tests for convenience, and it seems like
a good addition to FreeBSD. -xattr and -xattrname will both consult all
available namespaces by default, but -xattrname allows filtering by
namespace using a "user:" or "system:" prefix.
Inspired by: https://github.com/apple-oss-distributions/shell_cmds
Reviewed by: kib, rmacklem
Sponsored by: Klara, Inc.
Differential Revision: https://reviews.freebsd.org/D55286
---
usr.bin/find/extern.h | 2 +
usr.bin/find/find.1 | 23 +++++++++-
usr.bin/find/function.c | 94 +++++++++++++++++++++++++++++++++++++++++
usr.bin/find/option.c | 2 +
usr.bin/find/tests/find_test.sh | 80 +++++++++++++++++++++++++++++++++++
5 files changed, 199 insertions(+), 2 deletions(-)
diff --git a/usr.bin/find/extern.h b/usr.bin/find/extern.h
index 02c85d06a34c..250b5fb6689c 100644
--- a/usr.bin/find/extern.h
+++ b/usr.bin/find/extern.h
@@ -122,6 +122,8 @@ exec_f f_sparse;
exec_f f_type;
exec_f f_user;
exec_f f_writable;
+exec_f f_xattr;
+exec_f f_xattrname;
extern int ftsoptions, ignore_readdir_race, isdepth, isoutput;
extern int issort, isxargs;
diff --git a/usr.bin/find/find.1 b/usr.bin/find/find.1
index 98521a98762d..afe10a36607d 100644
--- a/usr.bin/find/find.1
+++ b/usr.bin/find/find.1
@@ -28,7 +28,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
-.Dd July 26, 2025
+.Dd February 14, 2026
.Dt FIND 1
.Os
.Sh NAME
@@ -976,6 +976,23 @@ This test makes use of the
.Xr access 2
system call, and so can be fooled by NFS servers which do UID mapping (or root-squashing).
This is a GNU find extension.
+.It Ic -xattr
+Matches files which have extended attributes set in any supported namespace.
+.It Ic -xattrname Ar xattr
+Matches files which have the specified
+.Ar xattr
+extended attribute set.
+All supported namespaces are searched by default, but
+.Ar xattr
+may be prefixed with
+.Dq user:
+or
+.Dq system:
+to filter by namespace.
+.Pp
+Note that named attributes are not supported, only extended attributes as set
+by, e.g.,
+.Xr setextattr 8 .
.El
.Sh OPERATORS
The primaries may be combined using the following operators.
@@ -1245,6 +1262,7 @@ section below for details.
.Xr whereis 1 ,
.Xr which 1 ,
.Xr xargs 1 ,
+.Xr extattr 2 ,
.Xr stat 2 ,
.Xr acl 3 ,
.Xr fts 3 ,
@@ -1253,7 +1271,8 @@ section below for details.
.Xr strmode 3 ,
.Xr ascii 7 ,
.Xr re_format 7 ,
-.Xr symlink 7
+.Xr symlink 7 ,
+.Xr setextattr 8
.Sh STANDARDS
The
.Nm
diff --git a/usr.bin/find/function.c b/usr.bin/find/function.c
index b260a71ef4a9..c62ac39a9e82 100644
--- a/usr.bin/find/function.c
+++ b/usr.bin/find/function.c
@@ -37,6 +37,7 @@
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/acl.h>
+#include <sys/extattr.h>
#include <sys/wait.h>
#include <sys/mount.h>
@@ -49,6 +50,7 @@
#include <limits.h>
#include <pwd.h>
#include <regex.h>
+#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -57,6 +59,8 @@
#include "find.h"
+static const char * const xattr_ns[] = EXTATTR_NAMESPACE_NAMES;
+
static PLAN *palloc(OPTION *);
static long long find_parsenum(PLAN *, const char *, char *, char *);
static long long find_parsetime(PLAN *, const char *, char *);
@@ -1752,6 +1756,96 @@ c_user(OPTION *option, char ***argvp)
return new;
}
+/*
+ * -xattr functions --
+ *
+ * True if the entry has any extended attribute in any namespace.
+ */
+int
+f_xattr(PLAN *plan __unused, FTSENT *entry)
+{
+ ssize_t asz;
+ bool deref_link;
+
+ deref_link = (ftsoptions & FTS_LOGICAL) != 0;
+ if (entry->fts_level == 0 && (ftsoptions & FTS_COMFOLLOW) != 0)
+ deref_link = true;
+
+ for (size_t ns = 0; ns < nitems(xattr_ns); ns++) {
+ if (ns == EXTATTR_NAMESPACE_EMPTY)
+ continue;
+
+ if (deref_link)
+ asz = extattr_list_file(entry->fts_accpath, ns, NULL, 0);
+ else
+ asz = extattr_list_link(entry->fts_accpath, ns, NULL, 0);
+ if (asz > 0)
+ return 1;
+ }
+
+ return 0;
+}
+
+static bool
+find_has_xattr(const char *path, int ns, const char *aname, bool deref_link)
+{
+ size_t asz;
+
+ if (deref_link)
+ asz = extattr_get_file(path, ns, aname, NULL, 0);
+ else
+ asz = extattr_get_link(path, ns, aname, NULL, 0);
+
+ return asz != (size_t)-1;
+}
+
+/*
+ * -xattrname xattr functions --
+ *
+ * True if the entry has the given extended attribute xattr. The xattr
+ * may be prefixed with "user:" or "system:" to scope the search
+ * explicitly, otherwise we assume the user namespace is requested.
+ */
+int
+f_xattrname(PLAN *plan, FTSENT *entry)
+{
+ const char *aname;
+ bool deref_link;
+
+ deref_link = (ftsoptions & FTS_LOGICAL) != 0;
+ if (entry->fts_level == 0 && (ftsoptions & FTS_COMFOLLOW) != 0)
+ deref_link = true;
+
+ aname = plan->c_data;
+ for (size_t ns = 0; ns < nitems(xattr_ns); ns++) {
+ const char *name;
+ size_t namelen;
+
+ if (ns == EXTATTR_NAMESPACE_EMPTY)
+ continue;
+
+ name = xattr_ns[ns];
+ namelen = strlen(xattr_ns[ns]);
+ if (strncmp(aname, name, namelen) == 0 &&
+ aname[namelen] == ':') {
+ aname += namelen + 1;
+ return find_has_xattr(entry->fts_accpath, ns, aname,
+ deref_link);
+ }
+ }
+
+ for (size_t ns = 0; ns < nitems(xattr_ns); ns++) {
+ if (ns == EXTATTR_NAMESPACE_EMPTY)
+ continue;
+
+ if (find_has_xattr(entry->fts_accpath, ns, aname,
+ deref_link))
+ return 1;
+ }
+
+ return 0;
+}
+
/*
* -xdev functions --
*
diff --git a/usr.bin/find/option.c b/usr.bin/find/option.c
index fa09231a3152..fe3d9b00f90f 100644
--- a/usr.bin/find/option.c
+++ b/usr.bin/find/option.c
@@ -162,6 +162,8 @@ static OPTION const options[] = {
{ "-user", c_user, f_user, 0 },
{ "-wholename", c_name, f_path, 0 },
{ "-writable", c_simple, f_writable, 0 },
+ { "-xattr", c_simple, f_xattr, 0 },
+ { "-xattrname", c_name, f_xattrname, 0 },
{ "-xdev", c_xdev, f_always_true, 0 },
// -xtype
};
diff --git a/usr.bin/find/tests/find_test.sh b/usr.bin/find/tests/find_test.sh
index 99d2f6af4d45..deb6a66a8dfb 100755
--- a/usr.bin/find/tests/find_test.sh
+++ b/usr.bin/find/tests/find_test.sh
@@ -174,9 +174,89 @@ find_printf_body()
find -s dir -printf '%Te\n'
}
+atf_test_case find_xattr
+find_xattr_head()
+{
+ atf_set "descr" "Test the -xattr primary"
+}
+find_xattr_body()
+{
+ mkdir dir
+ ln -s dir dirlink
+
+ # No xattrs here
+ atf_check find dir -xattr
+ atf_check find dirlink -xattr
+
+ # Set one on the directory and be sure that we also dereference symlinks
+ # as appropriate with -H/-L.
+ if ! setextattr user find_test.attr val dir; then
+ atf_skip "Failed to set xattr (not supported on this fs?)"
+ fi
+
+ atf_check -o match:"dir$" find dir -xattr
+ atf_check -o match:"dirlink$" find -H dirlink -xattr
+ atf_check -o match:"dirlink$" find -L dirlink -xattr
+
+ atf_check -o match:"dir$" -o match:"dirlink" find -sL . -xattr
+ atf_check -o match:"dir$" -o not-match:"dirlink$" find -sH . -xattr
+ atf_check -o match:"dir$" -o not-match:"dirlink$" find -s . -xattr
+}
+
+atf_test_case find_xattrname
+find_xattrname_head()
+{
+ atf_set "descr" "Test the -xattrname primary"
+ atf_set "require.user" "root"
+}
+find_xattrname_body()
+{
+ touch foo bar baz none
+
+ ln -s foo link
+ if ! setextattr user find_test.special1 val foo; then
+ atf_skip "Failed to set xattr (not supported on this fs?)"
+ fi
+
+ atf_check setextattr user find_test.special2 val bar
+ atf_check setextattr user find_test.special2 val baz
+
+ # We want an unqualified 'find_test.special2' search to find all three
+ # of these, while 'user:' and 'system:' filter appropriately.
+ atf_check setextattr system find_test.special2 val foo
+
+ atf_check find . -xattrname 'find_test.special3'
+
+ # Be sure that we get symlink dereferencing right, so that one can use
+ # -H/-L/-P to get the right behavior.
+ atf_check -o match:foo -o not-match:"bar|baz|link|none" \
+ find . -xattrname 'find_test.special1'
+ atf_check -o match:foo -o match:link \
+ find -H foo link -xattrname 'find_test.special1'
+ atf_check -o match:foo -o match:link -o not-match:"bar|baz|none" \
+ find -L . -xattrname 'find_test.special1'
+
+ atf_check -o match:foo -o match:bar -o match:baz \
+ -o not-match:"none|link" find . -xattrname 'find_test.special2'
+ atf_check -o not-match:"foo|none|link" -o match:bar -o match:baz \
+ find . -xattrname 'user:find_test.special2'
+ atf_check -o match:foo -o not-match:"bar|baz|none|link" \
+ find . -xattrname 'system:find_test.special2'
+
+ # Now set an extattr on the link itself and be sure that find(1) can
+ # detect it. With -L, we shouldn't see anything with a special3 xattr
+ # as symlinks are dereferenced.
+ atf_check setextattr -h user find_test.special3 val link
+ atf_check -o match:link find . -xattrname "find_test.special3"
+ atf_check find -L . -xattrname "find_test.special3"
+ atf_check find -H link -xattrname "find_test.special3"
+}
+
atf_init_test_cases()
{
atf_add_test_case find_newer_link
atf_add_test_case find_samefile_link
atf_add_test_case find_printf
+ atf_add_test_case find_xattr
+ atf_add_test_case find_xattrname
}