#include "filenamecat.h"
#include "quote.h"
#include "yesno.h"
+#include "canonicalize.h"
/* The official name of this program (e.g., no `g' prefix). */
#define PROGRAM_NAME "ln"
/* If true, make symbolic links; otherwise, make hard links. */
static bool symbolic_link;
+/* If true, make symbolic link relative to link location */
+static bool relative_link;
+
/* If true, ask the user before removing existing files. */
static bool interactive;
{"no-target-directory", no_argument, NULL, 'T'},
{"force", no_argument, NULL, 'f'},
{"interactive", no_argument, NULL, 'i'},
+ {"relative", no_argument, NULL, 'r'},
{"suffix", required_argument, NULL, 'S'},
{"target-directory", required_argument, NULL, 't'},
{"symbolic", no_argument, NULL, 's'},
static bool
do_link (const char *source, const char *dest)
{
+ char relative_source[PATH_MAX];
struct stat source_stats;
struct stat dest_stats;
char *dest_backup = NULL;
}
}
- ok = ((symbolic_link ? symlink (source, dest) : link (source, dest))
- == 0);
+ if (!symbolic_link)
+ {
+ ok = (link (source, dest) == 0);
+ }
+ else
+ {
+ if (relative_link)
+ {
+ char *canon_source;
+ char *canon_dest;
+ char *s, *sp = NULL;
+ char *d, *dp = NULL;
+ char *dest_base;
+ size_t relative_len, relative_start;
+
+ s = canon_source = canonicalize_filename_mode(source, CAN_ALL_BUT_LAST);
+ if (!s)
+ {
+ error(0, errno, "cannot canonicalize souce file name: %s", quote(source));
+ return false;
+ }
+
+ d = canon_dest = canonicalize_filename_mode(dest, CAN_ALL_BUT_LAST);
+ if (!d)
+ {
+ error(0, errno, "cannot canonicalize destination file name: %s", quote(source));
+ free(canon_source);
+ return false;
+ }
+ if (*s != '/' || *d != '/')
+ {
+ /* Canonical names should both start with '/'. */
+ error(0, EINVAL, "canonical names don't match");
+ free(canon_source);
+ free(canon_dest);
+ return false;
+ }
+
+ dest_base = base_name(canon_dest);
+
+ s++; d++;
+ sp = strchr(s, '/');
+ dp = strchr(d, '/');
+ while (strncmp(s, d, sp - s) == 0)
+ {
+ d = dp + 1;
+ s = sp + 1;
+ sp = strchr(s, '/');
+ dp = strchr(d, '/');
+ }
+
+ relative_start = s - canon_source;
+ memset(relative_source, 0, PATH_MAX);
+ relative_len = 0;
+
+ while ((relative_len < (PATH_MAX - 1 - 3)) && (strncmp(d, dest_base, dp - d) != 0))
+ {
+ d = dp + 1;
+ dp = strchr(d , '/');
+
+ strncat(relative_source, "../", PATH_MAX - 1 - relative_len);
+ relative_len += 3;
+ }
+
+ if (relative_len >= (PATH_MAX - 1 - 3))
+ {
+ error(0, ENAMETOOLONG, "destination too deep");
+ free(dest_base);
+ free(canon_source);
+ free(canon_dest);
+ return false;
+ }
+ strncat(relative_source, canon_source + relative_start, PATH_MAX - 1 - relative_len);
+
+ source = relative_source;
+
+ free(dest_base);
+ free(canon_source);
+ free(canon_dest);
+ }
+
+ ok = (symlink (source, dest) == 0);
+ }
+
/* If the attempt to create a link failed and we are removing or
backing up destinations, unlink the destination and try again.
-n, --no-dereference treat destination that is a symlink to a\n\
directory as if it were a normal file\n\
-i, --interactive prompt whether to remove destinations\n\
+ -r, --relative make symbolic links relative to link location\n\
-s, --symbolic make symbolic links instead of hard links\n\
"), stdout);
fputs (_("\
backup_suffix_string = getenv ("SIMPLE_BACKUP_SUFFIX");
symbolic_link = remove_existing_files = interactive = verbose
- = hard_dir_link = false;
+ = hard_dir_link = relative_link = false;
- while ((c = getopt_long (argc, argv, "bdfinst:vFS:T", long_options, NULL))
+ while ((c = getopt_long (argc, argv, "bdfinrst:vFS:T", long_options, NULL))
!= -1)
{
switch (c)
case 'n':
dereference_dest_dir_symlinks = false;
break;
+ case 'r':
+ relative_link = true;
+ break;
case 's':
symbolic_link = true;
break;
--- /dev/null
+#!/bin/sh
+# Test "ln --relative".
+
+# Copyright (C) 2024 Samsung Electronics
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+if test "$VERBOSE" = yes; then
+ set -x
+ ln --version
+fi
+
+pwd=`pwd`
+t0=`echo "$0"|sed 's,.*/,,'`.tmp; tmp=$t0/$$
+trap 'status=$?; cd "$pwd" && chmod -R u+rwx $t0 && rm -rf $t0 && exit $status' 0
+trap '(exit $?); exit $?' 1 2 13 15
+
+framework_failure=0
+mkdir -p $tmp/a || framework_failure=1
+mkdir -p $tmp/z || framework_failure=1
+touch $tmp/b || framework_failure=1
+touch $tmp/a/b || framework_failure=1
+cd $tmp || framework_failure=1
+
+if test $framework_failure = 1; then
+ echo "$0: failure in testing framework" 1>&2
+ (exit 1); exit 1
+fi
+
+fail=0
+
+ln -sr ./a/b ./y
+test $(readlink y) = 'a/b' || fail=1
+
+ln -sr ./a/b ./z/y
+test $(readlink z/y) = '../a/b' || fail=1
+
+ln -sr ./b ./z/x
+test $(readlink z/x) = '../b' || fail=1
+
+ln -sr ./b ./x
+test $(readlink x) = 'b' || fail=1
+
+(exit $fail); exit $fail