2017-11-13

Add/remove features and patches/language packs to offline Windows 10 WIM from ISO file with Powershell

I could not find a script that mounts an official MS Windows 10 ISO, adds .NET 3.5 and patches automatically. So.. I made one myself.

Please share any improvements and run at your own risk! 

#=== SCRIPT BEGINS ===
<#
.SYNOPSIS
Mount an ISO, enable and or disable features and patches to WIM and save.
.DESCRIPTION
Mount ISO, copy install.wim, mount the file and enable/disable Windows features and patches, and save the changes.
.NOTES
David Djerf, 2017-10-11 - First version that only adds .NET 3.5
David Djerf, 2017-10-13 - Added automatic mounted drive letter detection, made it possible to add and remove custom Windows features, and patches.
David Djerf, 2017-10-16 - Added support for patchfolders
David Djerf, 2017-12-01 - Added export index to new file
David Djerf, 2017-12-08 - Fixed Verbose message for patches not writing name of patches
    David Djerf, 2018-04-03 - Added detection for WIM or ESD install-file.

.PARAMETER ISOfile
Full path to Windows 10 ISO file
.PARAMETER WIMIndex
Index number to modify, eg. 1
You can list the index using the DISM command: dism /get-wiminfo /wimfile:<PATH_TO_WIM>
.PARAMETER NewFileName
Your_new_file.wim
Will default to install_modified.wim if not specified.
.PARAMETER WorkFolder
Folder where the magic happens...
If not specified C:\WorkFolder will be used
.PARAMETER Patches
Full path to windows patches in cab or msu format, multiple files accepted if comma separated.
eg. "c:\patches\KB12345.msu","c:\patches\KB654321.msu"
.PARAMETER Patchfolder
Path to a folder containing windows patches in cab or msu format. All patches will be applied.
eg. c:\patches
.PARAMETER WIMAddFeatures
Add Windows features based on their proper "Featurenames"
.PARAMETER WIMRemoveFeatures
Remove Windows features based on their proper "Featurenames" (get features with Dism /Image:C:\workfolder /Get-Features)
.PARAMETER ExportIndexFile
Will export the define index to a new file, provide the full file path.
.PARAMETER Verbose
Add -Verbose to get information about what step the script is running.
.EXAMPLE
modify-windows_wim.ps1 -ISOfile C:\Users\admin\Downloads\SW_DVD5_Win_Pro_Ent_Edu_N_10_1709_64BIT_English_MLF_X21-50143.ISO -WIMIndex 1 -Workfolder C:\mountfolder -NewFileName my_new_install.wim -WIMAddFeatures "NetFx3" -WIMRemoveFeatures "SMB1Protocol" -Patches "C:\Patches\KB123456.msu","C:\Patches\KB654321.msu" -Verbose
modify-windows_wim.ps1 -ISOfile C:\Users\admin\Downloads\SW_DVD5_Win_Pro_Ent_Edu_N_10_1709_64BIT_English_MLF_X21-50143.ISO -WIMIndex 1 -Workfolder C:\mountfolder -NewFileName my_new_install.wim -WIMAddFeatures "NetFx3" -WIMRemoveFeatures "SMB1Protocol" -Patchfolder "C:\Patches" -Verbose

#>
#Requires -Version 4.0
#Requires -RunAsAdministrator

[CmdletBinding(SupportsShouldProcess=$True)]
Param(
        [Parameter(Mandatory=$True,Position=0)]$ISOfile,
        [Parameter(Mandatory=$True,Position=1)]$WIMIndex,
        [Parameter(Mandatory=$False,Position=3)]$NewFileName,
        [Parameter(Mandatory=$False,Position=4)]$Workfolder,
        [string[]]$WIMAddFeatures,
        [string[]]$WIMRemoveFeatures,
        [string[]]$Patches,
        [string]$Patchfolder,
[string]$ExportIndexFile
    )

