git: e9ab8b45822f - stable/14 - ln: Improve link(1) variant of ln(1).

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Mon, 08 Apr 2024 10:11:05 UTC
The branch stable/14 has been updated by des:

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

commit e9ab8b45822f30173c408480e8df975084135731
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2024-04-04 14:14:50 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2024-04-08 10:10:41 +0000

    ln: Improve link(1) variant of ln(1).
    
    * Give link(1) its own usage message.
    * Use getprogname(3) instead of rolling our own.
    * Verify that the target file does not already exist.
    * Add tests specific to link(1).
    
    MFC after:      3 days
    Sponsored by:   Klara, Inc.
    Reviewed by:    allanjude
    Differential Revision:  https://reviews.freebsd.org/D44635
    
    (cherry picked from commit bee7cf9e97f6d7bdc918421a93270fa88659808b)
---
 bin/ln/ln.c             | 32 +++++++++++++++---------
 bin/ln/tests/ln_test.sh | 65 ++++++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 83 insertions(+), 14 deletions(-)

diff --git a/bin/ln/ln.c b/bin/ln/ln.c
index a0e19d702dea..3919494c3d4c 100644
--- a/bin/ln/ln.c
+++ b/bin/ln/ln.c
@@ -67,13 +67,14 @@ static bool	wflag;			/* Warn if symlink target does not
 static char	linkch;
 
 static int	linkit(const char *, const char *, bool);
+static void	link_usage(void) __dead2;
 static void	usage(void) __dead2;
 
 int
 main(int argc, char *argv[])
 {
 	struct stat sb;
-	char *p, *targetdir;
+	char *targetdir;
 	int ch, exitval;
 
 	/*
@@ -81,17 +82,20 @@ main(int argc, char *argv[])
 	 * "link", for which the functionality provided is greatly
 	 * simplified.
 	 */
-	if ((p = strrchr(argv[0], '/')) == NULL)
-		p = argv[0];
-	else
-		++p;
-	if (strcmp(p, "link") == 0) {
+	if (strcmp(getprogname(), "link") == 0) {
 		while (getopt(argc, argv, "") != -1)
-			usage();
+			link_usage();
 		argc -= optind;
 		argv += optind;
 		if (argc != 2)
-			usage();
+			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));
 	}
 
@@ -349,12 +353,18 @@ linkit(const char *source, const char *target, bool isdir)
 	return (0);
 }
 
+static void
+link_usage(void)
+{
+	(void)fprintf(stderr, "usage: link source_file target_file\n");
+	exit(1);
+}
+
 static void
 usage(void)
 {
-	(void)fprintf(stderr, "%s\n%s\n%s\n",
+	(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",
-	    "       link source_file target_file");
+	    "       ln [-s [-F] | -L | -P] [-f | -i] [-hnv] source_file ... target_dir");
 	exit(1);
 }
diff --git a/bin/ln/tests/ln_test.sh b/bin/ln/tests/ln_test.sh
index 8e5dcf81e61f..82bc556842d8 100644
--- a/bin/ln/tests/ln_test.sh
+++ b/bin/ln/tests/ln_test.sh
@@ -90,7 +90,7 @@ target_exists_hard_body()
 {
 	atf_check touch A B
 	atf_check -s exit:1 -e inline:'ln: B: File exists\n' \
-		ln A B
+	    ln A B
 }
 
 atf_test_case target_exists_symbolic
@@ -103,7 +103,7 @@ target_exists_symbolic_body()
 {
 	atf_check touch A B
 	atf_check -s exit:1 -e inline:'ln: B: File exists\n' \
-		ln -s A B
+	    ln -s A B
 }
 
 atf_test_case shf_flag_dir
@@ -210,10 +210,65 @@ sw_flag_head()
 sw_flag_body()
 {
 	atf_check -s exit:0 -e inline:'ln: warning: A: No such file or directory\n' \
-		ln -sw A B
+	    ln -sw A B
 	atf_check_symlink_to A B
 }
 
+atf_test_case link_argc
+link_argc_head() {
+	atf_set "descr" "Verify that link(1) requires exactly two arguments"
+}
+link_argc_body() {
+	atf_check -s exit:1 -e match:"usage: link" \
+	    link foo
+	atf_check -s exit:1 -e match:"No such file" \
+	    link foo bar
+	atf_check -s exit:1 -e match:"No such file" \
+	    link -- foo bar
+	atf_check -s exit:1 -e match:"usage: link" \
+	    link foo bar baz
+}
+
+atf_test_case link_basic
+link_basic_head() {
+	atf_set "descr" "Verify that link(1) creates a link"
+}
+link_basic_body() {
+	touch foo
+	atf_check link foo bar
+	atf_check_same_file foo bar
+	rm bar
+	ln -s foo bar
+	atf_check link bar baz
+	atf_check_same_file foo baz
+}
+
+atf_test_case link_eexist
+link_eexist_head() {
+	atf_set "descr" "Verify that link(1) fails if the target exists"
+}
+link_eexist_body() {
+	touch foo bar
+	atf_check -s exit:1 -e match:"bar.*exists" \
+	    link foo bar
+	ln -s non-existent baz
+	atf_check -s exit:1 -e match:"baz.*exists" \
+	    link foo baz
+}
+
+atf_test_case link_eisdir
+link_eisdir_head() {
+	atf_set "descr" "Verify that link(1) fails if the source is a directory"
+}
+link_eisdir_body() {
+	mkdir foo
+	atf_check -s exit:1 -e match:"foo.*directory" \
+	    link foo bar
+	ln -s foo bar
+	atf_check -s exit:1 -e match:"bar.*directory" \
+	    link bar baz
+}
+
 atf_init_test_cases()
 {
 	atf_add_test_case L_flag
@@ -229,4 +284,8 @@ atf_init_test_cases()
 	atf_add_test_case s_flag
 	atf_add_test_case s_flag_broken
 	atf_add_test_case sw_flag
+	atf_add_test_case link_argc
+	atf_add_test_case link_basic
+	atf_add_test_case link_eexist
+	atf_add_test_case link_eisdir
 }