Powershell Programmierung – Tipps für verständliche Scripte

powershell win32 processor1

Ein Script in Powershell ist immer schnell geschrieben. Ich selber finde auch hin und wieder beim Durchschauen von alten Scripten, dass die nicht immer so sauber sind, wie manch ein aktuelles Script. Oder die Ausführung von Befehlen ist sehr umständlich und komplex. Mit der Erfahrung wächst die Komplexität der Scripte und damit auch der Dokumentationsbedarf, bzw. muss es übersichtlicher wieder leicht verständlich werden. Für die Übersichtlichkeit ist das Einrücken wichtig.

Der richtige Editor!

Fangen wir erst mal ganz vorne beim Editor an. Nutzt für die Programmierung nicht einfach Notepad, sondern verwendet einen Editor der Powershell Code erkennt und interpretieren kann. Zum Beispiel stellt Microsoft die Powershell ISE zur Verfügung. Codeerkennung, Befehlsvorschläge und einen nutzbaren Parser erleichtern euch die Programmierung. Notepad++ ist hier eine nützliche Alternative zur Powershell ISE.

Script-Struktur

Meine Scripte sind immer nach der gleichen Struktur aufgebaut.

  • Script-Kopf: etwas Doku
  • Konfigurationsteil: für die Definition von Variable, Arrays etc.
  • Arbeitsteil: beinhaltet die MAGIE

Script-Kopf

Meine Scripte beginnen mittlerweile immer mit dem gleichen Kopf der folgende Infos enthält:

  • Scriptname
  • Versionsnummer zum Beispiel 1.<DATUM in yyyymmdd>_<Tagesversion>
  • Kurze Beschreibung was es machen soll
  • Requirements zum Beispiel speziele PS Version oder Ähnlich
  • Ausführungshinweise, zum Beispiel auszuführen als Administrator

Die Versionsnummer schreibe ich mitlerweile in eine Variable, die ich später für das Logfile ausgeben lassen. Bei manchen wird die Version auch in eine extra Datei geschrieben, welche später importiert wird und ich sehe, auf welchen Computer ich welcher Version haben.

# Scriptname: Serverinventory_local_2
# Author: mathias@....de
$script_version = "1.20230220 (1)"
# documentation page:-
#
# description: Script collects systeminformation for.... 
# Requirements:
# - Powershell
# - 7Zip
#
# Configuration: 
# - no configuration needed
#
# Execution:
# - only execute the script as user of the domain OR as scheduled tasks as SYSTEM
# - CSV-File will be stored in C:\temp
#
#### Configuration ####

Extra Konfigurationsdatei für das Powershell Script

In kleineren Scripten habe ich für die Konfiguration eine Sektion unterhalb des Script-Kopfes. Bei umfangreicheren Scripten oder Scripten die Auditiert werden, ist die Konfiguration ausgelagert.

  • ad_systemreport.ps1 (Hauptscript)
  • ad_systemreport_config.ps1 (Config)
#### Auszug ad_systemreport_config.ps1 (Config)
$azure_ad_PSModule="AzureADPreview" # AzureADPreview OR AzureAD
    
$azure_ad_MaxInactiveTime=@{disable="90";delete="120"}

$azure_ad_StaleAgeInDays=@{disable="90";delete="120"}
#### Auszug ad_systemreport.ps1 (Hauptscript)

$configfile_path = "C:\scripts\ad_systemreport_config.ps1"

start-transcript $log

### load location specific configuration file
  if(-not (Test-Path $configfile_path)){

    write-host " - ERROR: can't load the config: $configfile_path"

  }else{
    . $configfile_path
  }

Kommentare

Deshalb versuch ich immer Brotkrümel anhand von Kommentaren im Code zu hinterlegen, die auch u.a. ein Bsp für die Daten beinhalten. Das erleichtert im Nachgang das verstehen des Codes. Es hört sich doof an, aber die Doku mit Kommentaren ist wichtiger als ihr denkt! Nichts finde ich schlimmer, wenn man sich nicht zügig in ein Script einlesen kann. Kommentiert eure Scripte und Zeile immer so, dass ihr selbst nach einem harten Wochenende, schnell versteht was die Zeile macht!

#### get installed Windows Updates
write-host "- get Windows Updates"
                    
try{
  get-wmiobject -class win32_quickfixengineering | Select-Object -Property Source,Description,HotFixID,InstalledBy,InstalledOn | Export-Csv $csv_wu_file
}
catch{
   "==> ERROR "
}
finally{

}

