49

Is there an easy way to replace all symbolic links with the file they link to?

2
  • 3
    What problem are you trying to solve?
    – Daniel Beck
    Jun 28, 2011 at 19:46
  • 3
    I have a bunch of symbolic links to another drive. I need move all files off of that drive that are referenced elsewhere so I can remove it. Jun 28, 2011 at 19:49

16 Answers 16

22

For some definitions of "easy":

#!/bin/sh
set -e
for link; do
    test -h "$link" || continue

    dir=$(dirname "$link")
    reltarget=$(readlink "$link")
    case $reltarget in
        /*) abstarget=$reltarget;;
        *)  abstarget=$dir/$reltarget;;
    esac

    rm -fv "$link"
    cp -afv "$abstarget" "$link" || {
        # on failure, restore the symlink
        rm -rfv "$link"
        ln -sfv "$reltarget" "$link"
    }
done

Run this script with link names as arguments, e.g. through find . -type l -exec /path/tos/script {} +

3
  • 2
    Thanks for the edit, anon, but the script does handle filenames with spaces just fine, by quoting variables in all necessary places. Changing "$var" to "${var}" is a noop in sh/bash. I removed the one bashism ([[), the rest is compatible with POSIX sh.
    – user1686
    May 6, 2014 at 5:46
  • An even more robust method for handling symlink failures is to cp to a temporary file in the same directory then mv -f that temporary file into the original. This should guard against problems like people holding down Ctrl-C or the filesystem filling up between the rm and cp commands (thus preventing even the new ln from working). Temporary filenames are easy to spot with ls -a for later cleanup if needed.
    – Mr Fooz
    Aug 5, 2015 at 16:46
  • As a side note, I seriously can't remember why I didn't just use abstarget=$(readlink -f "$link") instead of the case block, but it might have been portability reasons.
    – user1686
    Jul 31, 2017 at 12:23
33

If I understood you correctly the -L flag of the cp command should do exactly what you want.

Just copy all the symlinks and it will replace them with the files they point to:

cp -L file tmp/ && rm file && mv tmp/file .
1
  • 8
    Thanks - almost what I needed: I had to do something like: cp -L files tmp/ && rm files && cp tmp/files . If you clarify, you'll probably help more people...
    – sage
    Sep 18, 2013 at 0:05
20

Might be easier to just use tar to copy the data to a new directory.

-H      (c and r mode only) Symbolic links named on the command line will
        be followed; the target of the link will be archived, not the
        link itself.

You could use something like this

tar -hcf - sourcedir | tar -xf - -C newdir

tar --help:
-H, --format=FORMAT        create archive of the given format
-h, --dereference          follow symlinks; archive and dump the files they point to
5
  • 1
    I love your solution, but I don't understand the last part of your message. For me you should do : tar -hcf - sourcedir | tar -xf - -C newdir Apr 9, 2012 at 13:52
  • 1
    Agreed, this answer would need updating as per comment.
    – astabada
    Jun 9, 2014 at 13:44
  • 1
    cp -L is the simpler solution
    – plesiv
    Sep 18, 2015 at 13:25
  • @plesiv true, and posix-compliant, but not all cp implementations will copy directories.
    – Wyatt Ward
    Nov 7, 2016 at 5:27
  • @Everyone: Use -h rather than -H, because -H is specific to bsdtar and is not supported by GNU tar. Also, it only follows symlinks given on the command line, but we want to follow all symlinks in sourcedir. (Posting as a comment because my suggested edit was rejected.)
    – tom
    May 25 at 13:53
6
find ./ -type l -exec sh -c 'for i in "$@"; do cp --preserve --remove-destination "$(readlink -f "$i")" "$i"; done' sh {} +

based on https://superuser.com/a/1301199/499386 of MastroGeppetto and with edits from @tom

0
5

"easy" will be a function of you, most likely.

I'd probably write a script that uses the "find" command line utility to find symbolically linked files, then calls rm and cp to remove and replace the file. You'd probably do well to also have the action called by find check that there's enough free space left before moving the sym link too.

Another solution may be to mount the file system in question through something that hides the sym links (like samba), then just copy everything out of that. But in many cases, something like that would introduce other problems.

A more direct answer to your question is probably "yes".

Edit: As per the request for more specific info. According to the man page for find, this command will list all sym link files in to a depth of 2 directories, from /:

find / -maxdepth 2 -type l -print

(this was found here)

To get find to execute something on finding it:

find / -maxdepth 2 -type l -exec ./ReplaceSymLink.sh {} \;

I believe that'll call some script I just made up, and pass in the file name you just found. Alternatively, you could capture the find output to file (use "find [blah] > symlinks.data", etc) then pass that file in to a script you've written to handle copying over the original gracefully.

3
  • ? A bit generic - can you elaborate?
    – Linker3000
    Jun 28, 2011 at 20:04
  • 2
    The syntax is -exec ./ReplaceSymLink.sh {} \;
    – user1686
    Jun 28, 2011 at 20:37
  • This is go to find the items and the one marked as the answer is used to replace the links! Jun 28, 2011 at 22:07
4

This is the oneliner I've used: it assumes all local files are links to another file in "source". You can refine file selection with patterns or "find". Please, understand before using.

for f in *; do cp --remove-destination "source/$f" "$f"; done
3
  • how about using readlink: for f in *; do cp ----remove-destination "$(readlink $f)" "$f"; done
    – Diego
    Jun 3, 2018 at 20:09
  • Yes, it's a good idea, but you lose some control on the operation. One limit of my solution is that it does not work for files with blanks in the middle, or for very long lists. Probably the final solution is to put together the 'find --exec' above with the 'remove destination'. Anybody wants to try? Jun 5, 2018 at 7:32
  • 1
    blanks in the middle should be mitigated by putting "$f" in doublequotes. Here is how I use it with "find & xargs" instead of "for loop": find ./ -type l -print0|xargs -0 -n1 -i sh -c 'cp --remove-destination $(readlink "{}") "{}" ' I have posted it as a separate answer for the visibility. superuser.com/a/1329543/499386
    – Diego
    Jun 8, 2018 at 3:44
3
for f in *; do cp --remove-destination $(readlink "$f") "$f"; done
1
  • 1
    Note 1: This breaks on spaces. The fix is to change $(readlink "$f") to "$(readlink "$f")". Note 2: This works on relative symlinks, but only if used exactly like in the answer (namely, on files directly in the current directory). If $f is in some other directory and is a relative symlink, the command will fail. To handle relative symlinks use readlink -f "$f", but note that this is not supported on macOS and older BSD's. (Posting as a comment because my suggested edit was rejected.)
    – tom
    May 25 at 14:16
2

Using some of the previous ideas, the following command will recursively replace every symlink with a copy of the original:

find . -type l -exec bash -c "echo 'Replacing {} ...';  cp -LR '{}' '{}'.dereferenced;  rm '{}';  mv '{}'.dereferenced '{}'" \;
1
  • DO NOT USE THIS ON UNTRUSTED DIRECTORIES: It is vulnerable to command injection. Suggested correction: find . -type l -exec sh -c 'for i in "$@"; do echo "Replacing $i ..."; cp -LR "$i" "$i.dereferenced" && rm "$i" && mv "$i.dereferenced" "$i"; done' sh {} +
    – tom
    May 24 at 9:14
1
find . -type l -exec cp --dereference --recursive '{}' '{}'.dereferenced \;

Will make a copy of each symlinked file/folder in <filename>.dereferenced, which is safer (if less convenient) than just replacing them directly. Moving the copied data to the symlinks' filenames is left as an exercise for the reader.

1

In the following environment: Ubuntu 22.04.2 LTS cp (GNU coreutils) 8.32

I had some unexpected behavior when running some of the other solutions that included 'cp --remove-destination' when the link referenced a directory: cp: will not create hard link './//' to directory './/'

This one-liner handles the following edge cases:

  1. the link points to a nonexistent file location.
  2. the link is relative.
  3. the link is a directory.

It will also maintain the file mode of the copied files (cp -p).

find . -type l  -exec bash -c 'link="$(readlink -f "{}")"; [[ -a "${link}" ]] && { rm -f "{}"; cp -pr "${link}" "{}"; }  ' \;
1
  • DO NOT USE THIS ON UNTRUSTED DIRECTORIES: It is vulnerable to command injection. Suggested correction: find . -type l -exec sh -c 'for i in "$@"; do link="$(readlink -f "$i")"; [ -e "$link" ] && { rm -f "$i"; cp -pr "$link" "$i"; }; done' sh {} +
    – tom
    May 24 at 9:17
1

Building on the above examples this is my one-liner in a verbose style. The obscure things to know are that the "{}" is a placeholder for the arguments from -exec, + marks the end of the command passed to -exec (manual), and the "sh" argument before the "{}" is required because sh -c COMMAND ARGS... expects the first ARG to be the name of the shell.

find ./ -type l -exec sh -c 'for i in "$@"; do cp --preserve --remove-destination "$(readlink -f "$i")" "$i"; done' sh {} +
6
  • Hi @tom thanks for your suggestion, what does the sh {} + part do?
    – Diego
    2 days ago
  • 1
    Hi @Diego! The {} + part is for efficiency. find -exec has two forms (man): (1) -exec command args... \; runs the command once on each file and replaces {} with the filename everywhere. (2) -exec command args... {} + runs the command on multiple files at a time, replacing the {} (which must be at the end) with the filenames as multiple arguments. It's more efficient because it avoids spawning lots of sh instances. (1/3)
    – tom
    2 days ago
  • The reason for the sh before the {} is that the invocation of sh -c is sh -c COMMAND CMD_NAME ARG1 ARG2 .... CMD_NAME sets arg0 of the shell; normally it's set to the name of the executable, and it's displayed in shell error messages for example. So if you use find ... -exec sh -c 'echo "$@"' {} + (without the sh before the {}), then the first filename would be interpreted as the name of the shell and wouldn't be included in $@, which is bad. (2/3)
    – tom
    2 days ago
  • For security, the critical fix is to pass {} as a separate argument instead of making it part of COMMAND. So if you prefer the -exec \; form, it would be -exec sh -c 'echo "$1"' sh {} \; (note that we still need to pass sh as arg0 so that the filename will be interpreted as arg1). The reason to avoid using {} within the command is that it's vulnerable to injection; even if you surround it in ", the command will fail on filenames that contain ". (3/3)
    – tom
    2 days ago
  • 1
    Thanks, @tom, I have accepted your edits. This is one of things that are used now and again so it's good to have a proper one-liner.
    – Diego
    yesterday
0

Lot's of good answers there but I since you were looking for something easy

for i in *; do link=$(readlink $i) && rm $i && mv $link $i; done
1
  • A little enhancement will help to find symbolic links: for lnk in `find . -type l`; do src=$(readlink $lnk) && rm $lnk && mv $src $lnk; done (not tested with hierarchy though, be careful with backticks)
    – Roman Susi
    Feb 13, 2014 at 8:03
0

I had the same problem with gwenview copying links when you would normally expect it to follow the link and copy the file.

What I did to 'clean' the directory by following all the links and copying all the pointed-to files to the directory, was create a scratch directory, run e.g.

"for file in `ls`; do cp -L $file scratchdir/$file; done; mv scratchdir/* ./" 

this is too dangerous to put in a script as there is no checking but it fixed my problem.

0

I did a one-line version, since my web-hotel didn't allowed bash-scripts and it worked fine for me:

before:
> find . -type l | wc -l
358

> for link in `find . -type l` ; do echo "$link : "; ls -al $link ; cp $link notalink; unlink $link; mv notalink $link ; ls -al $link; done
...

after:
> find . -type l | wc -l
0
0

fastest solution, because it avoids creating a new shell for every copy

replaceSymlinkWithFileInDir() {
  while IFS= read -r -d $'\0' l; do
    cp --remove-destination "$(readlink -f "$l")" "$l"
  done < <(find "${1:-.}" -type l -print0)
}

to replace only one symlink

replaceSymlinkWithFile() {
  if [ -L "$1" ]; then
    cp --remove-destination "$(readlink -f "$1")" "$1"
  fi
}

for loops break with many files.

copy + move is wasteful, the files should be replaced in-place.

function test files:

cd $(mktemp -d)

echo hello > a
ln -s a b
ln -s b c

benchmark:

f1() {
  while IFS= read -r -d $'\0' l; do
    cp --remove-destination "$(readlink -f "$l")" "$l"
  done < <(find . -type l -print0)
}

f2() {
  find ./ -type l -print0 |
  xargs -0 -i sh -c 'cp --remove-destination "$(readlink -f "{}")" "{}" '
}

f3() {
  find ./ -type l -exec \
  sh -c 'cp --remove-destination "$(readlink -f "$1")" "$1"' -- '{}' ';'
}

cd $(mktemp -d)

for f in f1 f2 f3; do
t1=$(date +%s.%N)
for round in $(seq 10); do
rm *
echo hello >0
for i in $(seq 40); do ln -s $((i-1)) $i; done
$f
t2=$(date +%s.%N)
dt=$(echo $t1 $t2 | awk '{print $2 - $1}')
done
echo $f: $dt seconds
done

result

f1: 10.8137 seconds
f2: 16.3744 seconds
f3: 16.8887 seconds
0
-1

Plenty of really good answers here, but I thought I'd add my 2 cents. This is quick and dirty (and might not work on all systems), but it works for my use case:

realpath * | xargs -n1 -I{} mv {} .

1
  • Welcome to SuperUser! Please do not add new answers to very old questions unless the already top voted answers are outright dangerous due to some changes made by the os or any other concerning party. Oct 7, 2022 at 14:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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