logo

utils-std

Collection of commonly available Unix tools git clone https://anongit.hacktivis.me/git/utils-std.git/
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:

Mcmd/ln.124+++++++++++++++++++++---
Mcmd/ln.c31++++++++++++++++++++++++-------
Mtest-cmd/ln.sh30+++++++++++++++++++++++++++++-
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