Executable shimming (like symlinks but better)

Posted by : on

Category : powershell   scripts   useless   shim


The other day, I needed to run an executable I just downloaded but it was not in my PATH. So I quickly changed my PATH variable and made a mistake doing so, hence I needed to recreate my PATH variable and this was a pain! From there, I decided to create a module to make executables shims for programs that are not in the PATH. All the shims will be in the same directory and that directory added to my PATH.

You ask: What is a shim ? Shimming is like symlinking, but it works much better. It’s a form of redirection, where you create a “shim” that redirects input to the actual binary process and shares the output. It can also work to simply call the actual binary when it shims GUI applications.

We like to call this “batch redirection that works”.

This also allows applications and tools to be on the “PATH” without cluttering up the PATH environment variable.

So when you create a shim, you create an executable that starts the target program


Benefits

These are the benefits of creating a shim:

  • Provides an exe file that calls a target executable.
  • Runs the target executable where it is, which means all dependencies and other things used are all in the original location
  • When items require elevated privileges, shims will raise UAC prompts.
  • The exe can be called from powershell, bash, cmd.exe, or other shells just like you would call the target.
  • Blocks and waits for command line apps to finish running, exits immediately when running a GUI app.
  • Uses the icon of the target if the target exists on creation.
  • Works better than symlinks. Symlinks on Windows fall down at file dependencies. So if your file depends on other files and DLLs, all of those need to also be linked.
  • Does not require special privileges like creating symlinks (symbolic links) do. So you can create shims without administrative rights.

Usage

Our Shim module is simple and contains the following functions:

  • Initialize-ShimModule
  • New-Shim
  • Remove-Shim
  • Repair-AllShims

How does it work?

Our module uses a tool from Chocolatey called ShimGen that inspects an executable and creates a small binary, known as a “shim”, that simply calls the executable. Out module calls this program to create the shim and then it places that shim in the “$($env:ShimsPath)”. It creates the shim by generating it at runtime based on the actual binary’s information.


Initialize-ShimModule

This function Initializes all the variables and directories required to create and store shims. Basically, it will check for the location of the shimgen.exe program, and create a directory where the shims will be located. Also, it will add this directory to the users PATH

	function Initialize-ShimModuleWithDefault{
	    [CmdletBinding(SupportsShouldProcess)]
	    param()

	    $ShimPath = "C:\Programs\Shims\"
	    New-Item -Path $ShimPath -ItemType Directory -Force -ErrorAction Ignore | Out-null
	    Write-Host -ForegroundColor DarkGreen "Initialize-ShimModule -Path `"$ShimPath`" -ShimGenPath `"C:\ProgramData\chocolatey\tools\shimgen.exe`""
	    Initialize-ShimModule -Path "$ShimPath" -ShimGenPath "C:\ProgramData\chocolatey\tools\shimgen.exe"
	}


	function Initialize-ShimModule{
	<#
	    .Synopsis
	       Setup the shim system. Needs to be run only once
	    .Description
	       Setup the shim system by creating the registry keys, add a PATH entry

	    .Parameter Path
	       Path where we store all the shims 

	    .Example
	       Initialize-ShimModule -Path 'c:\Programs\Shims'
	#>

	    [CmdletBinding(SupportsShouldProcess)]
	    param(
	        [ValidateScript({
	            if(-Not ($_ | Test-Path) ){
	                throw "ValidateScript Path => File or folder does not exist"
	            }
	            if(-Not ($_ | Test-Path -PathType Container) ){
	                throw "ValidateScript Path => The Path argument must be a Directory. Files paths are not allowed."
	            }
	            return $true 
	        })]
	        [Parameter(Mandatory=$true,Position=0)]
	        [String]$Path,
	        [ValidateScript({
	            if(-Not ($_ | Test-Path) ){
	                throw "ValidateScript ShimGenPath => File or folder does not exist"
	            }
	            if(-Not ($_ | Test-Path -PathType Leaf) ){
	                throw "ValidateScript ShimGenPath => The Path argument must be a executable."
	            }
	            return $true 
	        })]        
	        [Parameter(Mandatory=$false,Position=1)]
	        [String]$ShimGenPath,
	        [Parameter(Mandatory=$false)]
	        [switch]$AddToPath
	    )

	    # throw errors on undefined variables
	    Set-StrictMode -Version 1

	    # stop immediately on error
	    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

	    if ($Path -notmatch '\\$'){
	        $Path += '\'
	    }
	    try {
	        $null=New-Item (Get-ShimModuleRegistryPath) -Force
	        $null=New-RegistryValue (Get-ShimModuleRegistryPath) "shims_location" $Path "string"      
	        $null=New-RegistryValue (Get-ShimModuleRegistryPath) "shimgen_exe_path" 'temp' "string"
	        $null=New-RegistryValue (Get-ShimModuleRegistryPath) "initialized" 1 "DWORD"
	        $ShimGenPath = Get-ShimGenExePath 

	        $null=New-RegistryValue (Get-ShimModuleRegistryPath) "shimgen_exe_path" "$ShimGenPath" "string"

	        if($AddToPath){
	          Write-Output "Setup: add to system path"
	          $Env:Path += ";$ShimLocation"
	        }
	        Write-Host -ForegroundColor DarkGreen "[OK] " -NoNewline
	        Write-Host "ShimGen Path set to $ShimGenPath"  
	        Write-Host -ForegroundColor DarkGreen "[OK] " -NoNewline
	        Write-Host "Shims location set to $Path"  

	        [Environment]::SetEnvironmentVariable("ShimsPath","$ShimLocation",[EnvironmentVariableTarget]::User)
	    }
	    catch{
	        Show-ExceptionDetails($_) -ShowStack
	    }

	}

