47

I am having a PowerShell script which is walking a directory tree, and sometimes I have auxiliary files hardlinked there which should not be processed. Is there an easy way of finding out whether a file (that is, System.IO.FileInfo) is a hard link or not?

If not, would it be easier with symbolic links (symlinks)?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Joey
  • 344,408
  • 85
  • 689
  • 683
  • 1
    When dealing with symlinks, hardlinks, junctions etc. on Windows it's essential to have **Link Shell Extension** installed https://schinagl.priv.at/nt/hardlinkshellext/hardlinkshellext.html – kamikater Nov 02 '19 at 13:25
  • 2
    @kamikater: Is that in any way relevant to this question? Does that shell extension make it easier for a script to determine this? – Joey Nov 02 '19 at 21:58
  • 1
    LSE just makes it easier for the user to see and understand symlinks and the like because of Windows Explorer's lack thereof. My suggestion is more of a general meaning that you want to see what you are dealing with and debugging hence just a comment. – kamikater Nov 03 '19 at 20:32

9 Answers9

49

Try this:

function Test-ReparsePoint([string]$path) {
  $file = Get-Item $path -Force -ea SilentlyContinue
  return [bool]($file.Attributes -band [IO.FileAttributes]::ReparsePoint)
}

It is a pretty minimal implementation, but it should do the trick. Note that this doesn't distinguish between a hard link and a symbolic link. Underneath, they both just take advantage of NTFS reparse points, IIRC.

mklement0
  • 382,024
  • 64
  • 607
  • 775
Keith Hill
  • 2,329
  • 1
  • 19
  • 7
  • 1
    Hard links are simply additional file entries in the MFT and as such appear as normal files, unless somebody looks at the number of links to that file. But I didn't try out a symlink so far. Indeed it has the ReparsePoint attribute set. Thanks. (Even though symlinks are more cumbersome to handle, since I don't have permissions to create them by default :/) – Joey May 03 '09 at 21:46
  • 3
    I think it's not true that hardlinks and symlinks use the same mechanism. As Johannes pointed out, hardlinks are just another entry in the MFT. A symlink is a Reparse point. They're different. http://stackoverflow.com/questions/817794/find-out-whether-a-file-is-a-symlink-in-powershell/2255548#2255548 – Cheeso Feb 12 '10 at 22:23
  • 1
    Is it possibile also check if the symbolic link is still valid? (Or in other words, if the destination directory has not been deleted) – Deviling Master Jan 11 '17 at 06:44
  • @DevilingMaster nope, not generally possible. Symlinks are allowed to point to remote file shares, for example. If the remote server is unavailable should the logic count that as "deleted" then? For junction points the logic is somewhat more simple, because no remotes are involved, but just like volume mount points they could merely point to a volume that currently isn't attached to the system. So for specific cases this may be doable, but generally not. – 0xC0000022L Feb 08 '23 at 22:11
45

If you have Powershell 5+ the following one-liner recursively lists all file hardlinks, directory junctions and symbolic links and their targets starting from d:\Temp\:

dir 'd:\Temp' -recurse -force | ?{$_.LinkType} | select FullName,LinkType,Target

Output:

FullName                                LinkType     Target
--------                                --------     ------
D:\Temp\MyJunctionDir                   Junction     {D:\exp\junction_target_dir}
D:\Temp\MySymLinkDir                    SymbolicLink {D:\exp\symlink_target_dir}
D:\Temp\MyHardLinkFile.txt              HardLink     {D:\temp\MyHardLinkFile2.txt, D:\exp\hlink_target.xml}
D:\Temp\MyHardLinkFile2.txt             HardLink     {D:\temp\MyHardLinkFile.txt, D:\exp\hlink_target.xml}
D:\Temp\MySymLinkFile.txt               SymbolicLink {D:\exp\symlink_target.xml}
D:\Temp\MySymLinkDir\MySymLinkFile2.txt SymbolicLink {D:\temp\normal file.txt}

If you care about multiple targets for hardlinks use this variation which lists targets tab-separated:

dir 'd:\Temp' -recurse -force | ?{$_.LinkType} | select FullName,LinkType,@{ Name = "Targets"; Expression={$_.Target -join "`t"} }

You may need administrator privileges to run this script on say C:\.

Anton Krouglov
  • 3,077
  • 2
  • 29
  • 50
  • 3
    `LinkType` does not appear to be reliable for reparse points. For example, on my computer running W10 with PS 5.1, `LinkType` is null for both "C:\ProgramData\Desktop" and "C:\Users\All Users", whereas `dir /aL` (Command Prompt, not PowerShell) indicates the first is a junction point and the second a symbolic link. – NewSites Oct 18 '19 at 16:07
  • Powershell is not a precise tool. It is just a best effort. You can get even more funnier result if combine say symlink and hardlink ask powershell to determine what is what. Also working with relative symlinks using PS is a pain. – Anton Krouglov Oct 18 '19 at 17:04
  • 1
    That's some troubling information. I've read a lot about PowerShell being the great replacement for Command Prompt, that it's Command Prompt on steroids. I've never seen anything about it not being precise. Can you give a reference on what you mean? I'm working on finding all the NTFS links on my computer. For reparse points, I think I've got it using Command Prompt `dir /aL`. For hard links, I'm planning to test PowerShell scripts using `LinkType` and `FSutil` and see if they give me the same result. But are you saying I may not be able to trust either result? – NewSites Oct 18 '19 at 17:14
  • 2
    As of this writing, the Target array is always empty on Windows Powershell Core 6.2.3. Maybe PS 7 will restore it. – No Refunds No Returns Jan 12 '20 at 14:18
  • 1
    I think this is the wrong approach in general, but for symlinks it works because the GUID used in the reparse point is "well known" (yielding a readable "link type"). Check for the attributes to see if it's a reparse point. Only *then* check for the type of reparse points (symlink, junction) which it is. Hardlink is irrelevant to the question, given the question is about symlinks (which are reparse points). – 0xC0000022L Feb 08 '23 at 22:06
21

Utilize Where-Object to search for the ReparsePoint file attribute.

Get-ChildItem | Where-Object { $_.Attributes -match "ReparsePoint" }
k3yz101
  • 509
  • 4
  • 6
14

For those that want to check if a resource is a hardlink or symlink:

(Get-Item ".\some_resource").LinkType -eq "HardLink"

(Get-Item ".\some_resource").LinkType -eq "SymbolicLink"
jzsf-sz
  • 313
  • 3
  • 5
2

My results on Vista, using Keith Hill's powershell script to test symlinks and hardlinks:

c:\markus\other>mklink symlink.doc \temp\2006rsltns.doc
symbolic link created for symlink.doc <<===>> \temp\2006rsltns.doc

c:\markus\other>fsutil hardlink create HARDLINK.doc  \temp\2006rsltns.doc
Hardlink created for c:\markus\other\HARDLINK.doc <<===>> c:\temp\2006rsltns.doc

c:\markus\other>dir
 Volume in drive C has no label.
 Volume Serial Number is C8BC-2EBD

 Directory of c:\markus\other

02/12/2010  05:21 PM    <DIR>          .
02/12/2010  05:21 PM    <DIR>          ..
01/10/2006  06:12 PM            25,088 HARDLINK.doc
02/12/2010  05:21 PM    <SYMLINK>      symlink.doc [\temp\2006rsltns.doc]
               2 File(s)         25,088 bytes
               2 Dir(s)   6,805,803,008 bytes free

c:\markus\other>powershell \script\IsSymLink.ps1 HARDLINK.doc
False

c:\\markus\other>powershell \script\IsSymLink.ps1 symlink.doc
True

It shows that symlinks are reparse points, and have the ReparsePoint FileAttribute bit set, while hardlinks do not.

Cheeso
  • 189,189
  • 101
  • 473
  • 713
2

here is a one-liner that checks one file $FilePath and returns if it is a symlink or not, works for files and directories

if((Get-ItemProperty $FilePath).LinkType){"symboliclink"}else{"normal path"}
Joey
  • 344,408
  • 85
  • 689
  • 683
Luan Vitor
  • 83
  • 6
  • 1
    For a one-liner that's (presumably) supposed to be typed out at the command-line, you can simplify it by omitting the sub-expression and the `return`s (heck, that's even warranted for a prettily-written function). – Joey Jan 31 '20 at 22:39
2

Just want to add my own two cents, this is a oneliner function which works perfectly fine for me:

Function Test-Symlink($Path){
    ((Get-Item $Path).Attributes.ToString() -match "ReparsePoint")
}
Kevin Holtkamp
  • 479
  • 4
  • 17
1

The following PowerShell script will list all the files in a directory or directories with the -recurse switch. It will list the name of the file, whether it is a regular file or a hardlinked file, and the size, separated by colons.

It must be run from the PowerShell command line. It doesn't matter which directory you run it from as that is set in the script.

It uses the fslink utility shipped with Windows and runs that against each file using the hardlink and list switches and counts the lines of output. If two or greater it is a hardlinked file.

You can of course change the directory the search starts from by changing the c:\windows\system in the command. Also, the script simply writes the results to a file, c:\hardlinks.txt. You can change the name or simply delete everything from the > character on and it will output to the screen.

Get-ChildItem -path C:\Windows\system -file -recurse -force | 
    foreach-object {
        if ((fsutil hardlink list $_.fullname).count -ge 2) {
            $_.PSChildname + ":Hardlinked:" + $_.Length
        } else {
            $_.PSChildname + ":RegularFile:" + $_.Length
        }
    } > c:\hardlinks.txt
Nathan Tuggy
  • 2,237
  • 27
  • 30
  • 38
b_ball
  • 19
  • 2
0

I ran into an issue with .Target being null (on OneDrive files) and I wanted to differentiate between SYMLINKD and SYMLINK, so I came up with this variation (doesn't handle HardLink). My goal was to capture what was there so I could recreate them elsewhere. Note that ? = Where-Object and % = Foreach-Object.

### ReparsePoint + Directory + Junction = mklink /j 
### ReparsePoint + Directory + SymbolicLink = mklink /d 
### ReparsePoint + SymbolicLink = mklink 

"cd $( $pwd.Path )"; Get-ChildItem | ? { $_.Attributes -match 'ReparsePoint' -and $_.Target -ne $null } | % {
    $linktype = $_.LinkType
    $target = Resolve-Path -Path $_.Target
    if ($_.Attributes -match 'Directory') {
        if ($linktype -eq "Junction") {
            "mklink /j `"$($_.Name)`" `"$target`""
        } else {
            "mklink /d `"$($_.Name)`" `"$target`""
        }
    } else {
        "mklink `"$($_.Name)`" `"$target`""
    }
}
DarkStar
  • 555
  • 4
  • 5