Begin{
IF (!$WorkFolder) { 
$WorkFolder = "C:\WorkFolder"
} # End of IF
IF (!$NewFileName) { 
$NewFileName = "install_modified.wim"
} # End of IF

# Make sure patchfolder is readable
IF ($Patchfolder) {
IF (Test-Path -path "$Patchfolder") {
Write-Verbose "'$Patchfolder' could be read"
} else { Write-Verbose "'$Patchfolder' could not be read, aborting!"
Write-Verbose "Make sure you the path is correct and can be read."
Break
} # End of IF
}

# Mount ISO if exist
IF (Test-Path -path "$ISOfile") {
Write-Verbose "'$ISOfile' could be read, Mounting ISO to $MountDrive"
$MountDrive=(Mount-Diskimage $ISOfile -PassThru | Get-Volume).DriveLetter+":"
} else { Write-Verbose "'$ISOfile' could not be read, aborting!"
Write-Verbose "Make sure you the path is correct and can be read."
Break
} # End of IF

# Failsafe, make sure wim or esd exists
IF (Test-Path -path "$MountDrive\Sources\install.wim") {
Write-Verbose "'$MountDrive\Sources\install.wim' exists, engage!"
        $InstallFile="$MountDrive\Sources\install.wim"
} elseif (Test-Path -path "$MountDrive\Sources\install.esd") {
Write-Verbose "'$MountDrive\Sources\install.esd' exists, engage!"
        $InstallFile="$MountDrive\Sources\install.esd"
} else { 
Write-Verbose "'$MountDrive\Sources\install.wim' or '$MountDrive\Sources\install.esd' does not exist, aborting!"
Write-Verbose "Make sure you specified the correct drive letter for the new virtual drive."
Dismount-Diskimage $ISOfile
Break
} # End of IF

    # Copy install-file to newfile in temp folder (to make sure the file is not on a network share that may be disconnected)
IF (Test-Path -path "$ENV:TEMP\$NewFileName") {
Write-Verbose "$NewFileName exists, make it writable"
Set-ItemProperty "$ENV:TEMP\$NewFileName" -name IsReadOnly -value $false
} else {
Write-Verbose "Copy $InstallFile to %TEMP%\$NewFileName"
Copy-Item $InstallFile -Destination "$ENV:TEMP\$NewFileName" -force
Write-Verbose "Make $NewFileName writable"
Set-ItemProperty "$ENV:TEMP\$NewFileName" -name IsReadOnly -value $false
} # End of IF

# Make sure $Workfolder exists and is empty
IF (Test-Path -path "$WorkFolder") {
Write-Verbose "$WorkFolder exists, discarding content"
Dismount-WindowsImage -Path $WorkFolder -Discard
Write-Verbose "$WorkFolder exists, removing folder"
Remove-Item -Recurse $WorkFolder -force
Write-Verbose "Create $WorkFolder"
New-Item -ItemType directory -Path $WorkFolder -force | Out-Null
} else {
Write-Verbose "Create $WorkFolder"
New-Item -ItemType directory -Path $WorkFolder -force | Out-Null
} # End of IF
Write-Verbose "Mount $NewFileName to $WorkFolder"
Mount-WindowsImage -ImagePath "$ENV:TEMP\$NewFileName" -Index $WIMIndex -Path $WorkFolder -Optimize
} # End of BEGIN

Process{
# Add Features if specified
IF ($WIMAddFeatures) { 
ForEach ($WIMAddFeature in $WIMAddFeatures) {
Write-Verbose "Adding $WIMAddFeature to $NewFileName"
dism /image:$WorkFolder /enable-feature /featurename:$WIMAddFeature /all /source:$MountDrive\sources\sxs
} # End of ForEach
} else {
Write-Verbose "No feature to add..."
}# End of IF

# Remove Features if specified
IF ($WIMRemoveFeatures) { 
ForEach ($WIMRemoveFeature in $WIMRemoveFeatures) {
Write-Verbose "Removing $WIMRemoveFeature to $NewFileName"
dism /image:$WorkFolder /disable-feature /featurename:$WIMRemoveFeature
} # End of ForEach
} else {
Write-Verbose "No feature to remove..."
}# End of IF

# Apply Windows patches if specified
IF ($Patches) {
$Patch=""
ForEach ($Patch in $Patches) {
Write-Verbose "Applying $Patch to $NewFileName"
dism /Image:$WorkFolder /add-package /packagepath:$Patch
} # End of ForEach
} else {
Write-Verbose "No patch to apply..."
}# End of IF

IF ($Patchfolder) {
$Patch=""
$Patches=Get-ChildItem $Patchfolder -Filter *.msu | % { $_.FullName }
ForEach ($Patch in $Patches) {
Write-Verbose "Applying $Patch to $NewFileName"
dism /Image:$WorkFolder /add-package /packagepath:$Patch
} # End of ForEach
} else {
Write-Verbose "No patch to apply..."
}# End of IF
} #End of Process

End{
Write-Verbose "Dismount ISO"
Dismount-Diskimage $ISOfile
Write-Verbose "Saving changes to $ENV:TEMP\$NewFileName"
Dismount-WindowsImage -Path $WorkFolder -Save -CheckIntegrity
IF ($ExportIndexFile) {
Write-Verbose "Exporting Index $WIMIndex to $ExportIndexFile"
Export-WindowsImage -SourceImagePath "$ENV:TEMP\$NewFileName" -SourceIndex $WIMIndex -DestinationImagePath $ExportIndexFile
} # End of if
Write-Verbose "Removing $WorkFolder"
Remove-Item -Recurse $WorkFolder -force
Write-Verbose "Finished saving $ENV:TEMP\$NewFileName"
Write-Verbose "You can now test your new file!"
}# End of End
#=== SCRIPT ENDS ===