New-Shim

Add a Shim entry in the Registry, create the shim to the target

    function New-Shim{
        <#
          .Synopsis
             Add a Shim entry in the Registry, create the shim to the target
          .Description
             Add a Shim entry in the Registry, create the shim to the target Takes the
             name of the target by default, optionaly you can specify a shim name.

          .Parameter Target
             Target Executable
          .Parameter Name
             The Name of the shim

          .Example
             Add-Shim "c:\Program Files\Visual Studio\Tools\makehn.exe"
             Add-Shim "c:\sysinternals\pslist.exe" -Name "listprocesses.exe"
        #>
        [CmdletBinding(SupportsShouldProcess=$true)]
        param (
         [parameter(Mandatory=$true)]
         [ValidateNotNullOrEmpty()]$Target,
         [parameter(Mandatory=$false)]
         [ValidateNotNullOrEmpty()]$Name,
         [parameter(Mandatory=$false)]
         [switch]$Force     
        )

        $Init = Get-IsShimInitialized
        if ( $Init -eq $False ) { throw 'not initialized'; return $false ;}
        $ShimGenExe=Get-ShimGenExePath
        $RegBasePath = (Get-ShimModuleRegistryPath)
        Write-Verbose "ShimGenExePath $ShimGenExe"
        if(-not(Test-Path $ShimGenExe)){
            Write-Error 'could not find shimgen.exe'
                return
        }
        $ShimLocation=Get-ShimLocation
        Write-Verbose "ShimLocation $ShimLocation"
        if(-not(Test-Path $ShimLocation)){
            throw 'could not find Shim location'

        }

        $Target = (Resolve-Path -Path $Target).Path
        Write-Verbose "Target $Target"
        if(-not(Test-Path $Target)){
            throw 'No such target' 
        }

        if ($ShimLocation -notmatch '\\$'){
            $ShimLocation += '\'
        }

        $Sfix = '.exe'
        if($Name -eq $null -Or $Name -eq ""){
            $Sfix=(Get-Item $Target).Extension
            $Name=(Get-Item $Target).BaseName + $Sfix
        }
        try {

             $ShimFullPath = $ShimLocation + $Name
            if($Force){        
                $removed = Remove-Item -Path $ShimFullPath -Force -ErrorAction Ignore
            }
            Write-Verbose "Add-Shim: name is $ShimFullPath"
           


            Write-Log "Creating new shim"

            $exists1=Test-RegistryValue "$RegBasePath\$Name" 'target'
            $exists2=Test-RegistryValue "$RegBasePath\$Name" 'shim'
            if($exists1 -or $exists2){
                Write-Log  "shim already exists, delete before adding. Use -Force or See Remove-Shim"
                throw 'shim already exists, delete before adding. See "Remove-Shim"'
                return
            }
            Write-Verbose "New-Shim: $ShimFullPath"

            $Res = Test-Path $ShimFullPath
            if($Res -eq $true){
                 Write-Log  "ALREADY EXISTS : $ShimFullPath"
                 throw  "ALREADY EXISTS : $ShimFullPath"
                 return $null
            }
            Write-Log "$ShimFullPath ==> $Target"

            $Res = Invoke-ShimGenProgram $ShimFullPath $Target
            if($Res -eq $False){
                 Write-Log  "FAILURE : Invoke-ShimGenProgram $ShimFullPath $Target"
                 throw "FAILURE : Invoke-ShimGenProgram $ShimFullPath $Target"
                 return $null
            }
            $Res = Test-Path $ShimFullPath
            if($Res -eq $False){
                 Write-Log  "NOT FOUND : $ShimFullPath"
                 throw  "NOT FOUND : $ShimFullPath"
                 return $null
            }
            [pscustomobject]$Obj = @{
                'target' = $Target 
                'shim'   = $ShimFullPath
            }


            if($Res -eq $True){
              $null=New-RegistryValue "$RegBasePath\$Name" 'target' $Target "string"
              $null=New-RegistryValue "$RegBasePath\$Name" 'shim'   $ShimFullPath "string"
              Write-Log "Successfully created shim"
              Write-Log "type '$Name' to run program."
              return $ShimFullPath
            }
        }
        catch{
            Show-ExceptionDetails($_) -ShowStack
        }
    }


