💾 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
-=-=-=-=-=-=-
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?
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.
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 ...
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_
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.")
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_))
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
----