shortcut.ps1
|
class Shortcut { # Full path of the shortcut's own .lnk file (WScript.Shell FullName). [string] $Location # Location of the file or folder the shortcut opens: its target. [string] $Target # Command-line arguments passed to the target. [string] $Arguments # Location the target runs in, shown as "Start in" in the shortcut properties. [string] $RunLocation # Free-text description, shown as the shortcut's comment / tooltip. [string] $Description # Icon of the shortcut, or $null when the shortcut carries no icon. [ShortcutIcon] $Icon # Keyboard shortcut in modifier order, e.g. "Alt+Ctrl+N"; empty when unset. [string] $Hotkey # Window state the target launches in, shown as "Run" in the shortcut properties. [ShortcutWindowStyle] $WindowStyle # Whether the target launches elevated, ticked as "Run as administrator" in the advanced properties. [bool] $Elevated <# .SYNOPSIS The readable fields of a shortcut. .DESCRIPTION Holds the readable fields of a shortcut: its own location, target location, arguments, working directory, description, icon, hotkey and window style. The icon is a ShortcutIcon record, or $null when the shortcut carries no icon. The write-only RelativePath field is left out, as it cannot be read back. .EXAMPLE $shortcut = [Shortcut] @{ Location = "C:\Users\me\Desktop\MyApp.lnk"; Target = "C:\Program Files\MyApp\MyApp.exe" } #> } class ShortcutIcon { # Path of the icon file, e.g. "C:\Program Files\MyApp\MyApp.exe". May itself contain a comma. [string] $Location # Zero-based index of the icon within the icon file. [int] $Index # The icon as the combined "file,index" source WScript.Shell stores. [string] ToString() { return "$($this.Location),$($this.Index)" } <# .SYNOPSIS The icon of a shortcut. .DESCRIPTION Holds a shortcut's icon as its two parts, the icon file location and the index within it. ToString() combines them back into the "file,index" source a shortcut stores. A shortcut carrying no icon has no ShortcutIcon at all: its Icon field is $null. .EXAMPLE $icon = [ShortcutIcon] @{ Location = "C:\Program Files\MyApp\MyApp.exe"; Index = 3 } #> } <# .SYNOPSIS The window state a shortcut launches its target in. .DESCRIPTION The states offered as "Run" in the shortcut properties. A shortcut stores no other state: any other value is normalized to Normal. #> enum ShortcutWindowStyle { Normal = 1 Maximized = 3 Minimized = 7 } function Get-Shortcut { <# .SYNOPSIS Returns the properties of a shortcut. .DESCRIPTION Returns an object carrying every readable field of the specified shortcut. The write-only RelativePath field is not part of the output, as it cannot be read back. .PARAMETER Location The location of the shortcut to read. If the shortcut does not exist, an error is reported. .OUTPUTS Shortcut record with a Location, Target, Arguments, RunLocation, Description, Icon, Hotkey, WindowStyle and Elevated property. Icon is a ShortcutIcon record with a Location and Index property, or $null when the shortcut carries no icon. WindowStyle is a ShortcutWindowStyle: Normal, Maximized or Minimized. .EXAMPLE Get-Shortcut -Location "C:\Users\me\Desktop\MyShortcut.lnk" #> [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $Location ) # CreateShortcut hands back a blank shortcut for a location holding none, so the file is checked first if (-not (Test-Path -LiteralPath $Location -PathType Leaf)) { Write-Error "Shortcut not found: '$Location'" ` -ErrorId "ShortcutNotFound" ` -Category ObjectNotFound ` -TargetObject $Location return } $obj = $wshShell.CreateShortcut($Location) return [Shortcut] @{ Location = $obj.FullName Target = $obj.TargetPath Arguments = $obj.Arguments RunLocation = $obj.WorkingDirectory Description = $obj.Description Icon = ConvertTo-ShortcutIcon -Value $obj.IconLocation Hotkey = $obj.Hotkey WindowStyle = $obj.WindowStyle Elevated = Test-ShortcutElevated -Location $Location } } function New-ShortcutIcon { <# .SYNOPSIS Creates a shortcut icon. .DESCRIPTION Creates the ShortcutIcon record that New-Shortcut and Set-Shortcut take as their -Icon. Give the icon file on its own with -Location, optionally picking the icon within it with -Index, or give the combined source with -Value. .PARAMETER Location The path of the icon file. The icon file may itself contain a comma. .PARAMETER Index The index of the icon within the icon file. Default: 0. .PARAMETER Value The combined icon source in the form "file,index", e.g. "C:\Program Files\MyApp\MyApp.exe,3". An icon file on its own is rejected here; pass it as -Location instead. .OUTPUTS ShortcutIcon record with a Location and Index property, combined back by ToString(). .EXAMPLE New-ShortcutIcon "C:\Program Files\MyApp\MyApp.exe,3" .EXAMPLE New-ShortcutIcon -Location "C:\Program Files\MyApp\MyApp.exe" .EXAMPLE New-ShortcutIcon -Location "C:\Program Files\MyApp\MyApp.exe" -Index 3 #> [CmdletBinding(DefaultParameterSetName = "Value")] [OutputType([ShortcutIcon])] param ( [Parameter(Mandatory, Position = 0, ParameterSetName = "Value")] [string] $Value, [Parameter(Mandatory, ParameterSetName = "Location")] [string] $Location, [Parameter(ParameterSetName = "Location")] [int] $Index = 0 ) if ($PSCmdlet.ParameterSetName -eq "Location") { if (-not $Location) { Write-Error "Icon file is required: -Location." ` -ErrorId "BlankShortcutIconLocation" ` -Category InvalidArgument ` -TargetObject $Location ` -ErrorAction Stop } return [ShortcutIcon] @{ Location = $Location Index = $Index } } if ($Value -notmatch ',\d+$') { Write-Error "Icon source must be an icon file and an index, as 'file,index'. Pass an icon file on its own as -Location. value: '$Value'" ` -ErrorId "InvalidShortcutIconValue" ` -Category InvalidArgument ` -TargetObject $Value ` -ErrorAction Stop } # the icon file may contain a comma, so the index is split off at the last one $icon = ConvertTo-ShortcutIcon -Value $Value if (-not $icon) { Write-Error "Icon source names no icon file. value: '$Value'" ` -ErrorId "BlankShortcutIconLocation" ` -Category InvalidArgument ` -TargetObject $Value ` -ErrorAction Stop } return $icon } function New-Shortcut { <# .SYNOPSIS Creates a shortcut. .DESCRIPTION Creates a shortcut at the specified location, pointing at the specified target. Only the shortcut location and its target are mandatory; every other field is optional and falls back to a default: the target's folder as run location, no icon, no hotkey, a normal window and an unelevated target. An existing shortcut is left untouched and a terminating error is reported, unless -Force is given. .PARAMETER Location The location of the shortcut to create, including its .lnk extension. .PARAMETER Target The location of the file or folder the shortcut opens. .PARAMETER Arguments The command-line arguments to pass to the target. .PARAMETER RunLocation The location the target runs in, shown as "Start in" in the shortcut properties. Default: the folder of the target. .PARAMETER Description The free-text description of the shortcut, shown as its comment / tooltip. .PARAMETER Icon The icon of the shortcut, as a ShortcutIcon record built by New-ShortcutIcon or read by Get-Shortcut. Default: no icon. .PARAMETER Hotkey The keyboard shortcut that opens the shortcut, e.g. "Ctrl+Alt+N". .PARAMETER WindowStyle The window state the target launches in: Normal, Maximized or Minimized. Default: Normal. .PARAMETER Elevated Launch the target elevated, ticked as "Run as administrator" in the advanced properties. Alias: Administrator. .PARAMETER CreateFolder Create the folder of the shortcut if it does not exist. Without -CreateFolder, a missing folder is reported as a terminating error and no shortcut is created. .PARAMETER Force Completely overwrite the shortcut if it already exists. Omitted optional fields reset to their documented defaults. Without -Force, a terminating error is reported when the shortcut exists. .OUTPUTS Shortcut record describing the created shortcut, as read by Get-Shortcut. Nothing under -WhatIf. .EXAMPLE New-Shortcut "C:\Users\me\Desktop\MyApp.lnk" "C:\Program Files\MyApp\MyApp.exe" .EXAMPLE New-Shortcut -Location "C:\Users\me\Desktop\MyApp.lnk" -Target "C:\Program Files\MyApp\MyApp.exe" ` -Arguments "--profile Default" -Description "My favourite app" ` -Icon (New-ShortcutIcon -Location "C:\Program Files\MyApp\MyApp.exe" -Index 3) ` -Hotkey "Ctrl+Alt+M" -WindowStyle Maximized -Elevated .EXAMPLE New-Shortcut "C:\Tools\Shortcuts\MyApp.lnk" "C:\Program Files\MyApp\MyApp.exe" -CreateFolder .NOTES Alias: Administrator for -Elevated. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([Shortcut])] param ( [Parameter(Mandatory, Position = 0)] [string] $Location, [Parameter(Mandatory, Position = 1)] [string] $Target, [string] $Arguments, [string] $RunLocation, [string] $Description, [ShortcutIcon] $Icon, [string] $Hotkey, [ShortcutWindowStyle] $WindowStyle = [ShortcutWindowStyle]::Normal, [Alias("Administrator")] [switch] $Elevated, [switch] $CreateFolder, [switch] $Force ) if (-not $Force -and (Test-Path -LiteralPath $Location)) { Write-Error "Shortcut already exists, use -Force to overwrite. location: '$Location'" ` -ErrorId "ShortcutAlreadyExists" ` -Category ResourceExists ` -TargetObject $Location ` -ErrorAction Stop } # the folder is checked before the mutation, so -WhatIf reports the error a real run would hit $shortcutFolder = Split-Path -Parent $Location $missesFolder = $shortcutFolder -and -not (Test-Path -LiteralPath $shortcutFolder) if ($missesFolder -and -not $CreateFolder) { Write-Error "Shortcut folder not found, use -CreateFolder to create it: '$shortcutFolder'" ` -ErrorId "ShortcutFolderNotFound" ` -Category ObjectNotFound ` -TargetObject $shortcutFolder ` -ErrorAction Stop } # every field is written, so an overwritten shortcut keeps nothing of its former self if ($PSCmdlet.ShouldProcess($Location, "Create shortcut")) { if ($missesFolder) { New-Item -ItemType Directory -Path $shortcutFolder -Force | Out-Null } $obj = $wshShell.CreateShortcut($Location) $obj.TargetPath = $Target $obj.Arguments = $Arguments $obj.WorkingDirectory = $PSBoundParameters.ContainsKey("RunLocation") ? $RunLocation : (Split-Path -Parent $Target) $obj.Description = $Description # a shortcut carrying no icon stores ',0'; IconLocation rejects an empty string or $null $obj.IconLocation = $Icon ? $Icon.ToString() : ",0" $obj.Hotkey = $Hotkey $obj.WindowStyle = [int] $WindowStyle $obj.Save() Set-ShortcutElevated -Location $Location -Elevated $Elevated.IsPresent return Get-Shortcut -Location $Location } } function Set-Shortcut { <# .SYNOPSIS Sets the fields of a shortcut. .DESCRIPTION Sets any combination of the fields of an existing shortcut. Fields not passed are left as they are. Passing $null or an empty string clears a field, e.g. -Arguments $null, -Description '' or -Icon $null. At least one field is required. If the shortcut does not exist, an error is reported. .PARAMETER Location The location of the shortcut to set the fields on. .PARAMETER Target The location of the file or folder the shortcut opens. .PARAMETER Arguments The command-line arguments to pass to the target. .PARAMETER RunLocation The location the target runs in, shown as "Start in" in the shortcut properties. .PARAMETER Description The free-text description of the shortcut, shown as its comment / tooltip. .PARAMETER Icon The icon of the shortcut, as a ShortcutIcon record built by New-ShortcutIcon or read by Get-Shortcut. Pass $null to clear the icon. .PARAMETER Hotkey The keyboard shortcut that opens the shortcut, e.g. "Ctrl+Alt+N". .PARAMETER WindowStyle The window state the target launches in: Normal, Maximized or Minimized. .PARAMETER Elevated Launch the target elevated, ticked as "Run as administrator" in the advanced properties. Pass -Elevated:$false to launch it unelevated. Left out, the flag is not touched. Alias: Administrator. .PARAMETER PassThru Return the shortcut after setting its fields. Without it, nothing is returned. .OUTPUTS Shortcut record describing the shortcut, as read by Get-Shortcut, when -PassThru is given. .EXAMPLE Set-Shortcut "C:\Users\me\Desktop\MyApp.lnk" -Target "C:\Program Files\MyApp\MyApp.exe" .EXAMPLE Set-Shortcut "C:\Users\me\Desktop\MyApp.lnk" -WindowStyle Maximized -Elevated .EXAMPLE Set-Shortcut "C:\Users\me\Desktop\MyApp.lnk" -Arguments $null -Icon $null .EXAMPLE Set-Shortcut "C:\Users\me\Desktop\MyApp.lnk" -Icon (New-ShortcutIcon "C:\Program Files\MyApp\MyApp.exe,3") .NOTES Alias: Administrator for -Elevated. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([Shortcut])] param ( [Parameter(Mandatory, Position = 0)] [string] $Location, [string] $Target, [string] $Arguments, [string] $RunLocation, [string] $Description, [ShortcutIcon] $Icon, [string] $Hotkey, [ShortcutWindowStyle] $WindowStyle, [Alias("Administrator")] [switch] $Elevated, [switch] $PassThru ) # the elevation flag is patched into the file bytes, so it is not one of the WScript.Shell fields $paramNames = "Target", "Arguments", "RunLocation", "Description", "Icon", "Hotkey", "WindowStyle" $setsField = [bool] ($paramNames | Where-Object { $PSBoundParameters.ContainsKey($_) }) if (-not $setsField -and -not $PSBoundParameters.ContainsKey("Elevated")) { Write-Error "At least one shortcut field is required: -$($paramNames -join ', -'), -Elevated." ` -ErrorId "MissingShortcutField" ` -Category InvalidArgument ` -TargetObject $Location ` -ErrorAction Stop } if (-not (Test-Path -LiteralPath $Location -PathType Leaf)) { Write-Error "Shortcut not found: '$Location'" ` -ErrorId "ShortcutNotFound" ` -Category ObjectNotFound ` -TargetObject $Location return } if ($PSCmdlet.ShouldProcess($Location, "Set shortcut fields")) { # setting the elevation flag alone touches no field WScript.Shell writes if ($setsField) { $obj = $wshShell.CreateShortcut($Location) if ($PSBoundParameters.ContainsKey("Target")) { $obj.TargetPath = $Target } if ($PSBoundParameters.ContainsKey("Arguments")) { $obj.Arguments = $Arguments } if ($PSBoundParameters.ContainsKey("RunLocation")) { $obj.WorkingDirectory = $RunLocation } if ($PSBoundParameters.ContainsKey("Description")) { $obj.Description = $Description } # a shortcut carrying no icon stores ',0'; IconLocation rejects an empty string or $null if ($PSBoundParameters.ContainsKey("Icon")) { $obj.IconLocation = $Icon ? $Icon.ToString() : ",0" } if ($PSBoundParameters.ContainsKey("Hotkey")) { $obj.Hotkey = $Hotkey } if ($PSBoundParameters.ContainsKey("WindowStyle")) { $obj.WindowStyle = [int] $WindowStyle } $obj.Save() } if ($PSBoundParameters.ContainsKey("Elevated")) { Set-ShortcutElevated -Location $Location -Elevated $Elevated.IsPresent } } if ($PassThru) { return Get-Shortcut -Location $Location } } function local:Set-ShortcutElevated { <# .SYNOPSIS Sets or clears the "Run as administrator" flag on a shortcut. .DESCRIPTION Writes the flag straight into the .lnk file: byte 21 (0x15), bit 6 (0x20). WScript.Shell does not expose the flag, so the file is written as bytes. A file already carrying the requested state is left untouched. .PARAMETER Location The location of the shortcut to write the flag to. .PARAMETER Elevated Whether the shortcut launches its target elevated. .EXAMPLE Set-ShortcutElevated -Location "C:\Users\me\Desktop\MyApp.lnk" -Elevated $true #> [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $Location, [Parameter(Mandatory)] [bool] $Elevated ) $bytes = [System.IO.File]::ReadAllBytes($Location) $flags = $Elevated ? ($bytes[0x15] -bor 0x20) : ($bytes[0x15] -band 0xDF) if ($flags -eq $bytes[0x15]) { return } $bytes[0x15] = $flags [System.IO.File]::WriteAllBytes($Location, $bytes) } function local:ConvertTo-ShortcutIcon { <# .SYNOPSIS Converts a shortcut icon source into a ShortcutIcon record. .DESCRIPTION Splits an icon source of the form "file,index" into its icon file location and icon index. The icon file path may itself contain a comma, so the index is split off at the last one. A shortcut carrying no icon reports ',0': a blank location means there is no icon to describe, and $null is returned. .PARAMETER Value The icon source to convert, as reported by WScript.Shell: "file,index". .OUTPUTS ShortcutIcon record, or $null when the icon source names no icon file. .EXAMPLE ConvertTo-ShortcutIcon -Value "C:\Program Files\MyApp\MyApp.exe,3" #> [CmdletBinding()] [OutputType([ShortcutIcon])] param ( [Parameter(Mandatory)] [AllowEmptyString()] [string] $Value ) $separatorPosition = $Value.LastIndexOf(',') $location = $separatorPosition -ge 0 ? $Value.Substring(0, $separatorPosition) : $Value $index = $separatorPosition -ge 0 ? $Value.Substring($separatorPosition + 1) : 0 if (-not $location) { return $null } return [ShortcutIcon] @{ Location = $location Index = $index } } function local:Test-ShortcutElevated { <# .SYNOPSIS Tests whether a shortcut launches its target elevated. .DESCRIPTION Reads the "Run as administrator" flag straight from the .lnk file: byte 21 (0x15), bit 6 (0x20). WScript.Shell does not expose the flag, so the file is read as bytes. .PARAMETER Location The location of the shortcut to read the flag from. .OUTPUTS Boolean indicating whether the shortcut launches its target elevated. .EXAMPLE Test-ShortcutElevated -Location "C:\Users\me\Desktop\MyApp.lnk" #> [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory)] [string] $Location ) $bytes = [System.IO.File]::ReadAllBytes($Location) return ($bytes[0x15] -band 0x20) -eq 0x20 } |