PowerShell-Skripte umsortieren, ohne Aufrufer zu brechen: Weiterleitung am alten Pfad, die mitschreibt
Ein gewachsener Skriptordner auf einer Freigabe sollte neu sortiert werden, aber niemand wusste, welche geplanten Aufgaben, Monitoring-Sensoren und Verknüpfungen die Skripte über ihren Pfad aufrufen. Statt eines Stichtags bleibt an jedem alten Pfad eine kleine Weiterleitung liegen, die den Aufrufer protokolliert und das Skript am neuen Ort startet. Nach ein paar Tagen zeigt das Protokoll, was umgestellt werden muss.
$ git log --graph Der Weg zur Lösung
-
Problem
Unbekannte Aufrufer
Der Skriptordner sollte neu sortiert werden, aber geplante Aufgaben, Monitoring und Verknüpfungen riefen die Skripte über Pfade auf, die niemand vollständig kannte.
-
Erkenntnis
Weiterleitung statt Stichtag
Statt hart umzustellen, bleibt am alten Pfad eine Weiterleitung, die den Aufrufer protokolliert und durchreicht. Das Protokoll liefert die Liste dessen, was umgestellt werden muss.
-
Irrweg
Präfixvergleich mit $PSScriptRoot
Pfade in den Argumenten über den Anfang des Pfads zu erkennen, scheiterte im Test an mehreren Schreibweisen desselben Ordners (8.3-Kurzname, UNC oder Laufwerksbuchstabe).
-
Lösung
Abgleich über Ordner- und Dateinamen
Umgeschrieben wird nur ein Pfad auf eine fehlende Datei in einem Ordner mit dem alten Namen, deren Gegenstück im neuen Ordner existiert. In beide Richtungen getestet.
-
Erkenntnis
Zwei Commits für die Git-Historie
Erst git mv committen, dann die Weiterleitungen. So erkennt Git die Umbenennung, und der Verlauf jedes Skripts bleibt verfolgbar.
Auf einer Dateifreigabe lag ein über Jahre gewachsener Ordner mit PowerShell-Skripten, alles flach nebeneinander. Er sollte eine Ordnung bekommen, zum Beispiel vCenter\ nach Virtualisierung\vCenter\. Das Problem dabei: Niemand wusste sicher, wer die Skripte über ihren Pfad aufruft. Geplante Aufgaben auf verschiedenen Servern, Sensoren im Monitoring (bei uns PRTG), Desktop-Verknüpfungen von Kollegen. Einfach verschieben hätte bedeutet, dass irgendwo still etwas nicht mehr läuft, und das merkt man oft erst Wochen später.
Das Problem
Einen Stichtag ansagen und hoffen hilft nicht, denn die Aufrufer sind ja gerade unbekannt. Die Skripte nach Aufrufen zu durchsuchen, findet nur die Aufrufe, die selbst im Repo liegen. Aufgabenplanung und Monitoring auf fremden Rechnern bleiben unsichtbar.
Die Lösung
Die Skripte ziehen trotzdem um. An jedem alten Pfad bleibt aber eine kleine, signierte Weiterleitung liegen. Sie
- schreibt Pfade in den Argumenten um, die auf eine Datei im alten Ordner zeigen, die es nur noch im neuen gibt (etwa
-ConfigPathauf eine.psd1), - ermittelt die aufrufende Prozesskette über
Win32_Process(drei Ebenen überParentProcessId, jeweils Name, Prozessnummer und gekürzte Kommandozeile). Daran erkennt man, ob die Aufgabenplanung (svchostbzw.taskeng), ein Monitoring-Sensor oder ein Mensch in der Konsole aufruft, - schreibt eine Zeile in
Logs\Umzug\aufrufe.log: Zeit, Benutzer, Rechner, alter Pfad, eigene Kommandozeile, Aufrufer, umgeschriebene Argumente, - warnt mit „umgezogen nach …“ und startet das Skript am neuen Ort mit denselben Argumenten. Der Exitcode geht an den Aufrufer zurück.
Nach ein paar Tagen wertet man das Protokoll aus, stellt die gefundenen Aufrufer um und löscht die Weiterleitungen.
Die Weiterleitung
Beispiel für ein Skript, das von vCenter\ nach Virtualisierung\vCenter\ gezogen ist. Die Datei liegt unter dem alten Namen im alten Ordner. Sie hat bewusst keinen param-Block, damit alles in $args landet (warum das wichtig ist, steht unten).
<#
UMGEZOGEN nach: Virtualisierung\vCenter\skript.ps1
Übergangsweise Weiterleitung: protokolliert, wer diesen alten Pfad noch aufruft
(Logs\Umzug\aufrufe.log, mit aufrufendem Prozess - z. B. Aufgabenplanung oder PRTG),
und startet dann das Skript am neuen Ort mit denselben Argumenten. Argumente, die
auf eine nicht mehr vorhandene Datei im alten Ordner zeigen, werden auf den neuen
Ordner umgeschrieben. Entfernen, sobald im Protokoll keine Aufrufe mehr auftauchen.
#>
$altOrdner = $PSScriptRoot
$neuOrdner = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\Virtualisierung\vCenter'))
$ziel = Join-Path $neuOrdner 'skript.ps1'
# Umgeschrieben wird über Ordner- und Dateinamen, nicht über den vollen Pfad: derselbe
# Ordner hat mehrere Schreibweisen (UNC-Pfad, zweiter Servername, Laufwerksbuchstabe,
# 8.3-Kurznamen), ein Präfixvergleich fände nur eine davon.
$altName = Split-Path -Leaf $altOrdner
$hinweise = @()
for ($i = 0; $i -lt $args.Count; $i++) {
$wert = $args[$i]
if ($wert -isnot [string] -or $wert -notmatch '[\\/]' -or $wert.StartsWith('-')) { continue }
try {
if (Test-Path -LiteralPath $wert) { continue }
$ordnerName = Split-Path -Leaf (Split-Path -Parent $wert)
$kandidat = Join-Path $neuOrdner (Split-Path -Leaf $wert)
if ($ordnerName -eq $altName -and (Test-Path -LiteralPath $kandidat)) {
$args[$i] = $kandidat
$hinweise += "Argument umgeschrieben: $wert -> $kandidat"
}
} catch {
# Kein gültiger Pfad - unverändert weitergeben
}
}
# Das Protokoll darf den eigentlichen Lauf nie verhindern
try {
$kette = @()
$p = Get-CimInstance Win32_Process -Filter "ProcessId=$PID" -ErrorAction Stop
for ($i = 0; $i -lt 3 -and $p -and $p.ParentProcessId; $i++) {
$p = Get-CimInstance Win32_Process -Filter "ProcessId=$($p.ParentProcessId)" -ErrorAction Stop
if ($p) {
$befehl = "$($p.CommandLine)"
if ($befehl.Length -gt 300) { $befehl = $befehl.Substring(0, 300) + ' ...' }
$kette += "$($p.Name) [$($p.ProcessId)] $befehl"
}
}
$eigen = Get-CimInstance Win32_Process -Filter "ProcessId=$PID" -ErrorAction SilentlyContinue
$zeile = '{0} | {1}\{2} auf {3} | alter Pfad: {4} | eigener Aufruf: {5} | Aufrufer: {6}{7}' -f
(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $env:USERDOMAIN, $env:USERNAME, $env:COMPUTERNAME,
$PSCommandPath, "$($eigen.CommandLine)", ($kette -join ' <- '),
$(if ($hinweise) { ' | ' + ($hinweise -join '; ') } else { '' })
$logOrdner = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\Logs\Umzug'))
if (-not (Test-Path -LiteralPath $logOrdner)) {
New-Item -ItemType Directory -Path $logOrdner -Force | Out-Null
}
Add-Content -LiteralPath (Join-Path $logOrdner 'aufrufe.log') -Value $zeile -Encoding UTF8
} catch {
Write-Warning "Aufruf konnte nicht protokolliert werden: $($_.Exception.Message)"
}
Write-Warning "Dieses Skript ist umgezogen nach $ziel - bitte Aufruf/Verknüpfung/Aufgabe anpassen."
& $ziel @args
exit $LASTEXITCODE
Bei vielen Skripten lohnt sich eine Vorlage mit Platzhaltern für Zielordner, Dateiname und Logordner, die ein kleines Skript je Datei ersetzt, speichert und signiert.
Signieren und speichern
Liegen die Skripte auf einer UNC-Freigabe und gilt AllSigned oder RemoteSigned, muss auch die Weiterleitung signiert sein. Außerdem speichern als UTF-8 mit BOM und mit CRLF, sonst liest Windows PowerShell 5.1 die Umlaute falsch.
$zert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1
Set-AuthenticodeSignature -FilePath '\\server\freigabe\vCenter\skript.ps1' -Certificate $zert
In Git: zwei Commits
Erst nur verschieben und committen, dann die Weiterleitungen an den alten Pfaden anlegen und getrennt committen. So erkennt Git die Umbenennung, und git log --follow findet den Verlauf jedes Skripts weiter.
New-Item -ItemType Directory Virtualisierung
git mv vCenter Virtualisierung/vCenter
git commit -m "vCenter-Skripte nach Virtualisierung verschoben"
# jetzt die Weiterleitungen unter vCenter\ anlegen und signieren
git add vCenter
git commit -m "Weiterleitungen an den alten Pfaden"
Das Protokoll lesen
Eine Zeile aus dem Test sieht so aus:
2026-10-14 15:19:43 | DOMAENE\benutzer auf PC-01 | alter Pfad: \\server\freigabe\alt\ziel.ps1 | eigener Aufruf: "powershell.exe" -NoProfile -File \\server\freigabe\alt\ziel.ps1 -ConfigPath ... | Aufrufer: pwsh.exe [1234] ... <- cmd.exe [5678] ... | Argument umgeschrieben: \\server\freigabe\alt\conf.psd1 -> \\server\freigabe\neu\conf.psd1
Steht svchost.exe oder taskeng.exe in der Kette, ist es eine geplante Aufgabe auf dem genannten Rechner. Taucht ein Monitoring-Prozess auf, ist es ein Sensor. Steht dort explorer.exe oder eine Konsole, hat jemand von Hand aufgerufen, meist über eine Verknüpfung. Wenn nach einigen Tagen für einen alten Pfad nichts mehr dazukommt, kann seine Weiterleitung weg.
Warum das funktioniert
@argsreicht benannte Parameter weiter. Ohneparam-Block sammelt PowerShell alles in$args. Die Elemente für Parameternamen (-ConfigPath,-Reboot) tragen dabei eine interne Markierung, und deshalb bindet& $ziel @argssie im Ziel wieder als benannte Parameter und Schalter.- Umgeschrieben wird an Ort und Stelle. Nur die Wert-Elemente werden über den Index ersetzt (
$args[$i] = $kandidat), alles, was mit-beginnt, wird übersprungen. Die Namens-Elemente bleiben unangetastet und behalten ihre Markierung. Getestet mit Windows PowerShell 5.1 und dem Aufruf-ConfigPath <alter Ordner>\conf.psd1 -Reboot Wert: Im Ziel mitparam([string]$ConfigPath, [switch]$Reboot, [string]$Name)kamenConfigPathmit dem neuen Ordner,Reboot = TrueundName = Wertan. - Der Exitcode kommt durch. Im Test kam der Exitcode 7 des Ziels beim Aufrufer an. Fehler fallen in Aufgabenplanung und Monitoring also weiter auf.
exitbeendet nur das Skript. Ruft jemand die Weiterleitung in einer offenen Sitzung mit&auf, bleibt seine Sitzung offen.- Das Protokollieren steckt in
try/catch. Darf ein Aufgabenkonto nicht in den Logordner schreiben, gibt es nur eine Warnung, und das eigentliche Skript läuft trotzdem.
Stolperfallen
- Pfade nicht über ein Präfix vergleichen. Mein erster Versuch, Argumente umzuschreiben, prüfte, ob sie mit
$PSScriptRootbeginnen. Das scheiterte schon im Test: Derselbe Ordner hat mehrere Schreibweisen, etwa einen 8.3-Kurznamen wieADMINI~1neben dem langen Namen, im Echtbetrieb auch UNC-Pfad neben Laufwerksbuchstaben. Zuverlässig ist der Abgleich über Ordner- und Dateinamen wie oben. In beide Richtungen getestet: Eine vorhandene Datei in einem anderen Ordner bleibt unverändert, ebenso eine fehlende Datei ohne Gegenstück im neuen Ordner. - Keine neue Argumentliste bauen. Ausprobiert habe ich es nicht, ich vermute aber: Wer statt der Änderung an Ort und Stelle eine neue Liste baut (
$neu = @(...); & $ziel @neu), verliert die Markierungen, und-Rebootkäme im Ziel als Text an einer Position an. - Zugangsdaten im Protokoll. Die Kommandozeilen der Aufrufer werden zwar auf 300 Zeichen gekürzt, landen aber in einer Datei auf der Freigabe. Übergibt ein Aufruf ein Passwort als Argument, steht es dort. Rechte auf den Logordner eng setzen und das Protokoll nach dem Umzug löschen.
- Ziel und Logordner relativ zu
$PSScriptRootberechnen, nie mit einem festen Pfad. Dann funktioniert die Weiterleitung über jede Schreibweise der Freigabe. - Verschieben und Weiterleitungen in einem Commit sieht Git als „geändert + neu“ statt „umbenannt“, und der Verlauf reißt ab.
- Kodierung: Ohne BOM zeigt Windows PowerShell 5.1 Umlaute in Kommentaren und Meldungen als Zeichensalat.