2017-06-01

UE-V Template Scoother Software - Beyond Compare v4

You can now find this template in my GIThub repository


<?xml version="1.0"?>
<SettingsLocationTemplate xmlns="http://schemas.microsoft.com/UserExperienceVirtualization/2013A/SettingsLocationTemplate">
 <Name>Beyond Compare</Name>
 <ID>Scooter_Software-Beyond_Compare-v4</ID>
 <Version>1</Version>
 <Author>
  <Name>David Djerf</Name>
 </Author>
 <Processes>
  <Process>
   <Filename>BCompare.exe</Filename>
   <ProductVersion>
    <Major Minimum="4" Maximum="4" />
   </ProductVersion>
   <FileVersion>
    <Major Minimum="4" Maximum="4" />
   </FileVersion>
  </Process>
 </Processes>
 <Settings>
  <File>
   <Root>
    <EnvironmentVariable>APPDATA</EnvironmentVariable>
   </Root>
   <Path Recursive="true">Scooter Software\Beyond Compare 4</Path>
  </File>
 </Settings>
</SettingsLocationTemplate>

UE-V Template - Meldmerge.org - Meld

You can now find this template in my GIThub repository


<?xml version="1.0"?>
<SettingsLocationTemplate xmlns="http://schemas.microsoft.com/UserExperienceVirtualization/2013A/SettingsLocationTemplate">
 <Name>Meldmerge.org - Meld</Name>
 <ID>Meldmerge_org-Meld</ID>
 <Version>1</Version>
 <Author>
  <Name>David Djerf</Name>
 </Author>
 <Processes>
  <Process>
   <Filename>Meld.exe</Filename>
  </Process>
 </Processes>
 <Settings>
  <Registry>
   <Path>Software\GSettings\org\gnome\meld</Path>
  </Registry>
 </Settings>
</SettingsLocationTemplate>

UE-V Template - The Eclipse Foundation - Eclipse

You can now find this template in my GIThub repository


<?xml version="1.0"?>
<SettingsLocationTemplate xmlns="http://schemas.microsoft.com/UserExperienceVirtualization/2013A/SettingsLocationTemplate">
 <Name>Eclipse</Name>
 <ID>Eclipse_Foundation-Eclipse</ID>
 <Version>1</Version>
 <Author>
  <Name>David Djerf</Name>
 </Author>
 <Processes>
  <Process>
   <Filename>eclipse.exe</Filename>
  </Process>
 </Processes>
 <Settings>
  <File>
   <Root>
    <EnvironmentVariable>USERPROFILE</EnvironmentVariable>
   </Root>
   <Path Recursive="true">workspace\.metadata</Path>
  </File>
  <File>
   <Root>
    <EnvironmentVariable>USERPROFILE</EnvironmentVariable>
   </Root>
   <Path Recursive="true">.eclipse</Path>
  </File>
 </Settings>
</SettingsLocationTemplate>

UE-V Template - Open Visualization Tool (OVITO)

You can now find this template in my GIThub repository


<?xml version="1.0"?>
<SettingsLocationTemplate xmlns="http://schemas.microsoft.com/UserExperienceVirtualization/2013A/SettingsLocationTemplate">
 <Name>OVITO - Open Visualization Tool</Name>
 <ID>ovito_org-ovito</ID>
 <Version>1</Version>
 <Author>
  <Name>David Djerf</Name>
 </Author>
 <Processes>
  <Process>
   <Filename>ovito.exe</Filename>
  </Process>
 </Processes>
 <Settings>
  <Registry>
   <Path Recursive="true">SOFTWARE\Ovito</Path>
  </Registry>
 </Settings>
</SettingsLocationTemplate>

UE-V Template - CSDiff

You can now find this template in my GIThub repository


<?xml version="1.0"?>
<SettingsLocationTemplate xmlns="http://schemas.microsoft.com/UserExperienceVirtualization/2013A/SettingsLocationTemplate">
 <Name>ComponentSoftware - CSDiff</Name>
 <ID>ComponentSoftware-CSDiff</ID>
 <Version>1</Version>
 <Author>
  <Name>David Djerf</Name>
 </Author>
 <Processes>
  <Process>
   <Filename>CSDiff.exe</Filename>
  </Process>
 </Processes>
 <Settings>
  <Registry>
   <Path Recursive="true">Software\ComponentSoftware</Path>
   <Exclude>
    <Path>CSDiffPath</Path>
   </Exclude>
  </Registry>
 </Settings>
