Powershell

Some infos and tipps for the MS PowerShell. Currently in german only. Sorry.

Hier ein paar nützliche Tipps und Tricks für die Powershell. Theoretisch sollten alle Scripts direkt in der interaktiven powershell.exe mit Copy & Paste ausführbar sein. Ausnahmen mal ausgenommen ;-)

Ein Powershell Statement geht in aller Regel bis zum Zeilenende (wie hier in den meissten HOWTOs gezeigt). Aber manchmal kommt selbst da die Grammatik durcheinander?! Glücklicherweise darf ein Statement auch mit einem Semikolon (;) enden um mehrere Statements in einer einzelnen Zeile unterzubringen. Also empfehle ich aus eigener, leidiger Erfahrung jedes Statement (oder Zeile) wie in C# gewohnt mit einen Semikolon zu beenden. Dann gibt's sicher keine Brösel…

Als Beispiel zwei idente Code-Zeilen. Ich empfehle speziell in Scripts die 2. Variante/Zeile:

$l = Get-ChildItem
$l = Get-ChildItem;

Per default führt die Powershell2 keine Scriptsfiles (.ps1 files) aus. Der Grund ist, das MS beschlossen hat, dass per default nur signierte Scripts ausgeführt werden sollen. Grundsätzlich eine gute Idee. Aber als default eher lästig, weil dadurch „normale“ Scripts (also auch selbst geschriebene, die in aller Regel nicht digital signiert sind) einfach nicht ausgeführt werden können.

Dies kann man dadurch umgehen, dass die eigene User-ExecutionPolicy geändert wird:

PS> Set-ExecutionPolicy -ExecutionPolicy Unrestricted
Execution Policy Change
The execution policy helps protect you from scripts that you do not trust. 
Changing the execution policy might expose you to the security risks described 
in the about_Execution_Policies help topic. Do you want to change the execution
policy?
[Y] Yes  [N] No  [S] Suspend  [?] Help (default is "Y"): Y

Eine zweite Möglichkeit besteht darin (zB wenn die globale User-Einstellung nicht geändert werden soll/kann), beim Aufruf der PowerShell die ExecutionPolicy für diese Session als Parameter anzugeben. Wodurch sich eigentlich auch gleich die Frage der Sinnhaftigkeit dieser Sicherheitseinstellung generell stellt?! Egal. Das Ganze sieht dann so aus (in der cmd.exe):

C:\> powershell.exe -ExecutionPolicy Unrestricted
Windows PowerShell
Copyright (C) 2009 Microsoft Corporation. All rights reserved.

PS> 

Manchmal hat man ein PS-Script, das einfach nur ein paar Funktionen bereitstellt. Zum Beispiel eine Sammlung von notwendigen Functions. Solch ein Script kann einfach in ein Script oder in die (interaktive) Powershell geladen werden. Dazu muss das Script mit einen „.“ geladen werden:

# load the devart/oracle library
. .\lib_OraServerExec.ps1
 
# use the exported functions from the libray
$con  = Get-OraConnection -OraDataSource "DBNAME" -OraUsername "DBUSER" -OraPassword "DBPWD";
$con.Open();
$data = Run-OraQuery -OraConnection $con -Query "select sysdate from dual";
$con.Close();

Oftmals braucht man den Namen des Scripts und dessn vollständigen Pfad. In der bash erledigt die vordefinierte Variable %0 diese Aufgabe. Auch die Powershell geht das in etwa so:

$scriptName = $myInvocation.MyCommand.Name
$scriptPath = Split-Path ($myInvocation.MyCommand.Path);
 
Write-Host "$scriptName | $scriptPath ";
# myScript.ps1 | C:\scriptDir

Die Powershell lädt von sich aus nicht alle .Net Assemblies. Macht Sinn. Nur manchmal braucht man eben eine Assembly (zb Windows-Forms, .Net Daten-Provider). Das Nachladen der Assemblies funktioniert aber genauso wie unter .Net:

# Assemblies laden (aus GAC)
[reflection.assembly]::LoadWithPartialName("System.Windows.Forms")
 
# Assemblies laden (aus dll-file)
[reflection.assembly]::LoadFrom("C:\myAssembly.dll")

Der genaue Namen einer Assembly kann auch mal interessant sein:

