Question

In PowerShell, I need resolve the target path of a junction (symlink).

for example, say I have a junction c:\someJunction whose target is c:\temp\target

I tried variations of $junc = Get-Item c:\someJunction, but was only able to get c:\someJunction

How do I find the target path of the junction, in this example c:\temp\target, of a given junction?

Was it helpful?

Solution 3

You can get the path by doing the following:

Get-ChildItem -Path C:\someJunction

Edit for finding the path and not the contents of the folder

Add-Type -MemberDefinition @"
private const int FILE_SHARE_READ = 1;
private const int FILE_SHARE_WRITE = 2;

private const int CREATION_DISPOSITION_OPEN_EXISTING = 3;
private const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;

[DllImport("kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", CharSet = CharSet.Unicode, SetLastError = true)]
 public static extern int GetFinalPathNameByHandle(IntPtr handle, [In, Out] StringBuilder path, int bufLen, int flags);

[DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode, SetLastError = true)]
 public static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, int dwShareMode,
 IntPtr SecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);

 public static string GetSymbolicLinkTarget(System.IO.DirectoryInfo symlink)
 {
     SafeFileHandle directoryHandle = CreateFile(symlink.FullName, 0, 2, System.IntPtr.Zero, CREATION_DISPOSITION_OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, System.IntPtr.Zero);
     if(directoryHandle.IsInvalid)
     throw new Win32Exception(Marshal.GetLastWin32Error());

     StringBuilder path = new StringBuilder(512);
     int size = GetFinalPathNameByHandle(directoryHandle.DangerousGetHandle(), path, path.Capacity, 0);
     if (size<0)
     throw new Win32Exception(Marshal.GetLastWin32Error());
     // The remarks section of GetFinalPathNameByHandle mentions the return being prefixed with "\\?\"
     // More information about "\\?\" here -> http://msdn.microsoft.com/en-us/library/aa365247(v=VS.85).aspx
     if (path[0] == '\\' && path[1] == '\\' && path[2] == '?' && path[3] == '\\')
     return path.ToString().Substring(4);
     else
     return path.ToString();
 }
"@ -Name Win32 -NameSpace System -UsingNamespace System.Text,Microsoft.Win32.SafeHandles,System.ComponentModel

$dir = Get-Item D:\1
[System.Win32]::GetSymbolicLinkTarget($dir)

OTHER TIPS

New-Item, Remove-Item, and Get-ChildItem have been enhanced to support creating and managing symbolic links. The -ItemType parameter for New-Item accepts a new value, SymbolicLink. Now you can create symbolic links in a single line by running the New-Item cmdlet.

What's New in Windows PowerShell v5

I've checked the symlink support on the my Windows 7 machine, it's works fine.

PS> New-Item -Type SymbolicLink -Target C:\ -Name TestSymlink


    Directory: C:\Users\skokhanovskiy\Desktop


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d----l       06.09.2016     18:27                TestSymlink

Get target of the symbolic link as easy as to create it.

> Get-Item .\TestSymlink | Select-Object -ExpandProperty Target
C:\

There are some really complicated answers to this question! Here's a super simple and self explanatory one:

(Get-Item C:\somejunction).Target

This does the trick with less work, and works even for junctions on a remote server:

fsutil reparsepoint query "M:\Junc"

If you want just the target name:

fsutil reparsepoint query "M:\Junc" | where-object { $_ -imatch 'Print Name:' } | foreach-object { $_ -replace 'Print Name\:\s*','' }

so

function Get_JunctionTarget($p_path)
{
    fsutil reparsepoint query $p_path | where-object { $_ -imatch 'Print Name:' } | foreach-object { $_ -replace 'Print Name\:\s*','' }
}

Also, the code below is a slight modification of the code that Josh provided above. It can be put in a file that is read multiple times, and it handles the leading \\?\ correctly in the case of a network drive:

function Global:Get_UNCPath($l_dir)
{
    if( ( ([System.Management.Automation.PSTypeName]'System.Win32').Type -eq $null)  -or ([system.win32].getmethod('GetSymbolicLinkTarget') -eq $null) )
    {
        Add-Type -MemberDefinition @"
private const int CREATION_DISPOSITION_OPEN_EXISTING = 3;
private const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;

[DllImport("kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", CharSet = CharSet.Unicode, SetLastError = true)]
 public static extern int GetFinalPathNameByHandle(IntPtr handle, [In, Out] StringBuilder path, int bufLen, int flags);

[DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode, SetLastError = true)]
 public static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, int dwShareMode,
 IntPtr SecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);

 public static string GetSymbolicLinkTarget(System.IO.DirectoryInfo symlink)
 {
     SafeFileHandle directoryHandle = CreateFile(symlink.FullName, 0, 2, System.IntPtr.Zero, CREATION_DISPOSITION_OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, System.IntPtr.Zero);
     if(directoryHandle.IsInvalid)
     {
         throw new Win32Exception(Marshal.GetLastWin32Error());
     }
     StringBuilder path = new StringBuilder(512);
     int size = GetFinalPathNameByHandle(directoryHandle.DangerousGetHandle(), path, path.Capacity, 0);
     if (size<0)
     {
         throw new Win32Exception(Marshal.GetLastWin32Error());
     }
     // The remarks section of GetFinalPathNameByHandle mentions the return being prefixed with "\\?\"
     // More information about "\\?\" here -> http://msdn.microsoft.com/en-us/library/aa365247(v=VS.85).aspx
     string sPath = path.ToString();
     if( sPath.Length>8 && sPath.Substring(0,8) == @"\\?\UNC\" )
     {
         return @"\" + sPath.Substring(7);
     }
     else if( sPath.Length>4 && sPath.Substring(0,4) == @"\\?\" )
     {
         return sPath.Substring(4);
     }
     else
     {
         return sPath;
     }
 }
"@ -Name Win32 -NameSpace System -UsingNamespace System.Text,Microsoft.Win32.SafeHandles,System.ComponentModel
    }
    [System.Win32]::GetSymbolicLinkTarget($l_dir)
}

and given the function Get_UNCPath above, we can improve the function Get_JunctionTarget as follows:

function Global:Get_JunctionTarget([string]$p_path)
{
    $l_target = fsutil reparsepoint query $p_path | where-object { $_ -imatch 'Print Name\:' } | foreach-object { $_ -replace 'Print Name\:\s*','' }
    if( $l_target -imatch "(^[A-Z])\:\\" )
    {
        $l_drive = $matches[1]
        $l_uncPath = Get_UncPath $p_path
        if( $l_uncPath -imatch "(^\\\\[^\\]*\\)" )
        {
            $l_machine = $matches[1]
            $l_target = $l_target -replace "^$l_drive\:","$l_machine$l_drive$"
        }
    }
    $l_target
}

We end up using this function

function Get-SymlinkTargetDirectory {           
    [cmdletbinding()]
    param(
        [string]$SymlinkDir
    )
    $basePath = Split-Path $SymlinkDir
    $folder = Split-Path -leaf $SymlinkDir
    $dir = cmd /c dir /a:l $basePath | Select-String $folder
    $dir = $dir -join ' '
    $regx = $folder + '\ *\[(.*?)\]'
    $Matches = $null
    $found = $dir -match $regx
    if ($found) {
        if ($Matches[1]) {
            Return $Matches[1]
        }
    }
    Return '' 
}

At least in PSv5 it's as easy as this to list all targets of some dirs links (or further down a single one) and get it as objects and nicely formatted (e.g. all the *~ dirs are actually junctions):

C:\Jaspersoft> ls | select name, target

Name                          Target
----                          ------
apache-websrv~                {C:\Program Files (x86)\Apache24\}
jasperreports-server-cp-6.3.0 {}
jasperreports-server-cp~      {C:\Jaspersoft\jasperreports-server-cp-6.3.0}
jr-srv-cp~                    {C:\Jaspersoft\jasperreports-server-cp~}

for one link:

C:\Jaspersoft> ls . apache-websrv~ | select name, target

Name           Target
----           ------
apache-websrv~ {C:\Program Files (x86)\Apache24\}

or (to just get the Target as a String value for the C:\Jaspersoft\apache-websrv~ junction):

> ls  C:\Jaspersoft  apache-websrv~  | %{$_.target}
C:\Program Files (x86)\Apache24\

The standard ls would look like this for the examples:

C:\Jaspersoft> ls

    Verzeichnis: C:\Jaspersoft


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d----l       01.04.2019     15:05                apache-websrv~
d-----       02.04.2019     10:30                jasperreports-server-cp-6.3.0
d----l       05.10.2018     15:19                jasperreports-server-cp~
d----l       12.02.2019     11:46                jr-srv-cp~

(Other answers contained this in a way as well but not easily visible/understandable)

It seems that with PS5 (as mentioned here or somewhere else?), system-defined junctions/reparse points have a bug whereas symlinks and user-defined junctions/reparse points do not. These solutions seem to work for symlinks but not junctions. The only thing that I could get to work to show the proper info was the fsutil program. The only issue is that it outputs data similar to Format-Hex as shown below.

The fsutil tool give me this output when used with the Windows Store app stored in $path

PS C:\> $path = "$($env:USERPROFILE)\AppData\Local\Microsoft\WindowsApps\Skype.exe"
PS C:\> fsutil reparsepoint query $path
Reparse Data Length: 0x150
Reparse Data:
0000:  03 00 00 00 4d 00 69 00  63 00 72 00 6f 00 73 00  ....M.i.c.r.o.s.
0010:  6f 00 66 00 74 00 2e 00  53 00 6b 00 79 00 70 00  o.f.t...S.k.y.p.
0020:  65 00 41 00 70 00 70 00  5f 00 6b 00 7a 00 66 00  e.A.p.p._.k.z.f.
0030:  38 00 71 00 78 00 66 00  33 00 38 00 7a 00 67 00  8.q.x.f.3.8.z.g.
0040:  35 00 63 00 00 00 4d 00  69 00 63 00 72 00 6f 00  5.c...M.i.c.r.o.
0050:  73 00 6f 00 66 00 74 00  2e 00 53 00 6b 00 79 00  s.o.f.t...S.k.y.
0060:  70 00 65 00 41 00 70 00  70 00 5f 00 6b 00 7a 00  p.e.A.p.p._.k.z.
0070:  66 00 38 00 71 00 78 00  66 00 33 00 38 00 7a 00  f.8.q.x.f.3.8.z.
0080:  67 00 35 00 63 00 21 00  41 00 70 00 70 00 00 00  g.5.c.!.A.p.p...
0090:  43 00 3a 00 5c 00 50 00  72 00 6f 00 67 00 72 00  C.:.\.P.r.o.g.r.
00a0:  61 00 6d 00 20 00 46 00  69 00 6c 00 65 00 73 00  a.m. .F.i.l.e.s.
00b0:  5c 00 57 00 69 00 6e 00  64 00 6f 00 77 00 73 00  \.W.i.n.d.o.w.s.
00c0:  41 00 70 00 70 00 73 00  5c 00 4d 00 69 00 63 00  A.p.p.s.\.M.i.c.
00d0:  72 00 6f 00 73 00 6f 00  66 00 74 00 2e 00 53 00  r.o.s.o.f.t...S.
00e0:  6b 00 79 00 70 00 65 00  41 00 70 00 70 00 5f 00  k.y.p.e.A.p.p._.
00f0:  31 00 35 00 2e 00 36 00  34 00 2e 00 38 00 30 00  1.5...6.4...8.0.
0100:  2e 00 30 00 5f 00 78 00  38 00 36 00 5f 00 5f 00  ..0._.x.8.6._._.
0110:  6b 00 7a 00 66 00 38 00  71 00 78 00 66 00 33 00  k.z.f.8.q.x.f.3.
0120:  38 00 7a 00 67 00 35 00  63 00 5c 00 53 00 6b 00  8.z.g.5.c.\.S.k.
0130:  79 00 70 00 65 00 5c 00  53 00 6b 00 79 00 70 00  y.p.e.\.S.k.y.p.
0140:  65 00 2e 00 65 00 78 00  65 00 00 00 30 00 00 00  e...e.x.e...0...

OR

PS C:\> $path2 = "$HOME/My Documents"
PS C:\> fsutil reparsepoint query $path2
Reparse Tag Value : 0xa0000003
Tag value: Microsoft
Tag value: Name Surrogate
Tag value: Mount Point
Substitue Name offset: 0
Substitue Name length: 56
Print Name offset:     58
Print Name Length:     48
Substitute Name:       \??\C:\Users\chefh\Documents
Print Name:            C:\Users\chefh\Documents

