Yordis Prieto Lazo
Yordis Prieto Lazo

Reputation: 850

Symlink (ln) folders doesn't work as expected

I am creating a script that basically look for all the things that ends with .symlink and try to create a symlink to the $HOME directory removing the symlink part and adding dot in front of the name.

Below is the line showing how the destination is created.

dst="$HOME/.$(basename "${src%.*}")"

There are the two functions created for that

create_symlink () {
  local src=$1 dst=$2

  # Create the symlink
  ln -s -i "$src" "$dst"
  message "Linked $1 -------> $2" # This just use echo
}

run_symlinks() {
  message "Creating Symlinks"

  local root=$1

  # Find the files/folder and try to do the symlink
  for src in $(find -H $root -name '*.symlink')
  do
    dst="$HOME/.$(basename "${src%.*}")"

    create_symlink "$src" "$dst"
  done
}

The problem

I have a folder called atom.symlink that folder basically have some configuration for my environment with the files of Atom text editor inside. When I run run_symlinks function that folder is being synced but in the wrong place.

The output of message "Linked $1 -------> $2" is:

Linked $HOME/.dotfiles/src/symlinks/atom.symlink -------> $HOME/.atom

But when I look at the folder the symlink is actually to $HOME/.atom/atom.symlink instead of just to the .atom folder.

Note: the $HOME/.atom folder is not empty and I need to figure out how to make this script without worry about having an empty folder.

I tried to find the answer on Google but I could't even know how to ask this specific question about it.

Upvotes: 1

Views: 3054

Answers (2)

mklement0
mklement0

Reputation: 440471

To complement anishsane's helpful answer, which explains the problem with your approach well:

Your desire to retain possibly preexisting (non-symlink) target folders in $HOME and add to their content requires a fundamentally different approach:

The only way to solve this is to avoid symlinking the *.symlink directories themselves; instead, the files in these directories must be individually symlinked to the target folder, which is either a preexisting folder or one that must be created as a regular folder on demand.
That is the only way to guarantee that the existing content of the target folder is not (invariably) lost:

while IFS= read -r f; do
  # Strip $root from the file path, then remove suffix '.symlink' and add prefix '.'
  # to the path commponent(s) to get the link path.
  link="$HOME/$(sed 's#\([^/]\{1,\}\)\.symlink\(/\{0,1\}\)#.\1\2#g' <<<"${f#$root/}")"
  # Make sure that the parent path exists, creating it on demand (as *regular* folder(s)).
  mkdir -p -- "$(dirname -- "$link")"
  # Now we can create the symlink.
  echo "symlinking [$link] <- [$f]"
  # Note the need to redirect from `</dev/tty` so as not
  # to suppress the interactive prompt (`-i`).
  ln -s -i "$f" "$link" </dev/tty
done < <(find -H "$root" -type f \( -path '*.symlink' -or -path '*.symlink/*' \)) 

The approach is a follows:

  • The find command finds only files, namely those themselves named *.symlink, and those inside directories named *.symlink (whatever suffix the files themselves have).

  • For each file, the target symlink path is determined by removing the $root path prefix, and then removing suffix .symlink and adding prefix . to matching path components.

  • The existence of each target symlink path's parent path is ensured with mkdir -p: any existing path components are retained as-is, and any non-existent ones are created as regular folders.

  • Once the existence of the target folder for the symlink is ensured / established, the ln command can be invoked.

    • Note that -i - to present an interactive prompt asking for replacement in case the link's path already exists - requires stdin to be a terminal in order to kick in; thus, given that stdin is redirected to the process substitution providing the output from find, </dev/tty is needed to show the prompt.

Upvotes: 1

anishsane
anishsane

Reputation: 20980

From man ln:

   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)

   In  the 1st form, create a link to TARGET with the name LINK_NAME.  In the
   2nd form, create a link to TARGET in the current directory.   In  the  3rd
   and  4th  forms,  create  links  to each TARGET in DIRECTORY.  Create hard
   links by default, symbolic links with --symbolic.  By default, each destination
   (name  of  new link) should not already exist.  When creating hard
   links, each TARGET must exist.  Symbolic links can hold arbitrary text; if
   later  resolved,  a relative link is interpreted in relation to its parent
   directory.

After first iteration of your script, you have

$HOME/.atom/ -> $HOME/.dotfiles/src/symlinks/atom.symlink # This is the first form in above man page snippet.

In the second iteration, you fall in the 3rd form, because the target already existed & after symlink dereferencing, it's a directory.

So, the command run is same:

  ln -s -i $HOME/.dotfiles/src/symlinks/atom.symlink $HOME/atom

Only difference is that in the second iteration, the target is an existing directory (after dereferencing).

So you should first delete the target (rm -f "$dst") & then create a symlink. Luckily, ln can do it by itself:

Change your code to:

ln -sfn "$src" "$dst" # Note that with -f & -n in place, usage of -i is moot.

Upvotes: 3

Related Questions