Remove-Shim

Removes a shim by deleting the executable in the shim directory and removing the entry in the registry.

    function Remove-Shim{

        [CmdletBinding(SupportsShouldProcess=$true)]
        param (
         [parameter(Mandatory=$true)]
         [ValidateNotNullOrEmpty()]$Name
        )
        try{
            if ( -not (Get-IsShimInitialized) ) { throw 'not initialized'; return $false ;}

            $RegBasePath = (Get-ShimModuleRegistryPath)
            $DoneNoError = $True
            if($Name -ne ''){
                $ShimLocation=Get-ShimLocation
                Write-Verbose "ShimLocation $ShimLocation"
                if(-not(Test-Path $ShimLocation)){
                    throw 'could not find Shim location'

                }
                $ShimFullPath = $ShimLocation + $Name
                if ($ShimFullPath.get_Length() -gt 4)
                {
                    $lastchars=$ShimFullPath.Substring($ShimFullPath.get_Length()-4)
                    if($lastchars -notmatch ".exe")
                    {
                         $ShimFullPath += '.exe'
                    }
                }
                Write-Verbose "ShimFullPath $ShimFullPath"
                $RegBasePath = "$RegBasePath\$Name"
                Remove-Item -Path $RegBasePath -Force -recurse -ErrorAction Ignore | Out-null
       
                Remove-Item -Path $ShimFullPath -Force -ErrorAction Stop | Out-null
                  
            }
        }catch{
            $DoneNoError = $false
        }
        finally{
            if($DoneNoError ){
              Write-Host -ForegroundColor DarkGreen "[DONE] " -NoNewline
              Write-Host " Remove-Shim completed" -ForegroundColor DarkGray      
            }else{
                Write-Host -ForegroundColor DarkRed "[ ERROR ] " -NoNewline
                Write-Host " no such shim " -ForegroundColor DarkYellow 
            }
      }

      return $DoneNoError
    }