Reparse Data Length: 0x74      
Reparse Data:
0000:  00 00 38 00 3a 00 30 00  5c 00 3f 00 3f 00 5c 00  ..8.:.0.\.?.?.\.
0010:  43 00 3a 00 5c 00 55 00  73 00 65 00 72 00 73 00  C.:.\.U.s.e.r.s.
0020:  5c 00 63 00 68 00 65 00  66 00 68 00 5c 00 44 00  \.c.h.e.f.h.\.D.
0030:  6f 00 63 00 75 00 6d 00  65 00 6e 00 74 00 73 00  o.c.u.m.e.n.t.s.
0040:  00 00 43 00 3a 00 5c 00  55 00 73 00 65 00 72 00  ..C.:.\.U.s.e.r.
0050:  73 00 5c 00 63 00 68 00  65 00 66 00 68 00 5c 00  s.\.c.h.e.f.h.\.
0060:  44 00 6f 00 63 00 75 00  6d 00 65 00 6e 00 74 00  D.o.c.u.m.e.n.t.
0070:  73 00 00 00                                       s...

Here is the code that I came up with to parse that output. I know that it is quite messy code, but as I said I couldn't get the other solutions here to work for Windows Stores apps. This one does. Please test it in your uses and mention if there are issues.

***** Edited to allow files or directories. *****

$path = "$($env:USERPROFILE)\AppData\Local\Microsoft\WindowsApps\Skype.exe"
$path2 = "$HOME/My Documents"

function Get-ReparseTarget ($path)
{
    #   Grabs output of fsutil
    $a = fsutil reparsepoint query $path

    #   Regex to capture fsutil output
    $regex = '[0-9a-fA-F]+\:\s\s(?<chunk1>([0-9a-fA-F]{2}\s){1,8}\s)(?<chunk2>([0-9a-fA-F]{2}\s){1,8}\s){0,1}.+'

    #   Splits and trims the "chunks" then adds them together to create an array of hex character
    $c = $a.foreach({if ($_ -match $regex) {($Matches['chunk1'] -split ' ').trim() | `
        where {$_ -ne ""};($Matches['chunk2'] -split ' ').trim() |where {$_ -ne ""}}})

    #   Convert an Array of Hex(String) to Array of Bytes
    $f = [byte[]]($c | foreach{[Convert]::ToInt32($_,16)})

    #   Convert the Unicode to Ascii and convert '' (00 in hex) to spaces
    $g = [System.Text.Encoding]::Unicode.GetChars($f).foreach({if([int]$_ -eq 0) {' '} else {$_}})

    #   Combine Char[] to String then Split into the important bits and Select the Reparse Target Path 
    #   depending on whether the Path argument is a file or directory
    switch ((get-item -Force -Path $path))
    {
        {$_ -is [System.IO.FileInfo]} {$h = ($g[0..($g.Count -4)] -join "" -split " ", 4)[3]}
        {$_ -is [System.IO.DirectoryInfo]} {$h = ($g[0..($g.Count -2)] -join "" -split " ", 3)[2]}
        Default { Write-Error "Path must be either a file or directory"}
    }

    #   Return the path
    return $h
}

#  Quick Test
Get-ReparseTarget -path $path
Get-ReparseTarget -path $path2


# Input:
#   $path = "$($env:USERPROFILE)\AppData\Local\Microsoft\WindowsApps\Skype.exe"
# Run:
#   Get-ReparseTarget $path
# Returns:
#   C:\Program Files\WindowsApps\Microsoft.SkypeApp_15.64.80.0_x86__kzf8qxf38zg5c\Skype\Skype.exe

# Input:
#   $path2 = "$HOME/My Documents"
# Run:
#   Get-ReparseTarget $path2
# Returns:
#   C:\Users\chefh\Documents

Based on the answer of The Nerdy Chef I simplified the code a bit. Advantage: That way it works on junction too.

function Get-ReparseTarget {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory=$true)][string]$path
    )
    $fsutil = fsutil.exe reparsepoint query $path
    # gets the hex-stream out of fsutil output as array
    $hex = ($fsutil.Where({$_ -match "[0-9a-f]{4}: .*"}) | Select-String "[0-9a-f][0-9a-f] " -AllMatches).Matches.Value.Trim()
    # Convert to Bytestream
    $Bytestream = [byte[]]($hex | foreach{[Convert]::ToInt32($_,16)})
    # Unicode2Ascii + Trim the "Trailing Zero", which is added depending on the target type.
    $Unicode = ([System.Text.Encoding]::Unicode.GetChars($Bytestream) -join '').TrimEnd("`0")
    # We split by "Zero Character" and by "\??\", and keep the latest match, works for a file, a directory and a junction.
    $($Unicode -split "`0" -split "\\\?\?\\")[-1]
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top