Skip to Content

symlinks made easier

Posted on 5 mins read
Tags: python

For years I’ve been struggling with the ln command.

I never could remember how to use it, mixing the order of the parameters, and the man page did not help.

$ man ln

SYNOPSYS
      ln [OPTION]... [-T] TARGET LINK_NAME   (1st form)
      ln [OPTION]... TARGET                  (2nd form)
      ln [OPTION]... TARGET... DIRECTORY     (3rd form)
      ln [OPTION]... -t DIRECTORY TARGET...  (4th form)

So I thought, why not write a small wrapper around it?

Introduction #

A symlink is a special file that “points” to an other.

I’ve seen it used frequently in the download folder of servers:

$ ls -l  download
0.1
0.2
0.3
latest -> 0.3

Thus, when you go to http://example.com/latest you always get the latest release, and to deploy a new release, one can:

  • Upload the 0.4 release
  • Re-create the latest symlink

Which are “atomic” operations, meaning:

  • The filesystem is always in a coherent state
  • It’s easy to revert to a previous release if necessary.

So, my mental image of a link is an arrow, going from one filename to an other:

a -> b  (link from a to b)
or
a <- b  (link to a from b)

But a and b can be in any order.

Choosing parameter names #

The first thing I did was to use variable names that I could understand.

I choose the names from and to:

def ln(*, from_, to):
    os.symlink(to, from_)

I’m using Python3 syntax to make sure that both from and to have to be explicitly specified when calling the function.

I also use from_ with an underscore at the end because from is a Python keyword.

Note that in Python2, I would have written

def ln(from_=None, to=None):
    ...

but then nothing would have prevented people (including me), from using ln(a, b), which is exactly what I want to avoid.

I also wrote a test which forced me to get the order of the os.symlink() call right.

Because of course, I also don’t know how to call os.symlink(), arguments are named src and dest, and those names are as meaningless to me as the names in the ln man page …

Coming up with a Command Line Interface #

First attempt #

My first idea was to have two ways to call my ln wrapper, with names that remembered me about the direction of the arrow.

So something like ln-lt (for the lesser than sign, aka <) and ln-gt (for the greater than sign, aka >).

But that was confusing, and the code was not very readable:

def main_lt(a, b):
    _main("<", a, b)

def main_gt(a, b):
    _main(">", a, b)

def _main(direction, a, b):
    from_ = a
    to = b
    if direction == "<":
        # going the other way, need to swap:
        from_, to = to, from_

Second attempt #

And then I realized I could just use the names first and second, display the two possibilities and let the user (me) choose interactively:

def main(first, second):
    print("1.", first,  "->", second)
    print("2.", second, "->", first)

    answer = input("Which one? ")
    if answer == "1":
        from_ = first
        to = args.second
    elif answer == "2":
        to = first
        from_ = second
    else:
        sys.exit("Please choose between 1. and 2.")

Going Interactive #

Since I was already interacting with the user, the next logical step was to handle the case where the symlink already exists.

Normally, when I get an error from ln looking like:

$ ln -s bar foo
ln: failed to create symbolic link 'foo': File exists

my first instinct is to run ls -l to check that I’m actually overwriting a symbolic link, (which is easy to revert) and not a regular file (which could lead to data loss).

Then I use rm foo, which prompts me for a confirmation (because I’ve aliased rm to rm -i 1), or I re-run the ln command with the --force switch.

I realize I could avoid doing all that with just a few more lines of code:

if os.path.islink(from_):
    dest = os.readlink(from_)
    message = "{} -> {} already exists. Overwrite? (Y/n) "
    message = message.format(from_, dest)
    answer = input(message)
    if answer == "n":
        sys.exit(1)
    else:
        os.remove(from_)

if os.path.exists(from_) and not os.path.islink(from_):
    message = "Error: {} already exists and is not a symlink"
    sys.exit(message.format(from_))

Releasing to the world #

After that, I created a github repo, made a release on pip and created a quick demo on asciinema because that’s what the cool kids seem to do nowadays.

I don’t really expect contributions because the code does everything I need, and I don’t really expect you to want to use it.

(Maybe you’ve managed to remember the order of arguments because it’s EXISTING NEW, the same order as cp, or maybe you have a different mental image of symlinks, or you don’t use the command line at all, and nothing is wrong with you).

Nevertheless, I though it would be interesting to show an example of how you can tweak your tools to have an API and UI that matches how your brain works.

Plus it’s a nice way to show you how Python3 is awesome :P

Update: someone had the same kind of idea for implementing a safer rm. You can read more on github


  1. Old habit. This one is not likely to go away … ↩︎


Thanks for reading this far :)

I'd love to hear what you have to say, so please feel free to leave a comment below, or read the contact page for more ways to get in touch with me.

Note that to get notified when new articles are published, you can either:

Cheers!