-
-
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.