💾 Archived View for dmerej.info › en › blog › 0033-symlinks-made-easier.gmi captured on 2022-07-16 at 14:35:20. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

2017, Jan 31 - Dimitri Merejkowsky
License: CC By 4.0

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:

Which are "atomic" operations, meaning:

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` ), 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[1], made a release on pip[2] and created a quick demo on asciinema[3] because that's what the cool kids seem to do nowadays.

1: https://github.com/dmerejkowsky/ln.py

2: https://pypi.python.org/pypi/ln.py

3: https://asciinema.org/a/101084

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

----

Back to Index

Contact me