agtl
agtl

Reputation: 119

Symlinking relative paths without expanding symlinks

Working in C on linux, I have two character arrays containing paths relative to the cwd. Let's say they're "foo/txt" and "bar/txt". I want to create a symlink that points from the first to the second. Problem is if I go symlink("bar/txt", "foo/txt"), foo/txt will look for foo/bar/txt which doesn't exist. I could add ".." to the beginning of the target path but I don't know how many directory levels are contained in the source string

Compounding the issue is that the target my itself be a symlink and I do not want to traverse it, as might happen if I used realpath(). The directory structure might look like the following, once links are created as I intend (for my purposes it doesn't matter if the resulting symlinks are relative or absolute):

/somecwd/foo/txt -> ../bar/txt
/somecwd/bar/txt -> ../baz/txt
/somecww/baz/txt

Does anyone know how I might go about this? Any help is much appreciated.

Edit: Ideally this would work even if the paths that I'm given are absolute instead

Upvotes: 0

Views: 616

Answers (2)

Jonathan Leffler
Jonathan Leffler

Reputation: 753695

With the symlink() function, the BSD (Mac OS X) man page says:

 int symlink(const char *path1, const char *path2);

DESCRIPTION

A symbolic link path2 is created to path1 (path2 is the name of the file created, path1 is the string used in creating the symbolic link). Either name may be an arbitrary path name; the files need not be on the same file system.

Note that what you specify as path1 is used, verbatim, as the content of the symlink. Thus, to make the symlink accurately, you have to get the relative path correct. That is, the name passed as path1 must be the correct name relative to path2.

In other words, you can't completely avoid the process of mapping the names, with all the attendant difficulties.

I have a Perl script, relpath, that I cobbled together from the answer to Convert absolute path into relative path given a current directory and a fairly old news group message from comp.unix.shell. Converting some of this to C is not beyond the wit of mankind. In fact, it is bound to have been done numerous times. The difficulty will be finding the code.

relpath

#!/usr/bin/env perl
#
# @(#)$Id: relpath.pl,v 1.4 2014/12/08 18:23:17 jleffler Exp $
#
# Usage:    relpath source target [...]
#
# Purpose:  Print relative path of target w.r.t. source
#
# Based loosely on code from:
# http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2005-10/1256.html
# Via: https://stackoverflow.com/questions/2564634

use strict;
use warnings;
use File::Basename;
use Cwd qw(realpath getcwd);

if (scalar @ARGV < 2)
{
    my $arg0 = basename($0, ".pl");
    die "Usage: $arg0 from to [...]\n"
}

my $pwd;
my $verbose = 0;

# Fettle filename so it is absolute.
# Deals with '//', '/./' and '/../' notations, plus symlinks.
# The realpath() function does the hard work if the path exists.
# For non-existent paths, the code does a purely textual hack.
sub resolve
{
    my($name) = @_;
    my($path) = realpath($name);
    if (!defined $path)
    {
        # Path does not exist - do the best we can with lexical analysis
        # Assume Unix - not dealing with Windows.
        $path = $name;
        if ($name !~ m%^/%)
        {
            $pwd = getcwd if !defined $pwd;
            $path = "$pwd/$path";
        }
        $path =~ s%//+%/%g;     # Not UNC paths.
        $path =~ s%/$%%;        # No trailing /
        $path =~ s%/\./%/%g;    # No embedded /./
        # Try to eliminate /../abc/
        $path =~ s%/\.\./(?:[^/]+)(/|$)%$1%g;
        $path =~ s%/\.$%%;      # No trailing /.
        $path =~ s%^\./%%;      # No leading ./
        # What happens with . and / as inputs?
    }
    return($path);
}

sub print_result
{
    my($source, $target, $relpath) = @_;
    if ($verbose)
    {
        print "source  = $ARGV[0]\n";
        print "target  = $ARGV[1]\n";
        print "relpath = $relpath\n";
    }
    else
    {
        print "$relpath\n";
    }
}

# Nasty!
my($source) = resolve($ARGV[0]);
my(@source) = split '/', $source;
shift @ARGV;

sub relpath
{
    my($name) = @_;
    my($target) = resolve($name);
    print_result($source, $target, ".") if ($source eq $target);

    # Split!
    my(@target) = split '/', $target;

    my $count = scalar(@source);
       $count = scalar(@target) if (scalar(@target) < $count);
    my $relpath = "";
    my $i;

    # Both paths are absolute; Perl splits an empty field 0.
    for ($i = 1; $i < $count; $i++)
    {
        last if $source[$i] ne $target[$i];
    }

    for (my $s = $i; $s < scalar(@source); $s++)
    {
        $relpath = "$relpath/" if ($s > $i);
        $relpath = "$relpath..";
    }
    for (my $t = $i; $t < scalar(@target); $t++)
    {
        $relpath = "$relpath/" if ($relpath ne "");
        $relpath = "$relpath$target[$t]";
    }

    print_result($source, $target, $relpath);
}

foreach my $target (@ARGV)
{
    relpath($target);
}

test.relpath

NB: This requires relpath.pl rather than just relpath.

#!/bin/ksh
#
# @(#)$Id: test.relpath.sh,v 1.1 2010/04/25 15:19:20 jleffler Exp $
#
# Test relpath Perl script fairly exhaustively
# BUG: should include expected answers!

sed 's/#.*//;/^[    ]*$/d' <<! |

/home/part1/part2 /home/part1/part3
/home/part1/part2 /home/part4/part5
/home/part1/part2 /work/part6/part7
/home/part1       /work/part1/part2/part3/part4
/home             /work/part2/part3
/                 /work/part2/part3/part4

/home/part1/part2 /home/part1/part2/part3/part4
/home/part1/part2 /home/part1/part2/part3
/home/part1/part2 /home/part1/part2
/home/part1/part2 /home/part1
/home/part1/part2 /home
/home/part1/part2 /

/home/part1/part2 /work
/home/part1/part2 /work/part1
/home/part1/part2 /work/part1/part2
/home/part1/part2 /work/part1/part2/part3
/home/part1/part2 /work/part1/part2/part3/part4

home/part1/part2 home/part1/part3
home/part1/part2 home/part4/part5
home/part1/part2 work/part6/part7
home/part1       work/part1/part2/part3/part4
home             work/part2/part3
.                work/part2/part3

home/part1/part2 home/part1/part2/part3/part4
home/part1/part2 home/part1/part2/part3
home/part1/part2 home/part1/part2
home/part1/part2 home/part1
home/part1/part2 home
home/part1/part2 .

home/part1/part2 work
home/part1/part2 work/part1
home/part1/part2 work/part1/part2
home/part1/part2 work/part1/part2/part3
home/part1/part2 work/part1/part2/part3/part4

!

{
echo "Relative paths (source, target, relative path)"
while read source target
do
    echo "$source $target $(${PERL:-perl} relpath.pl $source $target)"
done |
awk '{ printf("%-20s   %-30s   %s\n", $1, $2, $3); }'
}

Example output

Note that the first relative path, ../part3, is correct if /home/part1/part2 is a directory, or is assumed to be a directory. This code requires care in interpreting the output. Note that the symlink() system call does not require that the path1 name refers to an existing file or directory (but, by contrast, path2 must not refer to an existing file or directory, though only the leaf element must not exist; all previous directories must exist).

Relative paths (source, target, relative path)
/home/part1/part2      /home/part1/part3                ../part3
/home/part1/part2      /home/part4/part5                ../../part4/part5
/home/part1/part2      /work/part6/part7                ../../../work/part6/part7
/home/part1            /work/part1/part2/part3/part4    ../../work/part1/part2/part3/part4
/home                  /work/part2/part3                ../work/part2/part3
/                      /work/part2/part3/part4          work/part2/part3/part4
/home/part1/part2      /home/part1/part2/part3/part4    part3/part4
/home/part1/part2      /home/part1/part2/part3          part3
/home/part1/part2      /home/part1/part2                .
/home/part1/part2      /home/part1                      ..
/home/part1/part2      /home                            ../..
/home/part1/part2      /                                ../../..
/home/part1/part2      /work                            ../../../work
/home/part1/part2      /work/part1                      ../../../work/part1
/home/part1/part2      /work/part1/part2                ../../../work/part1/part2
/home/part1/part2      /work/part1/part2/part3          ../../../work/part1/part2/part3
/home/part1/part2      /work/part1/part2/part3/part4    ../../../work/part1/part2/part3/part4
home/part1/part2       home/part1/part3                 ../part3
home/part1/part2       home/part4/part5                 ../../part4/part5
home/part1/part2       work/part6/part7                 ../../../work/part6/part7
home/part1             work/part1/part2/part3/part4     ../../work/part1/part2/part3/part4
home                   work/part2/part3                 ../work/part2/part3
.                      work/part2/part3                 work/part2/part3
home/part1/part2       home/part1/part2/part3/part4     part3/part4
home/part1/part2       home/part1/part2/part3           part3
home/part1/part2       home/part1/part2                 .
home/part1/part2       home/part1                       ..
home/part1/part2       home                             ../..
home/part1/part2       .                                ../../..
home/part1/part2       work                             ../../../work
home/part1/part2       work/part1                       ../../../work/part1
home/part1/part2       work/part1/part2                 ../../../work/part1/part2
home/part1/part2       work/part1/part2/part3           ../../../work/part1/part2/part3
home/part1/part2       work/part1/part2/part3/part4     ../../../work/part1/part2/part3/part4

Upvotes: 1

StenSoft
StenSoft

Reputation: 9609

As a fast solution (ignoring double slashes, ../ and ./), you can count the number of slashes in the source and prepend the same number of ../.

Upvotes: 0

Related Questions