</SettingsLocationTemplate>

Send escape to Powerpoint if idle in presentation mode for too long

Problem to solve

Students set powerpoint to presentation mode when they go to lunch etc leaving the computer unlocked and the screen saver wont start (I don't know why, because we allow them to lock the screen for 75 minutes before they get logged off.)

Not only is this a violation of the University computer agreement, they also open for others to use their account to browse the Internet or to copy/delete the files etc. In short, we need to fix this.

What we need to do is to check if the computer is idle for real (not the same as the screen saver looks for) and then let the powerpoint presentation run for 10 minutes at idle before we send escape to exit presentation mode. If they run the presentation for real, they will most likely send new key/mouse presses when they change slide and idletime resets.

To begin with we tried to use a powershell script made by my collegue Lillian.
We tried at least 5 different ways to run the script as a scheduled task, but it would never press escape in powerpoint. (if you make this work please let me know!)
The scheduled job used cscript to run a vbs-script that runs powershell that runs the powershell script. VBS script is used to make the powershell window invisible.
Then I made a very simple "send escape" EXE-compiled AutoIt script as scheduled task and it worked, next step was to convert the powershell code into AutoIt.

The script

If you want another allowed idle time, hust change the number "10" on the line including this:($iIdleTime/1000/60 > 10)

==== Script begins ====

#cs ----------------------------------------------------------------------------

 AutoIt Version: 3.3.14.2
 Author:         Lilian Zachrisson

 Script Function:
Closes PowerPoint presenteation if idle time 10 min or above.

#ce ----------------------------------------------------------------------------

; Script Start - Add your code below here
#include <MsgBoxConstants.au3>
#include <Timers.au3>

Local $iIdleTime = _Timer_GetIdleTime()

If ProcessExists("POWERPNT.EXE") And ($iIdleTime/1000/60 > 10) Then ; Check if the Powerpoint process is running.
$aWindows = WinList("[REGEXPTITLE:(?i)(PowerPoint Slide show.*)]")
For $i=1 To $aWindows[0][0]
; Close the Powerpoint window using the handle returned by WinWait.
WinClose($aWindows[$i][1])
MsgBox($MB_SYSTEMMODAL, "", "Powerpoint presentation was closed do to inactivity")
Next
EndIf

==== Script ends begins ====

How it runs

The script is compiled into an EXE-file with the Aut2exe.exe tool and is copied to the local computers by a GPO. (in the Computer part of GPO)
The same GPO create a scheduled task that runs every 5 minutes as the logged on user (in the USER part of GPO).

Problems encountered

Heads up! There is a bug(?) in the task scheduler that make repeating jobs halt if you run them manually (like when you spend the entire day testing stuff).
The solution for me was to disable the "allow task to run on demand" option and use two triggers. One that is on a time schedule and repeating every 5 minutes. The other is triggered on user logon and repeats every 5 minutes. Maybe Ill try to remove one of them later on (I'll update my text then).

2017-05-12

Cleanup All users desktop and Start menu

Problem to solve

Clean all shortcuts on the all users desktop (because users cannot delete them) and also remove unwanted shortcuts from the Start menu such as "Uninstall"-shortcuts.

I run this on our student computers in computer rooms where the Onedrive client cannot be used, instead we mount Onedrive as a mapped folder instead using OneDriveMapper.
We also want to remove unnecessary shortcuts for Java & Silverlight.

Description of the script

The script is based on several functions that could be used to remove files and folers other than the desktop and start menu shortcuts.

Remove-UnwantedFolders - Remove folders in a path
Remove-UnwantedFiles - Remove files in a path
Remove-UninstallLinks - Searches for and removes any shortcuts that begins with "Uninstall" in a path
Remove-EmptyFolders - Removes any empty folders in a path
Remove-FiletypeFromFolder - Removes all files of defined type from a path

Read the script comments for more details.
Change the files/folders/file types in the end of the script to adapt to your needs.

The script

=== SCRIPT Cleanup-Desktop_and_StartMenu.ps1 BEGINS ===
# David Djerf 2017-05-11

Function Remove-UnwantedFolders {
    <#
    .SYNOPSIS
        Removes unwanted folders in a path
    .DESCRIPTION
        Will remove one or more folders from a path, for example use this to remove unwanted folders in the start menu.
    .NOTES
   davpe67 2017-02-18
    .PARAMETER WorkFolder
   Define a path where script should seach recursive for the folders to remove
   For example: "C:\" or "$ENV:Temp"
    .PARAMETER SearchStartMenu
        Will Set Workfolder to "$ENV:Programdata\Microsoft\Windows\Start Menu"
    .PARAMETER UnwantedFolders
        Specify one or more folders you want to remove.
        For example: "Java","Silverlight"
    .EXAMPLE
   Remove-UnwantedFolders -SearchStartMenu -UnwantedFolders "Java","Unique Admin Generator" -WhatIf
    #>

    [cmdletbinding(SupportsShouldProcess=$True,DefaultParameterSetName="WorkFolder")]
    Param(
        [Parameter(Mandatory=$True, ParameterSetName="WorkFolder", Position=0, HelpMessage='Specify folder(s) serch in: ["C:\path\to\folders"]')]
        [string]$WorkFolder,
        [Parameter(Mandatory=$True, ParameterSetName="SearchStartMenu", Position=0)]
        [switch]$SearchStartMenu,
        [Parameter(Mandatory=$True, HelpMessage='Specify folder(s) to remove: ["Folder1","Folder"]')]
        [string[]]$UnwantedFolders
    )
    BEGIN { 
        Write-Verbose "Begin..."       
        IF ($SearchStartMenu) {
$IsAdmin=Test-Admin
Write-Verbose "`$IsAdmin is $IsAdmin"
IF ($IsAdmin -like $false) { Write-Error "Script need to run as administrator" ; Break }
            $WorkFolder = Resolve-Path $("$ENV:ProgramData\Microsoft\Windows\Start Menu") 
        } ELSE {
            $WorkFolder = Resolve-Path "$WorkFolder"
        } # End of IF ELSE
        Write-Verbose "`$WorkFolder=`"$WorkFolder`""
        Write-Verbose "`$UnwantedFolders=`"$UnwantedFolders`""
    } # End of Begin
    PROCESS {
        # Remove Unwanted Folders in Start Menu
        IF ($UnwantedFolders -and $WorkFolder) { # Only run if not empty
            Write-Verbose "Processing..."
            IF ($UnwantedFolders) {
                ForEach ($UnwantedFolder in $UnwantedFolders) {
                     Get-ChildItem -Recurse -Path $WorkFolder -Directory | Where-Object {$_.Name -like "$UnwantedFolder" } | Resolve-Path | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
                } # End of ForEach
            } # End of IF
        } # End of IF
    } # End of Process

} # End of function Remove-UnwantedFolders

Function Remove-UnwantedFiles {
    <#
    .SYNOPSIS
        Removes unwanted files in a path
    .DESCRIPTION
        Will remove one or more files from a path, for example use this to remove unwanted files in the start menu.
    .NOTES
   davpe67 2017-03-03
    .PARAMETER WorkFolder
   Define a path where script should seach recursive for the folders to remove
   For example: "C:\" or "$ENV:Temp"
    .PARAMETER SearchStartMenu
        Will Set Workfolder to "$ENV:Programdata\Microsoft\Windows\Start Menu"
    .PARAMETER UnwantedFiles
        Specify one or more folders you want to remove.
        For example: "Onedrive","Onedrive for Business"
    .EXAMPLE
   Remove-UnwantedFiles -SearchStartMenu -UnwantedFiles "Onedrive","Onedrive for Business" -WhatIf
    #>

    [cmdletbinding(SupportsShouldProcess=$True,DefaultParameterSetName="WorkFolder")]
    Param(
        [Parameter(Mandatory=$True, ParameterSetName="WorkFolder", Position=0, HelpMessage='Specify folder(s) serch in: ["C:\path\to\folders"]')]
        [string]$WorkFolder,
        [Parameter(Mandatory=$True, ParameterSetName="SearchStartMenu", Position=0)]
        [switch]$SearchStartMenu,
        [Parameter(Mandatory=$True, HelpMessage='Specify files(s) to remove: ["File1","File"]')]
        [string[]]$UnwantedFiles
    )
    BEGIN { 
        Write-Verbose "Begin..."       
        IF ($SearchStartMenu) {
$IsAdmin=Test-Admin
Write-Verbose "`$IsAdmin is $IsAdmin"
IF ($IsAdmin -like $false) { Write-Error "Script need to run as administrator" ; Break }
            $WorkFolder = Resolve-Path $("$ENV:ProgramData\Microsoft\Windows\Start Menu") 
        } ELSE {
            $WorkFolder = Resolve-Path "$WorkFolder"
        } # End of IF ELSE
        Write-Verbose "`$WorkFolder=`"$WorkFolder`""
        Write-Verbose "`$UnwantedFiles=`"$UnwantedFiles`""
    } # End of Begin
    PROCESS {
        # Remove Unwanted Folders in Start Menu
        IF ($UnwantedFiles -and $WorkFolder) { # Only run if not empty
            Write-Verbose "Processing..."
            IF ($UnwantedFiles) {
                ForEach ($UnwantedFile in $UnwantedFiles) {
                     Get-ChildItem -Recurse -Path $WorkFolder -File | Where-Object {$_.Name -like "$UnwantedFile" } | Resolve-Path | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
                } # End of ForEach
            } # End of IF
        } # End of IF
    } # End of Process

} # End of function Remove-UnwantedFiles


function Test-Admin {
<#
.SYNOPSIS
Test if script is run as administrator
.DESCRIPTION
Use to check if script is run with administrator privilegues or not.
Will return true or false.
.NOTES
Downloaded from: http://www.powertheshell.com/testadmin/
.EXAMPLE
Test-Admin
#>
  $wid = [System.Security.Principal.WindowsIdentity]::GetCurrent()
  $prp = New-Object System.Security.Principal.WindowsPrincipal($wid)
  $adm = [System.Security.Principal.WindowsBuiltInRole]::Administrator
  $prp.IsInRole($adm)  
}

Function Remove-UninstallLinks {
    <#
    .SYNOPSIS
        Removes unwanted Uninstall links in a path
    .DESCRIPTION
        Will remove links that begin with "uninstall" from a path, 
        for example use this to remove unwanted uninstall-links in the start menu.
    .NOTES
   davpe67 2017-02-18
    .PARAMETER WorkFolder
   Define a path where script should seach recursive for the folders to remove
   For example: "C:\" or "$ENV:Temp"
    .PARAMETER SearchStartMenu
        Will Set Workfolder to "$ENV:Programdata\Microsoft\Windows\Start Menu"
    .EXAMPLE
   Remove-UnwantedFolders -SearchStartMenu -WhatIf
    #>

    [cmdletbinding(SupportsShouldProcess=$True,DefaultParameterSetName="WorkFolder")]
    Param(
        [Parameter(Mandatory=$True, ParameterSetName="WorkFolder", Position=0, HelpMessage='Specify folder(s) serch in: ["C:\path\to\folders"]')]
        [string]$WorkFolder,
        [Parameter(Mandatory=$True, ParameterSetName="SearchStartMenu", Position=0)]
        [switch]$SearchStartMenu
    )
    BEGIN {
        Write-Verbose "Begin..."       
        IF ($SearchStartMenu) {
$IsAdmin=Test-Admin
Write-Verbose "`$IsAdmin is $IsAdmin"
IF ($IsAdmin -like $false) { Write-Error "Script need to run as administrator" ; Break }
            $WorkFolder = Resolve-Path $("$ENV:ProgramData\Microsoft\Windows\Start Menu") 
        } ELSE {
            $WorkFolder = Resolve-Path "$WorkFolder"
        } # End of IF ELSE
        Write-Verbose "`$WorkFolder=`"$WorkFolder`""
    } # End of Begin
    PROCESS {
        Write-Verbose "Processing..."
        # Remove all Uninstall-links
        $UninstallLinks=Get-ChildItem -Recurse -Path $WorkFolder -File "*.lnk" | Where-Object {$_.Name -like "Uninstall*" } | Resolve-Path
        $UninstallLinks | ForEach-Object { 
            Write-Verbose "Removing `"$_`""
            Remove-Item "$_" -Recurse -Force -ErrorAction SilentlyContinue
            }
    } # End of Process
} # End of Remove-UninstallLinks

Function Remove-EmptyFolders {
     <#
    .SYNOPSIS
        Removes empty folders in a path.
    .DESCRIPTION
        Will remove empty folders in a path.
        Script will loop x times to ensure folders that became empty, default number is 10.
    .NOTES
   davpe67 2017-02-18
    .PARAMETER WorkFolder
   Define a path where script should seach recursive for the folders to remove
   For example: "C:\" or "$ENV:Temp"
    .PARAMETER SearchStartMenu
        Will Set Workfolder to "$ENV:Programdata\Microsoft\Windows\Start Menu"
    .PARAMETER KeepFolders
        Will not remove folders with these names.
        For example: "StartUp","Maintenance"
    .PARAMETER Looptimes
        Script will loop this many times to remove new empty folders from previous runs.
    .EXAMPLE
   Remove-EmptyFolders -SearchStartMenu -Looptimes 10 -WhatIf
    #>
    [cmdletbinding(SupportsShouldProcess=$True,DefaultParameterSetName="WorkFolder")]
    Param(
        [Parameter(Mandatory=$True, ParameterSetName="WorkFolder", Position=0, HelpMessage='Specify folder(s) serch in: ["C:\path\to\folders"]')]
        [string]$WorkFolder,
        [Parameter(Mandatory=$True, ParameterSetName="SearchStartMenu", Position=0)]
        [switch]$SearchStartMenu,
        [int]$Looptimes,
        [string[]]$KeepFolders
    )
    BEGIN {
        Write-Verbose "Begin..."
        IF (!$Looptimes) { $Looptimes=10 }
        IF ($SearchStartMenu) {
$IsAdmin=Test-Admin
Write-Verbose "`$IsAdmin is $IsAdmin"
IF ($IsAdmin -like $false) { Write-Error "Script need to run as administrator" ; Break }
            $WorkFolder = Resolve-Path $("$ENV:ProgramData\Microsoft\Windows\Start Menu") 
        } ELSE {
            $WorkFolder = Resolve-Path "$WorkFolder"
        } # End of IF ELSE
        Write-Verbose "`$WorkFolder=`"$WorkFolder`""
    } # End of Begin
    PROCESS {
        Write-Verbose "Processing..."
        # Remove all folders with no links
        $LoopCounter=0
        do {
            Write-Verbose "Loop number $LoopCounter of $Looptimes"
            $EmptyFolders = Get-ChildItem $WorkFolder -Directory -Recurse -Force -Exclude $KeepFolders | Where-Object { (Get-ChildItem $_.fullName).count -eq 0 } | Select-Object -ExpandProperty FullName
            $EmptyFolders | ForEach-Object { Write-Verbose "Removing `"$_`"" }
            $EmptyFolders | ForEach-Object { Remove-Item "$_" -Recurse -Force -ErrorAction Continue }
            $LoopCounter=$LoopCounter+1
        } while ($EmptyFolders.count -gt 0 -and $LoopCounter -le $Looptimes)
    } # End of Process
} # End of Remove-EmptyFolders

