diff --git a/ln.c b/ln.c
index 3055c75..63e1d27 100644
--- a/ln.c
+++ b/ln.c
@@ -3,6 +3,8 @@
  *
  * Copyright (c) 1987, 1993, 1994
  *	The Regents of the University of California.  All rights reserved.
+ * Copyright (c) 2026
+ *	Project Tick. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -29,350 +31,600 @@
  * SUCH DAMAGE.
  */
 
-#include <sys/param.h>
-#include <sys/stat.h>
+#define _POSIX_C_SOURCE 200809L
 
-#include <err.h>
 #include <errno.h>
 #include <fcntl.h>
-#include <libgen.h>
-#include <limits.h>
+#include <stdarg.h>
 #include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/stat.h>
 #include <unistd.h>
 
-static bool	fflag;			/* Unlink existing files. */
-static bool	Fflag;			/* Remove empty directories also. */
-static bool	hflag;			/* Check new name for symlink first. */
-static bool	iflag;			/* Interactive mode. */
-static bool	Pflag;			/* Create hard links to symlinks. */
-static bool	sflag;			/* Symbolic, not hard, link. */
-static bool	vflag;			/* Verbose output. */
-static bool	wflag;			/* Warn if symlink target does not
-					 * exist, and -f is not enabled. */
-static char	linkch;
-
-static int	linkit(const char *, const char *, bool);
-static void	link_usage(void) __dead2;
-static void	usage(void) __dead2;
+struct ln_options {
+	bool force;
+	bool remove_dir;
+	bool no_target_follow;
+	bool interactive;
+	bool follow_source_symlink;
+	bool symbolic;
+	bool verbose;
+	bool warn_missing;
+	char linkch;
+};
+
+static const char *progname;
+
+static void	error_errno(const char *fmt, ...);
+static void	error_msg(const char *fmt, ...);
+static char	*join_path(const char *dir, const char *name);
+static int	link_usage(void);
+static int	ln_usage(void);
+static int	linkit(const struct ln_options *options, const char *source,
+		    const char *target, bool target_is_dir);
+static const char *path_basename_start(const char *path);
+static size_t	path_basename_len(const char *path);
+static char	*path_basename_dup(const char *path);
+static char	*path_dirname_dup(const char *path);
+static const char *program_name(const char *argv0);
+static int	prompt_replace(const char *target);
+static int	remove_existing_target(const struct ln_options *options,
+		    const char *target, const struct stat *target_sb);
+static int	samedirent(const char *path1, const char *path2);
+static bool	should_append_basename(const struct ln_options *options,
+		    const char *target, bool target_is_dir);
+static int	stat_parent_dir(const char *path, struct stat *sb);
+static void	warn_missing_symlink_source(const char *source,
+		    const char *target);
+
+static const char *
+program_name(const char *argv0)
+{
+	const char *name;
+
+	if (argv0 == NULL || argv0[0] == '\0')
+		return ("ln");
+	name = strrchr(argv0, '/');
+	return (name == NULL ? argv0 : name + 1);
+}
+
+static void
+verror_message(bool with_errno, const char *fmt, va_list ap)
+{
+	int saved_errno;
+
+	saved_errno = errno;
+	(void)fprintf(stderr, "%s: ", progname);
+	(void)vfprintf(stderr, fmt, ap);
+	if (with_errno)
+		(void)fprintf(stderr, ": %s", strerror(saved_errno));
+	(void)fputc('\n', stderr);
+}
+
+static void
+error_errno(const char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	verror_message(true, fmt, ap);
+	va_end(ap);
+}
+
+static void
+error_msg(const char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	verror_message(false, fmt, ap);
+	va_end(ap);
+}
+
+static void *
+xmalloc(size_t size)
+{
+	void *ptr;
+
+	ptr = malloc(size);
+	if (ptr == NULL) {
+		error_msg("out of memory");
+		exit(1);
+	}
+	return (ptr);
+}
+
+static char *
+xstrdup(const char *text)
+{
+	size_t len;
+	char *copy;
+
+	len = strlen(text) + 1;
+	copy = xmalloc(len);
+	memcpy(copy, text, len);
+	return (copy);
+}
+
+static char *
+path_basename_dup(const char *path)
+{
+	const char *start;
+	size_t len;
+	char *name;
+
+	start = path_basename_start(path);
+	len = path_basename_len(path);
+	name = xmalloc(len + 1);
+	memcpy(name, start, len);
+	name[len] = '\0';
+	return (name);
+}
+
+static const char *
+path_basename_start(const char *path)
+{
+	const char *end;
+	const char *start;
+
+	if (path[0] == '\0')
+		return (path);
+
+	end = path + strlen(path);
+	while (end > path && end[-1] == '/')
+		end--;
+	if (end == path)
+		return (path);
+
+	start = end;
+	while (start > path && start[-1] != '/')
+		start--;
+	return (start);
+}
+
+static size_t
+path_basename_len(const char *path)
+{
+	const char *end;
+	const char *start;
+
+	if (path[0] == '\0')
+		return (1);
+
+	end = path + strlen(path);
+	while (end > path && end[-1] == '/')
+		end--;
+	if (end == path)
+		return (1);
+
+	start = path_basename_start(path);
+	return ((size_t)(end - start));
+}
+
+static char *
+path_dirname_dup(const char *path)
+{
+	const char *end;
+	const char *slash;
+	size_t len;
+	char *dir;
+
+	if (path[0] == '\0')
+		return (xstrdup("."));
+
+	end = path + strlen(path);
+	while (end > path && end[-1] == '/')
+		end--;
+	if (end == path)
+		return (xstrdup("/"));
+
+	slash = end;
+	while (slash > path && slash[-1] != '/')
+		slash--;
+	if (slash == path)
+		return (xstrdup("."));
+
+	while (slash > path && slash[-1] == '/')
+		slash--;
+	if (slash == path)
+		return (xstrdup("/"));
+
+	len = (size_t)(slash - path);
+	dir = xmalloc(len + 1);
+	memcpy(dir, path, len);
+	dir[len] = '\0';
+	return (dir);
+}
+
+static char *
+join_path(const char *dir, const char *name)
+{
+	size_t dir_len;
+	size_t name_len;
+	bool need_sep;
+	char *path;
+
+	dir_len = strlen(dir);
+	name_len = strlen(name);
+	need_sep = dir_len != 0 && dir[dir_len - 1] != '/';
+
+	path = xmalloc(dir_len + (need_sep ? 1U : 0U) + name_len + 1U);
+	memcpy(path, dir, dir_len);
+	if (need_sep)
+		path[dir_len++] = '/';
+	memcpy(path + dir_len, name, name_len);
+	path[dir_len + name_len] = '\0';
+	return (path);
+}
+
+static int
+samedirent(const char *path1, const char *path2)
+{
+	const char *base1;
+	const char *base2;
+	struct stat sb1;
+	struct stat sb2;
+	size_t base1_len;
+	size_t base2_len;
+
+	if (strcmp(path1, path2) == 0)
+		return (1);
+
+	base1 = path_basename_start(path1);
+	base2 = path_basename_start(path2);
+	base1_len = path_basename_len(path1);
+	base2_len = path_basename_len(path2);
+	if (base1_len != base2_len || memcmp(base1, base2, base1_len) != 0)
+		return (0);
+
+	if (stat_parent_dir(path1, &sb1) != 0 || stat_parent_dir(path2, &sb2) != 0)
+		return (0);
+
+	return (sb1.st_dev == sb2.st_dev && sb1.st_ino == sb2.st_ino);
+}
+
+static bool
+should_append_basename(const struct ln_options *options, const char *target,
+    bool target_is_dir)
+{
+	struct stat sb;
+	const char *base;
+
+	base = strrchr(target, '/');
+	base = base == NULL ? target : base + 1;
+	if (base[0] == '\0' || (base[0] == '.' && base[1] == '\0'))
+		return (true);
+
+	if (options->remove_dir)
+		return (false);
+	if (target_is_dir)
+		return (true);
+	if (lstat(target, &sb) == 0 && S_ISDIR(sb.st_mode))
+		return (true);
+	if (options->no_target_follow)
+		return (false);
+	if (stat(target, &sb) == 0 && S_ISDIR(sb.st_mode))
+		return (true);
+	return (false);
+}
+
+static int
+stat_parent_dir(const char *path, struct stat *sb)
+{
+	char *dir;
+	size_t dir_len;
+	const char *base;
+
+	base = path_basename_start(path);
+	if (base == path)
+		return (stat(".", sb));
+
+	dir_len = (size_t)(base - path);
+	while (dir_len > 1 && path[dir_len - 1] == '/')
+		dir_len--;
+	if (dir_len == 1 && path[0] == '/')
+		return (stat("/", sb));
+
+	dir = xmalloc(dir_len + 1);
+	memcpy(dir, path, dir_len);
+	dir[dir_len] = '\0';
+	if (stat(dir, sb) != 0) {
+		free(dir);
+		return (-1);
+	}
+	free(dir);
+	return (0);
+}
+
+static int
+remove_existing_target(const struct ln_options *options, const char *target,
+    const struct stat *target_sb)
+{
+	if (options->remove_dir && S_ISDIR(target_sb->st_mode)) {
+		if (rmdir(target) != 0) {
+			error_errno("%s", target);
+			return (1);
+		}
+		return (0);
+	}
+
+	if (unlink(target) != 0) {
+		error_errno("%s", target);
+		return (1);
+	}
+	return (0);
+}
+
+static void
+warn_missing_symlink_source(const char *source, const char *target)
+{
+	char *dir;
+	char *resolved;
+	struct stat st;
+
+	if (source[0] == '/') {
+		if (stat(source, &st) != 0)
+			error_errno("warning: %s", source);
+		return;
+	}
+
+	dir = path_dirname_dup(target);
+	resolved = join_path(dir, source);
+	if (stat(resolved, &st) != 0)
+		error_errno("warning: %s", source);
+	free(dir);
+	free(resolved);
+}
+
+static int
+prompt_replace(const char *target)
+{
+	char answer[16];
+	bool stdin_is_tty;
+	int ch;
+
+	stdin_is_tty = isatty(STDIN_FILENO) == 1;
+	(void)stdin_is_tty;
+
+	(void)fflush(stdout);
+	(void)fprintf(stderr, "replace %s? ", target);
+	if (fgets(answer, sizeof(answer), stdin) == NULL) {
+		if (ferror(stdin)) {
+			error_errno("stdin");
+			return (-1);
+		}
+		if (!stdin_is_tty && feof(stdin)) {
+			(void)fprintf(stderr, "not replaced\n");
+			return (1);
+		}
+		(void)fprintf(stderr, "not replaced\n");
+		return (1);
+	}
+
+	if (strchr(answer, '\n') == NULL) {
+		while ((ch = getchar()) != '\n' && ch != EOF)
+			continue;
+		if (ferror(stdin)) {
+			error_errno("stdin");
+			return (-1);
+		}
+	}
+
+	if (answer[0] != 'y' && answer[0] != 'Y') {
+		(void)fprintf(stderr, "not replaced\n");
+		return (1);
+	}
+	return (0);
+}
+
+static int
+linkit(const struct ln_options *options, const char *source, const char *target,
+    bool target_is_dir)
+{
+	struct stat source_sb;
+	struct stat target_sb;
+	char *resolved_target;
+	bool exists;
+	int flags;
+	int ret;
+
+	resolved_target = NULL;
+	ret = 1;
+	if (!options->symbolic) {
+		if ((options->follow_source_symlink ? stat : lstat)(source,
+		    &source_sb) != 0) {
+			error_errno("%s", source);
+			goto cleanup;
+		}
+		if (S_ISDIR(source_sb.st_mode)) {
+			errno = EISDIR;
+			error_errno("%s", source);
+			goto cleanup;
+		}
+	}
+
+	if (should_append_basename(options, target, target_is_dir)) {
+		char *base;
+
+		base = path_basename_dup(source);
+		resolved_target = join_path(target, base);
+		free(base);
+	} else {
+		resolved_target = xstrdup(target);
+	}
+
+	if (options->symbolic && options->warn_missing)
+		warn_missing_symlink_source(source, resolved_target);
+
+	exists = lstat(resolved_target, &target_sb) == 0;
+	if (exists && !options->symbolic && samedirent(source, resolved_target)) {
+		error_msg("%s and %s are the same directory entry", source,
+		    resolved_target);
+		goto cleanup;
+	}
+
+	if (exists && options->force) {
+		if (remove_existing_target(options, resolved_target, &target_sb) != 0)
+			goto cleanup;
+	} else if (exists && options->interactive) {
+		ret = prompt_replace(resolved_target);
+		if (ret != 0)
+			goto cleanup;
+		if (remove_existing_target(options, resolved_target, &target_sb) != 0)
+			goto cleanup;
+	}
+
+	flags = options->follow_source_symlink ? AT_SYMLINK_FOLLOW : 0;
+	if (options->symbolic) {
+		if (symlink(source, resolved_target) != 0) {
+			error_errno("%s", resolved_target);
+			goto cleanup;
+		}
+	} else if (linkat(AT_FDCWD, source, AT_FDCWD, resolved_target, flags) != 0) {
+		error_errno("%s", resolved_target);
+		goto cleanup;
+	}
+
+	if (options->verbose)
+		(void)printf("%s %c> %s\n", resolved_target, options->linkch, source);
+
+	ret = 0;
+cleanup:
+	free(resolved_target);
+	return (ret);
+}
+
+static int
+link_usage(void)
+{
+	(void)fprintf(stderr, "usage: link source_file target_file\n");
+	return (2);
+}
+
+static int
+ln_usage(void)
+{
+	(void)fprintf(stderr,
+	    "usage: ln [-s [-F] | -L | -P] [-f | -i] [-hnvw] "
+	    "source_file [target_file]\n");
+	(void)fprintf(stderr,
+	    "       ln [-s [-F] | -L | -P] [-f | -i] [-hnvw] "
+	    "source_file ... target_dir\n");
+	return (2);
+}
 
 int
 main(int argc, char *argv[])
 {
+	struct ln_options options;
 	struct stat sb;
 	char *targetdir;
-	int ch, exitval;
-
-	/*
-	 * Test for the special case where the utility is called as
-	 * "link", for which the functionality provided is greatly
-	 * simplified.
-	 */
-	if (strcmp(getprogname(), "link") == 0) {
-		while (getopt(argc, argv, "") != -1)
-			link_usage();
+	int ch;
+	int exitval;
+
+	progname = program_name(argv[0]);
+	memset(&options, 0, sizeof(options));
+	/* FreeBSD hard-link semantics default to -L, unlike GNU ln's -P. */
+	options.follow_source_symlink = true;
+
+	if (strcmp(progname, "link") == 0) {
+		opterr = 0;
+		while ((ch = getopt(argc, argv, "")) != -1)
+			return (link_usage());
 		argc -= optind;
 		argv += optind;
 		if (argc != 2)
-			link_usage();
-		if (lstat(argv[1], &sb) == 0)
-			errc(1, EEXIST, "%s", argv[1]);
-		/*
-		 * We could simply call link(2) here, but linkit()
-		 * performs additional checks and gives better
-		 * diagnostics.
-		 */
-		exit(linkit(argv[0], argv[1], false));
+			return (link_usage());
+		if (lstat(argv[1], &sb) == 0) {
+			errno = EEXIST;
+			error_errno("%s", argv[1]);
+			return (1);
+		}
+		return (linkit(&options, argv[0], argv[1], false));
 	}
 
-	while ((ch = getopt(argc, argv, "FLPfhinsvw")) != -1)
+	opterr = 0;
+	while ((ch = getopt(argc, argv, "FLPfhinsvw")) != -1) {
 		switch (ch) {
 		case 'F':
-			Fflag = true;
+			options.remove_dir = true;
 			break;
 		case 'L':
-			Pflag = false;
+			options.follow_source_symlink = true;
 			break;
 		case 'P':
-			Pflag = true;
+			options.follow_source_symlink = false;
 			break;
 		case 'f':
-			fflag = true;
-			iflag = false;
-			wflag = false;
+			options.force = true;
+			options.interactive = false;
+			options.warn_missing = false;
 			break;
 		case 'h':
 		case 'n':
-			hflag = true;
+			options.no_target_follow = true;
 			break;
 		case 'i':
-			iflag = true;
-			fflag = false;
+			options.interactive = true;
+			options.force = false;
 			break;
 		case 's':
-			sflag = true;
+			options.symbolic = true;
 			break;
 		case 'v':
-			vflag = true;
+			options.verbose = true;
 			break;
 		case 'w':
-			wflag = true;
+			options.warn_missing = true;
 			break;
 		case '?':
 		default:
-			usage();
+			if (optopt != 0)
+				error_msg("unknown option -- %c", optopt);
+			return (ln_usage());
 		}
+	}
 
 	argv += optind;
 	argc -= optind;
 
-	linkch = sflag ? '-' : '=';
-	if (!sflag)
-		Fflag = false;
-	if (Fflag && !iflag) {
-		fflag = true;
-		wflag = false;		/* Implied when fflag is true */
+	options.linkch = options.symbolic ? '-' : '=';
+	if (!options.symbolic)
+		options.remove_dir = false;
+	if (options.remove_dir && !options.interactive) {
+		options.force = true;
+		options.warn_missing = false;
 	}
 
 	switch (argc) {
 	case 0:
-		usage();
-		/* NOTREACHED */
-	case 1:				/* ln source */
-		exit(linkit(argv[0], ".", true));
-	case 2:				/* ln source target */
-		exit(linkit(argv[0], argv[1], false));
+		return (ln_usage());
+	case 1:
+		return (linkit(&options, argv[0], ".", true));
+	case 2:
+		return (linkit(&options, argv[0], argv[1], false));
 	default:
-		;
+		break;
 	}
-					/* ln source1 source2 directory */
+
 	targetdir = argv[argc - 1];
-	if (hflag && lstat(targetdir, &sb) == 0 && S_ISLNK(sb.st_mode)) {
-		/*
-		 * We were asked not to follow symlinks, but found one at
-		 * the target--simulate "not a directory" error
-		 */
+	if (options.no_target_follow && lstat(targetdir, &sb) == 0 &&
+	    S_ISLNK(sb.st_mode)) {
 		errno = ENOTDIR;
-		err(1, "%s", targetdir);
-	}
-	if (stat(targetdir, &sb))
-		err(1, "%s", targetdir);
-	if (!S_ISDIR(sb.st_mode))
-		usage();
-	for (exitval = 0; *argv != targetdir; ++argv)
-		exitval |= linkit(*argv, targetdir, true);
-	exit(exitval);
-}
-
-/*
- * Two pathnames refer to the same directory entry if the directories match
- * and the final components' names match.
- */
-static int
-samedirent(const char *path1, const char *path2)
-{
-	const char *file1, *file2;
-	char pathbuf[PATH_MAX];
-	struct stat sb1, sb2;
-
-	if (strcmp(path1, path2) == 0)
-		return 1;
-	file1 = strrchr(path1, '/');
-	if (file1 != NULL)
-		file1++;
-	else
-		file1 = path1;
-	file2 = strrchr(path2, '/');
-	if (file2 != NULL)
-		file2++;
-	else
-		file2 = path2;
-	if (strcmp(file1, file2) != 0)
-		return 0;
-	if (file1 - path1 >= PATH_MAX || file2 - path2 >= PATH_MAX)
-		return 0;
-	if (file1 == path1)
-		memcpy(pathbuf, ".", 2);
-	else {
-		memcpy(pathbuf, path1, file1 - path1);
-		pathbuf[file1 - path1] = '\0';
-	}
-	if (stat(pathbuf, &sb1) != 0)
-		return 0;
-	if (file2 == path2)
-		memcpy(pathbuf, ".", 2);
-	else {
-		memcpy(pathbuf, path2, file2 - path2);
-		pathbuf[file2 - path2] = '\0';
-	}
-	if (stat(pathbuf, &sb2) != 0)
-		return 0;
-	return sb1.st_dev == sb2.st_dev && sb1.st_ino == sb2.st_ino;
-}
-
-/*
- * Create a link to source.  If target is a directory (and some additional
- * conditions apply, see comments within) the link will be created within
- * target and have the basename of source.  Otherwise, the link will be
- * named target.  If isdir is true, target has already been determined to
- * be a directory; otherwise, we will check, if needed.
- */
-static int
-linkit(const char *source, const char *target, bool isdir)
-{
-	char path[PATH_MAX];
-	char wbuf[PATH_MAX];
-	char bbuf[PATH_MAX];
-	struct stat sb;
-	const char *p;
-	int ch, first;
-	bool append, exists;
-
-	if (!sflag) {
-		/* If source doesn't exist, quit now. */
-		if ((Pflag ? lstat : stat)(source, &sb)) {
-			warn("%s", source);
-			return (1);
-		}
-		/* Only symbolic links to directories. */
-		if (S_ISDIR(sb.st_mode)) {
-			errno = EISDIR;
-			warn("%s", source);
-			return (1);
-		}
-	}
-
-	/*
-	 * Append a slash and the source's basename if:
-	 * - the target is "." or ends in "/" or "/.", or
-	 * - the target is a directory (and not a symlink if hflag) and
-         *   Fflag is not set
-	 */
-	if ((p = strrchr(target, '/')) == NULL)
-		p = target;
-	else
-		p++;
-	append = false;
-	if (p[0] == '\0' || (p[0] == '.' && p[1] == '\0')) {
-		append = true;
-	} else if (!Fflag) {
-		if (isdir || (lstat(target, &sb) == 0 && S_ISDIR(sb.st_mode)) ||
-		    (!hflag && stat(target, &sb) == 0 && S_ISDIR(sb.st_mode))) {
-			append = true;
-		}
-	}
-	if (append) {
-		if (strlcpy(bbuf, source, sizeof(bbuf)) >= sizeof(bbuf) ||
-		    (p = basename(bbuf)) == NULL /* can't happen */ ||
-		    snprintf(path, sizeof(path), "%s/%s", target, p) >=
-		    (ssize_t)sizeof(path)) {
-			errno = ENAMETOOLONG;
-			warn("%s", source);
-			return (1);
-		}
-		target = path;
-	}
-
-	/*
-	 * If the link source doesn't exist, and a symbolic link was
-	 * requested, and -w was specified, give a warning.
-	 */
-	if (sflag && wflag) {
-		if (*source == '/') {
-			/* Absolute link source. */
-			if (stat(source, &sb) != 0)
-				 warn("warning: %s inaccessible", source);
-		} else {
-			/*
-			 * Relative symlink source.  Try to construct the
-			 * absolute path of the source, by appending `source'
-			 * to the parent directory of the target.
-			 */
-			strlcpy(bbuf, target, sizeof(bbuf));
-			p = dirname(bbuf);
-			if (p != NULL) {
-				(void)snprintf(wbuf, sizeof(wbuf), "%s/%s",
-						p, source);
-				if (stat(wbuf, &sb) != 0)
-					warn("warning: %s", source);
-			}
-		}
-	}
-
-	/*
-	 * If the file exists, first check it is not the same directory entry.
-	 */
-	exists = lstat(target, &sb) == 0;
-	if (exists) {
-		if (!sflag && samedirent(source, target)) {
-			warnx("%s and %s are the same directory entry",
-			    source, target);
-			return (1);
-		}
-	}
-	/*
-	 * Then unlink it forcibly if -f was specified
-	 * and interactively if -i was specified.
-	 */
-	if (fflag && exists) {
-		if (Fflag && S_ISDIR(sb.st_mode)) {
-			if (rmdir(target)) {
-				warn("%s", target);
-				return (1);
-			}
-		} else if (unlink(target)) {
-			warn("%s", target);
-			return (1);
-		}
-	} else if (iflag && exists) {
-		fflush(stdout);
-		fprintf(stderr, "replace %s? ", target);
-
-		first = ch = getchar();
-		while(ch != '\n' && ch != EOF)
-			ch = getchar();
-		if (first != 'y' && first != 'Y') {
-			fprintf(stderr, "not replaced\n");
-			return (1);
-		}
-
-		if (Fflag && S_ISDIR(sb.st_mode)) {
-			if (rmdir(target)) {
-				warn("%s", target);
-				return (1);
-			}
-		} else if (unlink(target)) {
-			warn("%s", target);
-			return (1);
-		}
+		error_errno("%s", targetdir);
+		return (1);
 	}
-
-	/* Attempt the link. */
-	if (sflag ? symlink(source, target) :
-	    linkat(AT_FDCWD, source, AT_FDCWD, target,
-	    Pflag ? 0 : AT_SYMLINK_FOLLOW)) {
-		warn("%s", target);
+	if (stat(targetdir, &sb) != 0) {
+		error_errno("%s", targetdir);
 		return (1);
 	}
-	if (vflag)
-		(void)printf("%s %c> %s\n", target, linkch, source);
-	return (0);
-}
-
-static void
-link_usage(void)
-{
-	(void)fprintf(stderr, "usage: link source_file target_file\n");
-	exit(1);
-}
+	if (!S_ISDIR(sb.st_mode))
+		return (ln_usage());
 
-static void
-usage(void)
-{
-	(void)fprintf(stderr, "%s\n%s\n",
-	    "usage: ln [-s [-F] | -L | -P] [-f | -i] [-hnv] source_file [target_file]",
-	    "       ln [-s [-F] | -L | -P] [-f | -i] [-hnv] source_file ... target_dir");
-	exit(1);
+	exitval = 0;
+	for (int i = 0; i < argc - 1; i++)
+		exitval |= linkit(&options, argv[i], targetdir, true);
+	return (exitval);
 }
