Forked from ThioJoe/Get_All_Shell_Folder_Shortcuts.ps1
Created
August 22, 2024 12:06
-
-
Save patrickSchliengerCampestrini/60c86d0afe1eefa01f9790cfe20898aa to your computer and use it in GitHub Desktop.
Fetches all shell folders from Windows Registry and creates a shortcut to each, while attempting to determine the proper name and icon. Also outputs CSV file with results.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment