86

I'm creating a shell script that would take a filename/path to a file and determine if the file is a symbolic link or a hard link.

The only thing is, I don't know how to see if they are a hard link. I created 2 files, one a hard link and one a symbolic link, to use as a test file. But how would I determine if a file is a hard link or symbolic within a shell script?

Also, how would I find the destination partition of a symbolic link? So let's say I have a file that links to a different partition, how would I find the path to that original file?

9
  • 21
    What do you mean by hard link? All files are hard links.
    – terdon
    Nov 12, 2014 at 17:55
  • 3
    @terdon ln /foo/bar/ /foo/bar2 makes a hardlink while ln -s /foo/bar /foo/bar2 makes a symlink, thats what he means? Nov 12, 2014 at 18:17
  • 16
    @DisplayName yes, but all files are hard links to their inode. That's how Linux file systems work. In your example, bar2 and bar are both hard links, just pointing to the same inode.
    – terdon
    Nov 12, 2014 at 18:20
  • 12
    @DisplayName yes, they are hard links to other inodes. There is no contradiction here. A file is a link to an inode. That's the definition of a file. In your case, you have these links in different places but that doesn't change the underlying data structure. My point is that both bar and bar2 are equally important. One is not a link to the other, they are both links but point to the same inode.
    – terdon
    Nov 12, 2014 at 18:28
  • 3
    @Scott no, I'm saying that regular files are hardlinks and that hardlinks created by ln are no different than regular files.
    – terdon
    Nov 13, 2014 at 18:14

7 Answers 7

61

Jim's answer explains how to test for a symlink: by using test's -L test.

