-
-
Save ThioJoe/16eac0ea7d586c4edba41b454b58b225 to your computer and use it in GitHub Desktop.
# NOTE - THIS SCRIPT IS NOW OBSOLETE - SEE MY OTHER REPO FOR A MUCH MORE COMPREHENSIVE TOOL: https://github.com/ThioJoe/Windows-Super-God-Mode | |
# Get All Shell Folder Shortcuts Script (Updated 8/10/2024) | |
# Original source: https://gist.github.com/ThioJoe/16eac0ea7d586c4edba41b454b58b225 | |
# This PowerShell script is designed to find and create shortcuts for all special shell folders in Windows. | |
# These folders can be identified through their unique Class Identifiers (CLSIDs) or by their names. | |
# The script also generates CSV files listing these folders and associated tasks/links. | |
# How to Use: | |
# 1. Open PowerShell and navigate to the path containing this script using the 'cd' command. | |
# 2. Run the following command to allow running scripts for the current session: | |
# Set-ExecutionPolicy -ExecutionPolicy unrestricted -Scope Process | |
# 3. Without closing the PowerShell window, run the script by typing the name of the script file starting with .\ for example: | |
# .\Get_All_Shell_Folder_Shortcuts.ps1 | |
# 4. Wait for it to finish, then check the "Shell Folder Shortcuts" folder for the generated shortcuts. | |
# ------------------------- OPTIONAL ARGUMENTS ------------------------- | |
# -SaveCSV | |
# Switch (Takes no values) | |
# Save info about all the shortcuts into CSV spreadsheet files for each type of shell folder | |
# | |
# -SaveXML | |
# Switch (Takes no values) | |
# Save the XML content from shell32.dll as a file containing info about the task links | |
# | |
# -Output | |
# String (Optional) | |
# Specify a custom output folder path (relative or absolute) to save the generated shortcuts. If not provided, a folder named "Shell Folder Shortcuts" will be created in the script's directory. | |
# | |
# -DeletePreviousOutputFolder | |
# Switch (Takes no values) | |
# Delete the previous output folder before running the script if one exists matching the one that would be created | |
# | |
# -Verbose | |
# Switch (Takes no values) | |
# Enable verbose output for more detailed information during script execution | |
# | |
# -DontGroupTasks | |
# Switch (Takes no values) | |
# Prevent grouping task shortcuts, meaning the application name won't be prepended to the task name in the shortcut file | |
# | |
# -SkipCLSID | |
# Switch (Takes no values) | |
# Skip creating shortcuts for shell folders based on CLSIDs | |
# | |
# -SkipNamedFolders | |
# Switch (Takes no values) | |
# Skip creating shortcuts for named special folders | |
# | |
# -SkipTaskLinks | |
# Switch (Takes no values) | |
# Skip creating shortcuts for task links (sub-pages within shell folders and control panel menus) | |
# | |
# -DLLPath | |
# String (Optional) | |
# Specify a custom DLL file path to load the shell32.dll content from. If not provided, the default shell32.dll will be used. | |
# NOTE: Because of how Windows works behind the scenes, DLLs reference resources in corresponding .mui and .mun files. | |
# The XML data (resource ID 21) that is required in this script is actually located in shell32.dll.mun, which is located at "C:\Windows\SystemResources\shell32.dll.mun" | |
# This means if you want to reference the data from a DLL that is NOT at C:\Windows\System32\shell32.dll, you should directly reference the .mun file instead. It will auto redirect if it's at that exact path, but not otherwise. | |
# > This is especially important if wanting to reference the data from a different computer, you need to be sure to copy the .mun file | |
# See: https://stackoverflow.com/questions/68389730/windows-dll-function-behaviour-is-different-if-dll-is-moved-to-different-locatio | |
# | |
# --------------------------------------------------------------------- | |
# | |
# EXAMPLE USAGE FROM COMMAND LINE: | |
# .\Get_All_Shell_Folder_Shortcuts.ps1 -SaveXML -SaveCSV | |
# | |
# --------------------------------------------------------------------- | |
[CmdletBinding()] | |
param( | |
[switch]$DontGroupTasks, | |
[switch]$SaveXML, | |
[switch]$SaveCSV, | |
[switch]$DeletePreviousOutputFolder, | |
[string]$DLLPath, | |
[string]$Output, | |
[switch]$SkipCLSID, | |
[switch]$SkipNamedFolders, | |
[switch]$SkipTaskLinks | |
) | |
# Set the output folder path for the generated shortcuts based on the provided argument or default location. Convert to full path if necessary | |
if ($Output) { | |
# Convert to full path only if necessary, otherwise use as is | |
if (-not [System.IO.Path]::IsPathRooted($Output)) { | |
$mainShortcutsFolder = Join-Path $PSScriptRoot $Output | |
} else { | |
$mainShortcutsFolder = $Output | |
} | |
} else { | |
# Default output folder path is a subfolder named "Shell Folder Shortcuts" in the script's directory | |
$mainShortcutsFolder = Join-Path $PSScriptRoot "Shell Folder Shortcuts" | |
} | |
# `Join-Path` is used to construct the folder path by combining the script's root directory with the folder name. | |
#$mainShortcutsFolder = Join-Path $PSScriptRoot "Shell Folder Shortcuts" | |
# Set filenames for various output files (CSV and optional XML) | |
$clsidCsvPath = Join-Path $mainShortcutsFolder "CLSID_Shell_Folders.csv" | |
$namedFoldersCsvPath = Join-Path $mainShortcutsFolder "Named_Shell_Folders.csv" | |
$taskLinksCsvPath = Join-Path $mainShortcutsFolder "Task_Links.csv" | |
$xmlContentFilePath = Join-Path $mainShortcutsFolder "Shell32_XML_Content.xml" | |
$resolvedXmlContentFilePath = Join-Path $mainShortcutsFolder "Shell32_XML_Content_Resolved.xml" | |
# Check if the output folder already exists and delete it if the -DeletePreviousOutputFolder switch is used | |
if ($DeletePreviousOutputFolder -and (Test-Path $mainShortcutsFolder)) { | |
Write-Host "Deleting previous output folder: $mainShortcutsFolder" | |
Remove-Item -Path $mainShortcutsFolder -Recurse -Force | |
} | |
# `New-Item` creates the directory if it does not exist; `-Force` ensures it is created without errors if it already exists. | |
New-Item -Path $mainShortcutsFolder -ItemType Directory -Force | Out-Null | |
# Function to create a folder with a custom icon | |
function New-FolderWithIcon { | |
param ( | |
[string]$FolderPath, | |
[string]$IconFile, | |
[string]$IconIndex # Changed to string to allow both positive and negative values | |
) | |
# Create the folder | |
New-Item -Path $FolderPath -ItemType Directory -Force | Out-Null | |
# If there's not a negative sign at the beginning of the index, add one | |
if ($IconIndex -notmatch '^-') { | |
$IconIndex = "-$IconIndex" | |
} | |
# Create desktop.ini content | |
$desktopIniContent = @" | |
[.ShellClassInfo] | |
IconResource=$IconFile,$IconIndex | |
"@ | |
# Create desktop.ini file | |
$desktopIniPath = Join-Path $FolderPath "desktop.ini" | |
Set-Content -Path $desktopIniPath -Value $desktopIniContent -Encoding ASCII | |
# Set desktop.ini attributes | |
(Get-Item $desktopIniPath).Attributes = 'Hidden,System' | |
# Set folder attributes | |
(Get-Item $FolderPath).Attributes = 'ReadOnly,Directory' | |
} | |
# Create relevant subfolders for different types of shortcuts, and set custom icons for each folder | |
# Notes for choosing an icon: | |
# - You can use the tool 'IconsExtract' from NirSoft to see icons in a DLL file and their indexes: https://www.nirsoft.net/utils/iconsext.html | |
# - Another good dll to use for icons is "C:\Windows\System32\imageres.dll" which has a lot of icons | |
if (-not $SkipCLSID) { | |
$CLSIDshortcutsOutputFolder = Join-Path $mainShortcutsFolder "CLSID Shell Folder Shortcuts" | |
New-FolderWithIcon -FolderPath $CLSIDshortcutsOutputFolder -IconFile "C:\Windows\System32\shell32.dll" -IconIndex "20" | |
} | |
if (-not $SkipNamedFolders) { | |
$namedShortcutsOutputFolder = Join-Path $mainShortcutsFolder "Named Shell Folder Shortcuts" | |
New-FolderWithIcon -FolderPath $namedShortcutsOutputFolder -IconFile "C:\Windows\System32\shell32.dll" -IconIndex "46" | |
} | |
if (-not $SkipTaskLinks) { | |
$taskLinksOutputFolder = Join-Path $mainShortcutsFolder "All Task Links" | |
New-FolderWithIcon -FolderPath $taskLinksOutputFolder -IconFile "C:\Windows\System32\shell32.dll" -IconIndex "137" | |
} | |
# The following block adds necessary .NET types to PowerShell for later use. | |
# The `Add-Type` cmdlet is used to add C# code that interacts with Windows API functions. | |
# These functions include loading and freeing DLLs, finding and loading resources, and more. | |
Add-Type -TypeDefinition @" | |
using System; | |
using System.Runtime.InteropServices; | |
using System.Text; | |
public class Windows | |
{ | |
[DllImport("user32.dll", CharSet = CharSet.Auto)] | |
public static extern int LoadString(IntPtr hInstance, uint uID, StringBuilder lpBuffer, int nBufferMax); | |
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] | |
public static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags); | |
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] | |
public static extern IntPtr FindResource(IntPtr hModule, int lpName, string lpType); | |
[DllImport("kernel32.dll", SetLastError = true)] | |
public static extern IntPtr LoadResource(IntPtr hModule, IntPtr hResInfo); | |
[DllImport("kernel32.dll", SetLastError = true)] | |
public static extern IntPtr LockResource(IntPtr hResData); | |
[DllImport("kernel32.dll", SetLastError = true)] | |
public static extern uint SizeofResource(IntPtr hModule, IntPtr hResInfo); | |
[DllImport("kernel32.dll", SetLastError = true)] | |
public static extern bool FreeLibrary(IntPtr hModule); | |
} | |
"@ | |
# Function: Get-LocalizedString | |
# This function retrieves a localized (meaning in the user's language) string from a DLL based on a reference string given in the registry | |
# `StringReference` is a reference in the format "@<dllPath>,-<resourceId>". | |
function Get-LocalizedString { | |
param ( [string]$StringReference ) | |
# Check if the provided string matches the expected format for a resource reference. | |
if ($StringReference -match '@(.+),-(\d+)') { | |
$dllPath = [Environment]::ExpandEnvironmentVariables($Matches[1]) # Extract and expand the DLL path. | |
$resourceId = [uint32]$Matches[2] # Extract the resource ID. | |
# Load the specified DLL into memory. | |
$hModule = [Windows]::LoadLibraryEx($dllPath, [IntPtr]::Zero, 0) | |
if ($hModule -eq [IntPtr]::Zero) { | |
Write-Error "Failed to load library: $dllPath" | |
return $null | |
} | |
# Prepare a StringBuilder object to hold the localized string. | |
$stringBuilder = New-Object System.Text.StringBuilder 1024 | |
# Load the string from the DLL. | |
$result = [Windows]::LoadString($hModule, $resourceId, $stringBuilder, $stringBuilder.Capacity) | |
# Free the loaded DLL from memory. Must add '[void]' or else PowerShell will make the function return as an array. | |
[void][Windows]::FreeLibrary($hModule) | |
# If the string was successfully loaded, return it. | |
if ($result -ne 0) { | |
return $stringBuilder.ToString() | |
} else { | |
Write-Error "Failed to load string resource: $resourceId from $dllPath" | |
return $null | |
} | |
} else { | |
Write-Error "Invalid string reference format: $StringReference" | |
return $null | |
} | |
} | |
# Function: Get-FolderName | |
# This function retrieves the name of a shell folder given its CLSID, to be used for the shortcuts later | |
# It attempts to find the name by checking several potential locations in the registry. | |
function Get-FolderName { | |
param ( | |
[string]$clsid # The CLSID of the shell folder. | |
) | |
# Initialize $nameSource to track where the folder name was found (for reporting purposes in CSV later) | |
$nameSource = "Unknown" | |
Write-Verbose "Attempting to get folder name for CLSID: $clsid" | |
# Step 1: Check the default value in the registry at HKEY_CLASSES_ROOT\CLSID\<clsid>. | |
$defaultPath = "Registry::HKEY_CLASSES_ROOT\CLSID\$clsid" | |
Write-Verbose "Checking default value at: $defaultPath" | |
$defaultName = (Get-ItemProperty -Path $defaultPath -ErrorAction SilentlyContinue).'(default)' | |
# If a default name is found, check if it's a reference to a localized string. | |
if ($defaultName) { | |
Write-Verbose "Found default name: $defaultName" | |
if ($defaultName -match '@.+,-\d+') { | |
Write-Verbose "Default name is a localized string reference" | |
$resolvedName = Get-LocalizedString $defaultName | |
if ($resolvedName) { | |
$nameSource = "Localized String" | |
Write-Verbose "Resolved default name to: $resolvedName" | |
return @($resolvedName, $nameSource) | |
} | |
else { | |
Write-Verbose "Failed to resolve default name, using original value" | |
} | |
} | |
$nameSource = "Default Value" | |
return @($defaultName, $nameSource) | |
} | |
else { | |
Write-Verbose "No default name found" | |
} | |
# Step 2: Check for a `TargetKnownFolder` in the registry, which points to a known folder. | |
$initPropertyBagPath = "Registry::HKEY_CLASSES_ROOT\CLSID\$clsid\Instance\InitPropertyBag" | |
Write-Verbose "Checking for TargetKnownFolder at: $initPropertyBagPath" | |
$targetKnownFolder = (Get-ItemProperty -Path $initPropertyBagPath -ErrorAction SilentlyContinue).TargetKnownFolder | |
# If a TargetKnownFolder is found, check its description in the registry. | |
if ($targetKnownFolder) { | |
Write-Verbose "Found TargetKnownFolder: $targetKnownFolder" | |
$folderDescriptionsPath = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FolderDescriptions\$targetKnownFolder" | |
Write-Verbose "Checking for folder name at: $folderDescriptionsPath" | |
$folderName = (Get-ItemProperty -Path $folderDescriptionsPath -ErrorAction SilentlyContinue).Name | |
if ($folderName) { | |
$nameSource = "Known Folder ID" | |
Write-Verbose "Found folder name: $folderName" | |
return @($folderName, $nameSource) | |
} | |
else { | |
Write-Verbose "No folder name found in FolderDescriptions" | |
} | |
} | |
else { | |
Write-Verbose "No TargetKnownFolder found" | |
} | |
# Step 3: Check for a `LocalizedString` value in the CLSID registry key. | |
$localizedStringPath = "Registry::HKEY_CLASSES_ROOT\CLSID\$clsid" | |
Write-Verbose "Checking for LocalizedString at: $localizedStringPath" | |
$localizedString = (Get-ItemProperty -Path $localizedStringPath -ErrorAction SilentlyContinue).LocalizedString | |
# If a LocalizedString is found, resolve it using the Get-LocalizedString function. | |
if ($localizedString) { | |
Write-Verbose "Found LocalizedString: $localizedString" | |
$resolvedString = Get-LocalizedString $localizedString | |
if ($resolvedString) { | |
$nameSource = "Localized String" | |
Write-Verbose "Resolved LocalizedString to: $resolvedString" | |
return @($resolvedString, $nameSource) | |
} | |
else { | |
Write-Verbose "Failed to resolve LocalizedString" | |
} | |
} | |
else { | |
Write-Verbose "No LocalizedString found" | |
} | |
# Step 4: Check the Desktop\NameSpace registry key for the folder name. | |
$namespacePath = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\$clsid" | |
Write-Verbose "Checking Desktop\NameSpace at: $namespacePath" | |
$namespaceName = (Get-ItemProperty -Path $namespacePath -ErrorAction SilentlyContinue).'(default)' | |
# If a name is found here, return it. | |
if ($namespaceName) { | |
$nameSource = "Desktop Namespace" | |
Write-Verbose "Found name in Desktop\NameSpace: $namespaceName" | |
return @($namespaceName, $nameSource) | |
} | |
else { | |
Write-Verbose "No name found in Desktop\NameSpace" | |
} | |
# Step 5: New check - Recursively search in HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer | |
$explorerPath = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer" | |
Write-Verbose "Recursively checking Explorer registry path for CLSID: $explorerPath" | |
function Search-RegistryKey { | |
param ( | |
[string]$Path, | |
[string]$Clsid | |
) | |
$keys = Get-ChildItem -Path $Path -ErrorAction SilentlyContinue | |
foreach ($key in $keys) { | |
if ($key.PSChildName -eq $Clsid) { | |
$defaultValue = (Get-ItemProperty -Path $key.PSPath -ErrorAction SilentlyContinue).'(default)' | |
if ($defaultValue -and $defaultValue -ne "") { | |
return $defaultValue | |
} | |
} | |
$subResult = Search-RegistryKey -Path $key.PSPath -Clsid $Clsid | |
if ($subResult) { | |
return $subResult | |
} | |
} | |
return $null | |
} | |
$explorerName = Search-RegistryKey -Path $explorerPath -Clsid $clsid | |
if ($explorerName) { | |
$nameSource = "Explorer Registry" | |
Write-Verbose "Found name in Explorer registry: $explorerName" | |
return @($explorerName, $nameSource) | |
} | |
else { | |
Write-Verbose "No name found in Explorer registry" | |
} | |
# Step 6: If no name is found, return the CLSID itself as a last resort to be used for the shortcut | |
Write-Verbose "Returning CLSID as folder name" | |
$nameSource = "Unknown" | |
return @($clsid, $nameSource) | |
} | |
# Function: Create-Shortcut | |
# This function creates a shortcut for a given CLSID or shell folder. | |
# It assigns the appropriate target path, arguments, and icon based on the CLSID information. | |
function Create-Shortcut { | |
param ( | |
[string]$clsid, # The CLSID of the shell folder | |
[string]$name, # The name of the shortcut | |
[string]$shortcutPath, # The full path where the shortcut will be created | |
[string]$pageName = "" # Optional: the name of a specific page within the shell folder (usually used for control panels) | |
) | |
try { | |
Write-Verbose "Creating Shortcut For: $name" | |
# Create a COM object representing the WScript.Shell, which is used to create shortcuts | |
$shell = New-Object -ComObject WScript.Shell | |
# Create the actual shortcut at the specified path | |
$shortcut = $shell.CreateShortcut($shortcutPath) | |
# Set the 'target' to explorer so it opens with File Explorer. The 'arguments' part of the target will be set next and contain the 'shell:' part of the command. | |
$shortcut.TargetPath = "explorer.exe" | |
# If a specific page is provided, include it in the arguments, otherwise just set the shell command used to open the folder | |
if ($pageName) { | |
$shortcut.Arguments = "shell:::$clsid\$pageName" | |
} else { | |
$shortcut.Arguments = "shell:::$clsid" | |
} | |
# Attempt to find a custom icon for the shortcut by checking the registry | |
$iconPath = (Get-ItemProperty -Path "Registry::HKEY_CLASSES_ROOT\CLSID\$clsid\DefaultIcon" -ErrorAction SilentlyContinue).'(default)' | |
if ($iconPath) { | |
Write-Verbose "Setting custom icon: $iconPath" | |
$shortcut.IconLocation = $iconPath | |
} | |
# Otherwise use the Windows default folder icon | |
else { | |
Write-Verbose "No custom icon found. Setting default folder icon." | |
$shortcut.IconLocation = "%SystemRoot%\System32\shell32.dll,3" | |
} | |
$shortcut.Save() | |
# Release the COM object to free up resources. | |
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($shell) | Out-Null | |
return $true | |
} | |
catch { | |
Write-Host "Error creating shortcut for $name`: $($_.Exception.Message)" | |
return $false | |
} | |
} | |
function Get-TaskIcon { | |
param ( | |
[string]$controlPanelName, | |
[string]$applicationId | |
) | |
$iconPath = $null | |
if ($controlPanelName) { | |
# Try to get icon from control panel name | |
$regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ControlPanel\NameSpace\$controlPanelName" | |
$iconPath = (Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue).Icon | |
} | |
if (-not $iconPath -and $applicationId) { | |
# Try to get icon from application ID | |
$regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ControlPanel\NameSpace\$applicationId" | |
$iconPath = (Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue).Icon | |
if (-not $iconPath) { | |
# If not found, try CLSID path | |
$regPath = "Registry::HKEY_CLASSES_ROOT\CLSID\$applicationId\DefaultIcon" | |
$iconPath = (Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue).'(default)' | |
} | |
} | |
if ($iconPath) { | |
return Fix-CommandPath $iconPath | |
} | |
# Default icon if none found | |
return "%SystemRoot%\System32\shell32.dll,0" | |
} | |
# Fix issues with command paths spotted in at least one XML entry, where double percent signs were present at the beginning like %%windir% | |
function Fix-CommandPath { | |
param ( | |
[string]$command | |
) | |
# Fix double % at the beginning | |
if ($command -match '^\%\%') { | |
$command = $command -replace '^\%\%', '%' | |
} | |
# Expand environment variables | |
#$command = [Environment]::ExpandEnvironmentVariables($command) | |
return $command | |
} | |
function Create-TaskLink-Shortcut { | |
param ( | |
[string]$name, | |
[string]$shortcutPath, | |
[string]$command, | |
[string]$controlPanelName, | |
[string]$applicationId, | |
[string[]]$keywords | |
) | |
try { | |
Write-Verbose "Creating Task Link Shortcut For: $name" | |
$shell = New-Object -ComObject WScript.Shell | |
$shortcut = $shell.CreateShortcut($shortcutPath) | |
# Parse the command | |
if ($command -match '^(\S+)\s*(.*)$') { | |
$targetPath = Fix-CommandPath $Matches[1] | |
$arguments = Fix-CommandPath $Matches[2] | |
$shortcut.TargetPath = $targetPath | |
$shortcut.Arguments = $arguments | |
} else { | |
$fixedCommand = Fix-CommandPath $command | |
$shortcut.TargetPath = $fixedCommand | |
} | |
$iconPath = Get-TaskIcon -controlPanelName $controlPanelName -applicationId $applicationId | |
$shortcut.IconLocation = $iconPath | |
# Combine keywords into a single string and set as Description | |
if ($keywords -and $keywords.Count -gt 0) { | |
$descriptionLimit = 259 # Limit for Description field in shortcuts or else it causes some kind of buffer overflow | |
$keywordString = "" | |
$separator = " " | |
foreach ($keyword in $keywords) { | |
$potentialNewString = if ($keywordString) { $keywordString + $separator + $keyword } else { $keyword } | |
if ($potentialNewString.Length -le $descriptionLimit) { | |
$keywordString = $potentialNewString | |
} else { | |
break | |
} | |
} | |
$shortcut.Description = $keywordString | |
} | |
$shortcut.Save() | |
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) | Out-Null | |
return $true | |
} catch { | |
Write-Host "Error creating shortcut for $name`: $($_.Exception.Message)" | |
return $false | |
} | |
} | |
# Function: Get-Shell32XMLContent | |
# This function extracts and returns the XML content embedded in the shell32.dll file, which contains info about sub-pages within certain shell folders and control panel menus. | |
# Apparently these are sometimes referred to as "Task Links" | |
function Get-Shell32XMLContent { | |
param( | |
[switch]$SaveXML, | |
[string]$CustomDLL | |
) | |
# If a custom DLL path is provided, use it; otherwise, use the default shell32.dll path | |
if ($CustomDLL) { | |
Write-Verbose "Using custom DLL path: $CustomDLL" | |
$dllPath = $CustomDLL | |
} else { | |
Write-Verbose "Using default shell32.dll path" | |
$dllPath = "shell32.dll" | |
} | |
# Constants used for loading the shell32.dll as a data file. | |
$LOAD_LIBRARY_AS_DATAFILE = 0x00000002 | |
$DONT_RESOLVE_DLL_REFERENCES = 0x00000001 | |
# Initialize an empty string to hold the XML content | |
$xmlContent = "" | |
# Load shell32.dll as a data file, preventing the DLL from being fully resolved as it is not necessary | |
$shell32Handle = [Windows]::LoadLibraryEx($dllPath, [IntPtr]::Zero, $LOAD_LIBRARY_AS_DATAFILE -bor $DONT_RESOLVE_DLL_REFERENCES) | |
if ($shell32Handle -eq [IntPtr]::Zero) { | |
Write-Error "Failed to load $dllPath" | |
return $null | |
} | |
try { | |
# Attempt to find the XML resource within shell32.dll. '21' is necessary to use here. | |
$hResInfo = [Windows]::FindResource($shell32Handle, 21, "XML") | |
if ($hResInfo -eq [IntPtr]::Zero) { | |
Write-Error "Failed to find XML resource" | |
Write-Error "Did you move the DLL from the original location? If so you may need to directly specify the corresponding .mun file instead of the DLL." | |
Write-Error "See the comments for the DLLPath argument at the top of the script for more info." | |
return $null | |
} | |
# Load the XML resource data. | |
$hResData = [Windows]::LoadResource($shell32Handle, $hResInfo) | |
if ($hResData -eq [IntPtr]::Zero) { | |
Write-Error "Failed to load XML resource" | |
return $null | |
} | |
# Lock the resource in memory to access its data. | |
$pData = [Windows]::LockResource($hResData) | |
if ($pData -eq [IntPtr]::Zero) { | |
Write-Error "Failed to lock XML resource" | |
return $null | |
} | |
# Get the size of the XML resource and copy it into a byte array. | |
$size = [Windows]::SizeofResource($shell32Handle, $hResInfo) | |
$byteArray = New-Object byte[] $size | |
[System.Runtime.InteropServices.Marshal]::Copy($pData, $byteArray, 0, $size) | |
# Convert the byte array to a UTF-8 string. | |
$xmlContent = [System.Text.Encoding]::UTF8.GetString($byteArray) | |
$xmlContent = $xmlContent -replace "`0", "" # Remove any null characters from the string, though this probably isn't necessary | |
} | |
finally { | |
# Ensure that the loaded DLL is always freed from memory. | |
[void][Windows]::FreeLibrary($shell32Handle) | |
} | |
# Clean and trim any extraneous whitespace from the XML content | |
$xmlContent = $xmlContent.Trim() | |
# Save XML content if the SaveXML switch is used | |
if ($SaveXML) { | |
Save-PrettyXML -xmlContent $xmlContent -outputPath $xmlContentFilePath | |
} | |
# Return the XML content as a string | |
return $xmlContent | |
} | |
# Save the XML content from shell32.dll to a file for reference if the user uses the -SaveXML switch | |
function Save-PrettyXML { | |
param ( | |
[string]$xmlContent, | |
[string]$outputPath | |
) | |
try { | |
# Load the XML content | |
$xmlDoc = New-Object System.Xml.XmlDocument | |
$xmlDoc.LoadXml($xmlContent) | |
# Create XmlWriterSettings for pretty-printing | |
$writerSettings = New-Object System.Xml.XmlWriterSettings | |
$writerSettings.Indent = $true | |
$writerSettings.IndentChars = " " | |
$writerSettings.NewLineChars = "`r`n" | |
$writerSettings.NewLineHandling = [System.Xml.NewLineHandling]::Replace | |
# Create XmlWriter and write the document | |
$writer = [System.Xml.XmlWriter]::Create($outputPath, $writerSettings) | |
$xmlDoc.Save($writer) | |
$writer.Close() | |
Write-Verbose "XML content formatted and saved: $outputPath" | |
} | |
catch { | |
Write-Error "Failed to format and save XML: $_" | |
} | |
} | |
# Function: Get-TaskLinks | |
# This function parses the XML content extracted from shell32.dll to find "task links", which are basically sub-menu pages, often in the Control Panel | |
function Get-TaskLinks { | |
param( | |
[switch]$SaveXML, | |
[string]$DLLPath | |
) | |
$xmlContent = Get-Shell32XMLContent -SaveXML:$SaveXML -CustomDLL:$DLLPath | |
try { | |
$xml = [xml]$xmlContent | |
Write-Verbose "XML parsed successfully." | |
} catch { | |
Write-Error "Failed to parse XML content: $_" | |
return | |
} | |
# Create a copy of the XML for resolved content | |
$resolvedXml = $xml.Clone() | |
$nsManager = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) | |
$nsManager.AddNamespace("cpl", "http://schemas.microsoft.com/windows/cpltasks/v1") | |
$nsManager.AddNamespace("sh", "http://schemas.microsoft.com/windows/tasks/v1") | |
$nsManager.AddNamespace("sh2", "http://schemas.microsoft.com/windows/tasks/v2") | |
$tasks = @() | |
$allTasks = $xml.SelectNodes("//sh:task", $nsManager) | |
foreach ($task in $allTasks) { | |
$taskId = $task.GetAttribute("id") | |
$nameNode = $task.SelectSingleNode("sh:name", $nsManager) | |
$controlPanel = $task.SelectSingleNode("sh2:controlpanel", $nsManager) | |
$commandNode = $task.SelectSingleNode("sh:command", $nsManager) | |
$keywordsNodes = $task.SelectNodes("sh:keywords", $nsManager) | |
# Resolve name | |
$name = $null | |
if ($nameNode -and $nameNode.InnerText) { | |
if ($nameNode.InnerText -match '@(.+),-(\d+)') { | |
$name = Get-LocalizedString $nameNode.InnerText | |
# Update resolved XML | |
$resolvedNameNode = $resolvedXml.SelectSingleNode("//sh:task[@id='$taskId']/sh:name", $nsManager) | |
if ($resolvedNameNode) { | |
$resolvedNameNode.InnerText = $name | |
} | |
} else { | |
$name = $nameNode.InnerText | |
} | |
} | |
if ($name) { | |
$name = $name.Trim() | |
} elseif ($task.Name -eq "sh:task" -and $task.GetAttribute("idref")) { | |
Write-Verbose "Skipping category entry: $($task.OuterXml)" | |
continue | |
} else { | |
Write-Warning "Task $taskId is missing a name and is not a category reference. This may indicate an issue: $($task.OuterXml)" | |
continue | |
} | |
$command = $null | |
$appName = $null | |
$page = $null | |
$appId = $task.ParentNode.id | |
if (-not $appId) { | |
$appId = $task.GetAttribute("id") | |
} | |
if ($controlPanel) { | |
$appName = $controlPanel.GetAttribute("name") | |
$page = $controlPanel.GetAttribute("page") | |
$command = "control.exe /name $appName" | |
if ($page) { | |
$command += " /page $page" | |
} | |
} elseif ($commandNode) { | |
$command = $commandNode.InnerText | |
} | |
$keywords = @() | |
foreach ($keywordNode in $keywordsNodes) { | |
$keyword = $null | |
if ($keywordNode.InnerText -match '@(.+),-(\d+)') { | |
$keyword = Get-LocalizedString $keywordNode.InnerText | |
# Update resolved XML | |
$resolvedKeywordNode = $resolvedXml.SelectSingleNode("//sh:task[@id='$taskId']/sh:keywords[text()='$($keywordNode.InnerText)']", $nsManager) | |
if ($resolvedKeywordNode) { | |
$resolvedKeywordNode.InnerText = $keyword | |
} | |
} else { | |
$keyword = $keywordNode.InnerText | |
} | |
if ($keyword) { | |
$splitKeywords = $keyword.Split(';', [StringSplitOptions]::RemoveEmptyEntries) | |
foreach ($splitKeyword in $splitKeywords) { | |
if ($splitKeyword) { | |
$keywords += $splitKeyword.Trim() | |
} | |
} | |
} | |
} | |
# If no app name, look it up via CLSID in the registry | |
if (-not $appName) { | |
$appName = (Get-ItemProperty -Path "Registry::HKEY_CLASSES_ROOT\CLSID\$appId" -ErrorAction SilentlyContinue)."System.ApplicationName" | |
} | |
# If still no app name, check if any other task has the same app ID with a name, and if so use that, but only the first instance | |
if (-not $appName) { | |
$otherTask = $tasks | Where-Object { $_.ApplicationId -eq $appId -and $_.ApplicationName } | Select-Object -First 1 | |
if ($otherTask) { | |
$appName = $otherTask.ApplicationName | |
} | |
} | |
if ($name -and ($command -or $appName)) { | |
# Determine the ControlPanelName value before creating the object | |
if ($controlPanel) { | |
$controlPanelName = $controlPanel.GetAttribute("name") | |
} else { | |
$controlPanelName = $null | |
} | |
# Now create the $newTask object using the pre-determined $controlPanelName | |
$newTask = [PSCustomObject]@{ | |
TaskId = $taskId | |
ApplicationId = $appId | |
Name = $name | |
ApplicationName = $appName | |
Page = $page | |
Command = $command | |
Keywords = $keywords | |
ControlPanelName = $controlPanelName | |
} | |
# Check if a task with the same name and command already exists | |
$isDuplicate = $tasks | Where-Object { $_.Name -eq $newTask.Name -and $_.Command -eq $newTask.Command } | |
if (-not $isDuplicate) { | |
$tasks += $newTask | |
} else { | |
Write-Verbose "Skipping duplicate task: $($newTask.Name)" | |
} | |
} | |
} | |
# Store the resolved XML content in a new variable | |
$resolvedXmlContent = $resolvedXml.OuterXml | |
# Save XML content if the SaveXML switch is used | |
if ($SaveXML) { | |
Save-PrettyXML -xmlContent $resolvedXmlContent -outputPath $resolvedXmlContentFilePath | |
} | |
return $tasks | |
} | |
# Function: Create-NamedShortcut | |
# This function creates a shortcut for a named special folder. | |
# The shortcut points directly to the folder using its name (e.g., "Documents", "Pictures"). | |
function Create-NamedShortcut { | |
param ( | |
[string]$name, # The name of the special folder. | |
[string]$shortcutPath, # The full path where the shortcut will be created. | |
[string]$iconPath # The path to the folder's custom icon (if any). | |
) | |
try { | |
Write-Verbose "Creating named shortcut for $name" | |
# Create a COM object representing the WScript.Shell, which is used to create shortcuts. | |
$shell = New-Object -ComObject WScript.Shell | |
# Create the actual shortcut at the specified path. | |
$shortcut = $shell.CreateShortcut($shortcutPath) | |
$shortcut.TargetPath = "explorer.exe" # The shortcut will open in Windows Explorer. | |
$shortcut.Arguments = "shell:$name" # Set the shortcut to open the specified folder by name. This will also be in the target path box for the shortcut. | |
# Set the custom icon if one is provided in the registry | |
if ($iconPath) { | |
Write-Verbose "Setting custom icon: $iconPath" | |
$shortcut.IconLocation = $iconPath | |
} | |
else { | |
Write-Verbose "Setting default folder icon" | |
$shortcut.IconLocation = "%SystemRoot%\System32\shell32.dll,3" # Default folder icon. | |
} | |
# Save the shortcut. | |
$shortcut.Save() | |
# Release the COM object to free up resources. | |
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($shell) | Out-Null | |
return $true | |
} | |
catch { | |
Write-Host "Error creating shortcut for $name`: $($_.Exception.Message)" | |
return $false | |
} | |
} | |
# Function: Create-CLSIDCsvFile | |
# This function generates a CSV file containing details about all processed CLSID shell folders. | |
function Create-CLSIDCsvFile { | |
param ( | |
[string]$outputPath, # The full path where the CSV file will be saved. | |
[array]$clsidData # An array of objects containing CLSID data. | |
) | |
# Initialize the CSV content with headers. | |
$csvContent = "CLSID,ExplorerCommand,Name,NameSource,CustomIcon`n" | |
# Loop through each CLSID data object and append its details to the CSV content. | |
foreach ($item in $clsidData) { | |
$explorerCommand = "explorer shell:::$($item.CLSID)" # The command to open the shell folder. | |
$iconPath = if ($item.IconPath) { | |
"`"$($item.IconPath -replace '"', '""')`"" # Escape double quotes in the icon path. | |
} else { | |
"None" | |
} | |
# Escape any double quotes in the name. | |
$escapedName = $item.Name -replace '"', '""' | |
# Convert the sub-items array to a string, separating items with a pipe character. | |
#$subItemsString = ($item.SubItems | ForEach-Object { "$($_.Name):$($_.Page)" }) -join '|' | |
# Append the CLSID details to the CSV content. | |
$csvContent += "$($item.CLSID),`"$explorerCommand`",`"$escapedName`",$($item.NameSource),$iconPath`n" | |
} | |
# Write the CSV content to the specified output file. | |
$csvContent | Out-File -FilePath $outputPath -Encoding utf8 | |
} | |
# Function: Create-NamedFoldersCsvFile | |
# This function generates a CSV file containing details about all processed named special folders. | |
function Create-NamedFoldersCsvFile { | |
param ( | |
[string]$outputPath # The full path where the CSV file will be saved. | |
) | |
# Initialize the CSV content with headers. | |
$csvContent = "Name,ExplorerCommand,RelativePath,ParentFolder`n" | |
# Retrieve all named special folders from the registry. | |
$namedFolders = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FolderDescriptions" | |
# Loop through each named folder and append its details to the CSV content. | |
foreach ($folder in $namedFolders) { | |
$folderProperties = Get-ItemProperty -Path $folder.PSPath | |
$name = $folderProperties.Name # Extract the name of the folder. | |
if ($name) { | |
$explorerCommand = "explorer shell:$name" # The command to open the folder. | |
$relativePath = $folderProperties.RelativePath -replace ',', '","' # Escape commas in the relative path. | |
$parentFolderGuid = $folderProperties.ParentFolder # Extract the parent folder GUID (if any). | |
$parentFolderName = "None" # Default value if there is no parent folder. | |
if ($parentFolderGuid) { | |
# If a parent folder GUID is found, retrieve the name of the parent folder. | |
$parentFolderPath = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FolderDescriptions\$parentFolderGuid" | |
$parentFolderName = (Get-ItemProperty -Path $parentFolderPath -ErrorAction SilentlyContinue).Name | |
} | |
# Append the named folder details to the CSV content. | |
$csvContent += "`"$name`",`"$explorerCommand`",`"$relativePath`",`"$parentFolderName`"`n" | |
} | |
} | |
# Write the CSV content to the specified output file. | |
$csvContent | Out-File -FilePath $outputPath -Encoding utf8 | |
} | |
# Function: Create-TaskLinksCsvFile | |
# This function generates a CSV file containing details about all processed 'task links' aka sub-pages. | |
function Create-TaskLinksCsvFile { | |
param ( | |
[string]$outputPath, | |
[array]$taskLinksData | |
) | |
$csvContent = "XMLTaskId,ApplicationId,ApplicationName,Name,Page,Command,Keywords`n" | |
foreach ($item in $taskLinksData) { | |
$taskId = $item.TaskId -replace '"', '""' | |
$applicationId = $item.ApplicationId -replace '"', '""' | |
$applicationName = $item.ApplicationName -replace '"', '""' | |
$name = $item.Name -replace '"', '""' | |
$page = $item.Page -replace '"', '""' | |
$command = $item.Command -replace '"', '""' | |
$keywords = ($item.Keywords -join ';') -replace '"', '""' | |
$csvContent += "`"$taskId`",`"$applicationId`",`"$applicationName`",`"$name`",`"$page`",`"$command`",`"$keywords`"`n" | |
} | |
$csvContent | Out-File -FilePath $outputPath -Encoding utf8 | |
} | |
# Take the app name like Microsoft.NetworkAndSharingCenter and prepare it to be displayed in the shortcut name like "Network and Sharing Center - Whatever Task name" | |
function Prettify-App-Name { | |
param( | |
[string]$AppName, | |
[string]$TaskName | |
) | |
# List of words to rejoin if split | |
$wordsToRejoin = @( | |
"Bit Locker", | |
"Side Show", | |
"Auto Play" | |
# Add more words as needed | |
) | |
# Remove "Microsoft." prefix if present | |
$AppName = $AppName -replace '^Microsoft\.', '' | |
# Split camelCase into separate words, handling consecutive uppercase letters | |
$AppName = $AppName -creplace '(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|\b(?=[A-Z]{2,}\b)', ' ' | |
# Rejoin specific words | |
foreach ($word in $wordsToRejoin) { | |
$AppName = $AppName -replace $word, $word.Replace(' ', '') | |
} | |
# Combine AppName and TaskName | |
$PrettyName = "$AppName - $TaskName" | |
# Sanitize the name to remove invalid characters for file names | |
$PrettyName = $PrettyName -replace '[\\/:*?"<>|]', '_' | |
return $PrettyName | |
} | |
# ---------------------------------------------- ---------------------------------------------------------------- | |
# ---------------------------------------------- Main Script ---------------------------------------------- | |
# ---------------------------------------------- ---------------------------------------------------------------- | |
# If statement to check if CLSID is skipped by argument | |
if (-not $SkipCLSID) { | |
# Retrieve all CLSIDs with a "ShellFolder" subkey from the registry. | |
# These CLSIDs represent shell folders that are embedded within Windows. | |
$shellFolders = Get-ChildItem -Path 'Registry::HKEY_CLASSES_ROOT\CLSID' | | |
Where-Object {$_.GetSubKeyNames() -contains "ShellFolder"} | | |
Select-Object PSChildName | |
Write-Host "`n----- Processing $($shellFolders.Count) Shell Folders -----" | |
# Create an array to store information about each CLSID (name, icon, etc.). | |
$clsidInfo = @() | |
# Loop through each relevant CLSID that was found and process it to create shortcuts. | |
foreach ($folder in $shellFolders) { | |
$clsid = $folder.PSChildName # Extract the CLSID. | |
Write-Verbose "Processing CLSID: $clsid" | |
# Retrieve the name of the shell folder using the Get-FolderName function and the source of the name within the registry | |
$resultArray = Get-FolderName -clsid $clsid | |
$name = $resultArray[0] | |
$nameSource = $resultArray[1] | |
# Sanitize the folder name to make it a valid filename. | |
$sanitizedName = $name -replace '[\\/:*?"<>|]', '_' | |
$shortcutPath = Join-Path $CLSIDshortcutsOutputFolder "$sanitizedName.lnk" | |
Write-Verbose "Attempting to create shortcut: $shortcutPath" | |
$success = Create-Shortcut -clsid $clsid -name $name -shortcutPath $shortcutPath | |
if ($success) { | |
Write-Host "Created CLSID Shortcut For: $name" | |
} | |
else { | |
Write-Host "Failed to create shortcut for $name" | |
} | |
# Check for sub-items (pages) related to the current CLSID (e.g., control panel items). | |
$appName = (Get-ItemProperty -Path "Registry::HKEY_CLASSES_ROOT\CLSID\$clsid" -ErrorAction SilentlyContinue)."System.ApplicationName" | |
# Store the CLSID information for later use (e.g., in CSV file generation). | |
$iconPath = (Get-ItemProperty -Path "Registry::HKEY_CLASSES_ROOT\CLSID\$clsid\DefaultIcon" -ErrorAction SilentlyContinue).'(default)' | |
$clsidInfo += [PSCustomObject]@{ | |
CLSID = $clsid | |
Name = $name | |
NameSource = $nameSource | |
IconPath = $iconPath | |
} | |
} | |
} | |
if (-not $SkipNamedFolders) { | |
# Retrieve all named special folders from the registry. | |
$namedFolders = Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FolderDescriptions" | |
Write-Host "`n----- Processing $($namedFolders.Count) Special Named Folders -----" | |
# Loop through each named folder and create a shortcut for it. | |
foreach ($folder in $namedFolders) { | |
$folderProperties = Get-ItemProperty -Path $folder.PSPath | |
$folderName = $folderProperties.Name # Extract the name of the folder. | |
$iconPath = $folderProperties.Icon # Extract the custom icon path (if any). | |
if ($folderName) { | |
Write-Verbose "Processing named folder: $folderName" | |
# Sanitize the folder name to make it a valid filename. | |
$sanitizedName = $folderName -replace '[\\/:*?"<>|]', '_' | |
$shortcutPath = Join-Path $namedShortcutsOutputFolder "$sanitizedName.lnk" | |
Write-Verbose "Attempting to create shortcut: $shortcutPath" | |
$success = Create-NamedShortcut -name $folderName -shortcutPath $shortcutPath -iconPath $iconPath | |
if ($success) { | |
Write-Host "Created Shortcut For Named Folder: $folderName" | |
} | |
else { | |
Write-Host "Failed to create shortcut for named folder $folderName" | |
} | |
} | |
else { | |
Write-Verbose "Skipping folder with no name: $($folder.PSChildName)" | |
} | |
} | |
} | |
if (-not $SkipTaskLinks) { | |
# Process Task Links - Use the extracted XML data from Shell32 to create shortcuts for task links | |
Write-Host "`n -----Processing Task Links -----" | |
# Retrieve task links from the XML content in shell32.dll. | |
$taskLinks = Get-TaskLinks -SaveXML:$SaveXML -DLLPath:$DLLPath | |
$createdShortcutNames = @{} # Track created shortcuts to be able to tasks with the same name but different commands by appending a number | |
foreach ($task in $taskLinks) { | |
$originalName = $task.Name | |
# Use Prettify-App-Name function by default, unless DontGroupTasks is specified | |
if (-not $DontGroupTasks -and $task.ApplicationName) { | |
$sanitizedName = Prettify-App-Name -AppName $task.ApplicationName -TaskName $originalName | |
} else { | |
$sanitizedName = $originalName -replace '[\\/:*?"<>|]', '_' | |
} | |
# Check if a shortcut with this name already exists, if so set a unique number to the end of the name | |
$nameCounter = 1 | |
$uniqueName = $sanitizedName | |
while ($createdShortcutNames.ContainsKey($uniqueName)) { | |
$nameCounter++ | |
$uniqueName = "${sanitizedName} ($nameCounter)" | |
} | |
$shortcutPath = Join-Path $taskLinksOutputFolder "$uniqueName.lnk" | |
$createdShortcutNames[$uniqueName] = $true | |
# Determine the command based on available information. Some task XML entries have the entire command already given, others are implied to be used with control.exe | |
if ($task.Command) { | |
$command = $task.Command | |
} elseif ($task.ApplicationName -and $task.Page) { | |
$command = "control.exe /name $($task.ApplicationName) /page $($task.Page)" | |
} else { | |
Write-Verbose "Skipping task $originalName due to insufficient command information" | |
continue | |
} | |
$success = Create-TaskLink-Shortcut -name $uniqueName -shortcutPath $shortcutPath -command $command -controlPanelName $task.ControlPanelName -applicationId $task.ApplicationId -keywords $task.Keywords | |
if ($success) { | |
Write-Host "Created task link shortcut for $uniqueName" | |
} else { | |
Write-Host "Failed to create task link shortcut for $uniqueName" | |
} | |
} | |
} | |
# Create the CSV files using the stored CLSID and 'task link' (aka menu sub-pages) data. Skip each depending on the corresponding switch. | |
$CLSIDDisplayPath = "" | |
$namedFolderDisplayPath = "" | |
$taskLinksDisplayPath = "" | |
if ($SaveCSV -and -not $SkipCLSID) { | |
Create-CLSIDCsvFile -outputPath $clsidCsvPath -clsidData $clsidInfo | |
$CLSIDDisplayPath = "`n" + $clsidCsvPath | |
} | |
if ($SaveCSV -and -not $SkipNamedFolders) { | |
Create-NamedFoldersCsvFile -outputPath $namedFoldersCsvPath | |
$namedFolderDisplayPath = "`n" + $namedFoldersCsvPath | |
} | |
if ($SaveCSV -and -not $SkipTaskLinks) { | |
Create-TaskLinksCsvFile -outputPath $taskLinksCsvPath -taskLinksData $taskLinks | |
$taskLinksDisplayPath = "`n" + $taskLinksCsvPath | |
} | |
# Output a message indicating that the script execution is complete and the CSV files have been created. | |
Write-Host "`n*** Script execution complete ***`n" | |
# If SaveXML switch was used, also output the paths to the saved XML files | |
if ($SaveXML -and (-not $SkipTaskLinks)) { | |
Write-Host "XML files have been saved at:`n$xmlContentFilePath`n$resolvedXmlContentFilePath`n" | |
} | |
# As long as any of the CSV files were created, output the paths to them - check by seeing if strings are empty | |
if ($CLSIDDisplayPath -or $namedFolderDisplayPath -or $taskLinksDisplayPath){ | |
$csvPrintString = "CSV files have been created at:" + "$CLSIDDisplayPath" + "$namedFolderDisplayPath" + "$taskLinksDisplayPath" + "`n" | |
Write-Host $csvPrintString | |
} |
I was contemplating on doing it myself until you mentioned it, Thanks!
thanks ^^
I was contemplating on doing it myself until you mentioned it, Thanks!
hahaha same!
interesting!
Thanks :)
Added support for sub-items / menu pages. You'll see a new CSV file created with those results and additional shortcuts in the CLSID Shell Folder Shortcuts
:
Note: For some of these there may be multiple differently-named shortcuts that go to the same actual page. It's just the way the pages were listed in the schema in Windows. I guess it's to account for multiple actions being possible on the same menu.
Was this like half chat gpt? 🤣
This script is now made obsolete by my other Repo where I have added a ton more features:
https://github.com/ThioJoe/Windows-Super-God-Mode
Thank you Joe. This helped my need for even more shortcuts in my file explorer.