Function Remove-FiletypeFromFolder {
    <#
    .SYNOPSIS
        Removes files of defined type in a path
    .DESCRIPTION
        Will remove all files of defined file type in a path and subfolders, 
        could be used to remove all links from the common desktop.
        Use the -whatif to see what files that will be removed.
    .NOTES
   davpe67 2017-02-25
davpe67 2017-05-11 Added support for exclution of files
    .PARAMETER WorkFolder
   Define a path where script should seach recursive for the folders to remove
   For example: "C:\" or "$ENV:Temp"
    .PARAMETER SearchStartMenu
        Will Set Workfolder to "$ENV:Programdata\Microsoft\Windows\Start Menu"
.PARAMETER SearchPublicDesktop
        Will Set Workfolder to "$ENV:PUBLIC\Desktop"
    .PARAMETER KeepFiles
        Define files that should not be deleted
    .EXAMPLE
   Remove-FiletypeFromFolder -SearchPublicDesktop -FileTypes ".lnk" -WhatIf

        Remove-FiletypeFromFolder -SearchPublicDesktop -FileTypes ".lnk","*.tmp" -Verbose
    #>

    [cmdletbinding(SupportsShouldProcess=$True,DefaultParameterSetName="WorkFolder")]
    Param(
        [Parameter(Mandatory=$True, ParameterSetName="WorkFolder", Position=0, HelpMessage='Specify folder(s) serch in: ["C:\path\to\folders"]')]
        [string]$WorkFolder,
        [Parameter(Mandatory=$True, ParameterSetName="SearchStartMenu", Position=0)]
        [switch]$SearchStartMenu,
        [Parameter(Mandatory=$True, ParameterSetName="SearchPublicDesktop", Position=0)]
        [switch]$SearchPublicDesktop,
        [Parameter(Mandatory=$True, HelpMessage='Specify filetypes to search for: [".lnk",".tmp"]')]
        [string[]]$FileTypes,
        [string[]]$KeepFiles
    )
    BEGIN {
        Write-Verbose "Begin..."
        $IsAdmin=Test-Admin
Write-Verbose "`$IsAdmin is $IsAdmin"
        IF ($SearchStartMenu) {
IF ($IsAdmin -like $false) { Write-Error "Script need to run as administrator" ; Break }
            $WorkFolder = Resolve-Path $("$ENV:ProgramData\Microsoft\Windows\Start Menu") 
        } ELSEIF ($SearchPublicDesktop) {
            IF ($IsAdmin -like $false) { Write-Error "Script need to run as administrator" ; Break }
            $WorkFolder = Resolve-Path $("$ENV:PUBLIC\Desktop") 
        } ELSE {
            $WorkFolder = Resolve-Path "$WorkFolder"
        } # End of IF ELSE
        Write-Verbose "`$WorkFolder=`"$WorkFolder`""
    } # End of Begin
    PROCESS {
        Write-Verbose "Processing..."
        # Search and remove
        foreach ($FileType in $FileTypes) { 
            $FilesToRemove=Get-ChildItem $WorkFolder -Force -Exclude $KeepFiles | Resolve-Path
            $FilesToRemove | ForEach-Object { 
                Write-Verbose "Removing `"$_`""
                Remove-Item "$_" -Recurse -Force
                }
            } # End of foreach
    } # End of Process
} # End of Remove-FiletypeFromFolder

