Page MenuHomeFreeBSD

readlink: Standalone implementation
Needs ReviewPublic

Authored by des on May 2 2026, 6:54 PM.
Tags
None
Referenced Files
F174273742: D56772.id187664.diff
Thu, Oct 1, 10:30 PM
F174273739: D56772.id177151.diff
Thu, Oct 1, 10:30 PM
F174273737: D56772.id177055.diff
Thu, Oct 1, 10:30 PM
F174273736: D56772.id177054.diff
Thu, Oct 1, 10:30 PM
F174273728: D56772.id177053.diff
Thu, Oct 1, 10:30 PM
F174273727: D56772.id.diff
Thu, Oct 1, 10:30 PM
F174266456: D56772.id177055.diff
Thu, Oct 1, 9:00 PM
F174266432: D56772.id187664.diff
Thu, Oct 1, 9:00 PM
Subscribers

Details

Reviewers
kevans
markj
Summary

Although readlink(1) may seem like a subset of stat(1), it is such a
small subset that the added complexity of supporting two commands in a
single codebase far outweighs the cost of duplicating what little code
they have in common. This implementation is also much closer to POSIX
conformance than the previous one.

Diff Detail

Repository
rG FreeBSD src repository
Lint
Lint Skipped
Unit
Tests Skipped
Build Status
Buildable 77315
Build 74198: arc lint + arc unit

Event Timeline

expand notlink test case

usr.bin/readlink/readlink.c
16
52

The man page says this should "print the canonical path of the target, even if the final component of the resolved path does not exist," but:

$ ls /foo
ls: /foo: No such file or directory
$ readlink -f /foo/bar/baz
/foo
usr.bin/stat/stat.c
162

Extra newline

168
des marked 3 inline comments as done.May 5 2026, 7:45 PM
des added inline comments.
usr.bin/readlink/readlink.c
52

I realize this is incorrect, but it is not a regression as the current implementation has the exact same bug. Fixing it is non-trivial.

usr.bin/readlink/readlink.c
52

The current implementation's behaviour is different though:

$ ls /foo
ls: /foo: No such file or directory
$ readlink -f /foo/bar/baz
/foo
$ echo $?
0

vs. the current behaviour on main:

$ ls /foo
ls: /foo: No such file or directory
$ readlink -f /foo/bar/baz
$ echo $?
1
usr.bin/readlink/readlink.c
52

They are both supposed to print an error message and return a non-zero exit code, but that requires determining which component failed, which essentially means implementing at least part of realpath(3) ourselves.

usr.bin/readlink/readlink.c
52

Uh sorry I was thinking of a different test case than what you are showing here. This can be fixed with an lstat(2).

usr.bin/readlink/readlink.c
52

Picking this up again and I'm confused. It seems to me the historical behavior does not match the documentation while the new behavior does. The intent is that readlink -f /path/to/file should succeed as long as every component up to but possibly not including the final one can be resolved, just like the legacy semantics of realpath(3), so it can be used to normalize the path of a file we are about to create. Are you worried about code that relies on the historical (incorrect) behavior?

usr.bin/readlink/readlink.c
52

ooh, I read your message backward, sorry...

des marked 2 inline comments as done.Fri, Sep 25, 11:33 AM
usr.bin/readlink/readlink.1
30
$ readlink -f /etc/foo
/etc

is that the right behaviour? I'd expect it to print /etc/foo based on this description. Though, the description is also a bit unclear, it sounds like this should simply result in an error since /etc/foo isn't a symlink. That's the current behaviour.

usr.bin/readlink/readlink.c
66

Missing free(dir)