Generated Windows 10 Start Menu Script

We are now preparing to go to Windows 10 and one of the biggest challenges have been the new Start menu. Microsoft seem to think that every user in the world should manage their own Start menu, and to help them with that Microsoft create a default Start Menu full of bloat.

The official way to create a custom start menu is to set it up and run the Export-Startmenu powershell command. One of the problem with that is that you get a start menu based on the exact versions that are installed when you run the command. Some programs we use have version numbers in the path. That will result in black empty spots in the Start menu and no shortcuts.

Another reason why this is just stupid is the number of different computer rooms we have that all have different programs. We dont have time to create new Start menu layout files every week for 150-200 rooms. How were they thinking at Microsoft?

The solution is a powershell script that looks for pre-defined software groups and group members.
It scans the classic start menu for shortcuts and then add these to an XML file that we point to in a GPO.

Computer Configuration > Policies > Administrative Templates > Start Menu and Taskbar > Start Layout

The script and a scheduled task is added to the same GPO and we run the script on creation/modification, Startup, user logoff. 
To make it trigger on logoff I had to make it trigger on an event.

Log: Microsoft-Windows-User Profile Service/Operational
Source: Microsoft-Windows-User Profile Service
Event ID: 4

Scheduled task
Add arguments:
-ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File "<YOUR PATH HERE>\Generate-StartMenu.ps1"

I make the scheduled task run as SYSTEM and trigger at startup (repeat every 15 min), user disconnection, at task creation & at the logoff event id 4 I wrote about above.

The new start menu layout is only loaded during logon, if you change the XML-file you will have to logoff and login again. And you will get somthing like this:

In the script you have to set the path to wherever you want the startmenu.xml to be saved.
You can also change the group names and the group members to fit your needs.

Please share any modifications you do that improves on this script!

Update 2017-07-02
We have had problems with the startmenu.xml becoming locked by the system and the new file could not be written. I've made an update that renames the first, lets hope this fixes the problem.
During testing strange things have happened twice. The renamed file became untouchable by all users, could not even read the ACLs or take ownership of the file. Don't know why, but after a restart the file is gone so it doesn't seem to be a blocker. 

Update 2018-04-20
Script updated to better handle multiple shortcuts with the same name.

Known issues
Sometimes if a program have multiple shortcuts and you want to match "Name*", it will add all of them on the same line. You will notice this as a black hole in the start menu. Sometimes you have to write the full name of the shortcut and in one case I could not make it match at all, the shortcut included (), its on our TODO-list.
Make tests and adjust your start menu groups until you're happy!

### Script begins ###
# By lillu24 & davpe67 LiU-DRS 2017-02-27
# Updated 2017-06-28, Added rename of file to avoid locked file problem
# Updated 2017-11-14, added removal of Edge shortcut on taskbar (keeping File explorer)

# Where to save startmenu xml
#$StartMenuFile = "$ENV:temp\startmenu\startmenu.xml"
$StartMenuFile = "$ENV:programfiles\LiU\startmenu\startmenu.xml"
#$OldStartMenuFile = "$ENV:temp\startmenu\startmenu.old"
$OldStartMenuFile = "$ENV:programfiles\LiU\startmenu\startmenu.old"

# Set this to SilentlyContinue for no debug, or Continue for debug output
$DebugPreference = "SilentlyContinue"

