Zum Inhalt springen
>_marc-oltersdorf.de

PowerShell-Skripte umsortieren, ohne Aufrufer zu brechen: Weiterleitung am alten Pfad, die mitschreibt

Marc Oltersdorf 6 Min. Lesezeit

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

  1. 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.

  2. 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.

  3. 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).

  4. 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.

  5. 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

  1. schreibt Pfade in den Argumenten um, die auf eine Datei im alten Ordner zeigen, die es nur noch im neuen gibt (etwa -ConfigPath auf eine .psd1),
  2. ermittelt die aufrufende Prozesskette über Win32_Process (drei Ebenen über ParentProcessId, jeweils Name, Prozessnummer und gekürzte Kommandozeile). Daran erkennt man, ob die Aufgabenplanung (svchost bzw. taskeng), ein Monitoring-Sensor oder ein Mensch in der Konsole aufruft,
  3. schreibt eine Zeile in Logs\Umzug\aufrufe.log: Zeit, Benutzer, Rechner, alter Pfad, eigene Kommandozeile, Aufrufer, umgeschriebene Argumente,
  4. 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

  • @args reicht benannte Parameter weiter. Ohne param-Block sammelt PowerShell alles in $args. Die Elemente für Parameternamen (-ConfigPath, -Reboot) tragen dabei eine interne Markierung, und deshalb bindet & $ziel @args sie 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 mit param([string]$ConfigPath, [switch]$Reboot, [string]$Name) kamen ConfigPath mit dem neuen Ordner, Reboot = True und Name = Wert an.
  • Der Exitcode kommt durch. Im Test kam der Exitcode 7 des Ziels beim Aufrufer an. Fehler fallen in Aufgabenplanung und Monitoring also weiter auf.
  • exit beendet 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 $PSScriptRoot beginnen. Das scheiterte schon im Test: Derselbe Ordner hat mehrere Schreibweisen, etwa einen 8.3-Kurznamen wie ADMINI~1 neben 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 -Reboot kä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 $PSScriptRoot berechnen, 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.

Hat dir das geholfen?

RSS abonnieren

$ ls ../ Passt dazu