#### get local users
#UserName  SID  LastLogin   UserFlags                                                     
#--------  ---  ---------    ---------                                                     
#SophosSAUWMT15aaa   S-1-5-21-339632xxxx-3209xxx420-1131xxxx68-1000  5/11/2019 8:11:49 AM  SCRIPT, PASSWD_CANT_CHANGE, NORMAL_ACCOUNT, DONT_EXPIRE_PAS
#

Logfile

Da sicherlich viele eure Scripte via Scheduler laufen oder auch unter anderen Nutzern ausgeführt werden, fehlt einem die Ausgabe zur Laufzeit. Auch um zu sehen wie das Script zuletzt lief, lasse ich in allen größeren Scripte das Transcript „start-transcript“ mitlaufen. Damit auch einzelne Schritte, Entscheidung (IF) und Fehler später im Logfile erscheinen, baue ich oft „write-host ‚- ERROR: User could not created‘ “ mit ein.

$log = "c:\temp\script_run_log.txt"
# start Log
start-transcript $log

write-host "get all AD-Computer"
.... CODE ...

stop-transcript

Übergabe an Parametern am Bsp. Add-Aduser

Viele Wege führen nach Rom! Gerade in Powershell bietet sehr viele unterschiedliche Möglichkeiten Befehle zu verketten, ausführen die am Ende zum gleichen Ziel führen.

In frühere Scripten habe ich Parameter für POwershell Befehle in der Variablen geschrieben, welche ich für das Logfile vor der Befehlsausführung ausgegeben habe. Viel zusätzlicher Code für etwas mehr Übersichtlichkeit und Transparenz bei der Ausführung. Folgend das Beispiel:

vorher

### Config für den anzulegenden Nutzer
$acc_firstname=""
if($ad_data_csv[$z].Firstname.Length -gt 1){         
   $acc_firstname=$ad_data_csv[$z].Firstname
} 

$acc_lastname=""
if($ad_data_csv[$z].Lastname.Length -gt 1){         
  $acc_lastname=$ad_data_csv[$z].Lastname
} 

$acc_displayname=$ad_data_csv[$z].Firstname+" "+$ad_data_csv[$z].Lastname

### Configausgabe 
write-host "- create AD-Account: $acc_username on $acc_domaincontroller"
write-host "- path: $acc_ou_path"
write-host "- firstname: $acc_firstname"
write-host "- lastname: $acc_lastname"
write-host "- displayname: $acc_displayname"

### Befehlsausführung
New-ADUser $acc_username -server $acc_domaincontroller -GivenName $acc_firstname -SurName $acc_lastname -DisplayName $acc_displayname -UserPrincipalName $acc_upn -EmailAddress $acc_email -Description $acc_description -Department $acc_department -path $acc_ou_path -ChangePasswordAtLogon $true -AccountPassword (ConvertTo-SecureString $acc_passwd -AsPlainText -force) -Enabled $True -PasswordNeverExpires $false -PassThru

nachher

### Config für den anzulegenden Nutzer
$new_ADuser_params=@{ } # Array init
$new_ADuser_params['Name'] = $ad_data_csv[$z].SamAccountName
$new_ADuser_params['UserPrincipalName'] = $acc_username+"@"+$ad_data_csv[$z].Domain
$new_ADuser_params['GivenName'] = $ad_data_csv[$z].Firstname
$new_ADuser_params['SurName'] = $ad_data_csv[$z].Lastname
$new_ADuser_params['Enabled'] = $true

### Configausgabe
$new_ADuser_params

### Befehlsausführung
New-ADUser @new_ADuser_params

Verteilung von Scripten

Auch das aktuell halten von Scripten die irgendwann über die Serverlandschaft verteilt sind ist ein wichtiger Punkt. Meistens habe ich einen zentralen Server mit einer Freigabe, auf welche nur eine Gruppe (Active-Directory Gruppe) von Servern lesend zugreifen darf. In den scheduled Tasks mache ich dann vor dem ausführen des Powershell Scriptes eine „xcopy“ Anweisung mit rein. So kann man auf einfache Art und Weise sicherstellen, dass das Script aktuell ist.

weiterführendes

Es gibt sicherlich noch weitere Ideen und Ansätze für saubere Code. Zum Beispiel gibt es die „ungarische Notation“, welche die Bezeichnung von Variablen und Array behandelt. Wer sich dazu einlesen möchte, findet einen ausführliche Artikel in Wikipedia.

weitere Beiträge um das Themen Powershell Programmierung