# Remove old startmenu.old file
IF (Test-path $OldStartMenuFile) {
    Write-Debug "The file `"$OldStartMenuFile`" already exists and will be removed!"
    Remove-item $OldStartMenuFile -Force
} Else {
    Write-Debug "The file `"$OldStartMenuFile`" does not exist! Lets move along then..."

# Rename startmenu.xml to startmenu.old
IF (Test-path $StartMenuFile) {
    Write-Debug "renaming file `"$OldStartMenuFile`"..."
    Rename-Item -Path $StartMenuFile -NewName $OldStartMenuFile -Force
} Else {
    Write-Debug "The file `"$OldStartMenuFile`" does not exist! Lets move along then..."

# One last check to see if file exists or not
IF (Test-path $StartMenuFile) {
    Write-Error "Could not rename `"$OldStartMenuFile`", script aborted"
} Else {
    Write-Debug "The file `"$OldStartMenuFile`" does not exist! Lets move along then..."

# Make sure folder exist and halt if it can't be created
$StartMenuFolder=(Split-path -parent $StartMenuFile)
IF (Test-path $StartMenuFolder) { } ELSE  { New-Item -ItemType Directory -Path $StartMenuFolder }
IF (Test-path $StartMenuFolder) { } ELSE  { Write-Error "Could not create `"$StartMenuFolder`", script aborted" ; Break }

# Specify number of cols in startmenu
$NumCol = 6
# Add the new group in $MenuGroups
# Fomrat: "order. group title" = "list of SoftwareLinks"
$MenuGroups = @{
    "1. Internet & Network tools" = "Microsoft.MicrosoftEdge_8wekyb3d8bbwe!MicrosoftEdge","Google Chrome","MobaXterm Personal Edition","Firefox","PuTTY","Skype","Slack","Start Google Earth","Thinlinc Client","Thunderbird","WinSCP"
    "2. Microsoft Office" = "Access 2016","Excel 2016","Outlook 2016","PowerPoint 2016","Project 2016","Publisher 2016","Word 2016"
    "3. Text, file & programming tools" = "Android Studio","CSDiff","CodeBlocks","Dr. Racket","Eclipse","FileZilla","GIT GUI","GNU Emacs","JCreator*","LyX","TeXworks","TeXnicCenter","Meld","Notepad++","OpenCode*","Racket","Visual Studio 20*"
    "4. Media tools" = "Adobe Illustrator CC*","Adobe InDesign CC*","Adobe Photoshop CC*","Adobe Premiere Pro CC*","Audacity","Dia","GIMP*","IrfanView 64*","Krita*","Paint","paint.net","SketchUp","VLC media player"
    "5. Scientific software" = "20-Sim*","Advanced Design System*","Aimsun*","ANSYS AIM*","ArcGlobe*","ArcMap*","ArcScene*","Arena","BioProcessTrainer","CATIA*","*Comsol Multiphysics*","Creo Modelcheck*","Creo Parametric*","Dia","G*Power","IBM SPSS Statistics 24","LabTutor","NI LabVIEW*","LS-DYNA Manager","LTspice IV","Maple*","Matlab*","modeFRONTIER 2017","Wolfram Mathematica*","Mendeley Desktop","MicroCal PEAQ-ITC Analysis Software","Minitab*","NetBeans IDE*","NVivo","Protein purifier","PTV Visum 1*","PTV Visum Safety 1*","TRL streetaudit"
    "6. LibreOffice" = "LibreOffice Base","LibreOffice Calc","LibreOffice Draw","LibreOffice Impress","LibreOffice Math","LibreOffice Writer"
    "7. Other tools" = "3ds Max 2017.lnk","AutoCAD Architecture*.lnk","Adobe Acrobat DC","Beyond Compare*","Emme Desktop.lnk","FEM-Design*","Microsoft.WindowsCalculator_8wekyb3d8bbwe!App","Evernote","InnoSetup Compiler","Microsoft User Experience Virtualization (UE-V) Template Generator","Password Safe","Revit 2017.lnk","VMWare Workstation"

# Buildning up base startmenu xml
[xml]$StartMenuXml = '<LayoutModificationTemplate
  <LayoutOptions StartTileGroupsColumnCount="1" StartTileGroupCellWidth="'+$NumCol+'" />
  <DefaultLayoutOverride LayoutCustomizationRestrictionType="OnlySpecifiedGroups">
      <defaultlayout:StartLayout GroupCellWidth="'+$NumCol+'" xmlns:defaultlayout="http://schemas.microsoft.com/Start/2014/FullDefaultLayout">
  <CustomTaskbarLayoutCollection PinListPlacement="Replace">
    <taskbar:DesktopApp DesktopApplicationLinkPath="%APPDATA%\Microsoft\Windows\Start Menu\Programs\System Tools\File Explorer.lnk"/>
# Selecting XML element where all sofware will be placed
$DefaultLayoutElement = $StartMenuXml.GetElementsByTagName("defaultlayout:StartLayout")
# Fetcing all software links on startamenu
$SoftwareLinks = Get-ChildItem "$env:PROGRAMDATA\Microsoft\Windows\Start Menu" -recurse -filter "*.lnk"
# Looping all menu groups defined above
foreach ($MenuGroup in $MenuGroups.Keys | Sort-Object) {
    # Init xml element for sofware group
    $SoftwareGroupXml = $StartMenuXml.CreateElement("start","Group", "http://schemas.microsoft.com/Start/2014/StartLayout")
    # Init row and col
    $col = 0
    $row = 0
    # Looping all software links in startmenu
    foreach ($Software in $MenuGroups[$MenuGroup]) {
        # Check if it is time for a new col
        if (($col%($NumCol-1) -eq 1) -and ($col -ne 1)) {
            $row +=1
            $col = 0
        # Check if specifide software is found in startmenu. If so, add software element
        if ($SoftwareLinks.Name -like "$Software.lnk") {
            $SoftwareLink = $SoftwareLinks | where {$_ -like "$Software.lnk"}
            $child = $StartMenuXml.CreateElement("start","DesktopApplicationTile","http://schemas.microsoft.com/Start/2014/StartLayout")
            if ($SoftwareLink.FullName.GetType().BaseType -eq [System.Array]) { ## If multiple links, use the first one
            } else {
        # Or check if Microsoft app is specifide. If som add app element   
         elseif ($Software -like "Microsoft.*!*") {
            $child = $StartMenuXml.CreateElement("start","Tile","http://schemas.microsoft.com/Start/2014/StartLayout")
        # Add common attribueds is software or app is found and append xml element
        if (($child.HasAttributes) -and (($Software -like "Microsoft.*!*") -or ($SoftwareLinks.Name -like "$Software.lnk"))) {
            $SoftwareGroupXml.AppendChild($child) | Out-Null
            $col +=1
    # If a software group is not null, add it!
    if ($SoftwareGroupXml.HasChildNodes) {
        $DefaultLayoutElement.AppendChild($SoftwareGroupXml) | Out-Null

# Save to file
### Script ends ###