But testing for a "hard link" is, well, strictly speaking not what you want. Hard links work because of how Unix handles files: each file is represented by a single inode. Then a single inode has zero or more names or directory entries or, technically, hard links (what you're calling a "file").

Thankfully, the stat command, where available, can tell you how many names an inode has.

So you're looking for something like this (here assuming the GNU or busybox implementation of stat):

if [ "$(stat -c %h -- "$file")" -gt 1 ]; then
    echo "File has more than one name."
fi

The -c '%h' bit tells stat to just output the number of hardlinks to the inode, i.e., the number of names the file has. -gt 1 then checks if that is more than 1.

Note that symlinks, just like any other files, can also be linked to several directories so you can have several hardlinks to one symlink.

3
  • ok, just to be clear, i can output the number of hardlinks that the file has using the stat command and if its greater then 1, then it has another file linked somewhere on the partition.
    – k-Rocker
    Nov 12, 2014 at 18:34
  • 1
    @k-Rocker Yes. Then it has a second name somewhere on the partition.
    – derobert
    Nov 12, 2014 at 18:38
  • 4
    On OS X or *BSD, it's stat -f %l /path/to/file. You could also use gstat -c %h /path/to/file if you have the GNU coreutils installed without their default names (with Homebrew on OS X).
    – GDP2
    Jul 11, 2016 at 22:28
42

An example:

$ touch f1
$ ln f1 f2
$ ln f1 f3
$ ln -s f1 s1
$ ln -s f2 s2
$ ln -s ./././f3 s3
$ ln -s s3 s4
$ ln s4 s5
$ ls -li
total 0
10802124 -rw-r--r-- 3 stephane stephane 0 Nov 12 19:55 f1
10802124 -rw-r--r-- 3 stephane stephane 0 Nov 12 19:55 f2
10802124 -rw-r--r-- 3 stephane stephane 0 Nov 12 19:55 f3
10802345 lrwxrwxrwx 1 stephane stephane 2 Nov 12 19:56 s1 -> f1
10802346 lrwxrwxrwx 1 stephane stephane 2 Nov 12 19:56 s2 -> f2
10802347 lrwxrwxrwx 1 stephane stephane 8 Nov 12 19:56 s3 -> ./././f3
10802384 lrwxrwxrwx 2 stephane stephane 2 Nov 12 19:56 s4 -> s3
10802384 lrwxrwxrwx 2 stephane stephane 2 Nov 12 19:56 s5 -> s3

The f1, f2 and f3 directory entries are the same file (same inode: 10802124, you'll notice the number of links is 3). They are hard links to the same regular file.

s4 and s5 are also the same file (10802384). They are of type symlink, not regular. They point to a path, here s3. Because s4 and s5 are entries of the same directory, that relative path s3 point to the same file (the one with inod 10802347) for both.

If you do a ls -Ll, that is asking to get file information after resolving symlinks:

$ ls -lLi
total 0
10802124 -rw-r--r-- 3 stephane stephane 0 Nov 12 19:55 f1
10802124 -rw-r--r-- 3 stephane stephane 0 Nov 12 19:55 f2
10802124 -rw-r--r-- 3 stephane stephane 0 Nov 12 19:55 f3
10802124 -rw-r--r-- 3 stephane stephane 0 Nov 12 19:55 s1
10802124 -rw-r--r-- 3 stephane stephane 0 Nov 12 19:55 s2
10802124 -rw-r--r-- 3 stephane stephane 0 Nov 12 19:55 s3
10802124 -rw-r--r-- 3 stephane stephane 0 Nov 12 19:55 s4
10802124 -rw-r--r-- 3 stephane stephane 0 Nov 12 19:55 s5

You'll find they all resolve to the same file (10802124).

You can check if a file is a symlink with [ -L file ]. Similarly, you can test if a file is a regular file with [ -f file ], but in that case, the check is done after resolving symlinks.

hardlinks are not a type of file, they are just different names for a file (of any type).

You can check whether a file is (hard) linked more than once to one or several directories by checking its link count

POSIXly:

has_more_than_one_link() (
  export LC_ALL=C # only locale where the output of ls -n is specified
  export TZ=UTC0  # simplify the mtime field (which we don't need anyway)
                  # generation as an optimisation.
  unset -v IFS    # for default separator for read to include space and
                  # tab, the only 2 "blank" characters in the C/POSIX locale
  ls -nqd -- "$1" | {
    read -r ignore links ignore &&
      [ "$links" -gt 0 ]
  }
)

Or with zsh:

has_more_than_one_link() () (($#)) $1(Nl+1)

Where the body of the function is an anonymous function whose body is (($#)) (returns true if the number of arguments is > 0) and arguments are the expansion of the $1(Nl+1) wildcard, which expands to $1 iff the number of links of that file is greater than 1.

Or using its stat builtin:

zmodload zsh/stat
has_more_than_one_link() {
  stat -LA2 +nlink -- "$1" && (( $2 > 1 ))
}

(here storing the number of links in $2 to avoid having to declare a separate variable local).

Some systems also have an external stat command (which generally post-date the one in zsh), though they all have different interfaces between each other and from zsh's.

With the GNU implementation of stat, you need to handle a file called - specially as GNU stat interprets it as meaning to do a fstat() on stdin:

has_more_than_one_link() {
  local nlink
  [ "$1" != - ] || set ./-
  nlink=$(stat -c %h -- "$1") &&
    [ "$nlink" -gt 1 ]
}

(here assuming a POSIX-like shell with support for local scope).

26

Using the -h and -L operators of the test command:

-h file 
true if file is a symbolic link

-L file 
true if file is a symbolic link

http://www.mkssoftware.com/docs/man1/test.1.asp

According to this SO thread, they have the same behavior, but -L is preferred.

4
  • ok cool, but what about hard links? i checked the tread, but nothing about hard links. If -L returns false, does that mean its a hard link? or just a regular file?
    – k-Rocker
    Nov 12, 2014 at 17:44
  • 2
    Hard links share the same inode. Also, soft links show an l at the beginning of its ls -l output... I think you may be able to put those rules together in a script, plus the [[ -L file ]] test to see if the given file is either soft or hard.
    – jimm-cl
    Nov 12, 2014 at 17:46
  • 1
    ok, also, how would i find the destination partition of the symbolic link?
    – k-Rocker
    Nov 12, 2014 at 18:24
  • Be cautious of using this since file not exists return false but it doesn't means hard link.
    – 林果皞
    Nov 14, 2019 at 18:31
9

There are a lot of quite correct answers here, but I don't think anybody really tackled the original misperception. The original question is basically "when I make a symbolic link, it's easy to identify it afterwards. But I can't figure out how to identify a hard link." And yes, the answers basically boil down to "you can't," and more or less explain why, but nobody seems to have acknowledged that, really, it IS confusing and strange.

If you're reading all this and you've figured out what's going on, then you're good; you don't need to read my little bit. If you're still confused, then keep going.

The really really short answer is that a hard link isn't really a link at all, not in the way a symbolic link is. It's a new entry in the directory structure that points to the same bunch of bytes that the original directory entry did, and once you've created it, it is just as 'real' and legitimate as the first one. Every 'normal' file on your drive has at least one hard link; without that, you wouldn't see it in any directory, and would be unable to refer to it or use it. So if you have a file Fred.txt, and you hard link Wilma.txt and Barney.txt to it, all three names (and directory entries) refer to the same file, and they are all equally valid. There isn't any way for the OS to tell that one of the entries was created when you hit "save" in your text editor, and the others were made with the "ln" command.

The OS does have to keep track of how many different entries are pointing to the same file, though. If you delete Wilma.txt, no surprise that you don't free up any space on your drive. But if you delete Fred.txt (the 'original' file), you still won't free up any space on your drive, because the data on the drive that was known as Fred.txt is still also Barney.txt. Only when you delete all of the directory entries will the OS de-allocate the space that the data itself was occupying.

If Barney.txt had been a symbolic link, then deleting Fred.txt would have de-allocated the space, and Barney.txt would now be a broken link. Also, if you move or rename a file that has a symbolic link pointing at it, you'll break the link. But you can move or rename a hard-linked file all you want without breaking the other directory entries that point to that file/data, because all of them are directory entries that refer to the same block of data on the drive (by using the inode # of that data).

[It's two years later, and that last bit confused me for a minute, so I think I'll clarify. If you type "mv ./Wilma.txt ../elsewhere/Betty.txt" it seems like you're moving the file, but in fact, you are not. What you're really doing is removing a line item from the directory list of your current directory, the one that says "the name 'Wilma.txt' is associated with the data that can be found by using inode #######," and adding a new line item to the directory list of directory ../elsewhere that says "the name 'Betty.txt' is associated with the data that can be found via inode #######". This is why you can 'move' a 2 gigabyte file as quickly as a 2 kilobyte file, as long as you're moving them to another location on the same drive.]

Because the OS has to keep track of how many different directory entries are pointing to the same chunk of data, you can tell if a particular file has been hard linked to, even though you can't tell for certain if the directory entry that you're looking at is the 'original' one or not. One way is the "ls" command, specifically "ls -l" (that's a lower-case L after the dash)

To borrow an earlier example....

 -rw-r--r-- 3 stephane stephane 0 Nov 12 19:55 f1

The first letter's a dash, so it's not a directory or something else exotic, it's a 'regular' ordinary file. But if it were truly ordinary, that number after the rwx-ish part would be "1", as in, "there is one directory entry pointing to this block of data." But it's part of a demonstration of hard links, so instead it says "3".

Note that this can possibly lead to weird and mysterious behavior (if you haven't wrapped your head around hard links, that is). If you open Fred.txt in your text editor and make some changes, will you see the same changes in Wilma.txt and Barney.txt? Maybe. Probably. If your text editor saves the changes by opening the original file and writing the changes to it, then yes, all three names will still be pointing at the same (newly changed) text. But if your text editor creates a new file (Fred-new-temp.txt), writes your changed version to that, then deletes Fred.txt, then renames Fred-new-temp.txt to Fred.txt, Wilma and Barney will still be pointing to the original version, not the new changed version. If you don't understand hard links, this could drive you slightly mad. :) [Okay, I don't actually personally know of any text editors that would do the new-file/rename thing, but I do know of lots of other programs that do exactly that, so stay alert.]

A final note: one of the things that 'fsck' (file system check) checks for is if there are blocks of data on your drive that somehow are no longer referenced by any directory entries. Sometimes something goes wrong, and the only directory entry that points to an inode gets deleted but the drive space itself doesn't get marked as "available." So one of fsck's jobs is to match up all the allocated space with all the directory entries to make sure that there aren't any unreferenced files. If it finds some, it creates new directory entries and puts them in "lost+found".

3
  • Just wondering, what are those "other programs that do exactly that"?
    – phk
    Sep 3, 2016 at 8:45
  • @phk Don't know what he is thinking about specifically, but that is a common enough approach for doing actions which can take a long time and will leave you in an uncertain state if failed. For example, if you try to download from a remote server, and you know there is a chance that the server could timeout, then one approach would be to download the entire contents to a temp file. That way if something goes wrong with the download you still have the original file. Nov 11, 2016 at 21:00
  • 1
    The only program I know for certain is FreeHand, because if/when it crashes during a save, there's a temporary file left behind, rather than a mangled original file. But I've seen other programs do it too; I just can't give you specific examples at this time.
    – Snarke
    Nov 13, 2017 at 22:36
8

you can use readlink FILE; echo $?. This returns 1 when it's a hardlink and 0 when it's a symlink.

From the man page: " When invoked as readlink, only the target of the symbolic link is printed. If the given argu- ment is not a symbolic link, readlink will print nothing and exit with an error."

1
  • 3
    Be cautious of using this since file not exists return 1 but it doesn't means hard link.
    – 林果皞
    Nov 14, 2019 at 18:34
0

Try this script to test files, directories and symlinks:

touch non_existing_file.pdf
touch existing_regular_file.pdf
ln -s non_existing_file.pdf       link_non_functional_file.pdf
ln -s existing_regular_file.pdf   link_functional_file.pdf
rm non_existing_file.pdf 

mkdir existing_directory
mkdir non_existing_directory
ln -s non_existing_directory    link_non_existing_directory
ln -s existing_directory        link_existing_directory
rmdir non_existing_directory



for file in non_existing_file.pdf existing_regular_file.pdf link_non_functional_file.pdf link_functional_file.pdf existing_directory link_non_existing_directory link_existing_directory
    do
    if [ ! -e $file ] && [ ! -L $file ]; then echo " $file does not exist"; else echo "$file exists somehow"; fi
    if [   -e $file ] && [ ! -L $file ] && [ ! -d $file ]; then echo " $file is a regular file"; else echo "$file is not a regular file"; fi
    if [   -e $file ] && [ ! -L $file ] && [ -d $file ]; then echo " $file is a regular directory"; else echo "$file is not a regular directory"; fi
    if [   -e $file ] && [   -L $file ] && [ ! -d $file ]; then echo " $file is fully functional symlink to a file"; else echo "$file is not a functional link to a file"; fi
    if [   -e $file ] && [   -L $file ] && [  -d $file ]; then echo " $file is fully functional symlink to a directory"; else echo "$file is not a functional link to a directory"; fi
    if [ ! -e $file ] && [   -L $file  ]; then echo " $file is a broken symlink"; else echo "$file is not a broken symlink"; fi
    done 
exit 

The output should be:

 non_existing_file.pdf does not exist
non_existing_file.pdf is not a regular file
non_existing_file.pdf is not a regular directory
non_existing_file.pdf is not a functional link to a file
non_existing_file.pdf is not a functional link to a directory
non_existing_file.pdf is not a broken symlink
existing_regular_file.pdf exists somehow
 existing_regular_file.pdf is a regular file
existing_regular_file.pdf is not a regular directory
existing_regular_file.pdf is not a functional link to a file
existing_regular_file.pdf is not a functional link to a directory
existing_regular_file.pdf is not a broken symlink
link_non_functional_file.pdf exists somehow
link_non_functional_file.pdf is not a regular file
link_non_functional_file.pdf is not a regular directory
link_non_functional_file.pdf is not a functional link to a file
link_non_functional_file.pdf is not a functional link to a directory
 link_non_functional_file.pdf is a broken symlink
link_functional_file.pdf exists somehow
link_functional_file.pdf is not a regular file
link_functional_file.pdf is not a regular directory
 link_functional_file.pdf is fully functional symlink to a file
link_functional_file.pdf is not a functional link to a directory
link_functional_file.pdf is not a broken symlink
existing_directory exists somehow
existing_directory is not a regular file
 existing_directory is a regular directory
existing_directory is not a functional link to a file
existing_directory is not a functional link to a directory
existing_directory is not a broken symlink
link_non_existing_directory exists somehow
link_non_existing_directory is not a regular file
link_non_existing_directory is not a regular directory
link_non_existing_directory is not a functional link to a file
link_non_existing_directory is not a functional link to a directory
 link_non_existing_directory is a broken symlink
link_existing_directory exists somehow
link_existing_directory is not a regular file
link_existing_directory is not a regular directory
link_existing_directory is not a functional link to a file
 link_existing_directory is fully functional symlink to a directory
link_existing_directory is not a broken symlink

You can choose the best test for your code.

-1

This didn't work for me:

    if [ "$(stat -c %h -- "$file")" -gt 1 ]; then
        echo "File has more than one name."
    fi

It didn’t work for me, keep it working):

#!/bin/bash
if [ "$1" = "" ]
    then
        echo "Please enter command argument."
        exit;
fi
links=$(stat -c %h "$1" 2>/dev/null)
inode=$(stat -c %i "$1" 2>/dev/null)
if [ $links -gt 1 ]
    then
        echo "File has hard links, select directory to search...";
            PS3='Please enter your choice: '
            select opt in / /home Quit
do
    case $opt in
        "/")
           find / -inum $inode 2>/dev/null
           break
           ;;
        "/home")
           find /home -inum $inode 2>/dev/null
           break
           ;;
        "Quit")
           echo "Quit"
           break
           ;;
        *) echo "invalid option $REPLY"
           ;;
    esac

done

    else
        echo "File has no hard links"
fi
4
  • welcome to U&L, can you post on this SO forum in english please ?
    – Archemar
    Feb 1, 2021 at 12:27
  • Certainly can! But I do not see any need for this, since I myself use the translator built into the browser and do not experience any difficulties in understanding! Moreover, as you noticed, the code is written in English. And to translate 2 lines from Russian, if necessary, I think it will not bother anyone. Or do you have a preconceived attitude towards Russian-speaking people? Feb 1, 2021 at 18:31
  • No, we simply have a rule that all content posted on this site needs to be in English. The vast majority of users here are not native English speakers, so everyone needs to translate from their language to English. So please make sure to only post in English in the future.
    – terdon
    Mar 17, 2021 at 11:27
  • 1
    I tried to translate but it seems like you are just giving solutions that don't work. Please edit the translation so it makes sense. Of course, your script doesn't actually work since you have a syntax error (if foo; then bar; fi not if foo; then do bar; done fi).
    – terdon
    Mar 17, 2021 at 11:30

You must log in to answer this question.

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