2025-03-23
The snippets in this post use the fish shell; the syntax is slightly different than POSIX shell.
Symlinks are a really useful tool when working with files.
They act like shortcuts: if you open a symlink, you’ll get the file it links to.
$ echo abc > file.txt
$ ln -s file.txt letters.txt
$ cat letters.txt
abc
Because it’s just a link, the symlink will stay in sync with the file contents (unlike a copy).
echo def >> file.txt
cat letters.txt
abc
def
I use them all the time in my config.
For example, in my .vim
, I link plugins
to pack/vendor/opt
:
.vim $ file -h plugins
plugins: symbolic link to pack/vendor/opt
This way I can just cd plugins
rather than cd pack/vendor/opt
.
Vim requires plugins to live in that directory, but I can use a symlink as a shortcut.
Another example: in my dotfiles, I symlink a local keyd_config
to /etc
:
$ sudo ln -s (pwd)/keyd_config /etc/keyd
That way I don’t have to use sudo
to edit the system-wide /etc
directory, but keyd
’s config is where it expects it.
Symlinks can be tricky to work with, in part because they’re so flexible. ln -s
will let you create a symlink to a file that doesn’t exist:
$ ls
$ ln -s nonexistant symlink
Then when you try to cat
it, it’ll say it’s not a file:
$ cat symlink
cat: symlink: No such file or directory
$ ls
symlink@
Huh? It’s clearly there… oh right, the @ means symlink.
You can make an infinite loop combining relative and absolute paths:
~/dotfiles $ ln -s .prettierrc ~
~/dotfiles $ cat ~/.prettierrc
cat: /Users/razzi/.prettierrc: Too many levels of symbolic links
What I want is to link the file called .prettierrc
in the current directory to my home directory, so that prettier
will find it.
But the first argument to ln -s
, .prettierrc
, makes a link to a relative path of .prettierrc
. When you attempt to access ~/.prettierrc
, it will say:
Where does
.prettierrc
link to? Oh, to.prettierrc
(the same file).
Then, because commands like cat
can follow multiple symlinks, it’ll open .prettierrc
(the symlink) again and again, until helpfully it reports “Too many levels” (otherwise it would spin forever).
You can get around this with absolute paths:
$ ln -s (pwd)/.prettierrc ~
$ cat ~/.prettierrc
semi: false
But that’s a hassle to type and remember.
An even simpler rough edge: if you only give ln -s
a single argument:
$ ln -s something
$ ls -l
... something@ -> something
It will make a symbolic link to itself… almost certainly not what you want.
To work around these strange behaviors, I created my own symlink commands in my fish functions.
symlink
is my replacement for ln -s
. It has the following features:
I also have a function unsymlink
, which removes symlinks but errors if you try to remove something that isn’t a symlink.
There are some utility functions as well:
symlinks
lists files that are symbolic links.is-symlink
checks if the argument is a symbolic link.Finally, move
, my user-friendlier replacement to mv
, handles a strange case.
If you try to use mv
on a symlink that ends with a slash (probably because tab completion put it there) it will error:
$ mkdir mydir
$ ln -s mydir mylink
$ mv mylink/ renamed
mv: cannot move 'mylink/' to 'renamed': Not a directory
move
also errors, but the error is more descriptive:
$ move mylink/ renamed
move: `from` argument "mylink/" is a symlink with a trailing slash.
move: to rename a symlink, remove the trailing slash from the argument.
The solution is to change your command to remove the slash, so that you move the symlink, not the directory it is pointing to.