symlinks made easier
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.4release
- Re-create the latestsymlink
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
- 
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:
- Subscribe to the RSS feed
- Follow me on Mastodon
- Follow me on dev.to (mosts of my posts are mirrored there)
- Or send me an email to subscribe to my newsletter
Cheers!