Remove-FiletypeFromFolder -SearchPublicDesktop -FileTypes "*.lnk" -KeepFiles "Mount Onedrive for Business.lnk"
Remove-UnwantedFolders -SearchStartMenu -UnwantedFolders "Java","Microsoft Silverlight"
Remove-UnwantedFiles -SearchStartMenu -UnwantedFiles "Onedrive*.lnk"
Remove-UninstallLinks -SearchStartMenu
Remove-EmptyFolders -SearchStartMenu -KeepFolders "StartUp","Maintenance"
=== SCRIPT Cleanup-Desktop_and_StartMenu.ps1 ENDS ===

How it runs

The script is run by a scheduled task that triggers on computer startup, user logon, and every hour and on the creation of the scheduled job. This is to make sure that any shortcuts that may have been added by software installation have been removed.

2017-05-10

Mount-Fileserver.ps1 - Mount file server folders based on numbers in username

The problem to solve

I had to mount secondary home folders that have users divided in separate folders based on a random number in their usernames.

The conditions were these:

1. Students have 4-5 letters and 3 numbers in usernames (abcde123) and their foler is in a sub-folder called users-N where N is the first number in their usernames.

2. Employees have 4-5 letters and 2 numbers in usernames and use another share on the same server.

This is how I did it:

The script

