home

An MKLINK PowerShell Module

22 Jan 2013

MKLINK is a very useful utility on Windows. Unfortunately there aren't any native PowerShell functions that replace it so we still have to shell out to the command prompt.

This can be made less painful with a helper function that will pipe all arguments to the MKLINK command.

function mklink { cmd /c mklink $args }

Personally though, I prefer to use native PowerShell functions whenever possible so I put together a module that provides wrappers for the key functionality of MKLINK.

Usage looks like this:

New-Symlink <link_path> <target_path> <force?>
New-Hardlink <link_path> <target_path> <force?>
New-Junction <link_path> <target_path> <force?>
mklink <args> # raw pipe to mklink

Import this module in your profile and you'll have it whenever you need it.

# Microsoft.PowerShell_profile.ps1

Import-Module mklink.psm1
# mklink.psm1
# Exports: New-Symlink, New-Hardlink, New-Junction, mklink

function Force-Resolve-Path {
    <#
    .SYNOPSIS
        Calls Resolve-Path but works for files that don't exist.
    .REMARKS
        From http://devhawk.net/2010/01/21/fixing-powershells-busted-resolve-path-cmdlet/
    #>
    param (
        [string] $FileName
    )
    
    $FileName = Resolve-Path $FileName -ErrorAction SilentlyContinue `
                                       -ErrorVariable _frperror
    if (-not($FileName)) {
        $FileName = $_frperror[0].TargetObject
    }
    
    return $FileName
}

function New-Symlink {
    <#
    .SYNOPSIS
        Creates a symbolic link.
    #>
    param (
        [Parameter(Position=0, Mandatory=$true)]
        [string] $Link,
        [Parameter(Position=1, Mandatory=$true)]
        [string] $Target,
        [Parameter(Position=2)]
        [switch] $Force
    )

    Invoke-MKLINK -Link $Link -Target $Target -Symlink -Force $Force
}

function New-Hardlink {
    <#
    .SYNOPSIS
        Creates a hard link.
    #>
    param (
        [Parameter(Position=0, Mandatory=$true)]
        [string] $Link,
        [Parameter(Position=1, Mandatory=$true)]
        [string] $Target,
        [Parameter(Position=2)]
        [switch] $Force
    )

    Invoke-MKLINK -Link $Link -Target $Target -HardLink -Force $Force
}

function New-Junction {
    <#
    .SYNOPSIS
        Creates a directory junction.
    #>
    param (
        [Parameter(Position=0, Mandatory=$true)]
        [string] $Link,
        [Parameter(Position=1, Mandatory=$true)]
        [string] $Target,
        [Parameter(Position=2)]
        [switch] $Force
    )

    Invoke-MKLINK -Link $Link -Target $Target -Junction -Force $Force
}

function Invoke-MKLINK {
    <#
    .SYNOPSIS
        Creates a symbolic link, hard link, or directory junction.
    #>
    [CmdletBinding(DefaultParameterSetName = "Symlink")]
    param (
        [Parameter(Position=0, Mandatory=$true)]
        [string] $Link,
        [Parameter(Position=1, Mandatory=$true)]
        [string] $Target,

        [Parameter(ParameterSetName = "Symlink")]
        [switch] $Symlink = $true,
        [Parameter(ParameterSetName = "HardLink")]
        [switch] $HardLink,
        [Parameter(ParameterSetName = "Junction")]
        [switch] $Junction,
        [Parameter()]
        [bool] $Force
    )
    
    # Resolve the paths incase a relative path was passed in.
    $Link = (Force-Resolve-Path $Link)
    $Target = (Force-Resolve-Path $Target)

    # Ensure target exists.
    if (-not(Test-Path $Target)) {
        throw "Target does not exist.`nTarget: $Target"
    }

    # Ensure link does not exist.
    if (Test-Path $Link) {
        if ($Force) {
            Remove-Item $Link -Recurse -Force
        }
        else {
            throw "A file or directory already exists at the link path.`nLink: $Link"
        }
    }

    $isDirectory = (Get-Item $Target).PSIsContainer
    $mklinkArg = ""

    if ($Symlink -and $isDirectory) {
        $mkLinkArg = "/D"
    }

    if ($Junction) {
        # Ensure we are linking a directory. (Junctions don't work for files.)
        if (-not($isDirectory)) {
            throw "The target is a file. Junctions cannot be created for files.`nTarget: $Target"
        }

        $mklinkArg = "/J"
    }

    if ($HardLink) {
        # Ensure we are linking a file. (Hard links don't work for directories.)
        if ($isDirectory) {
            throw "The target is a directory. Hard links cannot be created for directories.`nTarget: $Target"
        }

        $mkLinkArg = "/H"
    }

    # Capture the MKLINK output so we can return it properly.
    # Includes a redirect of STDERR to STDOUT so we can capture it as well.
    $output = cmd /c mklink $mkLinkArg `"$Link`" `"$Target`" 2>&1

    if ($lastExitCode -ne 0) {
        throw "MKLINK failed. Exit code: $lastExitCode`n$output"
    }
    else {
        Write-Output $output
    }
}

function mklink {
    <#
    .SYNOPSIS
        Helper function for calling mklink directly.
        All arguments are piped through to mklink.
        
        Hat tip to http://stackoverflow.com/questions/894430/powershell-hard-and-soft-links#comment9823010_5549583
    #>
    cmd /c mklink $args
}

Export-ModuleMember New-Symlink, New-Hardlink, New-Junction, mklink

I've heard it rumored that the PowerShell Community Extensions project has a lot of these functions as well, and more. It might be worth looking into if you want a more robust solution.

Want to learn PowerShell? Check out Windows PowerShell in Action by Bruce Payette. This is the book that got me started and it's one of the best tech books I've read.

blog comments powered by Disqus