commit: 24f119dcbe9a6dd5508090b87b278f4e3c5601df
parent 92c933a1d3fe2cce2bbf5c85c4b89b956c02c9bd
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Sat, 13 Dec 2025 15:17:39 +0100
cmd/ln: add support for -T option
Diffstat:
3 files changed, 74 insertions(+), 11 deletions(-)
diff --git a/cmd/ln.1 b/cmd/ln.1
@@ -11,11 +11,22 @@
.Nm
.Op Fl fnv
.Op Fl L Ns | Ns Fl P
+.Fl T
+.Ar source
+.Ar target
+.Nm
+.Op Fl fnv
+.Op Fl L Ns | Ns Fl P
.Ar source...
.Op Ar target
.Nm
+.Op Fl fnv
+.Fl Ts
+.Ar reference
+.Ar target
+.Nm
+.Op Fl fnv
.Fl s
-.Op Fl fn
.Ar reference...
.Op Ar target
.Sh DESCRIPTION
@@ -27,7 +38,9 @@ for each given
or
.Ar reference .
.Pp
-When
+Unless
+.Fl T
+is passed, when
.Ar target
is an existing directory or multiple
.Ar source
@@ -68,6 +81,10 @@ is a symbolic link, hard link it.
This is the default.
.It Fl s
Create symbolic links instead of hard links.
+.It Fl T
+Always treat
+.Ar target
+as a normal file.
.It Fl v
Print successfully created links.
.El
@@ -79,7 +96,8 @@ should be compliant with the
IEEE Std 1003.1-2024 (“POSIX.1”)
specification.
The
-.Fl n
+.Fl n ,
+.Fl T ,
and
.Fl v
options are extensions.
diff --git a/cmd/ln.c b/cmd/ln.c
@@ -25,7 +25,7 @@
const char *argv0 = "ln";
-static bool opt_s = false, force = false;
+static bool opt_s = false, force = false, opt_T = false;
static int link_flags = 0;
static int open_target_flags = O_RDONLY | O_PATH;
static int open_dest_flags = O_RDONLY | O_PATH | O_NOFOLLOW;
@@ -68,7 +68,7 @@ do_link(char *src, char *dest, int destfd)
return -1;
}
- if(destfd < 0)
+ if(destfd < 0 && !opt_T)
{
destfd = open(dest, open_dest_flags);
if(destfd < 0)
@@ -95,7 +95,7 @@ do_link(char *src, char *dest, int destfd)
}
int dirfd = AT_FDCWD;
- if(S_ISDIR(dest_stat.st_mode))
+ if(S_ISDIR(dest_stat.st_mode) && !opt_T)
{
dirfd = destfd;
}
@@ -166,8 +166,9 @@ static void
usage(void)
{
fprintf(stderr, "\
-Usage: ln [-fnv] [-L|-P] source... target\n\
- ln -s [-fnv] reference... target\n\
+Usage: ln [-fnv] [-L|-P] -T source target\n\
+ ln [-fnv] [-L|-P] source... [target]\n\
+ ln -s [-fnv] reference... [target]\n\
");
}
@@ -181,6 +182,7 @@ main(int argc, char *argv[])
{"force", no_argument, NULL, 'f'},
{"logical", no_argument, NULL, 'L'},
{"no-dereference", no_argument, NULL, 'n'},
+ {"no-target-directory", no_argument, NULL, 'T'},
{"physical", no_argument, NULL, 'P'},
{"symbolic", no_argument, NULL, 's'},
{"verbose", no_argument, NULL, 'v'},
@@ -189,9 +191,9 @@ main(int argc, char *argv[])
// clang-format on
// Need + as first character to get POSIX-style option parsing
- for(int c = -1; (c = getopt_long(argc, argv, "+:fnsLPv", opts, NULL)) != -1;)
+ for(int c = -1; (c = getopt_long(argc, argv, "+:fnsLPTv", opts, NULL)) != -1;)
#else
- for(int c = -1; (c = getopt_nolong(argc, argv, ":fnsLPv")) != -1;)
+ for(int c = -1; (c = getopt_nolong(argc, argv, ":fnsLPTv")) != -1;)
#endif
{
switch(c)
@@ -214,6 +216,9 @@ main(int argc, char *argv[])
case 'v':
verbose = true;
break;
+ case 'T':
+ opt_T = true;
+ break;
case '?':
GETOPT_UNKNOWN_OPT
usage();
@@ -232,6 +237,11 @@ main(int argc, char *argv[])
fprintf(stderr, "ln: error: Not enough operands, %d given, expect >= 1\n", argc);
return 1;
}
+ else if(opt_T && argc != 2)
+ {
+ fprintf(stderr, "ln: error: Option -T passed, but got %d arguments instead of 2\n", argc);
+ return 1;
+ }
else if(argc == 1)
{
target = (char *)".";
@@ -239,6 +249,13 @@ main(int argc, char *argv[])
}
else if(argc == 2)
{
+ if(opt_T)
+ {
+ int ret = do_link(argv[0], argv[1], -1);
+
+ return ret < 0 ? 1 : 0;
+ }
+
int targetfd = open(target, open_target_flags);
if(targetfd >= 0)
{
diff --git a/test-cmd/ln.sh b/test-cmd/ln.sh
@@ -2,7 +2,7 @@
# SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
# SPDX-License-Identifier: MPL-2.0
-plans=67
+plans=84
WD=$(dirname "$0")
target="${WD}/../cmd/ln"
. "${WD}/tap.sh"
@@ -114,3 +114,31 @@ t_args --exit=1 same:no_force "ln: error: Destination 'same' already exists
t_args same:force "ln: info: Source '$target' and destination 'same' refer to the same file
" -fs "$target" same
t_cmd same:cleanup '' rm same
+
+t_args Ts:create '' -Ts / Ts
+t_readlink Ts /
+t_args --exit=1 Ts:retarget "ln: error: Destination 'Ts' already exists
+" -Ts "$target" Ts
+t_args Ts:f:retarget '' -Ts -f "$target" Ts
+t_readlink Ts "$target"
+t_cmd Ts:cleanup '' rm Ts
+
+t_cmd Ts.d:mkdir '' mkdir -p Ts.d
+t_args --exit=1 Ts.d "ln: error: Destination 'Ts.d' already exists
+" -Ts / Ts.d
+t_args --exit=1 Ts.d:f "ln: error: Failed unlinking destination 'Ts.d': Is a directory
+" -Tsf / Ts.d
+t_cmd Ts.d:rmdir '' rm -r Ts.d
+
+t_args T:create '' -T "$target" T
+#t_args T:retarget '' -T "$target" T
+#t_args T:retarget:f '' -Tf "$target" T
+t_cmd T:cleanup '' rm T
+
+t_cmd T.d:mkdir '' mkdir -p T.d
+t_args --exit=1 T.d "ln: error: Destination 'T.d' already exists
+" -T "$target" T.d
+t_cmd '' '' test -d T.d
+t_args --exit=1 T.d:f "ln: error: Failed unlinking destination 'T.d': Is a directory
+" -Tf "$target" T.d
+t_cmd T.d:rmdir '' rm -r T.d