#========== Script Begins Mount-Fileserver.ps1 ==========
# Script to mount file storage at, Djerf 2017-04-26
#\\fileserver.example.net\\users-N\username # used for students with 3-numbers in username, N is the first number
#\\fileserver.example.net\\users-fo\username # used for employees with 2-numbers in username

# Change this to your file server, make sure to change the share names below
$FileServer="fileserver.example.net"

# Employees share
$EmployeeShare="users-fo"

# Students share, first number in username will be added
$StudentShare="users-"

$MountUsername = $Env:Username


if ($MountUsername -match "^[a-z]{4,5}[0-9]{2}$") {
Net use z: /delete
    Net use z: \\$FileServer\$EmployeeShare\$MountUsername
}

if ($MountUsername -match "^[a-z]{4,5}[0-9]{3}$") {
    $MountUserNumbersonly = $MountUsername -replace '\D+(\d+)','$1'
    $MountUserFirstNumber=$MountUserNumbersonly.Substring(0,1)
    Net use z: /delete
Net use z: \\$FileServer\$StudentShare-$MountUserFirstNumber\$MountUsername
}
#========== Script Ends ==========

What could be improved

I tried to use New-PSDrive to mount, but for some reason that only added the drive inside powershell and not in the File Explorer, so I had to go back to use the old "NET USE" command.
If anyone know why that happened and how to fix it, please let me know!
My work load is too high at the moment to spend more time to fix this when its working but ugly.

How it runs

I chose to use a GPO to deploy the script, so that it is stored locally on all computers that needs to mount this server. Experience say that that will work the best in the long term. I have been running scripts directly from the AD-servers, but from time to time that adds a lag. If I update the script the GPO will automatically upgrade the file on the clients.

The GPO will make sure to create a folder and copies the ps1-file and a launcher vbs script that I use to make the script invisible 

#========== Script Begins Mount-Fileserver-Launcher.vbs ==========
' Launcher to make scheduled task run invisible Djerf 2016-11-22
Dim shell,command
Set shell = CreateObject("WScript.Shell")
windowsdir = shell.ExpandEnvironmentStrings("%windir%")
programdir = shell.ExpandEnvironmentStrings("%programfiles%")
command =("""" & windowsdir & "\system32\WindowsPowerShell\v1.0\powershell.exe""" & " -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File " & """" & programdir & "\Script\Mount-Fileserver.ps1""")
shell.Run command,0
#========== Script Ends ==========
(This script should be modified to run the ps1-file from the same folder as the vbs-script, but I'm a total noob at vbs, so this is what I came up with after a lot of Googling.)

I create a scheduled task that runs the vbs-script on user logon, and the vbs script runs the ps1-script in the background and the correct folder is mounted.