I am looking for a PowerShell function like Get-LocalizedName($FilePath), returning the localized name of a file or its filename if it is not localized. I know that the localized names are stored in the LocalizedFileNames section of the respective desktop.ini files, but usually as resource file pointers rather than clear names.
Example: For the Administrative Tools folder and the locale de-DE, I want the clear name Windows-Verwaltungsprogramme instead of #%SystemRoot%\system32\shell32.dll,-21762.
I was not able to find such a function, and also was not successful in analyzing the attributes of Get-ChildItem or google a regarding solution.
Is there any such function that I could use from PowerShell (v7)?
The following solution works for folders only. See this answer for a solution that works for files, localized via LocalizedFileNames section of Desktop.ini.
This can be done using the Shell.Application COM object:
$shell = New-Object -ComObject Shell.Application
# Get full path to the admin tools folder
$adminToolsPath = [Environment]::GetFolderPath('AdminTools')
# Get the shell folder corresponding to this path
if( $folder = $shell.NameSpace( $adminToolsPath ) ) {
$folder.Title # Output localized title
}
# Alternative:
if( $folder = $shell.NameSpace( [Environment+SpecialFolder]::AdminTools ) ) {
$folder.Title # Output localized title
}
[Environment]::GetFolderPath() gives us the filesystem path of a system folder.
The Shell.Namespace() function returns a Folder object corresponding to this path which can be queried for its localized name.
The alternative shows how you can get the localized name more directly, by passing an enumeration value of [Environment+SpecialFolder] to the Shell.Namespace() function.
When passing a path to the Shell.Namespace() method, it works for any folder customized via "desktop.ini", even if it's not a system folder.
I finally found a solution for Get-LocalizedName, digging into 20 years old VBS code using GetDetailsOf:
function Get-LocalizedName {
Param([Parameter(Mandatory=$True)][string]$FilePath)
$ChildObj = Get-ChildItem $FilePath
$FldrName = $ChildObj.DirectoryName
$FileName = $ChildObj.Name
$Shell = New-Object -ComObject Shell.Application
$Folder = $Shell.Namespace($FldrName)
$File = $Folder.ParseName($FileName)
return $($Folder.GetDetailsOf($File,0))
}
Related
I am trying to create the URL file with a custom icon but, for some reason, it is not working.
This is my code:
#Downloading ico file
$WebClient = New-Object System.Net.WebClient
$WebClient.DownloadFile("url location of the icon file","C:\Users\Public\Pictures\filename.ico")
#Creating URL file
$wshShell = New-Object -ComObject "WScript.Shell"
$urlShortcut = $wshShell.CreateShortcut(
(Join-Path $wshShell.SpecialFolders.Item("AllUsersDesktop") "myname.url")
)
$urlShortcut.TargetPath = "https://somewebsite"
$urlShortcut.IconLocation = "C:\Users\Public\Pictures\filename.ico"
$urlShortcut.Save()
The icon file is downloaded, and the URL file is created but, the image is not changed. I've tried a few different things with no luck.
Would be nice if anyone has some input on this.
Kind regards,
The .IconLocation property isn't supported in your case, but there's a workaround:
# ... icon download code omitted
$shortcutFile = Join-Path $wshShell.SpecialFolders.Item('AllUsersDesktop') 'myname.url'
$iconFile = 'C:\Users\Public\Pictures\filename.ico'
$wshShell = New-Object -ComObject "WScript.Shell"
$urlShortcut = $wshShell.CreateShortcut($shortcutFile)
$urlShortcut.TargetPath = 'https://en.wikipedia.org'
$urlShortcut.Save()
# This updates the .url file directly to emulate what assigning
# an icon interactively, via File Explorer, does.
#"
IconIndex=0
HotKey=0
IconFile=$iconFile
"# | Add-Content -LiteralPath $shortcutFile
When you create a URL shortcut file (extension .url):
Only one writable property is supported by the resulting WshUrlShortcut object, namely TargetPath, which stores the target URL.
Notably, this prevent use of the IconLocation property, which is only available on executable shortcut files (extension .lnk), which are WshShortcut objects.
However, the .url file format does support custom icons (by default, the default browser's icon is used), but that requires assigning them interactively, via File Explorer.
Fortunately, .url files are plain-text, .ini-like files, so it's easy to programmatically update that file directly, so as to emulate the results of interactively assigning an icon, as shown above.
Alternatively - which may or may not be an option in your case - you can create a regular shortcut file, with extension .lnk, which - perhaps surprisingly - also works with URLs assigned to .TargetPath. Assigning to .IconLocation then works as usual.
However, there are ramifications:
Obviously, you'll end up with a different filename extension, and the shortcut file won't be readily recognizable as a URL shortcut by its extension.
.lnk files are binary files.
.lnk files with URLs as their target path inexplicably don't allow the URL to be edited via File Explorer later.
I am having a hard time defining this issue, but basically what I would like to know is "what are the symbolic variable names connected to files' metadata (preferably on a Windows installation)".
For example, taking a .mp3 file, checking its properties yields a Title, Bit Rate, Folder Path etc. descriptions. What I want to know is the name of the fields seen by programs (i.e. Title->title, Bit Rate->bit_rate etc.) if it makes any sense, as I've been trying to index some files and I'd like to gather as much info on them as possible.
I'm not convinced that there is such a thing as "symbolic names" for the metadata, especially not in relation to PowerShell. I suspect that Windows maintains support for a certain number of popular formats, and offers functionality through Explorer to view and sometimes edit them. I haven't found a source to prove this theory, but research implicitly supports it: there's several dozen search results about how to retrieve a file's metadata in PowerShell, and they all seem to suggest roughly the same approach (for example this blog post): using a Shell object to gather the information.
Since you tagged this PowerShell, here's my take on boiling it down to the essentials:
$path = 'C:\temp\file.txt' # pick a path
$parent = Split-Path -Parent $path # get the directory
$shell = New-Object -ComObject Shell.Application # get ourselves a shell
$folder = $shell.NameSpace($parent) # get a "folder namespace"
$file = $Folder.Items() | where { $_.Path -eq $path } # get the file itself from the folder
$count = 0 # zero our iterator
$object = New-Object PSObject # make a fresh object to hold our output
While ($folder.GetDetailsOf($folder.Items, $count) -ne "") { # iterate over the available metadata tags for the folder, and for each one get the value from the file
$object | Add-Member -Force NoteProperty ($folder.GetDetailsOf($folder.Items, $count)) ($folder.GetDetailsOf($file, $count))
$count += 1
}
Write-Output $object
Note that the attributes available for a given file are obviously not all of the attributes that could possibly be supported for any file, and additionally are not necessarily "symbolic names". I suspect that the process of querying the shell object causes it to examine the files in a folder and extract metadata that Windows recognizes--it might even do this based on the view type selected for the folder (Photos, Music, Documents, etc.).
As for writing the information, this might be possible through the same shell object, but I haven't explored that option. It's likely dependent on the specific format: for mp3 you probably want a library for viewing/editing mp3-specific metadata.
I'm looking for a command for Powershell that will return a String containing the filepath used for its configured Downloads folder.
I'm trying to do something along the lines of a Get-Content for $settings.download.default_directory but not getting results, not sure what i'm doing wrong.
(EDIT: My other post has a working solution. I'll leave this one here in case you find it useful.)
I can get you some of the way there, but not all the way.
Assuming you're referring to Chrome on Windows, it typically stores its preferences in "C:\Users\[user]\AppData\Local\Google\Chrome\User Data\Default\Preferences". So you'd start with something like:
$prefs = Get-Content "C:\Users\[user]\AppData\Local\Google\Chrome\User Data\Default\Preferences"
(Replace "[user]" with your account name, obviously.)
"Preferences" is a text file consisting of a single, gigantic string of comma-separated data. This is where it specifies my download directory, for example:
,"download":{"default_directory":"C:\\Users\\Matt\\Desktop",
Your next step might be to do '$prefs | Select-String [something...]' to parse that file for that string. However, I don't know how to do that part. It's a matter of telling it: "look for ',"download":', then pull out everything from the comma before 'download' until the next comma."
Once you have that, you can then split on double quotes to isolate the directory path, then remove the double slashes. So, assuming you have the above string in a variable called "$download", it might be something like:
$download = $download.Split('"')[5]
$download = $download.Replace('\\','\')
$download
Or you can do the above in a single line like this:
($download.Split('"')[5]).Replace('\\','\')
Btw, be super careful with quotes on this task! Since Chrome uses double quotes extensively in that preferences file, be sure to always wrap stuff in single quotes otherwise it's not going to work.
Also, as side note: in PowerShell, text in single quotes isn't parsed whereas text in double quotes is. So that's another reason why we should be wrapping stuff in single quotes here: we want the text as a simple string. If the string happened to contain, say, "$path" and we wrapped it in double-quotes, it would interpret that as the existing system variable "$path" and try to do something with it, which would be...undesirable!
I just realized there's no need to mess around with 'Select-String' - the prefs file is already comma separated, so we can just split on commas and search for the download dir based on a partial string.
This should work (change '[user]' in the first line to your account):
$prefs = Get-Content 'C:\Users\[user]\AppData\Local\Google\Chrome\User Data\Default\Preferences' # Create variable containing Chrome's preferences file for [user]
$prefs = $prefs.Split(',') # Split the file on commas (this converts the comma-separated string into an array of values)
$downloadString = $prefs | Where-Object {$_ -like '*download":*'} # Locate the string we want by searching on part of it that we already know will be there (note the '-like' and the wildcard asterisks)
$downloadString = $downloadString.Split('"')[5] # Split the string on double quotes and select the sixth item in the resulting array (arrays produced by 'Split' start at zero), which is the path we're looking for
$downloadString = $downloadString.Replace('\\','\') # Replace the double slashes in the path with single slashes
$downloadString # Print the result to the screen
There are various ways you can compress that down to just a couple of lines, but I figured since you're new, this way would make it easier to see how it works.
Hope that helped!
Chrome uses a Preferences file (no extension) to store the downloads folder path in a setting called default_directory IF the user has set up a custom path in Chrome's settings.
This Preferences file is a JSON file and can be easily read by PowerShell.
When the user has not set up a custom path, this setting is not available in the Preferences file and Chrome will use whatever is set for the current user in Windows.
To get that you will either need to read the path from registry, or use the Shell.Application COM object.
Both are handled in the code below:
$prefsPath = "$env:localappdata\Google\Chrome\User Data\Default\Preferences"
if (Test-Path -Path $prefsPath -PathType Leaf) {
$prefs = Get-Content -Path $prefsPath | ConvertFrom-Json
$downloadFolder = $prefs.download.default_directory
}
if ([string]::IsNullOrWhiteSpace($downloadFolder)) {
# Chrome is using the download folder set in Windows for the current user
# read from registry:
# the Downloads property is stored under Guid instead of friendly name..
$regPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders'
$downloadFolder = Get-ItemPropertyValue -Path $regPath -Name '{374DE290-123F-4565-9164-39C4925E467B}'
# or use the Shell.Application COM object
# $downloadFolder = (New-Object -ComObject Shell.Application).NameSpace('shell:Downloads').Self.Path
}
$downloadFolder
I am trying to write a script that automatically and silently moves a bunch of fonts into the Fonts special folder so they are available as if you had "installed" them from Explorer (by dragging and dropping, copying, or right-click and choosing Install). I have the Shell.Application part down all the way to the copy.
$FONTS = 0x14
$shell = New-Object -ComObject Shell.Application
$source = $shell.Namespace($downloaded_path)
$target = $shell.Namespace($FONTS)
$target.CopyHere($source.Items())
However, some systems may already have the fonts installed and I want the progress dialog to be hidden and any prompts to be silently accepted.
So, I'm investigating the Folder.CopyHere option flags.
4 Do not display a progress dialog box
16 Respond with "Yes to All" for any dialog box that is displayed.
I hope they are supported in this folder (some options are ignored by design). And I think these are in decimal, right? Do they need to be converted? However I pass them in, I still see both dialogs. I have tried
$options = 4 <-- don't expect int to work
$options = 0x4 <-- thought hexidecimal would be ok, the VB documentation shows &H4&
$options = "4" <-- string's the thing?
$options = [byte]4 <-- no luck with bytes
$options = [variant]4 <-- this isn't even a type accelerator!
And, if I can get one option working, how do I get both working? Do I bor them together? What about the formatting?
$options = 4 -bor 16
Or do I add them or convert them to hex?
$options = "{0:X}" -f (4 + 16)
You can use 4 -bor 16. It is hard to tell what this method expects since the type is VARIANT. I would have thought that it would take an integer value. If that doesn't work, this comment from the MSDN topic on Folder.CopyHere implies that a string should work:
function CopyFileProgress
{
param( $Source, $DstFolder, $CopyType = 0 )
# Convert the decimal to hex
$copyFlag = [String]::Format("{0:x}", $CopyType)
$objShell = New-Object -ComObject "Shell.Application"
$objFolder = $objShell.NameSpace($DestLocation)
$objFolder.CopyHere($Source, $copyFlag)
}
Although I wonder if the format string should be "0x{0:x}"?
Just be aware that for normal .NET flags style enums, you can pass multiple flags to a .NET (or command parameter) that is strongly typed to the enum like so:
$srv.ReplicationServer.Script('Creation,SomeOtherValue')
Oisin has written up some info on this subject in this blog post.
I had the same problem and found this in another thread, Worked perfectly for me.
If you want it to overwrite AND be silent change 0x10 to 0x14 (docs).
$destinationFolder.CopyHere($zipPackage.Items(), 0x14)
The Folder.CopyHere option flags may simply not work. This makes me sad. I'll have to investigate one of these other methods, all of which leave me in a bit of a bind.
Separate Process
Invoke the copy in a new process and hide the window using the ProcessStartInfo properties. I haven't implemented this yet, but I wonder if it will address the user-prompting for overwriting existing files?
Dim iProcess As New System.Diagnostics.ProcessStartInfo(AppDomain.CurrentDomain.BaseDirectory + “unzip.exe”)
iProcess.CreateNoWindow = True
Dim sArgs As String = ZippedFile
iProcess.Arguments = sArgs
iProcess.WindowStyle = ProcessWindowStyle.Hidden
Dim p As New System.Diagnostics.Process
iProcess.UseShellExecute = False
p = System.Diagnostics.Process.Start(iProcess)
p.WaitForExit(30000)
Dim s As Integer = p.ExitCode
iProcess.UseShellExecute = True
p.Dispose()
iProcess = Nothing
For Loop
Only copy non-existing items. This seems to fall down when I actually want to update an existing font with a new font file of the same name.
foreach($File in $Fontdir) {
$fontName = $File.Name.Replace(".ttf", " Regular")
$objFolderItem = $objFolder.ParseName($fontName);
if (!$objFolderItem) {
$objFolder.CopyHere($File.fullname,0x14)
}
}
Remove Existing
I'm thinking of removing all fonts of the same name as the ones I'm copying, then copying the set. Although that's kind of brutal. And I believe that there's another prompt if that font cannot be deleted because it's in use. sigh
The copy flags don't work for me. I setup a job in the install fonts script that detects the "Installing Fonts" window and send {Enter} to it so I am not overwriting existing fonts.
Start-Job –Name DetectAndClosePrompt –Scriptblock {
$i=1
[void] [System.Reflection.Assembly]::LoadWithPartialName("'System.Windows.Forms")
[void] [System.Reflection.Assembly]::LoadWithPartialName("'Microsoft.VisualBasic")
while ($i -eq 1) {
$windowPrompt = Get-Process -ErrorAction SilentlyContinue |? {$_.MainWindowTitle -like "*Installing Fonts*"}
[Microsoft.VisualBasic.Interaction]::AppActivate($windowPrompt.ID)
[System.Windows.Forms.SendKeys]::SendWait("{Enter}")
sleep 2
}
}
After all fonts are copied/installed... I remove the job, by name.
Get-Job DetectAndClosePrompt | Remove-Job -Force
That works for me on Windows 7, 8.x, & 10.
I'm seeing a number of Unzip folder operations, but really no one writing a solution to fit the Fonts folder situation. So I wrote my own! As it turns out, the Fonts folder does implement the Shell.Folder.CopyHere method, but does not honor any overloads passed for the second argument of the method. Why? Who knows! I suspect Raymond Chen of 'The Old new Thing' Windows Developer blog could explain it, but I don't know the answer. So we need instead to intelligently look for our fonts before trying to copy them, or we'll get a nasty message.
In my code, we check to see a font exists or not by checking for a match on the first four characters of the font name with a wildcard search. If the font doesn't exist, we assume this is the first time we're installing fonts on this system and set a special flag called $FirstInstall.
From then on in the script, if $FirstInstall is true, we install every font in the source font directory. On subsequent executions, we check to see if each font is a match, and if so, we abort that copy. If not, we go ahead and copy. This seems to work for most of my clients, thus far.
Here you go!
<#
.SYNOPSIS
Script to quietly handle the installation of fonts from a network source to a system
.DESCRIPTION
We Can't just move files into the %windir%\Fonts directory with a script, as a simple copy paste from command line doesn't trigger windows to note the new font
If we used that approach, the files would exist within the directory, but the font files woudln't be registered in windows, nor would applications
display the new font for use. Instead, we can make a new object of the Shell.Application type (effectively an invisible Windows Explorer Windows) and use its Copy method
Which is the functional equivalent of dragging an dropping font files into the Font folder, which does trigger the font to be installed the same as if you right clicked the font
and choose install.
.PARAMETER FontPath
The path of a folder where fonts reside on the network
.EXAMPLE
.\Install-Fonts.ps1 -FontPath "\\corp\fileshare\Scripts\Fonts"
Installing font...C:\temp\Noto\NotoSans-Bold.ttf
Installing font...C:\temp\Noto\NotoSans-BoldItalic.ttf
Installing font...C:\temp\Noto\NotoSans-Italic.ttf
Installing font...C:\temp\Noto\NotoSans-Regular.ttf
In this case, the fonts are copied from the network down to the system and installed silently, minus the logging seen here
import files needed for step 1, step 2, and step 5 of the migration process.
.EXAMPLE
.\Install-Fonts.ps1 -FontPath "\\corp\fileshare\Scripts\Fonts"
Font already exists, skipping
Font already exists, skipping
Font already exists, skipping
Font already exists, skipping
In this case, the fonts already existed on the system. Rather than display an annoying 'Overwrite font' dialog, we simply abort the copy and try the next file
.INPUTS
String.
.OUTPUTS
Console output
.NOTES
CREATED: 06/11/2015
Author: sowen#ivision.com
MODIFIED:06/11/2015
Author: sowen#ivision.com -Reserved...
#>
param
(
[Parameter(Mandatory)][string]$FontPath="C:\temp\Noto"
)
#0x14 is a special system folder pointer to the path where fonts live, and is needed below.
$FONTS = 0x14
#Make a refrence to Shell.Application
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.Namespace($FONTS)
ForEach ($font in (dir $fontsPath -Recurse -Include *.ttf,*.otf)){
#check for existing font (to suppress annoying 'do you want to overwrite' dialog box
if ((($objShell.NameSpace($FONTS).Items() | where Name -like "$($font.BaseName.Split('-')[0].substring(0,4))*") | measure).Count -eq 0){
$firstInstall = $true}
if ($firstInstall -ne $true) {Write-Output "Font already exists, skipping"}
else{
$objFolder.CopyHere($font.FullName)
Write-Output "Installing font...$($font.FullName)"
$firstInstall = $true
}
}
.\Install-Fonts.ps1 -FontPath "\\corp\fileshare\Scripts\Fonts"
There are several issues with #FoxDeploy's answer which is why it is not working. First issue is that you also want to check Fonts folder in %USERPROFILE% or you would get confirmation dialog. Second issue is that you want to avoid assuming '-' in font name.
Below is the fixed version that installs fonts from CodeFonts repo as an example:
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Drawing
# Clone chrissimpkins/codeface from which we will install fonts
if (!(Test-Path /GitHubSrc/codeface)){
git clone git://github.com/chrissimpkins/codeface.git /GitHubSrc/codeface
}
#0x14 is a special system folder pointer to the path where fonts live, and is needed below.
$FONTS = 0x14
$fontCollection = new-object System.Drawing.Text.PrivateFontCollection
#Make a refrence to Shell.Application
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.Namespace($FONTS)
# local path
$localSysPath = "$Env:USERPROFILE\AppData\Local\Microsoft\Windows\Fonts"
$localSysFonts = Get-ChildItem -Path $localSysPath -Recurse -File -Name | ForEach-Object -Process {[System.IO.Path]::GetFileNameWithoutExtension($_)}
$fontsPath="\GitHubSrc\codeface\fonts"
ForEach ($font in (dir $fontsPath -Recurse -Include *.ttf,*.otf)){
if ($localSysFonts -like $font.BaseName) {
Write-Output "SKIP: Font ${font} already exists in ${localSysPath}"
}
else {
$fontCollection.AddFontFile($font.FullName)
$fontName = $fontCollection.Families[-1].Name
#check for existing font (to suppress annoying 'do you want to overwrite' dialog box
if ((($objShell.NameSpace($FONTS).Items() | where Name -ieq $fontName) | measure).Count -eq 0){
Write-Output "INST: Font ${font}"
$objFolder.CopyHere($font.FullName)
$firstInstall = $true
}
else {
Write-Output "SKIP: Font ${font} already exists in SYSTEM FONTS"
}
}
# Read-Host -Prompt "Press Enter to continue"
}
You can just take a sum of your options. I was need to run CopyHere with two options - SILENT and NOCONFIRMATION. Look at the sample below:
function Unzip-Archive($targetpath, $destination)
{
$shell_app=new-object -com shell.application
$FOF_SILENT_FLAG = 4
$FOF_NOCONFIRMATION_FLAG = 16
$zip_file = $shell_app.namespace("$targetpath")
#Set the destination directory for the extracts
$destination = $shell_app.namespace("$destination")
#unzip the files
$destination.Copyhere($zip_file.items(), $FOF_SILENT_FLAG + $FOF_NOCONFIRMATION_FLAG)
}
I just got this to work by simply using + i.e.
function Expand-ZIPFile($file, $destination)
{
$shell = new-object -com shell.application
$zip = $shell.NameSpace($file)
foreach($item in $zip.items())
{
$shell.Namespace($destination).copyhere($item, 16+1024)
}
}
I'm trying to create a script to delete all files in a folder and it's subfolders that are older than 45 days - I know how to do this, the problem is the parent folder in question has several links to other folders within itself - how do I prevent the script from deleting the links (as a link is a file), but instead treats the links like folders and to look "inside" the links for files older than 45 days.
If that's not possible, then is it possible to create a dynamic variable or array so that the script looks inside each folder I need it to and delete any files older than 45 days? If so, how do I do that.
Currently my only other option would be to create a separate script for each folder (or create code for each script in one file) and either call them individually or use yet another script to call each script.
For reference, this is in a Windows Server 2008 R2 environment
I can't work out a full solution right now. If I get time I'll come back and edit with one. Essentially I would create a function that would call itself recursively for folders anf for links where the .TargetPath was a folder. The creation of the recursive function is pretty standard fair. The only slightly opaque part is getting the .TargetPath of a .lnk file:
$sh = New-Object -COM WScript.Shell
$sc = $sh.CreateShortcut('E:\SandBox\ScriptRepository.lnk')
$targetPath = $sc.TargetPath
That is the PS way. The VBScript version is pretty much the same with a different variable naming convention and a different method for COM object instantiation.
So here is a more complete solution. I have not set up test folders and files to test it completely, but it should be pretty much what you need:
function Remove-OldFile{
param(
$Folder
)
$sh = New-Object -COM WScript.Shell
foreach($item in Get-ChildItem $Folder){
if ($item.PSIsContainer){
Remove-OldFile $item.FullName
}elseif($item.Extension -eq '.lnk'){
Remove-OldFile $sh.CreateShortcut($item.FullName).TargetPath
}else{
if(((Get-Date) - $item.CreationTime).Days -gt 45){
$item.Delete()
}
}
}
}
Remove-OldFile C:\Scripts
Just for completeness, here is an untested off the cuff VBS solution. I warn you that it may have some syntax errors, but the logic should be fine.
RemoveOldFiles "C:\Scripts"
Sub RemoveOldFiles(strFolderPath)
Dim oWSH : Set oWSh = CreateObject("WScript.Shell")
Dim oFSO : Set oFSO = CreateObject("Scripting.FileSystemObject")
For Each oFolder in oFSO.GetFolder(strFolderPath).SubFolders
RemoveOldFiles oFolder.Path
Next
For Each oFile in oFSO.GetFolder(strFolderPath).Files
if LCase(oFSO.GetExtensionName(oFile.Name)) = "lnk" Then
RemoveOldFiles oWSH.CreateShortcut(oFile.Path).TargetPath
Else
If DateDiff("d", oFile.DateCreated, Date) > 45 Then
oFSO.DeleteFile(oFile)
End If
End If
Next
End Sub
Very high level answer:
Loop through all files in current folder.
If `file.name` ends with `.lnk` (we have a link/shortcut).
Get the path of the shortcut with `.TargetPath`
You can now pass .TargetPath the same way you would pass the name of a subdirectory when you find one to continue recursing through the directory tree.