nobe4 / Posts / When ln is not idempotent _

TL;DR: ln -T

I was refactoring more my Nix config, and specifically replacing my link farm with nix as described in my previous post.

When I noticed that links were being created in the wrong places. I would run ln more than once and would get links in random locations:

$ ln -vfs "$DOTFILE_FOLDER/nvim/" "$HOME/.config/nvim"
'/home/nobe4/.config/nvim' -> '/home/nobe4/dev/nobe4/dotfiles/nvim'

$ ls -la "$HOME/.config/nvim"
lrwxrwxrwx 1 nobe4 users 35 Feb  4 20:12 /home/nobe4/.config/nvim -> /home/nobe4/dev/nobe4/dotfiles/nvim
# all good!

# do some testing ...

$ ln -vfs "$DOTFILE_FOLDER/nvim/" "$HOME/.config/nvim"
'/home/nobe4/.config/nvim/nvim' -> '/home/nobe4/dev/nobe4/dotfiles/nvim'
# uh, what now?

$ ls -la $DOTFILE_FOLDER/nvim/nvim
lrwxrwxrwx 1 nobe4 users 35 Feb  4 20:12 /home/nobe4/dev/nobe4/dotfiles/nvim/nvim -> /home/nobe4/dev/nobe4/dotfiles/nvim
# what the hell

In a simplified form:

$ mkdir a
$ ln -vfs a b
'b' -> 'a'
$ ln -vfs a b
'b/a' -> 'a'
$ tree
.
├── a
│   └── a -> a
└── b -> a

That prompted me to do an overly zealous study of what ln do in various scenario. The conclusion of this post is everyone’s favorite: RTFM.

The test scenario 🔗

All the commands are ran with:

link () {
    ln --verbose --force --symbolic "$(pwd)/${1:?}" "$(pwd)/${2:?}"
}
Legend
  • �: Missing File, Directory, or Link.
  • F: File
  • D: Directory
  • FL: Link to a File
  • DL: Link to a Directory
  • �FL: Broken link to a File
  • �DL: Broken link to a Directory
  • /: Ends with a /

$1 \ $2FD
01428
/11529
F21630 ⚠️
F/31731
D41832 ⚠️
D/51933 ⚠️
�FL62034
�FL/72135
FL82236 ⚠️
FL/92337
�DL102438
�DL/112539
DL122640 ⚠️
DL/132741 ⚠️

Notes:

Full test results
  • run.sh

    #!/usr/bin/env bash
    link () {
        ln --verbose --force --symbolic "$(pwd)/${1:?}" "$(pwd)/${2:?}"
    }
    
    clean() { find . -not -name run.sh -delete ; }
    
    run() {
        echo -e "\n### [${id}] ###: $1"
        eval "$1"
        tree -I 'run.sh' | head -n -2
        find . -xtype l -exec echo "Broken link: {}" \;
        id=$((id + 1))
        clean
    }
    
    id=0
    cases=(
        # column 1
        "link a b"
        "link a/ b"
        "touch a && link a b"
        "touch a && link a/ b"
        "mkdir a && link a b"
        "mkdir a && link a/ b"
        "link a b && link b c"
        "link a b && link b/ c"
        "touch a && link a b && link b c"
        "touch a && link a b && link b/ c"
        "link a/ b && link b c"
        "link a/ b && link b/ c"
        "mkdir a && link a b && link b c"
        "mkdir a && link a b && link b/ c"
        # column 2
        "touch b && link a b"
        "touch b && link a/ b"
        "touch b && touch a && link a b"
        "touch b && touch a && link a/ b"
        "touch b && mkdir a && link a b"
        "touch b && mkdir a && link a/ b"
        "touch c && link a b && link b c"
        "touch c && link a b && link b/ c"
        "touch c && touch a && link a b && link b c"
        "touch c && touch a && link a b && link b/ c"
        "touch c && link a/ b && link b c"
        "touch c && link a/ b && link b/ c"
        "touch c && mkdir a && link a b && link b c"
        "touch c && mkdir a && link a b && link b/ c"
        # column 2
        "mkdir b && link a b"
        "mkdir b && link a/ b"
        "mkdir b && touch a && link a b"
        "mkdir b && touch a && link a/ b"
        "mkdir b && mkdir a && link a b"
        "mkdir b && mkdir a && link a/ b"
        "mkdir c && link a b && link b c"
        "mkdir c && link a b && link b/ c"
        "mkdir c && touch a && link a b && link b c"
        "mkdir c && touch a && link a b && link b/ c"
        "mkdir c && link a/ b && link b c"
        "mkdir c && link a/ b && link b/ c"
        "mkdir c && mkdir a && link a b && link b c"
        "mkdir c && mkdir a && link a b && link b/ c"
    )
    
    for t in "${cases[@]}"; do
        run "$t"
    done
    
  • Output

    ### [0] ###: link a b
    '/tmp/ln/b' -> '/tmp/ln/a'
    .
    └── b -> /tmp/ln/a
    Broken link: ./b
    
    ### [1] ###: link a/ b
    '/tmp/ln/b' -> '/tmp/ln/a/'
    .
    └── b -> /tmp/ln/a/
    Broken link: ./b
    
    ### [2] ###: touch a && link a b
    '/tmp/ln/b' -> '/tmp/ln/a'
    .
    ├── a
    └── b -> /tmp/ln/a
    
    ### [3] ###: touch a && link a/ b
    '/tmp/ln/b' -> '/tmp/ln/a/'
    .
    ├── a
    └── b -> /tmp/ln/a/
    Broken link: ./b
    
    ### [4] ###: mkdir a && link a b
    '/tmp/ln/b' -> '/tmp/ln/a'
    .
    ├── a
    └── b -> /tmp/ln/a
    
    ### [5] ###: mkdir a && link a/ b
    '/tmp/ln/b' -> '/tmp/ln/a/'
    .
    ├── a
    └── b -> /tmp/ln/a/
    
    ### [6] ###: link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b
    Broken link: ./c
    Broken link: ./b
    
    ### [7] ###: link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b/
    Broken link: ./c
    Broken link: ./b
    
    ### [8] ###: touch a && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b
    
    ### [9] ###: touch a && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b/
    Broken link: ./c
    
    ### [10] ###: link a/ b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a/'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── b -> /tmp/ln/a/
    └── c -> /tmp/ln/b
    Broken link: ./c
    Broken link: ./b
    
    ### [11] ###: link a/ b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a/'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── b -> /tmp/ln/a/
    └── c -> /tmp/ln/b/
    Broken link: ./c
    Broken link: ./b
    
    ### [12] ###: mkdir a && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b
    
    ### [13] ###: mkdir a && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b/
    
    ### [14] ###: touch b && link a b
    '/tmp/ln/b' -> '/tmp/ln/a'
    .
    └── b -> /tmp/ln/a
    Broken link: ./b
    
    ### [15] ###: touch b && link a/ b
    '/tmp/ln/b' -> '/tmp/ln/a/'
    .
    └── b -> /tmp/ln/a/
    Broken link: ./b
    
    ### [16] ###: touch b && touch a && link a b
    '/tmp/ln/b' -> '/tmp/ln/a'
    .
    ├── a
    └── b -> /tmp/ln/a
    
    ### [17] ###: touch b && touch a && link a/ b
    '/tmp/ln/b' -> '/tmp/ln/a/'
    .
    ├── a
    └── b -> /tmp/ln/a/
    Broken link: ./b
    
    ### [18] ###: touch b && mkdir a && link a b
    '/tmp/ln/b' -> '/tmp/ln/a'
    .
    ├── a
    └── b -> /tmp/ln/a
    
    ### [19] ###: touch b && mkdir a && link a/ b
    '/tmp/ln/b' -> '/tmp/ln/a/'
    .
    ├── a
    └── b -> /tmp/ln/a/
    
    ### [20] ###: touch c && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b
    Broken link: ./c
    Broken link: ./b
    
    ### [21] ###: touch c && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b/
    Broken link: ./c
    Broken link: ./b
    
    ### [22] ###: touch c && touch a && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b
    
    ### [23] ###: touch c && touch a && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b/
    Broken link: ./c
    
    ### [24] ###: touch c && link a/ b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a/'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── b -> /tmp/ln/a/
    └── c -> /tmp/ln/b
    Broken link: ./c
    Broken link: ./b
    
    ### [25] ###: touch c && link a/ b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a/'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── b -> /tmp/ln/a/
    └── c -> /tmp/ln/b/
    Broken link: ./c
    Broken link: ./b
    
    ### [26] ###: touch c && mkdir a && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b
    
    ### [27] ###: touch c && mkdir a && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b/
    
    ### [28] ###: mkdir b && link a b
    '/tmp/ln/b/a' -> '/tmp/ln/a'
    .
    └── b
        └── a -> /tmp/ln/a
    Broken link: ./b/a
    
    ### [29] ###: mkdir b && link a/ b
    '/tmp/ln/b/a' -> '/tmp/ln/a/'
    .
    └── b
        └── a -> /tmp/ln/a/
    Broken link: ./b/a
    
    ### [30] ###: mkdir b && touch a && link a b
    '/tmp/ln/b/a' -> '/tmp/ln/a'
    .
    ├── a
    └── b
        └── a -> /tmp/ln/a
    
    ### [31] ###: mkdir b && touch a && link a/ b
    '/tmp/ln/b/a' -> '/tmp/ln/a/'
    .
    ├── a
    └── b
        └── a -> /tmp/ln/a/
    Broken link: ./b/a
    
    ### [32] ###: mkdir b && mkdir a && link a b
    '/tmp/ln/b/a' -> '/tmp/ln/a'
    .
    ├── a
    └── b
        └── a -> /tmp/ln/a
    
    ### [33] ###: mkdir b && mkdir a && link a/ b
    '/tmp/ln/b/a' -> '/tmp/ln/a/'
    .
    ├── a
    └── b
        └── a -> /tmp/ln/a/
    
    ### [34] ###: mkdir c && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c/b' -> '/tmp/ln/b'
    .
    ├── b -> /tmp/ln/a
    └── c
        └── b -> /tmp/ln/b
    Broken link: ./c/b
    Broken link: ./b
    
    ### [35] ###: mkdir c && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c/b' -> '/tmp/ln/b/'
    .
    ├── b -> /tmp/ln/a
    └── c
        └── b -> /tmp/ln/b/
    Broken link: ./c/b
    Broken link: ./b
    
    ### [36] ###: mkdir c && touch a && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c/b' -> '/tmp/ln/b'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c
        └── b -> /tmp/ln/b
    
    ### [37] ###: mkdir c && touch a && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c/b' -> '/tmp/ln/b/'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c
        └── b -> /tmp/ln/b/
    Broken link: ./c/b
    
    ### [38] ###: mkdir c && link a/ b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a/'
    '/tmp/ln/c/b' -> '/tmp/ln/b'
    .
    ├── b -> /tmp/ln/a/
    └── c
        └── b -> /tmp/ln/b
    Broken link: ./c/b
    Broken link: ./b
    
    ### [39] ###: mkdir c && link a/ b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a/'
    '/tmp/ln/c/b' -> '/tmp/ln/b/'
    .
    ├── b -> /tmp/ln/a/
    └── c
        └── b -> /tmp/ln/b/
    Broken link: ./c/b
    Broken link: ./b
    
    ### [40] ###: mkdir c && mkdir a && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c/b' -> '/tmp/ln/b'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c
        └── b -> /tmp/ln/b
    
    ### [41] ###: mkdir c && mkdir a && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c/b' -> '/tmp/ln/b/'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c
        └── b -> /tmp/ln/b/
    

The problem 🔗

Going back to my initial scenario:

$ ln -vfs "$DOTFILE_FOLDER/nvim/" "$HOME/.config/nvim"

We now understand the weird behavior:

  1. First run, the $HOME/.config/nvim link is created, it points to the directory $DOTFILE_FOLDER/nvim/.

    $HOME/.config/nvim -> $DOTFILE_FOLDER/nvim
    
  2. Second run, ln sees a directory, and assumes that you want to link into the directory, rather than link the directory.

    $DOTFILE_FOLDER/nvim/nvim -> $HOME/.config/nvim -> $DOTFILE_FOLDER/nvim
    
  3. The result is a new link inside the linked directory:

    $DOTFILE_FOLDER/nvim/nvim -> $DOTFILE_FOLDER/nvim
    

The solution 🔗

Per usual, RTFM:

   -T, --no-target-directory
          treat LINK_NAME as a normal file always

This means that no matter what LINK_NAME is, it always takes as the name of the link, never the directory in which to put the link.

Using this new flag:

$ ln -vfsT "$DOTFILE_FOLDER/nvim/" "$HOME/.config/nvim"
'/home/nobe4/.config/nvim' -> '/home/nobe4/dev/nobe4/dotfiles/nvim'

$ ls -la "$HOME/.config/nvim"
lrwxrwxrwx 1 nobe4 users 35 Feb  4 20:12 /home/nobe4/.config/nvim -> /home/nobe4/dev/nobe4/dotfiles/nvim
# all good!

# do some testing ...

$ ln -vfsT "$DOTFILE_FOLDER/nvim/" "$HOME/.config/nvim"
'/home/nobe4/.config/nvim' -> '/home/nobe4/dev/nobe4/dotfiles/nvim'
# looking good!

$ ls -la $DOTFILE_FOLDER/nvim/nvim
ls: cannot access '/home/nobe4/dev/nobe4/dotfiles/nvim/nvim': No such file or directory
# yeee

In a simplified form:

$ mkdir a
$ ln -vfsT a b
'b' -> 'a'
$ ln -vfsT a b
'b' -> 'a'
$ tree
.
├── a
└── b -> a

The new test scenario 🔗

All the commands are ran with:

link () {
    ln --verbose --force --symbolic --no-target-directory "$(pwd)/${1:?}" "$(pwd)/${2:?}"
}
$1 \ $2FD
01428 💥
/11529 💥
F21630 💥
F/31731 💥
D41832 💥
D/51933 💥
�FL62034 💥
�FL/72135 💥
FL82236 💥
FL/92337 💥
�DL102438 💥
�DL/112539 💥
DL122640 💥
DL/132741 💥

Notes:

Full test results
  • run.sh

      link () {
    -    ln --verbose --force --symbolic "$(pwd)/${1:?}" "$(pwd)/${2:?}"
    +    ln --verbose --force --symbolic --no-target-directory "$(pwd)/${1:?}" "$(pwd)/${2:?}"
      }
    
  • Output

    ### [0] ###: link a b
    '/tmp/ln/b' -> '/tmp/ln/a'
    .
    └── b -> /tmp/ln/a
    Broken link: ./b
    
    ### [1] ###: link a/ b
    '/tmp/ln/b' -> '/tmp/ln/a/'
    .
    └── b -> /tmp/ln/a/
    Broken link: ./b
    
    ### [2] ###: touch a && link a b
    '/tmp/ln/b' -> '/tmp/ln/a'
    .
    ├── a
    └── b -> /tmp/ln/a
    
    ### [3] ###: touch a && link a/ b
    '/tmp/ln/b' -> '/tmp/ln/a/'
    .
    ├── a
    └── b -> /tmp/ln/a/
    Broken link: ./b
    
    ### [4] ###: mkdir a && link a b
    '/tmp/ln/b' -> '/tmp/ln/a'
    .
    ├── a
    └── b -> /tmp/ln/a
    
    ### [5] ###: mkdir a && link a/ b
    '/tmp/ln/b' -> '/tmp/ln/a/'
    .
    ├── a
    └── b -> /tmp/ln/a/
    
    ### [6] ###: link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b
    Broken link: ./c
    Broken link: ./b
    
    ### [7] ###: link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b/
    Broken link: ./c
    Broken link: ./b
    
    ### [8] ###: touch a && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b
    
    ### [9] ###: touch a && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b/
    Broken link: ./c
    
    ### [10] ###: link a/ b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a/'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── b -> /tmp/ln/a/
    └── c -> /tmp/ln/b
    Broken link: ./c
    Broken link: ./b
    
    ### [11] ###: link a/ b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a/'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── b -> /tmp/ln/a/
    └── c -> /tmp/ln/b/
    Broken link: ./c
    Broken link: ./b
    
    ### [12] ###: mkdir a && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b
    
    ### [13] ###: mkdir a && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b/
    
    ### [14] ###: touch b && link a b
    '/tmp/ln/b' -> '/tmp/ln/a'
    .
    └── b -> /tmp/ln/a
    Broken link: ./b
    
    ### [15] ###: touch b && link a/ b
    '/tmp/ln/b' -> '/tmp/ln/a/'
    .
    └── b -> /tmp/ln/a/
    Broken link: ./b
    
    ### [16] ###: touch b && touch a && link a b
    '/tmp/ln/b' -> '/tmp/ln/a'
    .
    ├── a
    └── b -> /tmp/ln/a
    
    ### [17] ###: touch b && touch a && link a/ b
    '/tmp/ln/b' -> '/tmp/ln/a/'
    .
    ├── a
    └── b -> /tmp/ln/a/
    Broken link: ./b
    
    ### [18] ###: touch b && mkdir a && link a b
    '/tmp/ln/b' -> '/tmp/ln/a'
    .
    ├── a
    └── b -> /tmp/ln/a
    
    ### [19] ###: touch b && mkdir a && link a/ b
    '/tmp/ln/b' -> '/tmp/ln/a/'
    .
    ├── a
    └── b -> /tmp/ln/a/
    
    ### [20] ###: touch c && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b
    Broken link: ./c
    Broken link: ./b
    
    ### [21] ###: touch c && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b/
    Broken link: ./c
    Broken link: ./b
    
    ### [22] ###: touch c && touch a && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b
    
    ### [23] ###: touch c && touch a && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b/
    Broken link: ./c
    
    ### [24] ###: touch c && link a/ b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a/'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── b -> /tmp/ln/a/
    └── c -> /tmp/ln/b
    Broken link: ./c
    Broken link: ./b
    
    ### [25] ###: touch c && link a/ b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a/'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── b -> /tmp/ln/a/
    └── c -> /tmp/ln/b/
    Broken link: ./c
    Broken link: ./b
    
    ### [26] ###: touch c && mkdir a && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b
    
    ### [27] ###: touch c && mkdir a && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    '/tmp/ln/c' -> '/tmp/ln/b/'
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c -> /tmp/ln/b/
    
    ### [28] ###: mkdir b && link a b
    ln: /tmp/ln/b: cannot overwrite directory
    .
    └── b
    
    ### [29] ###: mkdir b && link a/ b
    ln: /tmp/ln/b: cannot overwrite directory
    .
    └── b
    
    ### [30] ###: mkdir b && touch a && link a b
    ln: /tmp/ln/b: cannot overwrite directory
    .
    ├── a
    └── b
    
    ### [31] ###: mkdir b && touch a && link a/ b
    ln: /tmp/ln/b: cannot overwrite directory
    .
    ├── a
    └── b
    
    ### [32] ###: mkdir b && mkdir a && link a b
    ln: /tmp/ln/b: cannot overwrite directory
    .
    ├── a
    └── b
    
    ### [33] ###: mkdir b && mkdir a && link a/ b
    ln: /tmp/ln/b: cannot overwrite directory
    .
    ├── a
    └── b
    
    ### [34] ###: mkdir c && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    ln: /tmp/ln/c: cannot overwrite directory
    .
    ├── b -> /tmp/ln/a
    └── c
    Broken link: ./b
    
    ### [35] ###: mkdir c && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    ln: /tmp/ln/c: cannot overwrite directory
    .
    ├── b -> /tmp/ln/a
    └── c
    Broken link: ./b
    
    ### [36] ###: mkdir c && touch a && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    ln: /tmp/ln/c: cannot overwrite directory
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c
    
    ### [37] ###: mkdir c && touch a && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    ln: /tmp/ln/c: cannot overwrite directory
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c
    
    ### [38] ###: mkdir c && link a/ b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a/'
    ln: /tmp/ln/c: cannot overwrite directory
    .
    ├── b -> /tmp/ln/a/
    └── c
    Broken link: ./b
    
    ### [39] ###: mkdir c && link a/ b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a/'
    ln: /tmp/ln/c: cannot overwrite directory
    .
    ├── b -> /tmp/ln/a/
    └── c
    Broken link: ./b
    
    ### [40] ###: mkdir c && mkdir a && link a b && link b c
    '/tmp/ln/b' -> '/tmp/ln/a'
    ln: /tmp/ln/c: cannot overwrite directory
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c
    
    ### [41] ###: mkdir c && mkdir a && link a b && link b/ c
    '/tmp/ln/b' -> '/tmp/ln/a'
    ln: /tmp/ln/c: cannot overwrite directory
    .
    ├── a
    ├── b -> /tmp/ln/a
    └── c
    

Conclusion 🔗

-T’s behavior reminds me a lot of the JavaScript’s ===: it’s the most logical to me, and yet not the default behavior.

But also, RTFM.