Repair-AllShims

Will rebuild all shims from the registry entries. This is useful in the case where you deleted the shims by mistake.

  function Repair-AllShims{
  <#
      .Synopsis
         Update all the shims on disk using the entries backed up in the registry
      .Description
         Update all the shims on disk using the entries backed up in the registry
  #>
      [CmdletBinding(SupportsShouldProcess=$true)]
      param ()
      $Init = Get-IsShimInitialized
      #if ( $Init -eq $False ) { throw 'not initialized'; return $false ;}
      $Script:StepNumber = 0
      $Script:TotalSteps = 1
      $Script:ProgressMessage = "REPAIRING ALL SHIMS..."
      $Script:ProgressTitle = "REPAIRING ALL SHIMS..."
      Invoke-AutoUpdateProgress_Shim  
      
      # throw errors on undefined variables
      Set-StrictMode -Version 1

      # stop immediately on error
      $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

      try {
          $ShimGenExe=Get-ShimGenExePath
          if(-not(Test-Path $ShimGenExe)){
            Write-Error "could not find shimgen.exe $ShimGenExe"
              return
          }
          $RegBasePath = (Get-ShimModuleRegistryPath)
       
          
          $AllShimEntries=(Get-Item "$RegBasePath\*").PSChildName
          $count=$AllShimEntries.Count
          Write-Verbose "Repair-AllShims: get entries in $RegBasePath* : $count"
          $Script:StepNumber = 0
          $Script:TotalSteps = $count
          $TargetPath = 
          
          foreach($Shim in $AllShimEntries){
              Invoke-AutoUpdateProgress_Shim
              $Script:ProgressMessage = "Reset shim $Shim ($Script:StepNumber / $Script:TotalSteps)"
            $targetexists=Test-RegistryValue "$RegBasePath\$Shim" 'target'
            $shimexists=Test-RegistryValue "$RegBasePath\$Shim" 'shim'
            if($targetexists -and $shimexists){
                
              $Target=(Get-ItemProperty "$RegBasePath\$Shim").Target
              $Shim=(Get-ItemProperty "$RegBasePath\$Shim").Shim

              Remove-Item -Path $Shim -Force -ErrorAction Ignore | Out-null
              New-Item -Path $Shim -ItemType File -Force -ErrorAction Ignore | Out-null
              $Fullname = (Get-Item  -Path $Shim).Fullname
              Remove-Item -Path $Shim -Force -ErrorAction Ignore | Out-null
              Invoke-ShimGenProgram -Name $Fullname -Target $Target | Out-null
              Sleep 1
              
            }
            
          }
          
      

      }catch{
        Show-ExceptionDetails($_) -ShowStack
      }
      finally{
        Write-Host -ForegroundColor DarkGreen "[DONE] " -NoNewline
        Write-Host " Repair-AllShims completed" -ForegroundColor DarkGray
        $ShimLocation = Get-ShimLocation
        $Files = (gci -Path $ShimLocation -File -Filter '*.exe').Fullname
        foreach($f in $Files){
          Write-Host -ForegroundColor DarkRed "[Shim] " -NoNewline
          Write-Host "$f" -ForegroundColor DarkYellow
        }
    }
  }

Install the contextual menu actions

There’s a script that you can use to create contextual menu actions to Add a new shim and remove others. This is optional and meant as a shortcut only.

    .\Install-Menu.ps1

Contextual Menu


Get the code

PowerShell.Module.Shim on GitHub

Important Note Do You have Issues accessing the core repository? Don’t be shy and send me an EMAIL at guillaumeplante.qc@gmail.com and I will fix access for you


About Guillaume Plante
Guillaume Plante

A developper with a passion for technology, music, astronomy and art. Coding range: hardware/drivers, security, ai,. c/c++, powershell

Email : guillaumeplante.qc@gmail.com

Website : https://arsscriptum.ddns.net

Useful Links