Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F160465486
D56772.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
14 KB
Referenced Files
None
Subscribers
None
D56772.diff
View Options
diff --git a/ObsoleteFiles.inc b/ObsoleteFiles.inc
--- a/ObsoleteFiles.inc
+++ b/ObsoleteFiles.inc
@@ -51,6 +51,9 @@
# xargs -n1 | sort | uniq -d;
# done
+# 20260504: readlink is now a separate utility
+OLD_FILES+=usr/tests/usr.bin/stat/readlink_test
+
# 20260503: Kill off MANSUBDIRs
OLD_FILES+=usr/share/man/man8/amd64/apm.8.gz
OLD_FILES+=usr/share/man/man8/amd64/apmconf.8.gz
diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist
--- a/etc/mtree/BSD.tests.dist
+++ b/etc/mtree/BSD.tests.dist
@@ -1217,6 +1217,8 @@
..
procstat
..
+ readlink
+ ..
renice
..
rs
diff --git a/usr.bin/Makefile b/usr.bin/Makefile
--- a/usr.bin/Makefile
+++ b/usr.bin/Makefile
@@ -118,6 +118,7 @@
procstat \
protect \
rctl \
+ readlink \
renice \
resizewin \
rev \
diff --git a/usr.bin/stat/Makefile b/usr.bin/readlink/Makefile
copy from usr.bin/stat/Makefile
copy to usr.bin/readlink/Makefile
--- a/usr.bin/stat/Makefile
+++ b/usr.bin/readlink/Makefile
@@ -1,9 +1,6 @@
.include <src.opts.mk>
-PROG= stat
-
-LINKS= ${BINDIR}/stat ${BINDIR}/readlink
-MLINKS= stat.1 readlink.1
+PROG= readlink
HAS_TESTS=
SUBDIR.${MK_TESTS}+= tests
diff --git a/usr.bin/readlink/readlink.1 b/usr.bin/readlink/readlink.1
new file mode 100644
--- /dev/null
+++ b/usr.bin/readlink/readlink.1
@@ -0,0 +1,51 @@
+.\"-
+.\" Copyright (c) 2026 Dag-Erling Smørgrav
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd May 2, 2026
+.Dt READLINK 1
+.Os
+.Sh NAME
+.Nm readlink
+.Nd display the target of a symbolic link
+.Sh SYNOPSIS
+.Nm
+.Op Fl fn
+.Ar file
+.Op Ar ...
+.Sh DESCRIPTION
+If
+.Ar file
+is a symbolic link, the
+.Nm
+utility prints the target of said symbolic link to standard output.
+Otherwise, it prints an error message to standard error and exits with
+a non-zero status.
+.Pp
+The following options are available:
+.Bl -tag -width indent
+.It Fl f
+Print the canonical path of the target, even if the final component of
+the resolved path does not exist.
+.It Fl n
+Do not print a newline.
+.El
+.Sh EXIT STATUS
+.Ex -std
+.Sh SEE ALSO
+.Xr realpath 1 ,
+.Xr readlink 2
+.Sh STANDARDS
+The
+.Nm
+utility conforms to
+.St -p1003.1-2024 .
+Support for multiple arguments and the
+.Fl f
+option are non-standard extensions.
+.Sh AUTHORS
+The
+.Nm
+utility and this manual page were written by
+.An Dag-Erling Sm\(/orgrav Aq Mt des@FreeBSD.org .
diff --git a/usr.bin/readlink/readlink.c b/usr.bin/readlink/readlink.c
new file mode 100644
--- /dev/null
+++ b/usr.bin/readlink/readlink.c
@@ -0,0 +1,66 @@
+/*-
+ * Copyright (c) 2026 Dag-Erling Smørgrav <des@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <err.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+static void usage(void)
+{
+ fprintf(stderr, "usage: readlink [-fn] file [...]\n");
+ exit(EXIT_FAILURE);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char path[PATH_MAX];
+ ssize_t len;
+ bool fflag, nflag;
+ int opt;
+
+ fflag = nflag = false;
+ while ((opt = getopt(argc, argv, "fn")) != -1) {
+ switch (opt) {
+ case 'f':
+ fflag = true;
+ break;
+ case 'n':
+ nflag = true;
+ break;
+ default:
+ usage();
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (argc == 0)
+ usage();
+
+ while (argc-- > 0) {
+ if (fflag) {
+ if (realpath(*argv, path) == NULL && errno != ENOENT)
+ err(EXIT_FAILURE, "%s", *argv);
+ } else {
+ if ((len = readlink(*argv, path, sizeof(path))) < 0)
+ err(EXIT_FAILURE, "%s", *argv);
+ path[len] = '\0';
+ }
+ (void)printf("%s%s", path, nflag ? "" : "\n");
+ argv++;
+ }
+
+ if (fflush(stdout) != 0)
+ err(EXIT_FAILURE, "stdout");
+
+ exit(EXIT_SUCCESS);
+}
diff --git a/usr.bin/stat/tests/Makefile b/usr.bin/readlink/tests/Makefile
copy from usr.bin/stat/tests/Makefile
copy to usr.bin/readlink/tests/Makefile
--- a/usr.bin/stat/tests/Makefile
+++ b/usr.bin/readlink/tests/Makefile
@@ -1,4 +1,3 @@
ATF_TESTS_SH+= readlink_test
-ATF_TESTS_SH+= stat_test
.include <bsd.test.mk>
diff --git a/usr.bin/readlink/tests/readlink_test.sh b/usr.bin/readlink/tests/readlink_test.sh
new file mode 100755
--- /dev/null
+++ b/usr.bin/readlink/tests/readlink_test.sh
@@ -0,0 +1,105 @@
+#
+# Copyright (c) 2026 Dag-Erling Smørgrav <des@FreeBSD.org>
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+
+atf_test_case basic
+basic_head()
+{
+ atf_set "descr" "Basic usage"
+}
+basic_body()
+{
+ atf_check ln -s target link
+ atf_check -o inline:"target\n" readlink link
+ atf_check mkdir target
+ atf_check -o inline:"target\n" readlink link
+}
+
+atf_test_case noarg
+noarg_head()
+{
+ atf_set "descr" "No argument"
+}
+noarg_body()
+{
+ atf_check -s exit:1 -e match:"usage:" readlink
+}
+
+atf_test_case notlink
+notlink_head()
+{
+ atf_set "descr" "Argument is not a symbolic link"
+}
+notlink_body()
+{
+ atf_check touch file
+ atf_check mkdir dir
+ atf_check -s exit:1 -e match:"Invalid argument" readlink file
+ atf_check -s exit:1 -e match:"Invalid argument" readlink dir
+}
+
+atf_test_case multi
+multi_head()
+{
+ atf_set "descr" "Multiple arguments"
+}
+multi_body()
+{
+ atf_check ln -s targetA linkA
+ atf_check ln -s targetB linkB
+ atf_check -o inline:"targetA\ntargetB\n" readlink linkA linkB
+}
+
+atf_test_case f_flag
+f_flag_head()
+{
+ atf_set "descr" "Test the -f flag"
+}
+f_flag_body()
+{
+ cd "$(realpath "$PWD")" || atf_fail "failed to canonicalize $PWD"
+ atf_check ln -s target link
+ atf_check -o inline:"$PWD/target\n" readlink -f link
+}
+
+atf_test_case n_flag
+n_flag_head()
+{
+ atf_set "descr" "Test the -n flag"
+}
+n_flag_body()
+{
+ atf_check ln -s target link
+ atf_check -o inline:"target" readlink -n link
+}
+
+atf_test_case stdout
+stdout_head()
+{
+ atf_set "descr" "Failure to write to stdout"
+}
+stdout_body()
+{
+ atf_check ln -s target link
+ (
+ trap "" PIPE
+ sleep 1
+ readlink link 2>stderr
+ echo $? >result
+ ) | true
+ atf_check -o inline:"1\n" cat result
+ atf_check -o match:"stdout" cat stderr
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case basic
+ atf_add_test_case noarg
+ atf_add_test_case notlink
+ atf_add_test_case multi
+ atf_add_test_case f_flag
+ atf_add_test_case n_flag
+ atf_add_test_case stdout
+}
diff --git a/usr.bin/stat/Makefile b/usr.bin/stat/Makefile
--- a/usr.bin/stat/Makefile
+++ b/usr.bin/stat/Makefile
@@ -2,9 +2,6 @@
PROG= stat
-LINKS= ${BINDIR}/stat ${BINDIR}/readlink
-MLINKS= stat.1 readlink.1
-
HAS_TESTS=
SUBDIR.${MK_TESTS}+= tests
diff --git a/usr.bin/stat/stat.1 b/usr.bin/stat/stat.1
--- a/usr.bin/stat/stat.1
+++ b/usr.bin/stat/stat.1
@@ -33,8 +33,7 @@
.Dt STAT 1
.Os
.Sh NAME
-.Nm stat ,
-.Nm readlink
+.Nm stat
.Nd display file status
.Sh SYNOPSIS
.Nm
@@ -42,9 +41,6 @@
.Op Fl f Ar format | Fl l | r | s | x
.Op Fl t Ar timefmt
.Op Ar
-.Nm readlink
-.Op Fl fn
-.Op Ar
.Sh DESCRIPTION
The
.Nm
@@ -57,24 +53,6 @@
.Nm
displays information about the file descriptor for standard input.
.Pp
-When invoked as
-.Nm readlink ,
-only the target of the symbolic link is printed.
-If the given argument is not a symbolic link and the
-.Fl f
-option is not specified,
-.Nm readlink
-will print nothing and exit with an error.
-If the
-.Fl f
-option is specified, the output is canonicalized by following every symlink
-in every component of the given path recursively.
-.Nm readlink
-will resolve both absolute and relative paths, and return the absolute pathname
-corresponding to
-.Ar file .
-In this case, the argument does not need to be a symbolic link.
-.Pp
The information displayed is obtained by calling
.Xr lstat 2
with the given argument and evaluating the returned structure.
@@ -194,9 +172,6 @@
or
.Xr lstat 2
fail.
-When run as
-.Nm readlink ,
-error messages are automatically suppressed.
.It Fl f Ar format
Display information using the specified format.
See the
@@ -512,7 +487,7 @@
which default to
.Cm S .
.Sh EXIT STATUS
-.Ex -std stat readlink
+.Ex -std stat
.Sh EXAMPLES
If no options are specified, the default format is
"%d %i %Sp %l %Su %Sg %r %z \e"%Sa\e" \e"%Sm\e" \e"%Sc\e" \e"%SB\e" %k %b %#Xf %N".
@@ -624,7 +599,6 @@
.Xr file 1 ,
.Xr ls 1 ,
.Xr lstat 2 ,
-.Xr readlink 2 ,
.Xr stat 2 ,
.Xr printf 3 ,
.Xr strftime 3
diff --git a/usr.bin/stat/stat.c b/usr.bin/stat/stat.c
--- a/usr.bin/stat/stat.c
+++ b/usr.bin/stat/stat.c
@@ -138,7 +138,6 @@
#define SHOW_filename 'N'
#define SHOW_sizerdev 'Z'
-static void usage(const char *);
static void output(const struct stat *, const char *, const char *, int);
static int format1(const struct stat *, /* stat info */
const char *, /* the file name */
@@ -152,7 +151,6 @@
static int listholes(const char *);
static const char *timefmt;
-static int linkfail;
static bool nonl;
#define addchar(s, c, nl) \
@@ -161,18 +159,27 @@
(*nl) = ((c) == '\n'); \
} while (0/*CONSTCOND*/)
+
+static void
+usage(void)
+{
+ (void)fprintf(stderr, "usage: stat [-FHhLnq] "
+ "[-f format | -l | -r | -s | -x] "
+ "[-t timefmt] [file|handle ...]");
+ exit(1);
+}
+
int
main(int argc, char *argv[])
{
struct stat st;
char dname[sizeof(_PATH_DEV) + SPECNAMELEN] = _PATH_DEV;
- const char *statfmt, *options, *synopsis;
+ const char *statfmt;
const char *file;
fhandle_t fhnd;
- int ch, rc, errs, am_readlink, fn, fmtchar;
+ int ch, rc, errs, fn, fmtchar;
bool lsF, holes, usestat, nfs_handle, quiet;
- am_readlink = 0;
errs = 0;
lsF = false;
fmtchar = '\0';
@@ -181,24 +188,10 @@
nfs_handle = false;
nonl = false;
quiet = false;
- linkfail = 0;
statfmt = NULL;
timefmt = NULL;
- if (strcmp(getprogname(), "readlink") == 0) {
- am_readlink = 1;
- options = "fn";
- synopsis = "[-fn] [file ...]";
- statfmt = "%Y";
- fmtchar = 'f';
- quiet = 1;
- } else {
- options = "Ff:HhLlnqrst:x";
- synopsis = "[-FHhLnq] [-f format | -l | -r | -s | -x] "
- "[-t timefmt] [file|handle ...]";
- }
-
- while ((ch = getopt(argc, argv, options)) != -1)
+ while ((ch = getopt(argc, argv, "Ff:HhLlnqrst:x")) != -1)
switch (ch) {
case 'F':
lsF = true;
@@ -223,10 +216,6 @@
break;
/* remaining cases are purposefully out of order */
case 'f':
- if (am_readlink) {
- statfmt = "%R";
- break;
- }
statfmt = optarg;
/* FALLTHROUGH */
case 'l':
@@ -239,7 +228,7 @@
fmtchar = ch;
break;
default:
- usage(synopsis);
+ usage();
}
argc -= optind;
@@ -248,7 +237,7 @@
if (holes) {
if (fmtchar || lsF || nfs_handle || usestat || timefmt)
- usage(synopsis);
+ usage();
if (argc > 0) {
while (argc-- > 0) {
if (listholes(*argv) != 0) {
@@ -299,7 +288,7 @@
timefmt = "%c";
break;
default:
- usage(synopsis);
+ usage();
/*NOTREACHED*/
}
@@ -354,7 +343,6 @@
if (rc == -1) {
errs = 1;
- linkfail = 1;
if (!quiet)
warn("%s", file);
} else
@@ -365,7 +353,7 @@
fn++;
} while (argc > 0);
- return (am_readlink ? linkfail : errs);
+ return (errs);
}
/*
@@ -385,13 +373,6 @@
return (str);
}
-static void
-usage(const char *synopsis)
-{
- (void)fprintf(stderr, "usage: %s %s\n", getprogname(), synopsis);
- exit(1);
-}
-
/*
* Parses a format string.
*/
@@ -797,7 +778,6 @@
} else {
snprintf(path, sizeof(path), " -> ");
if (realpath(file, path + 4) == NULL) {
- linkfail = 1;
l = 0;
path[0] = '\0';
}
@@ -815,7 +795,6 @@
snprintf(path, sizeof(path), " -> ");
l = readlink(file, path + 4, sizeof(path) - 4 - 1);
if (l == -1) {
- linkfail = 1;
l = 0;
path[0] = '\0';
}
@@ -823,7 +802,6 @@
sdata = path + (ofmt == FMTF_STRING ? 0 : 4);
}
else {
- linkfail = 1;
sdata = "";
}
formats = FMTF_STRING;
diff --git a/usr.bin/stat/tests/Makefile b/usr.bin/stat/tests/Makefile
--- a/usr.bin/stat/tests/Makefile
+++ b/usr.bin/stat/tests/Makefile
@@ -1,4 +1,3 @@
-ATF_TESTS_SH+= readlink_test
ATF_TESTS_SH+= stat_test
.include <bsd.test.mk>
diff --git a/usr.bin/stat/tests/readlink_test.sh b/usr.bin/stat/tests/readlink_test.sh
deleted file mode 100755
--- a/usr.bin/stat/tests/readlink_test.sh
+++ /dev/null
@@ -1,75 +0,0 @@
-#
-# Copyright (c) 2017 Dell EMC
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
-#
-
-atf_test_case basic
-basic_head()
-{
- atf_set "descr" "Verify that calling readlink without any flags " \
- "prints out the symlink target for a file"
-}
-basic_body()
-{
- atf_check ln -s foo bar
- atf_check -o inline:"foo\n" readlink bar
-}
-
-atf_test_case f_flag
-f_flag_head()
-{
- atf_set "descr" "Verify that calling readlink with -f will not emit " \
- "an error message/exit with a non-zero code"
-}
-f_flag_body()
-{
- cd "$(realpath "$PWD")"
- atf_check touch A.file
- atf_check ln -s nonexistent A.link
- atf_check -o inline:"nonexistent\n" \
- -s exit:1 readlink A.file A.link
- atf_check -o inline:"$(realpath A.file)\n$PWD/nonexistent\n" \
- -s exit:1 readlink -f A.file A.link
-}
-
-atf_test_case n_flag
-n_flag_head()
-{
- atf_set "descr" "Verify that calling readlink with -n will not emit " \
- "a newline character."
-}
-n_flag_body()
-{
- atf_check ln -s nonexistent.A A
- atf_check ln -s nonexistent.B B
- atf_check -o inline:"nonexistent.A\nnonexistent.B\n" readlink A B
- atf_check -o inline:"nonexistent.Anonexistent.B" readlink -n A B
-}
-
-atf_init_test_cases()
-{
- atf_add_test_case basic
- atf_add_test_case f_flag
- atf_add_test_case n_flag
-}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Jun 25, 7:03 PM (13 h, 34 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
34327340
Default Alt Text
D56772.diff (14 KB)
Attached To
Mode
D56772: readlink: Standalone implementation
Attached
Detach File
Event Timeline
Log In to Comment