# Assembly-Infos einer .net Klasse anzeigen
[reflection.assembly]::GetAssembly([myClass]).Fullname

Get-ChildItem listet stets alle „Kinder“ auf. Also Files und Unterverzeichniss. Will man nur Verzeichnisse haben, so kann man einfach die Liste mit Where-Object filtern. Zusätzlich kann mit Sort-Object die Liste noch alphabetisch sortiert werden:

$src = 'c:\tmp';
$dirs = Get-ChildItem $src | Where-Object { $_.PSIsContainer } | Sort-Object -Property Fullname;
Write-Host $dirs;

Finde alle Datein, die einen bestimmten Namen haben:

# find all files on C:\ with name 'myapp.exe'
PS C:\> Get-ChildItem c:\ -Recurse | Where-Object {$_.name -eq 'myapp.exe'}

Finde alle Datein (aber nicht Verzeichnisse!), die nicht eine bestimmte Extension haben. Bsp: alle Datein, die nicht auf .mp3 enden:

Get-ChildItem -Recurse | Where-Object {! $_.PSIsContainer} | Where-Object {$_.name -inotlike '*.mp3'} | Format-List -Property name

Finde alle alle files die älter oder neuer sind als ein bestimmtes datum:

PS C:\Users\bernd> $olderThan
Sonntag, 27. Februar 2011 08:46:48
PS C:\Users\bernd> $searchPath
C:\Users\bernd
 
# older than 27.Feb
PS C:\Users\bernd> $of = Get-ChildItem -Path $searchPath -Recurse | Where-Object {$_.LastWriteTime.CompareTo($olderThan) -lt 0 }
 
# newer than 27.Feb
PS C:\Users\bernd> $nf = Get-ChildItem -Path $searchPath -Recurse | Where-Object {$_.LastWriteTime.CompareTo($olderThan) -gt 0 }
 
PS C:\Users\bernd> $of | Format-Table -Property FullName
C:\Users\bernd\Documents\My Music\CDDB\rock
C:\Users\bernd\Documents\My Music\CDDB\soundtrack
C:\Users\bernd\Documents\My Music\CDDB\Status
C:\Users\bernd\Documents\My Music\CDDB\CDDB_Batch.txt
C:\Users\bernd\Documents\My Music\CDDB\CDexGenres.txt

Die statische Methode GetNames zeigt alle Elemente einer .Net-Enum auf. Hier am Beispiel von System.ConsoleColor

[System.Enum]::GetNames([System.ConsoleColor])
(Get-Acl $dir).Access | Format-Table -wrap

Nicht dass dieses hier wahnsinnig wichtig wäre, aber witzig allemal:

$host.UI.RawUI.WindowTitle = "schas"

Allerdings habe ich dann doch noch ein sinnvolles Einsatzgebiet für mich enteckt: ich schreibe das aktuelle Verzeichnis (pwd) in den Fenstertitel. Dies passiert durch ein Redefine meines Powershell-Promts

Auch nicht wahnsinnig wichtig, aber schon eher brauchbar. Alle definierten Farben können aus der Enum System.Consolecolor ausgelesen werden.

$host.UI.RawUI.BackgroundColor = [System.ConsoleColor]::DarkRed;
$host.UI.RawUI.ForegroundColor = [System.Consolecolor]::Gray; 

Die automatisch gesetzte Variable $host ist eine Referenz auf die Powershell selbst. Und daher kann man nicht nur den Titel des Fensters ändern sondern zB auch die Version der Powershell auslsesen:

PS C:\> $host.Version
Major  Minor  Build  Revision
-----  -----  -----  --------
2      0      -1     -1

In Perl gibt es zwei Arten von Pointern. Hard-References sind quasi wie die normalen, aus C bekannten, Pointer. Soft-References hingegen sind keine Pointer im eigentlichen Sinn, statt dessen kann eine (scalare) Variable einen String (anstatt einer Speicheradresse) beinhalten. Dieser textuelle Inhalt kann zur Laufzeit als Variablennamen oder als Funktionsnamen interpretiert werden, so als ob dieser Text „im Source Code“ drinnen stehen wuerde. Damit ist es Moeglich zur Laufzeit Variablen oder Funktionen anzusprechen, ohne den jeweiligen Namen direkt im Source Code zu benennen. Auch die Powershell kennt dieses Konzept der Soft-References:

PS C:\> $host.Version
Major  Minor  Build  Revision
-----  -----  -----  --------
2      0      -1     -1
 
# and now call the Methods/Properties with Soft-References:
PS C:\> $v = "Version"
PS C:\> $m = "Major"
 
PS C:\> $host.$v
Major  Minor  Build  Revision
-----  -----  -----  --------
2      0      -1     -1
 
PS C:\> $host.$v.$m
2

Nur weil die Powershell eine Konsole ist, heisst das noch lange nicht, dass man damit keine Windows-GUIs bauen kann. Als Beispiel ein einfacher Dialog mit einem Button:

$Erg = [reflection.assembly]::LoadWithPartialName("System.Windows.Forms")
$f =   New-Object "System.Windows.Forms.Form"
$b =   New-Object "System.Windows.Forms.Button"
$b.Text = "bla"
$f.Controls.Add($b)
$f.ShowDialog()

Zugegeben, ein fades und wenig ansehnliches Beispiel. Deshalb noch ein sinnigeres, in dem eine MessageBox geoeffnet wird und der User mit „Ja“ oder „Nein“ antworten kann. Die Antwort kann dann natürlich im Script weiterverwendet werden (sonst wär's ja wieder sinnfrei):

[bool] $userAnswer = $true;
 
[void] [reflection.assembly]::LoadWithPartialName("System.Windows.Forms");
$userAnswer = [System.Windows.Forms.MessageBox]::Show("Und? Jo oder Na?", "Da MsgBox-Titel", [System.Windows.Forms.MessageBoxButtons]::YesNo) -eq [System.Windows.Forms.DialogResult]::Yes;
 
if ($userAnswer) {
  Write-Host "Brav. Eh a 'Jo'!";
} else {
  Write-Host "Aha. Mogst net so recht?!";
}

Die Forms.MessageBox versucht sich per Default in den Vordergrund zu drängen. Allerdings lassen das manche Nicht WinForms-Applikationen eher nicht zu. Die Powershell (Shell generell) gehört da dazu. Aber auch kein Problem, dafür gibts eine Option für die statischen Show(..) Methode. Die nächste Beispielzeile könnte auch im vorigen Beispiel verwendet werden. Zusätzlich aber hat die Box ein Icon (Fragezeichen), der linke Button hat per Default den Focus und die Box ist garantiert am Screen1 zu sehen:

$userAnswer = [System.Windows.Forms.MessageBox]::Show("Und? Jo oder Na?", "Da MsgBox-Titel", [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question, [System.Windows.Forms.MessageBoxDefaultButton]::Button1, [System.Windows.Forms.MessageBoxOptions]::DefaultDesktopOnly) -eq [System.Windows.Forms.DialogResult]::Yes;

Komplexe GUIs und Dialoge

ProgressBar Dialog
Auch komplexere GUIs für Dialoge lassen sich damit bauen - wenn auch nicht so bequem wie mit dem VS-Designer. Hier ein Beispiel fuer eine Dialog - allerdings ohne Auswertung der Usereingaben:

## shows a FixedToolWindow with a ProgressBar and a Label
##
[Void] [reflection.assembly]::LoadWithPartialName("System.Windows.Forms");
 
# $pgBar 		progress-bar 
[System.Windows.Forms.ProgressBar] $pgBar = New-Object "System.Windows.Forms.ProgressBar";
$pgBar.Location = New-Object -TypeName "System.Drawing.Point" -ArgumentList 12,10;
$pgBar.Size     = New-Object -TypeName "System.Drawing.Size"  -ArgumentList 260,30;
$pgBar.Minimum  = 0;
$pgBar.Maximum  = 1000;
$pgBar.Step     = 1;
$pgBar.Value    = 600;
 
# $pgText		text under progress-bar
[System.Windows.Forms.Label] $pgText = New-Object "System.Windows.Forms.Label";
$pgText.Location = New-Object -TypeName "System.Drawing.Point" -ArgumentList 15,45;
$pgText.AutoSize = $true;
$pgText.Text = "..\dir1\dir2 (600 / 1000)";
 
# $pgForm		form as fixed toolbar-window
[System.Windows.Forms.Form] $pgForm = New-Object "System.Windows.Forms.Form";
$pgForm.Text = "Backup";
$pgForm.ClientSize =  New-Object -TypeName "System.Drawing.Size"  -ArgumentList 284,75;
$pgForm.FormBorderStyle = 'FixedToolWindow';
$pgForm.ShowInTaskbar = $true;
 
$pgForm.Controls.Add($pgText);
$pgForm.Controls.Add($pgBar);
 
[Void] $pgForm.ShowDialog();

powershellgui_progressbardialog.jpg

Change Password Dialog
Und noch ein etwas komplexeres Beispiel mit Input und Events (für die Buttons) und der nachfolgende Auswertung des Inputs. Das Script fragt (für Oracle) nach allen benötigten Parametern um das Passwort mithilfe des Oracle-Client sqlplus.exe eines Users zu ändern:

  • Oracle Connection String
  • Username
  • Altes Passwort
  • Neues Passwort

Nur bei einem Click auf „OK“ wird SQLPLUS das Passwort tatsaechlich geändert - durch einen Aufruf von sqlplus mit dem entsprechenden SQL Befehl. Keine Angst - dieses Beispiel ruft sqlplus.exe nicht wirklich auf, weil die entsprechende Zeile im Skript auskommentiert ist.

powershellgui_changeorclpwddialog.jpg
Den SourceCode dazu gibt es hier als Download.

Parameter einer Function (auch eines Scripts) können Optional oder Verpflichtend oder Vordefiniert sein. param heisst das Zauberwort dazu. Dabei gilt: Angegebene Parameter sind verpflichtend. Parameter mit Default-Werten (zB $null) sind optional.

function testparam {
  param( $muss, $kann = $null)
 
  Write-Output $muss
  if ($kann -ne $null) {Write-Output $kann}
}

Analog zu den Parametern einer Function kann auch für ein ganzes Script Parameter angegeben werden. Die funktionsweise ist die gleiche. Und auch die Auflösung durch die (interaktive) Powershell funktioniert hier (mittles TAB). Einzig der Aufruf von param sollte wohl eher am Anfang des Scripts stehen?…

param (
  [string]$SearchPattern = ""     ,
  [switch]$Verbose       = $false ,
  [switch]$Help          = $false
);
 
if ($Help) {
  Write-Output "this script lists Assemblies registe....";
}

Mit der Powershell kann man Processe starten, stopen, ihre geladenen Module (=File-Handles) auslesen, usw. das CmdLet Get-Process gibt dabei stets ein(e Liste von) System.Diagnostics.Process zurück. Und das Teil kann eben eine ganze Menge.

Zum stoppen eines Processes kann man entweder die .Net Methoden der Process-Klasse verwenden; oder etwas bequemer das Stop-Process CmdLet.

$p = Get-Process "sqlplus"         		# get the "sqlplus" process
$pm = $p.Modules                        # get all his loaded modules/handles (= DLL, Libs, etc)
$p.Close() -or $p.Kill()                # quit the process: 1st be friendly; if this dont work: kill him!
 
Stop-Process -Force -ErrorAction SilentlyContinue -Id $p.Id		# does the same as above 

Tja, aus irgendeinen (für mich nicht 100%ig einsichtigen) Grund liefert eine Function fast immer ein Array als Return-Wert zurück. Selbst wenn man explizit einen Scalar-Wert mittels return liefert. Das Ganze (mit der Motivation dahinter. Stichwort: Pipelining) ist auch relativ gut im eBook „EffectiveWindowsPowershell, Item4“ beschrieben.

Beispiel:

function RunSqlQuery {
 ...
 [System.Data.DataTable] $tbl = $DataSet.Tables[0]
 return $tbl  
}

Dieses Verhalten kann man aber dennoch austricksen. Da hab ich mal im oben genannten eBook, Item4 gelesen: „The good news is that we can work around PowerShell’s flattening behavior by creating a new collection that contains just one element - our original collection. PowerShell provides us with a nice shortcut to do just that…“. Naja, wie auch immer. Am Schluss sieht das Ganze dann so aus - Zu beachten ist das unscheinbare Komma bei return:

function RunSqlQuery {
 ...
 [System.Data.DataTable] $tbl = $DataSet.Tables[0]
 return ,$tbl  
}

Das Ergebnis der Function kann nun direkt „als Scalar“ verwendet werden:

$tbl = RunSqlQuery -Query $mySQL -SqlConnection $con;
foreach ($row in $tbl.Rows)
  Write-Host "$row.id $row.x $row.y";

Die beispielhaften Sourcecode-Auszüge mögen wohl so keinen Sinn ergeben. Deshalb möchte ich an dieser Stelle kurz Ihre Herkunft erklären: ich habe eine Powershell-Library für den SQLServer geschrieben. Die Library stellt dabei drei Funktionen bereit um

  1. ein System.Data.SqlClient.SqlConnection (= eine Connection zum SQLServer) Object zu erzeugen
  2. darauf eine Query (SELECT) abzusetzen und ein System.Data.DataTable mit dem Resultset zu erhalten
  3. oder eine Query ohne Resultset abzusetzen (aka INSERT, UPDATE, DELETE)

Das erste Codefragment stammt genau daher: mit einem einfachen return erhält man eine Liste von DataTables, das aus genau einem Element besteht. Es wird also eben nicht der gewünschte DataTable zurückgeliefert, auf dem man direkt operieren kann…
Den Source-Code für die Library gibt es hier. Ein Script zum testen der Library gibt es hier.

Und dann doch nicht immer?

Kleine Anmerkung am Rande: da oben steht fast immer. Will heissen, dass dies bei Int32, String und Co dann irgendwie doch nicht ganz so ist (da passiert dann also doch ein bischen magic oder wie?). Is uns aber auch Wurscht, denn der Trick funktioniert dennoch auch in diesen Fällen. Zum Beweis:

function bla  {$a = 1;    return $a }
function bla2 {$a = 1;    return ,$a}
 
$r = bla
$r.GetType().FullName   # System.Int32
 
$r = bla2
$r.GetType().FullName   # System.Int32

In der Powershell v2 kann man jetzt auch direkt .Net Klassen definieren. Bis dahin musste man immer den Umweg ueber C# und den entsprechenden Compiler gehen. Der Vorteil liegt klar auf der Hand: man kann nun innerhalb eines Scripts eigene Klassen definieren um zB komplexere Datenstrukturen als Arrays zu uebergeben. Und ausserdem koennen solche Klassen dynamisch erstellt werden, da der C# direkt aus einem String „kompiliert“ wird…

C:\PS>$source = @"
public class BasicTest
{
    public static int Add(int a, int b)
    {
        return (a + b);
    }
 
    public int Multiply(int a, int b)
    {
        return (a * b);
    }
}
"@
 
C:\PS> Add-Type -TypeDefinition $source
 
C:\PS> [BasicTest]::Add(4, 3)
 
C:\PS> $basicTestObject = New-Object BasicTest
C:\PS> $basicTestObject.Multiply(5, 2)

Die Powershell kann ueber den internen PC Lautsprecher Töne von sich geben. Also für kleine Musikuntermalung kann gesorgt werden.

# simple Beep with the ASCII-Bel ...
$b = [char]7;
Write-Host $b;
 
# ... or with the .Net-Framework
[console]::Beep();
 
# or a little more advanced
$frequency = 2000; # 2 kHz
$duration  = 3000; # 3000 msec
[console]::Beep($frequency , $duration);

Wer (welcher perl Programmierer zumindest) haette gedacht, dass es mittunter so schwer sein kann alle Newlines (\n\r unter Windows!) eines Strings in der Powershell zu entfernen?! Hier ein paar Versuche, Fehlschlaege und Loesungen:

PS C:\> $msg = "line1
>> line2";
PS C:\> $msg
line1
line2
 
PS C:\> $msg.Replace("`n", ' '); # sometimes didn't work?!
line1 line2
 
$msg.Replace([System.Environment]::NewLine, ' ')  # never works?!
line1
line2
 
PS C:\> [string]::join(' ', $msg.Split([System.Environment]::NewLine)); # works always. however, it's realy ugly
line1 line2

Ein neues Prompt in der Powershell ist einfach. Dazu muss im (eigenen User-)Profil die funktion promt neu definiert werden. Das entsprechende User-Profil File findet sich in WindowsXP unter
C:\Dokumente und Einstellungen\{USERNAME}\Eigene Dateien\WindowsPowerShell\profile.ps1.

## a new (and smaller) prompt. and the current path in the window-title
function prompt
{
  # get the current working-directory
  $str = $pwd.Path
 
  # set the window-title to the current working-dir
  $Host.UI.RawUI.WindowTitle = $str;
 
  # set the promt to my own format (shorten it, if its longer than 10 chars)
  if ($str.length -ge 10)
  {
    # The prompt will begin with "PS ...\",end with ">", 
    # and in between is onlz the last subdir of the current working-dir
    $str = '...\' + $pwd.Path.Split('\')[-1];
  }
  "PS $str> ";
}

… macht am prompt folgendes …

# Original Prompt
PS C:\Temp>
PS C:\Dokumente und Einstellungen\bernd\Eigene Dateien\PSProjekte>
 
# Neues, kurzes Prompt:
PS C:\Temp>
PS ...\PSProjekte>

… und der Window-Title des Powershell-Fensters zeigt nebenbei auch noch den kompletten, aktuellen Pfad an.

Das Ganze sieht dann in etwa so aus: powershell_new_prompt.jpg

Perl kann einen String einfach wiederholen lassen: 'A'x3 macht AAA, wiederholt also den string drei mal. Die Powershell kann das auch.

PS> "*" * 3
***

Will man einen char (zB spezielle ANSI-Zeichen) wiederholen lassen, muss man etwas in die Casting-Trickkiste greifen:

PS> ([string]([char]0xA5)) * 3   # 0xA5 is the YEN Symbol in ANSI
¥¥¥

Der Vollständigkeit halber hier noch eine brauchbare ASCII Tabelle und ANSI Tabelle.

Mit der Kombination aus ODBC, .Net, Powershell kann auch eine ODBC Datenquelle (zB eine Datenbank oder ein Excel-Sheet) abgefragt werden. Hier ein Beispiel, dass in der SQLServer-Datenquelle myAdventureWorksDB die Tabelle Person.Contact selektiert:

$sql="select TOP 3 ContactID, Title, FirstName, LastName from Person.Contact";
 
# get odbc connection & sql-command
$con = New-Object -TypeName System.Data.Odbc.OdbcConnection -ArgumentList "DSN=myAdventureWorksDB";
$cmd = New-Object -TypeName System.Data.Odbc.OdbcCommand -ArgumentList $sql;
$cmd.Connection = $con;
 
# get DataAdapter & DataSet for odbc
$da= New-Object -TypeName System.Data.Odbc.OdbcDataAdapter;
$ds = New-Object -TypeName System.Data.DataSet;
$da.SelectCommand = $cmd;
 
# fill DataSet
$con.Open();
[Void] $da.Fill($ds);
$con.Close();
 
# get the DatTable and nicly print out the resultset
[System.Data.DataTable] $tbl = $ds.Tables[0];
$tbl | Format-List;

Meines Wissens gibt es kein CmdLet, um den MD5 Hash einer Datei zu berechnen. Macht nichts, ist einfach.

function Get-MD5Hash {
  param ([System.IO.FileInfo] $File);
 
  # calc the MD5 hash of the file
  $cryptoServiceProvider = [System.Security.Cryptography.MD5CryptoServiceProvider];
  $hashAlgorithm = new-object $cryptoServiceProvider
  $stream = $file.OpenRead();
  $hashByteArray = $hashAlgorithm.ComputeHash($stream);   # each element is a System.Byte
  $stream.Close();
 
  # convert the byte-array to a hex-string
  [string] $hashByteString = '';
  foreach ($b in $hashByteArray) {
    $hashByteString += $b.ToString('x2');
  }
 
  return ,$hashByteString;
}

Das ganze kann dann in etwa so verwendet werden:

# get the MD5 hash
PS> Get-MD5Hash -File 'C:\TEMP\mysql\MySQL-5.5.13-1.linux2.6.x86_64.tar'
52ca03d5002e809b61b1a81e3c65fc40
 
# We have the MD5-Hash value from a Webpage. Let's compare it with our file
PS> $MySQL_v55_Linux = '52ca03d5002e809b61b1a81e3c65fc40'
PS> $MySQL_v55_Linux -eq (Get-MD5Hash -File 'C:\TEMP\MySQL-5.5.13-1.linux2.6.x86_64.tar')
True

Sehr hilfreiche Funktionen für Byte Arrays und Hex Funktionen in Powershell findet man in diesem Blog-Eintrag. Unter anderem auch wie man mit Little- und Big-Endian umgeht, UTF-Encoding, Ausgabe als Hex-String usw. Alles sinnvolle Funktionen. Lustigerweise findet sich aber auf dieser Seite nicht die einfachste Variante ein Byte in Hex darzustellen, nämlich als String-Formatierung - ich hab das mal dort gepostet…

PS> $a = 15
PS> $a.ToString('x2')
0f

Hier ein komplettes CmdLet/Skript um diverse Hashwerte (md5, sha) zu berechnen bzw zu vergleichen: Zum Download

In der Powershell werden immer alle Ergebnisse, die innerhalb einer function anfallen zusammengesammelt und als result-array an den Aufrufer zurückgegeben. Das gilt natuerlich nur fuer Aufrufe, deren Ergebnis nicht gespeichert werden (also zB keiner variable zugewiesen werden). Auch ein normaler Aufruf eines CmdLet erzeugt einen Returnwert, der zumeinst als Ausgabe am Screen sichtbar ist. Nicht immer möchte man so das haben. Dieses Verhalten kann zwar nicht abgeschaltet werden, aber man kann trotzdem etwas dagegen unternehmen. Man kann

  • das Ergebnis eines CmdLet in einer Variablen speichern (die man nicht braucht)
  • das Ergebnis eines CmdLet in das Äquivalent zu /dev/null (= Out-Null) pipen
  • den Funktionsaufruf als [void] casten - das funktioniert allerdings nicht bei CmdLet
# Hier tritt das Problem nicht auf - das Ergebnis des Aufrufs wird ja gespeichert:
PS> $d = New-Item -ItemType "directory" -Path "c:\TMP\MyDir"
 
 
# Ergnis eines CmdLet nach out-Null pipen
PS> New-Item -ItemType "directory" -Path "c:\TMP\MyDir" | Out-Null
 
# Ergbnis einer dotnet-methode nach [void] casten
[void] [reflection.assembly]::LoadFrom("${assdir}Mono.Security.dll")

Ein ausgechecktes SVN Directory hat unter anderen in jeden Unterverzeichnis ein .svn Verzeichnis. In diesem speichert der SVN-Client alle seine Daten ab (von wo wurde dieses verzeichnis ausgecheckt? Welches File/Dir war dabei die aktuelle Revision? usw.). Will man nun aus so einen SVN-Checkout ein ganz „normales“ Verzeichnis machen, muss man eigentlich einfach nur alle .svn Verzeichnisse rekursiv löschen. Und das geht so:

PS ...\cairngorm> $a = Get-ChildItem -Recurse .\ |Where-Object {$_.Name -eq '.svn'} | Remove-Item -Force -Confirm $true

Es gibt in der Powershell keinen direkten Befehl, um die Größe eines Verzeichnisses auszugeben. Unter Linux verwendet man dazu gerne `du -sh /TMP`. Trotzdem ist dies leicht mit etwas Code zu bewerkstelligen:

$path = 'C:\TEMP';
$folderSizeB = (Get-ChildItem "$path" -Recurse | Measure-Object Length -Sum).Sum;
$folderSizeMB = "{0:N2}" -f ($folderSizeB  / 1024 / 1024);
 
Write-Host "$path : $folderSizeMB MB";

Die Powershell kennt Wertebbereiche. Allerdings nur bei Zahlen (eigentlich nur bei int32). Praktisch waere es aber auch, wenn es das auch fuer Buchstaben bzw. Chars gaebe. Mit einem kleine Trick ist das aber auch moeglich: Die Buchstaben einfach als ASCII Werte repraesentieren:

PS> $a = [int][char]'a';
PS> $d = [int][char]'d';
PS> ($a..$d) | foreach { [char]$_ }
a
b
c
d

Damit laesst sich beispielsweise ganz bequem fuer DokuWiki eine „Alphabet-Ueberschrift“ realisieren. Ist auf jeden Fall einfacher als das Ganze haendisch tippen zu muessen:

PS> $a = [int][char]'A';
PS> $z = [int][char]'Z';
PS> $a..$z | foreach { "===== $([char]$_) =====`n" }
 
===== A =====
 
===== B =====
 
===== C =====
 
 ...[SNIP]...
 
===== Z =====

Variablen in der Powershell haben per default stets nur eine lokale Gueltigkeit. Das heisst, dass vermeintlich globale Variablen (also im Hauptskript deklarierte) nicht in Funktionen zur Verfuegung stehen. Beispiel:

$a = 1
function b {$a = 2; $a}
 
PS> $a
1
PS> b
2
PS> $a
1

Soll die Variable wirklich global sein - also im gesamten Code gueltig sein - so muss der Scope der Variable angepasst werden:

$a = 1
(Get-Variable a).Options = "AllScope"; # set scope to `realy global`
function b {$a = 2; $a}
 
PS> $a
1
PS> b
2
PS> $a
2

Jobs in der Powershell sind im Priznip nicht anders als Hintergrund Prozesse/Threads die gestartet werden koennen. Allerdings erweist sich die Übergabe von Werten an den Job als gar nicht so einfach wie gedacht. Warum? Weil, it's important to remember that each job runs in it's own independent runspace. I've been trying to drill this into my own head and here are some consequences of that fact: job will not have access to (i.e.) Any variable from any scope (local, global etc.), Any functions you have loaded, ….

Natürlich ist es möglich, Werte bzw eine Varible an einen Jobs beim starten zu übergeben. Dazu dient der Parameter -InputObject vom CmdLet Start-Job. Im Job-Script kann dann auf den Wert mit der Pseudovariable $input zugegriffen werden. Aber auch hier gibt es eine ordentliche Stolperfalle. Denn $input ist nicht direkt die übergebene Variable bzw dessen Inhalt, sondern es handelt sich um eine Variable vom Typ System.Management.Automation.Runspaces.PipelineReader`1+<GetReadEnumerator>d__0[System.Object]. Was immer das auch sein mag?!

Ein kleiner Workaround hilft aber auch hier. Die Pseudovariable $input ist stets (ein wenig umständlich) auf den eigentlich gewünschten Typ zu casten. Und das geht in etwa so:

[XZY] $in = $input.'<>4__this'.read();

Im weiteren Job-Script kann/soll dann nicht mehr $input verwendet werden, sondern das neue, gecastete $in.

Der Job kann natüerlich sein Ergebnis auch zurück liefern. Dazu bietet sich ein return an, das dann beim Aufrufer mit dem CmdLet Receive-Job abgefragt werden kann.

Zu guter letzt sollten fertige Jobs auch wieder mit dem CmdLet Remove-Job gelöscht werden.

# 1) Start the job 'J1' to work on a simple String. And a job 'J2' working on an array
# 2) Wait for all/both jobs to finish their own work
# 3) Get the results from both jobs 
# 4) Remove all/both jobs
 
 
[string] $foo    = "foobar";
[array]  $foo_ar = @("foo1", "foo2", "foo3");
 
# (1) start jobs 
 
# job #1: input-param is a scalar (be quiet)
Start-Job -Name J1 -InputObject $foo -ScriptBlock { 
    [string] $in = $input.'<>4__this'.read(); 
    return "<hey $in>"; 
  } | Out-Null;
 
# job #2: input-param is an anarray (be quiet)
Start-Job -Name J2 -InputObject $foo_ar -ScriptBlock { 
    [array] $in = $input.'<>4__this'.read();
    [array] $r = @();
    foreach ($i in $in) {$r += "<hey $i>";} 
    return $r;
  } | Out-Null;
 
# (2) wait (quiet) for all jobs to finish
Wait-Job * | Out-Null;	# 
 
# (3) now get all the results from all jobs
[string] $res_j1 = Receive-Job -Name J1;
[array]  $res_j2 = Receive-Job -Name J2;
 
# (4) well, all jobs are finished. so kill'em all
Remove-Job * ;
 
 
Write-Host -BackgroundColor Black -ForegroundColor Yellow  "got from Job J1: $res_j1";
Write-Host -BackgroundColor Black -ForegroundColor Cyan    "got from Job J2: $res_j2";

Nützliche Links:
http://blogs.technet.com/b/heyscriptingguy/archive/2011/01/12/schedule-powershell-scripts-that-require-input-values.aspx
http://powershell.org/wp/2010/06/03/a-look-at-powershell-jobs-part-1/