23

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
Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
Yollanda Beetroot
  • 333
  • 1
  • 2
  • 6

3 Answers3

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
Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
  • 1
    Thanks Gilles, for the script and the explanations. I'll try to understand that ! – Yollanda Beetroot Sep 08 '15 at 14:33
  • This should be marked as the right answer – Sridhar Sarnobat Dec 30 '16 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. – Sridhar Sarnobat Dec 30 '16 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 05 '18 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 '20 at 15:30
  • There are problems. First with ``target="$target/${original##*/}"`` if your original is for example "folder/" it causes complete removal of "folder/" from there. I would use just ``target="$target/`basename "${original}"`"``. Second problem with link creation and fix is ``ln -s -- "$target" "`realpath "$original"`"`` which removes last slash from "folder/". – Juraj Michalak Jul 28 '20 at 04:26
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
flaviut
  • 844
  • 1
  • 7
  • 15
  • this would create a directory instead of renaming a file in case the second argument was not supposed to be a target directory – axolotl Jun 16 '23 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.

MatthewRock
  • 6,826
  • 6
  • 31
  • 54
  • 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 – Yollanda Beetroot Sep 07 '15 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. – Gilles 'SO- stop being evil' Sep 07 '15 at 21:28
  • Also, it won't work if $1 is a file but $2 is a directory. – Sridhar Sarnobat Dec 30 '16 at 16:04