19

I am keeping my dotfiles under version control and the script deploying them creates hard links. I also use etckeeper to put my /etc under version control. Recently I have gotten warnings like this:

warning: hard-linked files could cause problems with bzr

A simple copy (cp filename.ext filename.ext) will not work:

cp: `filename.ext' and `filename.ext' are the same file

Renaming/moving a file - except across volumes - also doesn't break the hard-link.

So my question is: is there a way to break a hard-link to a file without actually having to know where the other hard-link/s to that file is/are?

0xC0000022L
  • 16,593

7 Answers7

17
cp -p filename filename.tmp
mv -f filename.tmp filename

Making it scriptable:

dir=$(dirname -- "$filename")
tmp=$(TMPDIR=$dir mktemp)
cp -p -- "$filename" "$tmp"
mv -f -- "$tmp" "$filename"

Doing the copy first, then moving it into place, has the advantage that the file atomically changes from being a hard link to being a separate copy (there is no point in time where filename is partial or missing).

7

You probably mean that you want to split the hard-link off to a separate, independent file.

mv hardlink tempname && cp tempname hardlink && rm tempname

A hardlink is the connection between an entry in the directory and the inode block on the disk.

inodes store file meta-data, and for small files, some file systems stores data in the inode, otherwise pointers to the data blocks, and for very large files indirect and double-indirect lists of pointers to disk allocation units.

Regardless, the connection between the file name (Which is what the ls command produce) and the inode block which stores this meta-data, is called a hard link.

Having multiple hard links to a single file means the same inode referenced by more than one directory entry, possibly in different directories (on a single file system)

rm deletes the file name entry from the directory. Once an inode is no longer referenced by any files, its space is freed up for use by other files.

Johan
  • 4,148
  • Indeed. This is what the cp and mv examples implied. So no way around using a temporary file, I see. – 0xC0000022L Mar 04 '13 at 13:20
  • @0xC0000022L, No, inodes don't contain pointers to other inodes. Just to data blocks (or they can double as data space if the object is small). – vonbrand Mar 04 '13 at 13:26
  • 4
    Make sure the permissions and other data is preserved when copying, i .e. use cp -a (at least GNU coreutils). – vonbrand Mar 04 '13 at 13:29
  • @0xC0000022L, if you look carefully, there is just a temporary name for the orginal file, a new file is created only in the last step. – vonbrand Mar 04 '13 at 13:32
  • @vonbrand They can indeed, depending on what file system is being used. – Johan Mar 04 '13 at 13:35
  • "Multiple Hardlinks to a single file" means the data is on the disk only once. To "split" the multiple links into separate independent files you will need to duplicate the data, hence the copy-step. – Johan Mar 04 '13 at 13:37
  • @Johan, got me curious: Which filesystem includes pointers to inodes in the inode, and to what purpose? – vonbrand Mar 04 '13 at 13:40
  • @vonbrand: thanks, I'm aware of the semantics otherwise. But I had hoped that if cp offers a way to hard-link files on the fly there would be some clever way to undo its effect as well with what I got aboard a Linux box. – 0xC0000022L Mar 04 '13 at 13:48
  • @vonbrand The last three blocks of the pointer list on ext2/3 etc are indirect (and double and triple) indirect lists. I first encountered this on VxFS many years ago. – Johan Mar 05 '13 at 08:04
  • @vonbrand OK I just checked this out. Calling those indirect pointers to be pointers to inodes is not exactly accurate. They are indirect pointers to disk allocation units. I learn every day. – Johan Mar 05 '13 at 08:09
6

Put this at the end of your ~/.bashrc file.

delink () { tmpfile="$1$(date)"; cp -a "$1" "$tmpfile"; mv "$tmpfile" "$1"; }

Run it like this

delink filename
Rucent88
  • 1,880
  • 4
  • 24
  • 37
3

The best way to do it with a bash script would be something like this:

if [ -f "$1" ] ; then
dir="$(dirname -- "$1")"
tmpfile="$(mktemp --tmpdir="$dir")"
cp --preserve=all -f -- "$1" "$tmpfile"
mv -f -- "$tmpfile" "$1"
fi

points to note:

  • check if the file is a regular file before trying to copy it
  • keep the old file in place until the copy is ready
  • use mktemp to generate a file which is guaranteed not to exist
  • use -f to force overwrite and --preserve=all to keep metadata as similar as possible to the original file
  • use -- and " to quote paths containing spaces and/or beginning with -

Doing the replacement without creating a temporary file isn't possible with current (3.16) linux system calls: while it is possible to overwrite a file atomically (i.e., remove the old file and replace with a new one as a single operation), it is not possible to do so with a file which has no name on the filesystem (i.e. a temporary file created using the O_TMPFILE flag of open function) because the rename function require a filename as input (there is no version of rename which takes as input a file descriptor - see here for details)

pqnet
  • 2,700
  • 1
    Note that you failed to quote the name in your dirname and mktemp calls. Fixed that for you... – derobert Aug 06 '14 at 12:10
  • @derobert oh thank you, but this won't work since there are nested double quotes... need another fix! Kinda hate bash – pqnet Aug 06 '14 at 12:13
  • 3
    It'll work because of the $( ... )-style command substitution. One reason its nicer than \ ... ``-style. – derobert Aug 06 '14 at 12:15
  • @derobert nice, didn't know that. Also, how can you use ` inside inline code tags? – pqnet Aug 06 '14 at 12:20
  • You can escape them with backslashes. So to get \`` you put in:`\``` (of course, I double-escaped that to get it to show you what you type). – derobert Aug 06 '14 at 12:22
-1

An empty in-place sed does the trick (tested with GNU sed):

sed -i '' <file>

You can use find and xargs to run that on many files:

find <paths...> -type f -links +1 -print0 | xargs -0 sed -i ''
-2

If your are looking for all the filename who are hardlink to this file then you can use:

find -samefile myknowhardlinkfile

also ls -il myknowhardlinkfile will show you the number of filename hardlinked to the same inode (third field).

101612442 -rw-rw-r--. 2 me me 0 Aug  5 07:07 myknowhardlinkfile
-3

The command you're looking for is unlink

Jenny D
  • 13,172