22

Can someone give me a command that would:

  • move a file towards a new directory
  • and leave a symlink in its old location towards its new one

3 Answers 3

27

mv moves a file, and ln -s creates a symbolic link, so the basic task is accomplished by a script that executes these two commands:

#!/bin/sh
mv -- "$1" "$2"
ln -s -- "$2" "$1"

There are a few caveats. If the second argument is a directory, then mv would move the file into that directory, but ln -s would create a link to the directory rather than to the moved file.

#!/bin/sh
set -e
original="$1" target="$2"
if [ -d "$target" ]; then
  target="$target/${original##*/}"
fi
mv -- "$original" "$target"
ln -s -- "$target" "$original"

Another caveat is that the first argument to ln -s is the exact text of the symbolic link. It's relative to the location of the target, not to the directory where the command is executed. If the original location is not in the current directory and the target is not expressed by an absolute path, the link will be incorrect. In this case, the path needs to be rewritten. In this case, I'll create an absolute link (a relative link would be preferable, but it's harder to get right). This script assumes that you don't have file names that end in a newline character.

#!/bin/sh
set -e
original="$1" target="$2"
if [ -d "$target" ]; then
  target="$target/${original##*/}"
fi
mv -- "$original" "$target"
case "$original" in
  */*)
    case "$target" in
      /*) :;;
      *) target="$(cd -- "$(dirname -- "$target")" && pwd)/${target##*/}"
    esac
esac
ln -s -- "$target" "$original"

If you have multiple files, process them in a loop.

#!/bin/sh
while [ $# -gt 1 ]; do
  eval "target=\${$#}"
  original="$1"
  if [ -d "$target" ]; then
    target="$target/${original##*/}"
  fi
  mv -- "$original" "$target"
  case "$original" in
    */*)
      case "$target" in
        /*) :;;
        *) target="$(cd -- "$(dirname -- "$target")" && pwd)/${target##*/}"
      esac
  esac
  ln -s -- "$target" "$original"
  shift
done
6
  • 1
    Thanks Gilles, for the script and the explanations. I'll try to understand that !
    – Yo B.
    Sep 8, 2015 at 14:33
  • This should be marked as the right answer Dec 30, 2016 at 16:06
  • To make this answer even more awesome, rsync --remove-source-files might be more informative to users who are moving a large file off a disk which may take some time. Dec 30, 2016 at 16:17
  • I think you should call sh with -e so it breaks on errors, for example if you stop the mv command because a file already exists
    – Alex
    Sep 5, 2018 at 19:30
  • Another caveat is that the first argument to ln -s is the exact text of the symbolic link. -- See the new-ish and awesome ln --relative option.
    – Roger Dahl
    Mar 14, 2020 at 15:30
2

I generally use the this one-line function:

function ml() { mkdir -p "$(dirname "$1")" && rsync -aP --no-links "$1" "$2" && ln -sf "$2" "$1" }

Usage is similar to mv or cp:

ml old_file target_dir/new_file_name

Breaking it down:

  • mkdir -p "$(dirname "$1")" - create the destination directory if it does not already exist
    • dirname "$1" - get the directory component of the path (strip the filename)
  • rsync -aP --no-links "$1" "$2" - copy the file over to the destination. Replace this with mv "$1" "$2" if both files are on the same filesystem for better performance.
    • -a - preserve ownership and all other permissions. You can tune this to only preserve the components you want.
    • -P - show progress.
    • --no-links - don't copy links--this means you can run this command as many times as you want on the same file, and you won't ever lose your file by accidentally overwriting your destination file with a symlink to itself.
  • ln -sf "$2" "$1" - overwrite the old file with a symlink to the new file
    • -s - use symbolic links
    • -f - overwrite the old file
1

Put this in .sh file and make it executable(chmod +x filename):

#!/bin/bash

mv "$1" "$2"
ln -s "$2" "$1"

Usage example:

./test.sh asdf otherdir/asdf

Note that this does no safety checks, etc. Depending on how complex is your task, this might be sufficient.

3
  • Thank you Matthew, this works perfectly when applied on one file. But can you help me generalising this to two directories like this : ./test.sh .mozila/firefox/zotero/*/*.pdf MyBbliography/*.pdf. The * does not seem to be working with your test.sh. Have you got a workaround ? Thx
    – Yo B.
    Sep 7, 2015 at 12:04
  • 2
    @kadok MatthewRock's script only works for a single file, and the target must be an absolute path or else the source must be in the current directory. Sep 7, 2015 at 21:28
  • Also, it won't work if $1 is a file but $2 is a directory. Dec 30, 2016 at 16:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.