24

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

28

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 ! Commented Sep 8, 2015 at 14:33
  • This should be marked as the right answer Commented 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. Commented 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
    Commented 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
    Commented 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
  • this would create a directory instead of renaming a file in case the second argument was not supposed to be a target directory
    – axolotl
    Commented Jun 16, 2023 at 18:01
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 Commented 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. Commented Sep 7, 2015 at 21:28
  • Also, it won't work if $1 is a file but $2 is a directory. Commented Dec 30, 2016 at 16:04

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .