Need help with powershell script to batch update specific shortcut targets and their actual filenames (ie: .LNK shortcut files)

Please help! - I need your assistance with coding a powershell script to batch update specific .LNK shortcut targets as well as their .LNK filenames.  I would also like for the powershell script to request input from the admin such as filename / filetype, Folder Target, OldserverName and NewServerName to pass to application via dialogue input boxes and use this input to take action otherwise it should allow to pass options to script via cmdline as well as provide the option of hardcoding into script to run automated.

For example; I have a TEST parent folder which has many .LNK shortcut files scattered in "D:\TEST" parent folder along with sub-folders which also contain misc. LNK files along with many other file types.

I would like for the powershell script to search specifically for the filename in question in our case "*.LNK" in "D:\TEST" and be recursive and identify any .LNK that contain the OldServerName in this example "\\oldserver" and *Replace* it with NewServerName such as "\\newserver".  Currently the below script will accomplish this without any issues.

However; If one of the files for example was called "Accounting on OldServer.lnk" and had a target of "\\oldserver\accounting" - the above script would currently update the file to point to "\\newserver\accounting" however - the name currently would remain as "Accounting on OldServer.lnk" instead of changing to "Accounting on NewServer.lnk"

I would also like for the script to audit every change it makes in an HTML LOG file.  So I would have an audit log of what changes it has completed.  It would be nice if it can create an HTML FILE that would have the below information:

Old Filename                                  New Filename                                  Old Target Path                                       New Target Path
Accounting on OldServer.lnk       Accounting on NewServer.lnk           \\oldserver\accounting                          \\newserver\accounting
HR on Oldserver.lnk                     HR on NewServer.lnk                       \\oldserver.domain.local\hr                \\newserver.domain.local\hr


It would be nice to have some type of error protection should below be example be encountered:

Old Filename:        IS on Oldserver.lnk
Old Target Path:   \\oldserver.domain.local\IS

Old Filename:        Sales on Oldserver.lnk
Old Target Path:    \\oldserver\Sales

And I type in for old server name "\\oldserver" and for new server name I type in "newserver.domain.local" - below would be the result which would cause a problem with "IS" shortcut.

New Filename:         IS on NewServer.lnk
New Target Path:    \\newserver.domain.local.domain.local\IS             --->  This would be invalid - due to no error protection.

New Filename:        Sales on NewServer.lnk
New Target Path:    \\newserver.domain.local\Sales


Below is script I currently have:
$fileName ="*.LNK" 
$folder = "D:\TEST" 
[string]$fromunc = "\\oldserver" 
[string]$tounc = "\\newserver" 
$list = Get-ChildItem -Path $folder -Filter $fileName -Recurse  | Where-Object { $_.Attributes -ne "Directory"} | select -ExpandProperty FullName 
$obj = New-Object -ComObject WScript.Shell 
 
ForEach($lnk in $list) 
      { 
      $obj = New-Object -ComObject WScript.Shell 
      $link = $obj.CreateShortcut($lnk) 
      [string]$path = $link.TargetPath  
      [string]$path = [string]$path.Replace($fromunc.tostring(),$tounc.ToString()) 
      $link.TargetPath = [string]$path 
      $link.Save()
  }

Open in new window

sylarAsked:
Who is Participating?

[Product update] Infrastructure Analysis Tool is now available with Business Accounts.Learn More

x
I wear a lot of hats...

"The solutions and answers provided on Experts Exchange have been extremely helpful to me over the last few years. I wear a lot of hats - Developer, Database Administrator, Help Desk, etc., so I know a lot of things but not a lot about one thing. Experts Exchange gives me answers from people who do know a lot about one thing, in a easy to use platform." -Todd S.

Shaun VermaakTechnical Specialist IVCommented:
Do you have Active Directory?
0
sylarAuthor Commented:
Shaun:   Thank you for comment.  Yes, I am in an AD environment.
0
Shaun VermaakTechnical Specialist IVCommented:
You can manage these shortcut via AD GPO User/Computer shortcuts
0
Webinar: Miercom Evaluates Wi-Fi Security

It's not just about Wi-Fi connectivity anymore. A wireless security breach can cost your business large amounts of time, trouble, and expense. Plus, hear first-hand from Miercom how WatchGuard's Wi-Fi security stacks up against the competition in our upcoming webinar!

sylarAuthor Commented:
Shaun:    Yes I am aware however these particular shortcuts which run in hundreds and are located everywhere I am not looking to manage via GPO and need a solution which works via a script that I can run once upon migrating to a new server.  I do not want to rely on AD GPO in my environment.
0
Shaun VermaakTechnical Specialist IVCommented:
$fileName ="*.LNK" 
$folder = "D:\TEST" 
[string]$fromunc = "oldserver" 
[string]$tounc = "newserver" 
$list = Get-ChildItem -Path $folder -Filter $fileName -Recurse  | Where-Object { $_.Attributes -ne "Directory"} | select -ExpandProperty FullName 
$obj = New-Object -ComObject WScript.Shell 
 
ForEach($lnk in $list) 
      { 
      $obj = New-Object -ComObject WScript.Shell 
      $lnkName = [string]$lnk.Replace($fromunc.tostring(),$tounc.ToString()) 
      $link = $obj.CreateShortcut($lnkName) 
      [string]$path = $link.TargetPath  
      [string]$path = [string]$path.Replace("\\$($fromunc.tostring())","\\$($tounc.ToString())") 
      $link.TargetPath = [string]$path 
      $link.Save()
  }

Open in new window

0
sylarAuthor Commented:
Shaun:    Thanks again for your help.   I have tried the above code - however; it does not seem to update the Target unc path at all - even though I can see the modified date of the shortcut changes to current date/time.  Also the filename of the actual shortcut is also not updated.  If I use my original code - I am able to update all the shortcuts with the updated Target unc path.
0
Qlemo"Batchelor", Developer and EE Topic AdvisorCommented:
Honestly, looking whether to use the domain or not makes things more complex as it is worth it, as you are under full control of the replacement parts - just don't use the domain there.
I've removed a lot of unnecessary stuff.
$folder  = "D:\TEST" 
$fromSrv = "Oldserver" 
$toSrv   = "Newserver"

# static code
$wShell = New-Object -ComObject WScript.Shell 

Get-ChildItem -File -Path $folder -Filter "*.lnk" -Recurse |
% {
  $link = $wShell.CreateShortcut($_.FullName)
  $log = PsCustomObject @{'Location'        = $_.DirectoryName
                          'Old Filename'    = $_.Name
                          'New Filename'    = $null
                          'Old Target Path' = $link.Targetpath
                          'New Target Path' = $null}

  $link.TargetPath       = $link.TargetPath       -replace ('\\'+$fromSrv), ('\\'+$toSrv)
  $link.WorkingDirectory = $link.WorkingDirectory -replace ('\\'+$fromSrv), ('\\'+$toSrv)
  $link.IconLocation     = $link.IconLocation     -replace ('\\'+$fromSrv), ('\\'+$toSrv)
  $link.Save()
  $log.'New Target Path' = $link.TargetPath
  $link = $null

  $log.'New FileName' = $_ | Rename-Item -NewName ($_.Name -replace $fromSrv, $toSrv) -PassThru | Select -Expand Name

  $log
} | ConvertTo-HTML C:\Temp\Results.html

Open in new window

The HTML log should be in C:\Temp\Results.csv, with basic formatting only.
The script requires PowerShell 4, but we can change that if necessary.
0
sylarAuthor Commented:
Qlemo:   I LOVE it.   Many thanks for your help.  The script originally errored out in two locations but I was able to identify the issues and fixed below:

Original code snippet:
$log = PsCustomObject @{'Location'        = $_.DirectoryName

changed to:
$log = [PsCustomObject] @{'Location'        = $_.DirectoryName

Original code snippet:
} | ConvertTo-HTML C:\Temp\Results.html

changed to:
} | ConvertTo-HTML | Out-File C:\Temp\Results.html

I do have two additional concerns:
1.   After testing the script on hundreds of .LNK files in the test folder / sub-folders, I noticed the script will update the modification date for .LNK that really do NOT match the old server criteria.  Based on the filter currently it looks like it will simply look for any LNK files - and NOT skip .LNK files that do not have the "oldserver" server name in target.  Is there a way to produce logic into the modified script that would account for that.  This way the script will skip over any .LNK files that DO NOT match the criteria.

2.    For the section below:
$folder  = "D:\TEST"
$fromSrv = "Oldserver"
$toSrv   = "Newserver"

Is there anyway we can make the script more interactive of which we can allow for it to accept parameters via cmdline and if none are passed to it, it will open a GUI dialogue box requesting for Folder Name, FromServer and ToServer reading in the defaults from the actually settings within the powershell script.  This way should I want to accept the defaults I can just hit enter all thru the dialogue boxes?

The current script now reads as below:
$folder  = "D:\TEST" 
$fromSrv = "Oldserver" 
$toSrv   = "Newserver"

# static code
$wShell = New-Object -ComObject WScript.Shell 

Get-ChildItem -File -Path $folder -Filter "*.lnk" -Recurse |
% {
  $link = $wShell.CreateShortcut($_.FullName)
  $log = [PsCustomObject] @{'Location'        = $_.DirectoryName
                          'Old Filename'    = $_.Name
                          'New Filename'    = $null
                          'Old Target Path' = $link.Targetpath
                          'New Target Path' = $null}

  $link.TargetPath       = $link.TargetPath       -replace ('\\'+$fromSrv), ('\\'+$toSrv)
  $link.WorkingDirectory = $link.WorkingDirectory -replace ('\\'+$fromSrv), ('\\'+$toSrv)
  $link.IconLocation     = $link.IconLocation     -replace ('\\'+$fromSrv), ('\\'+$toSrv)
  $link.Save()
  $log.'New Target Path' = $link.TargetPath
  $link = $null

  $log.'New FileName' = $_ | Rename-Item -NewName ($_.Name -replace $fromSrv, $toSrv) -PassThru | Select -Expand Name

  $log
} | ConvertTo-HTML | Out-File C:\Temp\Results.html

Open in new window

0
Qlemo"Batchelor", Developer and EE Topic AdvisorCommented:
Stupid errors, those :<. Great you were able to correct that yourself. I appreciate talking to someone who understands the code :D.

Of course we can improve the script. That it touches all links did not get my attention because the original script did it the same way ;-). Assuming that we only change links where the target path needs adjustment, we can filter on a lot of ways. With the current code is easier to apply some checks inside the foreach-object loop than to choose alternatives. Luckily we have old and new targetpath at hand, and can compare those ...
So: first step is to replace lines 20-26 with:
  $log.'New Target Path' = $link.TargetPath
  if ($log.'Old Taget Path' -ne $log.'New Target Path')
  {
    $link.Save()
    $log.'New FileName' = $_ | Rename-Item -NewName ($_.Name -replace $fromSrv, $toSrv) -PassThru | Select -Expand Name
    $log
  }
  $link = $null

Open in new window

Now for your other improvement request. Yes, we can make the script use parameters, and prompt if any of them has not been provided. But I won't use a GUI for that - constructing a real input form isn't done easily (manually, without using a wizard). The most simple way is to use  a text prompt, the next to use default input prompt windows.
I would approach it like this - replace the first three lines with:
param([String] $folder, [String] $fromSrv, [String] $toSrv)

$defaults = @{folder  = 'D:\TEST'
              fromSrv = 'Oldserver'
              toSrv   = 'Newserver'}

if (!$folder ) { $folder  = Read-Host "Please provide a folder name to start the link search in [$($defaults['folder'])]" }
if (!$fromSrv) { $fromSrv = Read-Host "Please provide the old server name [$($defaults['fromSrv'])]" }
if (!$tosrv  ) { $toSrv   = Read-Host "Please provide the new server name [$($defaults['toSrv'  ])]" }

if (!$folder ) { $folder  = $defaults['folder' ] }
if (!$fromSrv) { $fromSrv = $defaults['fromSrv'] }
if (!$toSrv  ) { $toSrv   = $defaults['toSrv'  ] }

Open in new window

You can now call the file with or without parameters, named or not:
.\change-links.ps1  'D:\TEST' 'Oldserver' 'Newserver'                         # note: sequence of values important!
.\change-links.ps1  -folder 'D:\TEST' -fromSrv 'Oldserver' -toSrv 'Newserver' # note: sequence does not matter
.\change-links.ps1  'D:\TEST' -fromSrv 'Oldserver' -toSrv 'Newserver'         # and mixed

Open in new window

As noted, you can provide parameters with names in any sequence, but without you need to keep it. So you can write
.\change-links.ps1  -toSrv 'Newserver'  -fromSrv 'Oldserver' 'D:\TEST'
.\change-links.ps1  -fromSrv 'Oldserver'  'D:\TEST' -toSrv 'Newserver' 
.\change-links.ps1  'D:\TEST' 'Oldserver' -toSrv 'Newserver' 
# but not
.\change-links.ps1  'Oldserver'  'D:\TEST' -toSrv 'Newserver' 

Open in new window

0
sylarAuthor Commented:
Qlemo:   Thank you for the prompt response.  You are a genius and so helpful.   Everything is working great with the exception of I am still noticing any .LNK shortcut files that do not currently have a target of what I am filtering for would have their dates modified to recent date and time even though we have NOT updated the target URL...

For ex:   I have 5 .LNK files in a directory named D:\TEST

Sales on Oldserver-1.lnk    (Target:  \\Oldserver\sales)    :  Date:     3-18-2017 / Time:   5:00PM
Sales on Oldserver-2.lnk    (Target:  \\Oldserver\sales)    :  Date:     3-18-2017 / Time:   5:00PM
Sales on Oldserver-3.lnk    (Target:  \\Oldserver\sales)    :  Date:     3-18-2017 / Time:   5:00PM
Sales on Oldserver-4.lnk    (Target:  \\Oldserver\sales)    :  Date:     3-18-2017 / Time:   5:00PM
Test on LABserver-1.lnk    (Target:  \\LABserver\test)       :  Date:     3-18-2017 / Time:   5:00PM

When I run the script against these .LNK files - all 5 files are updated with below:

Sales on Newserver-1.lnk    (Target:  \\Newserver\sales)    :  Date:     3-18-2017 / Time:   9:00PM
Sales on Newserver-2.lnk    (Target:  \\Newserver\sales)    :  Date:     3-18-2017 / Time:   9:00PM
Sales on Newserver-3.lnk    (Target:  \\Newserver\sales)    :  Date:     3-18-2017 / Time:   9:00PM
Sales on Newserver-4.lnk    (Target:  \\Newserver\sales)    :  Date:     3-18-2017 / Time:   9:00PM
Test on LABserver-1.lnk       (Target:  \\LABserver\test)       :  Date:     3-18-2017 / Time:   9:00PM

Notice even though the script was smart enough to identify that 'Test on Labserver-1.lnk' target does not match what we are filtering for it somehow continues to either rewrite the original contents or touch the file... I thought it was a windows bug - so I attempted to manually try to right-click on a shortcut look at the target then cancel out of the properties to see if just accessing the shortcut would cause that.  The great news it did not.  I then right-clicked the file again but this time instead of clicking cancel - I simply clicked 'OK' just to see if that would somehow cause the date/time to change - and the great news it did NOT.

I believe there is something we are missing with the code in which it is re-writing the original contents back out when it doesn't match which causes the date/time to update.  Could it be from the below snippet of code:

Get-ChildItem -File -Path $folder -Filter "*.lnk" -Recurse |
0
sylarAuthor Commented:
Qlmo:     Additionally with the same theory in mind - I noticed if I re-run the script again after it is successful on the four *.LNK files that it has successfully updated the targets for - it will process and re-touch the same four *.LNK again - even though they NOW no longer match the filter and the script will change the date/time of the four files again on the second run.
0
Qlemo"Batchelor", Developer and EE Topic AdvisorCommented:
There is a small typo causing that. The IF didn't work, there was always a change detected, and hence each link saved ...
Here the complete code - note that I have removed the log file now, as you should always either have a parameter for it, or provide log info as result in the pipeline (with the latter being my choice here):
param([String] $folder, [String] $fromSrv, [String] $toSrv)

$defaults = @{folder  = 'D:\TEST'
              fromSrv = 'Oldserver'
              toSrv   = 'Newserver'}

if (!$folder ) { $folder  = Read-Host "Please provide a folder name to start the link search in [$($defaults['folder'])]" }
if (!$fromSrv) { $fromSrv = Read-Host "Please provide the old server name [$($defaults['fromSrv'])]" }
if (!$tosrv  ) { $toSrv   = Read-Host "Please provide the new server name [$($defaults['toSrv'  ])]" }

if (!$folder ) { $folder  = $defaults['folder' ] }
if (!$fromSrv) { $fromSrv = $defaults['fromSrv'] }
if (!$toSrv  ) { $toSrv   = $defaults['toSrv'  ] }

# static code
$wShell = New-Object -ComObject WScript.Shell 

Get-ChildItem -File -Path $folder -Filter "*.lnk" -Recurse |
% {
  $link = $wShell.CreateShortcut($_.FullName)
  $log = [PsCustomObject] @{'Location'        = $_.DirectoryName
                            'Old Filename'    = $_.Name
                            'New Filename'    = $null
                            'Old Target Path' = $link.Targetpath
                            'New Target Path' = $null}

  $link.TargetPath       = $link.TargetPath       -replace ('\\'+$fromSrv), ('\\'+$toSrv)
  $link.WorkingDirectory = $link.WorkingDirectory -replace ('\\'+$fromSrv), ('\\'+$toSrv)
  $link.IconLocation     = $link.IconLocation     -replace ('\\'+$fromSrv), ('\\'+$toSrv)
  $log.'New Target Path' = $link.TargetPath
  if ($log.'Old Target Path' -ne $log.'New Target Path')
  {
    $link.Save()
    $log.'New FileName' = $_ | Rename-Item -NewName ($_.Name -replace $fromSrv, $toSrv) -PassThru | Select -Expand Name
    $log
  }
  $link = $null
}

Open in new window

You now call it this way:
.\change-links.ps1  'D:\TEST' 'Oldserver' 'Newserver'  | ConvertTo-HTML | Out-File C:\Temp\Results.html

Open in new window

Or you add another parameter for the log file - should be easy for you now ;-).
0

Experts Exchange Solution brought to you by

Your issues matter to us.

Facing a tech roadblock? Get the help and guidance you need from experienced professionals who care. Ask your question anytime, anywhere, with no hassle.

Start your 7-day free trial
sylarAuthor Commented:
Hi Qlemo:   You're amazing! - This worked like a charm and hit every bullet-point that I was looking for.  Thanks for the quick turn-around buddy.  I have learned a lot just from your examples.
0
sylarAuthor Commented:
Hi Qlemo:   You're amazing! - This worked like a charm and hit every bullet-point that I was looking for.  Thanks for the quick turn-around buddy.  I have learned a lot just from your examples.
0
Qlemo"Batchelor", Developer and EE Topic AdvisorCommented:
You have been a good apprentice :D.
0
It's more than this solution.Get answers and train to solve all your tech problems - anytime, anywhere.Try it for free Edge Out The Competitionfor your dream job with proven skills and certifications.Get started today Stand Outas the employee with proven skills.Start learning today for free Move Your Career Forwardwith certification training in the latest technologies.Start your trial today
Powershell

From novice to tech pro — start learning today.