This is translation of my article about some PowerShell tips & tricks not only for developers. I will split the article into more parts, so you won't get tired too early ;)

How many bytes is 1kB?

The today's tip will be very quick.
Do you exactly know how many bytes is 1MB or even 1GB? Are you tired of using 1024*1024*.. all the time? Then look at the examples below.
Note that there is no unit for 1 Byte. Just kidding ;)

[0]> 1kb, 1mb, 1gb, 1tb, 1pb
1024
1048576
1073741824
1099511627776
1125899906842624

What does it mean?
When working with files, you can think in terms of kilobytes/megabytes. Just use the size and add appropriate suffix. PowerShell will evaluate it automatically.

Meta: 2010-05-09, Pepa

This is translation of my article about some PowerShell tips & tricks not only for developers. I will split the article into more parts, so you won't get tired too early ;)

From time to time when working with Powershell you will find something interesting or a general pattern that makes your work easy or that helps you to understand some basics of PowerShell. In this article I will show you some of my tips and interesting useful patterns. Note that this article highlights only some of them. I had to select only some.

I won't touch advanced topics, that you will use only sometimes. I aim at everyday usage of PowerShell. Anyway, I hope at least some of them will be new to you.

Arbitrary names of functions

There are two scenarios how PowerShell is used. You have either your code snippets stored in script files and run them when needed (scheduled scripts, tools, modules, etc.). Or you just type in console and run the code immediatelly after you hit Enter.

When working with scripts, it is very important to keep them readable, comprehensible and independent. That's why you should use full cmdlet names and not only aliases (gci vs. Get-ChildItem). Alias gci can mean something completely different on other machine.

When working with console, your requirements change. You use aliases very frequently and type the shortest code, (gci|?{!$_.PSIsContainer}|select -exp Length). The important thing is to reach the goal, no matter how.

PowerShell makes things easy, because you can use pretty interesting names for your functions and aliases. Let's have a look at some examples:

[0]# well known functions ?? a ?:
[1]function ?? { if ($args[0]) { $args[0] } else { $args[1] } }
[2]function ?: { if (&$args[0]) { $args[1] } else { $args[2] } }

[3]?? $null 'default value'
default value
[4]?: {1} 'is 1' 'is not 1'
is 1
[5]?: {get-process nonexisting -ea 0} 'process exists' 'process doesn''t exist'
process doesn't exist

[6]function * { '*'*10 }
[7]function = { Set-Variable -scope Global -Name $args[0] -Value $args[1] }
[8]= a 100; $a
1
[9]$a = 200; $a #still works

Last example is tip by Johannes Rössel. He uses just Ctrl+D, Enter to exit Posh session. No more exit.

[10]# let's define function with name CTRL+D
[11]New-Item -Path "function:$([char][int]4)" -ItemType function -Value { exit }

Meta: 2010-05-04, Pepa

Joel Bennett published a module for logging via log4net. It is something I always wanted to do, but haven't time for it. Besides that, I wouldn't write it in such a quality as he did ;)

However, what I'm missing there is an ability to send a log message to udp port. It is great for application development. You can see in real-time what your application is doing. You can watch on what command it is waiting etc. And – you work with the messages in a much quicker way. No file opening, no search, you can filter the messages. But wait … is there any application that can read the messages?

Yes! I created a form application that can read the udp messages several years ago. Old, but still does its job well. There is an older description written in czech. Maybe automatic translation could help english readers.
In short it is able to show the messages in a grid. You can filter them so that only e.g. level warning and higher is displayed. You can swallow some messages based on regex or show them in a custom color.

Look at it in action how it works with Logger module by Joel.

Edit your Logger module

To get it working, you need to edit the module and add ability to log on a udp port.

  • To Get-Logger add switch
    [Switch]$Udp
  • Edit the Get-Logger function so that it looks like this:
    if($RollingFile) { $AppenderRefs +=  "<appender-ref ref=""RollingFileAppender"" />`n" }
    if($Udp)         { $AppenderRefs +=  "<appender-ref ref=""udpAppender"" />`n" } # new line
  • Add appender to the embedded configuration xml. You have to add it like this:
    <appender name="udpAppender" type="log4net.Appender.UdpAppender">
      <param name="RemoteAddress" value="127.0.0.1" />
      <param name="RemotePort" value="8080" />
      <param name="Encoding" value="utf-8" />
      <layout type="log4net.Layout.XmlLayout">
        <param name="Prefix" value="" />
      </layout>
      <param name="threshold" value="DEBUG" />
    </appender> <!-- new part -->
    <root>
      <level value="DEBUG" />
    </root>
    <logger name="$LoggerName">
      <level value="$LogLevel" />
      $AppenderRefs
    </logger>
    Note that it uses port 8080. The application has to listen on the same port.
  • And of course use it when calling Get-Logger
    $script:Logger = Get-Logger "PowerShellLogger" -Udp

Download

Meta: 2010-04-08, Pepa

Yesterday I published a little PowerShell function that allows you to select files visually. It's a wrapper around Get-ChildItem. Check the post.

Bernd Kriszio suggested that the first argument should be accepted from pipeline too. So, here is the second version.

Examples

The examples from previous post work as expected. And there is a little bit more to show you.

For each directory in m:\temp\blog a form is shown. Selected files are sent to the pipeline

[1]: gci m:\temp\blog | 
	? { $_.PsIsContainer } | 
	Select-File -rec | copy-item -Destination c:\temp\

For each directory (m:\temp\blog and m:\backup) form is shown. Again, the selected files are sent to the pipeline.

[2]: 'm:\temp\blog','m:\backup' | select-file

Code

function Select-File {
    param(
        [Parameter(Position=0,Mandatory=$true, ValueFromPipeline=$true,  
            ValueFromPipelineByPropertyName=$true)]
        [Alias("PsPath")]
        [string]$LiteralPath,
        [Parameter(Position=1,Mandatory=$false)][string]$filter,
        [Parameter()][switch]$recurse,
        [Parameter()][switch]$hideDir,
        [Parameter(Mandatory=$false)][string]$sortProperty='Name'
    )
    begin {
        Add-Type -Assembly System.Windows.Forms
        $global:SelectedItems = @()
        
    }
    process {
        $p = @{LiteralPath=$LiteralPath}
        if ($filter) { $p['filter'] = $filter }
        if ($recurse) { $p['recurse'] = $true }
        $files = @(Get-ChildItem @p | ? {!$_.PsIsContainer})
        
        if ($files.Length -eq 0) {
            #[Void][System.Windows.Forms.MessageBox]::Show("There is no item to select in $LiteralPath")
            return
        }
        $form = new-object Windows.Forms.Form  
        $form.Text = "Pick the files"
        $form.Size = new-object Drawing.Size @(500,600)  

        $panel = new-object System.Windows.Forms.ListView
        $panel.Dock = 'Fill'
        $panel.CheckBoxes = $true
        $panel.View = 'Details'
        $panel.FullRowSelect = $true
        [void]$panel.Columns.Add('', -1, 'Left')
        [void]$panel.Columns.Add('Dir', $(if($hideDir){0}else{-1}), 'Left')
        [void]$panel.Columns.Add('Name', -1, 'Left')
        [void]$panel.Columns.Add('Size', -1, 'Right')
        [void]$panel.Columns.Add('Modified', -1, 'Left')
        [void]$panel.Columns.Add('FullPath', 0, 'Left')
        $form.Controls.Add($panel)
            
        $dir = (Resolve-Path $LiteralPath).Path
        $dirE = [regex]::escape($dir)
        $items = @{}
        $files | 
            Sort $sortProperty |
            % { 
                $items[$_.FullName] = $_
                $cb = New-Object Windows.Forms.ListViewItem
                $cb.Checked = $false
                [void]$cb.SubItems.Add(($_.FullName -replace "($dirE).*",'$1'))    #directory
                [void]$cb.SubItems.Add(($_.FullName -replace "$dirE\\?(.*)",'$1')) #file
                [void]$cb.SubItems.Add($_.Length)                                  #length
                [void]$cb.SubItems.Add($_.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss')) #modified
                [void]$cb.SubItems.Add($_.FullName)                                #helper full path
                [void]$panel.Items.Add($cb)
            }
    
        $ok = new-object System.Windows.Forms.Button 
        $ok.add_Click({  
            $form.close()
        })  
        $ok.BackColor = [System.Drawing.Color]::Green
        $ok.Text      = "Ok"
        $ok.Dock      = [Windows.Forms.DockStyle]::Bottom
        $form.Controls.Add($ok)
    
        $Form.Add_Shown({$form.Activate()})   
        [void]$form.showdialog()  
        
        $panel.Items | 
            ? { $_.Checked } | 
            % { 
                $key = $_.SubItems[5].Text
                Write-Output $items[$key]
            }
    }
}

Now, when I think about the usage – originally I needed to select files only. However, why not to be able to select directories as well? Is it worthy spending my time? Maybe..

Download

Meta: 2010-04-02, Pepa

Note, that there is a new version of the script.

From time to time I need to select some files for operations like copying, reading content etc. I know that the files have some extension and their file name contains some word, but I'm not sure if my filters get all the files I need, because the rules what file should be selected might be quite complex.
And besides that I sometimes have some set of files that I need to select visually (~ look at each file and decide) and then process. Then it is very hard time for me.

Command like the following takes my time to create and can be quite error prone, because you know that there might be some old files that don't contain holiday nor summer in the name:

Get-ChildItem c:\temp -filter *.jpg -rec | 
   ? { $_.Name -match 'holiday|summer' -and $_.LastWriteTime -lt '2008-01-01' } 

That's why it would be much better to have a possibility to select files from a list by hand.

Solution

After you run Select-File a form appears with one button. After you select the files (there are checked), just press the button and checked items will be sent to the pipeline. Dead simple.

It can be used similarly as Get-ChildItem, but it is much more simple and it works only on file system.

Navigation

  • You can use keys to move up and down.
  • When holding Shift, more items are selected (but not checked).
  • To check the items press space.

Examples

Basic usage

[1]: Select-File M:\install\ | 
	? { $_.Length -lt 1kb } | 
	Copy-Item -Destination c:\temp\install

You can filter the items and search recursively:

[2]: Select-File M:\install\ -rec -filter *.exe | 
	copy-item -Destination C:\temp\install

You can sort the items:

[3]: Select-File M:\install\ -rec -sort LastWriteTime | 
	copy-item -Destination C:\temp\install

I think the parameters are quite self-explanatory so I won't explain them here.

Code

function Select-File {
    param(
        [Parameter(Position=0,Mandatory=$true)][string]$dir,
        [Parameter(Position=1,Mandatory=$false)][string]$filter,
        [Parameter()][switch]$recurse,
        [Parameter()][switch]$hideDir,
        [Parameter(Mandatory=$false)][string]$sortProperty='Name'
    )
    begin {
        Add-Type -Assembly System.Windows.Forms
        $global:SelectedItems = @()
        
        $items = @{}
        $form = new-object Windows.Forms.Form  
        $form.Text = "Pick the files"
        $form.Size = new-object Drawing.Size @(500,600)  

        $panel = new-object System.Windows.Forms.ListView
        $panel.Dock = 'Fill'
        $panel.CheckBoxes = $true
        $panel.View = 'Details'
        $panel.FullRowSelect = $true
        [void]$panel.Columns.Add('', -1, 'Left')
        [void]$panel.Columns.Add('Dir', $(if($hideDir){0}else{-1}), 'Left')
        [void]$panel.Columns.Add('Name', -1, 'Left')
        [void]$panel.Columns.Add('Size', -1, 'Right')
        [void]$panel.Columns.Add('Modified', -1, 'Left')
        [void]$panel.Columns.Add('FullPath', 0, 'Left')
        $form.Controls.Add($panel)
    
        $p = @{LiteralPath=$dir}
        if ($filter) { $p['filter'] = $filter }
        if ($recurse) { $p['recurse'] = $true }
        
        $dir = (Resolve-Path $dir).Path
        $dirE = [regex]::escape($dir)
        Get-ChildItem @p | 
            ? {!$_.PsIsContainer} | 
            Sort $sortProperty |
            % { 
                $items[$_.FullName] = $_
                $cb = New-Object Windows.Forms.ListViewItem
                $cb.Checked = $false
                [void]$cb.SubItems.Add(($_.FullName -replace "($dirE).*",'$1'))    #directory
                [void]$cb.SubItems.Add(($_.FullName -replace "$dirE\\?(.*)",'$1')) #file
                [void]$cb.SubItems.Add($_.Length)                                  #length
                [void]$cb.SubItems.Add($_.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss')) #modified
                [void]$cb.SubItems.Add($_.FullName)                                #helper full path
                [void]$panel.Items.Add($cb)
            }
    
        $ok = new-object System.Windows.Forms.Button 
        $ok.add_Click({  
            $global:SelectedItems = @($panel.Items | ? { $_.Checked })
            $form.close()
        })  
        $ok.BackColor = [System.Drawing.Color]::Green
        $ok.Text      = "Ok"
        $ok.Dock      = [Windows.Forms.DockStyle]::Bottom
        $form.Controls.Add($ok)
    
        $Form.Add_Shown({$form.Activate()})   
        [void]$form.showdialog()  
    }
    process {
        $SelectedItems | % { 
            $key = $_.SubItems[5].Text
            Write-Debug "Key: $key, Value: $($items[$key])"
            Write-Output $items[$key]
        }
    }
}

This is just first attempt how to solve my frustration. So, it is far from perfect solution. Just take it as inspiration for your further improvements ;)

Download

Meta: 2010-04-01, Pepa

Some time ago I read about a library for text comparison named DiffPlex. It looked interesting so I decided I would give it a try.

After some tests how it works I created a PowerShell module that creates diff of given old & new text and returns corresponding objects. It can write the result to console as well.

Example

I'll create two test files and the show how to compare them.

[1]: ipmo c:\temp\TextDiff.psm1
[2]: "This is first file
that will be used for demonstration
The time is $(date)" > c:\temp\diff1.txt
[3]: sleep -sec 2; "This is second file
that will be used for demonstration
The time is $(date)
Some added line" > c:\temp\diff2.txt

[4]: $d1 = Write-Diff -oldPath C:\temp\diff1.txt -newPath C:\temp\diff2.txt
This is first second file
that will be used for demonstration
The time is 03/30/2010 10:19:1610:19:17
Some added line

[4]: Write-Diff -oldText "a b`r`nc d" -newText "a d`r`n" > $null
a bd
c d

By default colorized output is written to console. You can use -silent to bypass the console output.
If you would like to work with the objects itself, it is possible as well.

[5]: $d1

Text                                     Type
----                                     ----
This                                     u         #unchanged
is                                       u
first                                    d         #deleted
second                                   a         #added
file                                     u
...                                      separator #newline separator
that will be used for demonstration...   u
The                                      u
time                                     u
is                                       u
03/30/2010                               u
10:19:16                                 d
10:19:17                                 a
...                                      separator
Some added line...                       a
...                                      u

Code

The code here doesn't contain error checking. You can download the full version.

You will also need assembly DiffPlex.dll that can be either downloaded at DiffPlex home page or below.

Add-Type -path (join-path $psflash data\src\PowerShell\bin\DiffPlex.dll)

function New-DiffItem {
    param(
        [Parameter(Mandatory=$true)]
        [string]$text, 
        [Parameter(Mandatory=$true)][ValidateSet('u','a','d','separator')]
        [string]$type
    )
    New-Object PSObject -Property @{ Text=$text; Type=$type }
}

function Get-DiffItem {
    param(
        [DiffPlex.DiffBuilder.Model.DiffPiece]$oldItem,
        [DiffPlex.DiffBuilder.Model.DiffPiece]$newItem,
        [string]$itemSeparator,
        [int]$index
    )
    switch($oldItem.Type) {
        'Unchanged' { New-DiffItem "$($oldItem.Text)$itemSeparator" u }
        'Deleted'   { New-DiffItem "$($oldItem.Text)$itemSeparator" d
                      if ($newItem.Type -eq 'Inserted') { 
                          New-DiffItem "$($newItem.Text)$itemSeparator" a 
                      }
                    }
        'Imaginary' { New-DiffItem "$($newItem.Text)$itemSeparator" a }
        'Modified'  {
            0..($oldItem.SubPieces.Count-1) | 
                % { 
                    $oldWord,$newWord = $oldItem.SubPieces[$_],$newItem.SubPieces[$_]
                    Get-DiffItem $oldWord $newWord -itemSeparator "" $_
                }
            New-DiffItem $itemSeparator separator
        }
    }
}
function Write-Diff {
    [CmdletBinding(DefaultParameterSetName='text')]
    param(
        [Parameter(Mandatory=$true,Position=0,ParameterSetName='text')]
        [string]$oldText,
        [Parameter(Mandatory=$true,Position=1,ParameterSetName='text')]
        [string]$newText,
        [Parameter(Mandatory=$true,Position=0,ParameterSetName='file')]
        [string]$oldPath,
        [Parameter(Mandatory=$true,Position=1,ParameterSetName='file')]
        [string]$newPath,
        [Parameter()][switch]$Silent
    )
    if ($oldPath) { 
        $oldText = [IO.File]::ReadAllText($oldPath, [Text.Encoding]::Default) 
    }
    if ($newPath) { 
        $newText = [IO.File]::ReadAllText($newPath, [Text.Encoding]::Default) 
    }
    
    $differ  = New-Object DiffPlex.Differ
    $sdiffer = New-Object DiffPlex.DiffBuilder.SideBySideDiffBuilder $differ
    $sdiff   = $sdiffer.BuildDiffModel($oldText, $newText)
    
    $ret = New-Object Collections.ArrayList
    0..($sdiff.OldText.Lines.Count-1) | 
        % { 
            $oldLine,$newLine = $sdiff.OldText.Lines[$_],$sdiff.NewText.Lines[$_]
            $ret += @(Get-DiffItem $oldLine $newLine -itemSeparator "`r`n" $_)
        }
    [PsObject[]]$ret
    if (!$Silent) {
        $ret | % { 
            $r = $_;
            switch($r.Type) {
                'u' { Write-Host $r.Text -NoNewline }
                'd' { Write-Host $r.Text -NoNewline -ForegroundColor Red }
                'a' { Write-Host $r.Text -NoNewline -ForegroundColor Green }
                'separator' { Write-Host $r.Text -NoNewline }
            }
        }
    }
}

Export-ModuleMember Write-Diff

Note that you will need to edit line Add-Type -path (join-path $psflash data\src\PowerShell\bin\DiffPlex.dll) according to the location where DiffPlex.dll is stored in your computer.

Download

Current version of DiffPlex.dll that can be downloaded at DiffPlex home page.

Meta: 2010-03-30, Pepa

Tags: PowerShell

Today I needed again to check some long running process and after it finished I wanted to see a notification that tells me that I can continue (I was waiting for msbuild to rebuild my source files).
As for notification I use Growl hacked by Jaykul. It works very well. If you haven't tried it, give it a chance.

The second part is the regular checking. In this case it could look something like this:

[1]: while((get-process msbuild)) { sleep -sec 10; }; growl 'msbuild ended'

Or the condition could be in other situations (get-date) -lt '12:05' or whatever.

You probably see the repeating pattern: while some condition (scriptblock) is true, wait for some time and then check the condition again. And – what's frustrating – it blocks my console!

That's why I created a simple solution that uses timer and built in way Register-ObjectEvent that can handle the events.
The features are:

  • Doesn't block my console!
  • Much more simplified usage, no repeated code.
  • You can check more independent events simultaneously.
  • Checks the condition every X milliseconds.
  • You can specify action that is executed during each check and after the condition (while cycle) finishes.

Code and examples

param(
    [Parameter(Mandatory=$false)][scriptblock]$processAction={},
    [Parameter(Mandatory=$false)][scriptblock]$endAction={},
    [Parameter(Mandatory=$true)][scriptblock]$endCondition,
    [Parameter(Mandatory=$false)][PsObject]$data,
    [Parameter(Mandatory=$true)][int]$interval
)

# unregister previous stale subscribers
if (!$global:__timers__tounregister) { $global:__timers__tounregister = @() }
$global:__timers__tounregister | 
    % { Get-EventSubscriber $_ -ErrorAction SilentlyContinue } | 
    Unregister-Event
$global:__timers__tounregister = @()

# just in case that the timers list would be helpful ;)
if (!$global:__timers__) {
    $global:__timers__ = @()
}
Write-Debug "Interval: $interval"
if ($interval -lt 10) { throw "Interval $interval too short" }

$timer = New-Object Timers.Timer
$timer.Interval = $interval

# note - redirections to $null are needed to avoid errors
# concretely - Get-Item : The WriteObject and WriteError methods 
#   cannot be called after the pipeline has been closed.
# however, it doesn't help sometimes :|
$action = {
    if (!(&$event.MessageData.End)) {
        $event.MessageData.Timer.Stop()
        & $event.MessageData.EndAction $event.MessageData.UserData > $null
        $global:__timers__tounregister += $event.MessageData.SourceIdentifier
        $global:__timers__ = @($__timers__ | ? {$_ -ne $event.MessageData.Timer })
    }
    else {
        & $event.MessageData.ProcessAction > $null
    }
}

$identifier = [Guid]::NewGuid()
$messData = New-Object PsObject -Property @{
    Timer=$timer; 
    SourceIdentifier=$identifier;
    UserData=$data;
    End=$endCondition;
    ProcessAction=$processAction;
    EndAction=$endAction;}
    
$job = Register-ObjectEvent `
    -InputObject $timer `
    -EventName Elapsed `
    -Action $action `
    -SourceIdentifier $identifier `
    -MessageData $messData
$timer.Start()

$global:__timers__ += $timer
$job
[1]: ps:\Start-Timer.ps1 -interval 1000 `
  -endCondition {get-process *msbuild* -ea 0 }`
  -endAction {[system.windows.forms.messagebox]::Show('msbuild ended') }

So I solved my problem! You can imagine many more examples from the simple ones (as following) to some advanced stuff like checking emails ;)

[2]: $t = (get-date).AddMinutes(1)
[3]: ps:\Start-Timer.ps1 -interval 1000 `
  -endCondition {(date) -lt $t}.GetNewClosure() `
  -endAction {[system.windows.forms.messagebox]::Show('game over') }
[4]: ps:\Start-Timer.ps1 -interval 300 `
  -endCondition {(get-item C:\temp\numbers.txt).Length -lt 1kb} `
  -endAction { [system.windows.forms.messagebox]::Show('Too big log file') }
  # now during this 'cycle' you will see a message box
[5]: 1..500 | % { sleep -mil 10; $_|Add-Content C:\temp\numbers.txt }
[4]: $t = (get-date).AddMinutes(1)
[5]: ps:\Start-Timer.ps1 -interval 300 `
  -endCondition {(get-date) -lt $t}.GetNewClosure() `
  -processAction { 
		if ((gi C:\temp\numbers.txt).Length -gt 1kb) { 
			Remove-Item C:\temp\numbers.txt } 
	}
[6]: 1..2000 | % { sleep -mil 20; $_|Add-content C:\temp\numbers.txt }

Simpler call

I added a function check to my profile to simplify how I call it:

function check {
    param(
        [Parameter(Mandatory=$true,Position=1)][PsObject]$condition,
        [Parameter(Mandatory=$true,Position=2)][string]$message,
        [Parameter(Mandatory=$false)][int]$interval=1000
    )
    $date = $condition -as [datetime]
    if ($date -ne $null) {
        $condition = { (get-date) -lt $date }.GetNewClosure()
    } elseif (!($condition -is [scriptblock])) {
        throw "$condition can not be converted to date and is not scriptblock"
    }
    $params = @{
        Interval=$interval;
        EndCondition=$condition;
        EndAction={growl $message }.GetNewClosure()
    }
    & (join-path $powershellDir tools\Start-Timer.ps1) @params
}

I use my function growl to notify me, but you can replace it by the message box as in previous examples.

[1]: check { Get-Process chrome -ea 0 } 'chrome closed'
[2]: check 6:00 'wake up!'

Traps

Closed pipeline

Nothing comes without costs. From time to time I have a problem with the last example – it fails with this message: Get-Item : The WriteObject and WriteError methods cannot be called after the pipeline has been closed. I don't know how to workaround it.

GetNewClosure() not working

Furthermore the code is more complicated than it could be. It is because I couldn't use GetNewClosure() because of this problem: New closure on scriptblock. If I could I would just skip passing the scriptblocks and timer in MessageData and avoid the hashtable.

Console hangs

Another complication comes with unregistering the subscriber. I reported a bug on connect: Posh console hangs when unregistering event.

Basically, the problem is that I tried to call something like this:

Register-ObjectEvent $timer Elapsed -SourceIdentifier III`
 -action { Get-EventSubscriber III | Unregister-Event }

If PowerShell tries to execute the action's body and you press Enter, the PowerShell console stops responding.

I guess it is caused by method PSLocalEventManager.DrainPendingActions(PSEventSubscriber subscriber) that is called during Unregister-Event. Inside the method there is a part of code like this:

while (this.IsExecutingEventAction)
{
    Thread.Sleep(100);
}

What does it mean? If some action is being executed then sleep for 100 milliseconds. But.. wait. This code is called from an action. So, in fact it checks that the action is running and calls Sleep. So, the action can not be finished, because … it is sleeping. And so on …

Performance

Try to run 3 timers, each with interval 100 milliseconds and specify only condition like 'time is less than...'. Look at your processor. It is quite busy, much more than I think it should be (at least on my home laptop). I guess that pure C# console application would run without any notice (but i didn't try it).

Download

This task was much more difficult than I thought it would be. Maybe I do the things wrong, maybe there are some bugs in PowerShell core, maybe I don't know some principles.
Anyway, I hope you learned something. At least you could remember method named GetNewClosure().

Meta: 2010-03-12, Pepa

This post is english translation of my article for czech Technet Flash Magazine.

The main reason why Microsoft started working on PowerShell was to give administrators better tool for their tasks than they had. With PowerShell they can automate their daily tasks very easily. PowerShell proved that it is really amazing tools and that's why Microsoft integrated PowerShell to Windows Serve 2008, Windows 7 or SQL Server 2008. There are other libraries for IIS, Exchange, VMware management and many more others.

So, when PowerShell is so great why only administrators should use it? Why not programmers or advanced users? If you have created any .bat file or were scriptinng in old command line, Powershell is the right choice for you. It's syntax is much more comfortable and related to common programming languages. Besides that there are very interesting commands (cmdlets) integrated into the core.

As you guess this article is written for programmers. It should point out some tips there could make their work easier.

Common simple console application problems

First of all I will emphasize the main reason why PowerShell is so popular (among programmers) – tasks that you normally solve with a single-purpose command line C# application can be solved in PowerShell very easily. What does it mean "very easily"? You only need to create a ps1 file and then run it in PowerShell console. (of course using script is not necessary, you can write the commands directly into the console)

How we did it without PowerShell? In Visual Studio we created a C# command line project and add some code into generated Main method. We needed to parse arguments again, browse the directory tree manually as in our previous project again, open/read files, process errors and exceptions etc. Then build the project and run the exe file. It is so tedious for one simple task and there are lots of files (source, project, etc.).

C# is great for complex processing where we can use all its nature. If you compare it to PowerShell there is significant code noise. Generally speaking if the task can be done in .NET, then it can be done in PowerShell. The typical task might be: go through all the files in given directory, open them and replace string XYZ with ABC.

Text replacement was just an example. Some time ago I saw someone who was trying to deal with duplicate pictures. He had many pictures from his camera and some of them were duplicated in more directories. He wanted to find the duplicates and ignore Exif because some of them were already edited. If you are interested in the resulting script, just have a look at stack overflow.

As a very short example let's try to sort pictures by their orientation (vertical vs. horizontal) and sort the result by the orientation and name. The pictures can be anywhere in the directory structure:

$d = gci D:\temp\ dscn*.jpg -rec | 
    % {
       $b = new-object System.Drawing.Bitmap $_.FullName
       New-Object PsObject -prop @{
        Orientation=if($b.Height -gt $b.Width) { 'v' } else {'h' };
        Path=$_.fullname }
       $b.Dispose()
    } | 
    sort -Property Orientation,Path

The code could be even shorter but then it would not be readable.
Can you see how easily we can gain the results? Did I hear woow? ;)
You can find many similar problems in you daily life, just use your phantasy. In the following I'll cover some concrete howtos and PowerShell features.

Exploring libraries

Sometimes you have to deal with unknown assemblies (or COM) or you have only some examples that you would like to check. For example I found a library that works with StackOverflow API. You don't know nothing about it, just some examples. So you download the zip and...

PS> Add-Type -Path c:\temp\StackOverflowApi\StackExchangeApi.dll
PS> $uri = new-object System.Uri 'http://code.google.com/p/stackexchange-api/'
PS> [StackExchangeApi.SXAPI]::Initialize('SXAPI Example Code', $uri)

We initialized the library as in the example and lets have a look at how we can get a user:

PS> [STackExchangeApi.User] | gm -static

   TypeName: StackExchangeApi.User
Name            MemberType Definition
----            ---------- ----------
Equals          Method     static bool Equals(System.Object objA, System.Object objB)
GetUsersForName Method     static System.Collections.Generic.ICollection`1[[StackExcha...
GetUserWithId   Method     static StackExchangeApi.User GetUserWithId(long id, StackEx...
ReferenceEquals Method     static bool ReferenceEquals(System.Object objA, System.Obje...

PS> [STackExchangeApi.User]::GetUsersForName

MemberType          : Method
OverloadDefinitions : {....GetUsersForName(string name, StackExchangeApi.StackExchangeSite site)}
...

Ok, that means that there is something new - StackExchangeApi.StackExchangeSite. What is it?

PS>  [STackExchangeApi.StackExchangeSite] | gm -sta

   TypeName: StackExchangeApi.StackExchangeSite

Name              MemberType Definition
----              ---------- ----------
...
ToObject          Method     static System.Object ToObject(type enumType, System.Object v...
Meta              Property   static StackExchangeApi.StackExchangeSite Meta {get;}
ServerFault       Property   static StackExchangeApi.StackExchangeSite ServerFault {get;}
StackOverflow     Property   static StackExchangeApi.StackExchangeSite StackOverflow {get...
SuperUser         Property   static StackExchangeApi.StackExchangeSite SuperUser {get;}

Ok, we got the idea. It is the site where we want to look for the user. Let's try it.

PS> [STackExchangeApi.User]::GetUsersForName('skeet', 'StackOverflow')

Site              : StackOverflow
Id                : 22656
Badges            : {Nice Answer, Nice Answer, Nice Answer, Nice Answer...}
RecentActivity    : {2341601, 2341288, 2341035, 2340025...}
ReputationGraph   : StackExchangeApi.ReputationGraph
Name              : Jon Skeet
Answers           : {7244, 9033, 131871, 137448...}
Questions         : {194484, 215548, 236907, 247621...}
FavoriteQuestions : {212726, 282329, 348175, 406760...}
Gravatar          : http://www.gravatar.com/avatar/6d8ebb117e8d83d74ea95fbdd0f87e13?s=50&amp;d=ide...
Reputation        : 141022
GoldBadgeCount    : 29
SilverBadgeCount  : 542
BronzeBadgeCount  : 1073

# and 4 more users are returned

PowerShell is based on .NET so you can easily look at the properties, call the methods, even use events etc. Do you know any quicker way how to experiment with unknown API?

The same holds for COM objects. Look at quick example how to work with Skype.

Working with XML

One of the feature that you will like is how PowerShell deals with XML. Imagine that you can work with XML like with an object – attributes become string properties and nested elements become object properties. Sounds familiar? Yes, it is similar to (de)serialization. But when deserializing a new object of some type is created. When working with XML from PowerShell we get XmlDocument which is very well known in .NET world.

It's time for an example. Let's create a new test file.

PS> @"
<root>
   <article date="2010-12-01">
    <name>Discover new dimensions</name>
    <body>Discover them now. Go!</body>
   </article>
   <article date="2000-01-01">
    <name>Future</name>
    <body>what will be with us in ten years?</body>
   </article>
</root>
"@ | Set-Content c:\temp\flashtest.xml

We will read the file and cast it to xml (this is how accelerators work) and look at the result.

PS>$x = [xml](gc c:\temp\flashtest.xml)
PS>$node = $x.root.article | Where-Object { [datetime]$_.date -lt [datetime]'2005-01-01' }
PS>$node
date         name                     body
----         ----                     ----
2000-01-01   Future                   what will be with us in ten years?
PS>$node.name = 'Near ' + $node.name
PS>$x.root.article[0].date = (get-date).ToString('yyyy-MM-dd')

Variable $x is of type XmlDocument, whereas $node of type XmlElement. If you want to set a new value of an attribute or of an element string, just use simple assignment. If you need to add a new element, then we have to switch to .NET methods.

PS>$note = $x.CreateElement('note')
PS>$note.InnerText = 'poor content'
PS>$node.AppendChild($note)
PS>$x.root.article[1]
date        name             body                                note
----        ----             ----                                ----
2000-01-01  Near Future      what will be with us in ten years?  poor content

Sometimes you would like to use XPath. In V1 you needed to do it using some .NET objects. In V2 it is much more easier, we were given cmdlet Select-Xml.

PS>Select-Xml -Xml $x -XPath '//article[contains(body/text(),"ten")]'
PS>#usage of namespace
PS>$ns = @{ e = "http://www.w3.org/1999/xhtml" } 
PS>Select-Xml -Path $somePath -Xpath //e:div[@id] -names $ns

If you wonder what can be used as a source for Select-Xml, just have a look at Get-Help Select-Xml. You will see three options: xml object(s) itself (as in the example above), path to file(s) or xml(s) as a string.

After you are done and you want to save the result, you have to use .NET method again:

PS>$x.Save('c:\temp\res.xml')

Regex tester

PowerShell is very handy when there is a need to test regular expressions. I won't describe what regular expressions are and what they solve. You can find more info at Regular-expressions.info.
I'll show you how to check that the regular expression does what expected.

Checking the regex in PowerShell is the quickest way if you have PowerShell console open (note: if you lost the console among the other windows, have a look at AutoHotkey). It is much more lengthy to test regexes on web online or run special programs. There are several possibilities.

Operator -match

This is the simplest approach. Left operand is (array of) string(s) and right operand is the regular expression.

# silly regex just for demo
PS>'jeho email je karel@novak.cz' -match ' \w+@[a-zA-Z_]+\.(?<d>[a-zA-Z]{2,3})' 
True
PS>$matches
Name                           Value
----                           -----
d                              cz
0                              karel@novak.cz

PowerShell uses collection $matches with regex groups from last evaluation with -match operator. Group 0 contains all the matching string. Regex is not case sensitive. In fact it corresponds to regular expression without any special options.

In case you will need case sensitivity, use operator -cmatch.

Operator -match can be also used as a filter. Open your console and try it, you will figure it out immediatelly:

PS>'1a','2b','1c' -match '1\w'
PS>'1a' -match '1\w'

Accelerator [regex]

You can use [regex] to create a regular expression object:

PS>$r = [regex]'^\w+@[a-zA-Z_]+\.(?<d>[a-zA-Z]{2,3})$'
PS>$r.GetType().FullName
System.Text.RegularExpressions.Regex

It creates instance of class Regex (well known for .NET programators) and saves it to variable $r. Again there are no special options. Let's have a look on the options:

PS>$r | fl Options

You can work with the object as you know from .NET. In case you don't remember some signatures, you may have a look at the members:

PS>$r | gm
   TypeName: System.Text.RegularExpressions.Regex
Name                MemberType Definition
----                ---------- ----------
...
GetGroupNames       Method     string[] GetGroupNames()
GetGroupNumbers     Method     int[] GetGroupNumbers()
…
IsMatch             Method     bool IsMatch(string input), bool IsMatch(strin
Match               Method     System.Text.RegularExpressions.Match Match(st
Matches             Method     System.Text.RegularExpressions.MatchCollection
Replace             Method     string Replace(string input, string replacemen
...

Object creation

The last option, that is closely related to the previous one, is to create regex using cmdlet New-Object. Then you can use any constructor of Regex class – e.g. specify options like multiline, singleline etc.

PS>$opts = [System.Text.RegularExpressions.RegexOptions]'MultiLine,SingleLine'
PS>$r = new-object Text.RegularExpressions '^\w+@[a-zA-Z_]+?\.(?<d>[a-zA-Z]{2,3})$',$opts

Look at the interesting syntax sugar how to specify regex options – I haven't found it documented anywhere. I found it accidentally at Oisin's blog Using Enumerated types (Enums) in PowerShell.

Generally it's a joy to work with enums in PowerShell. You don't need to specify type of the enum, just its name is enough.
Furthermore – try this:

PS>[string]::Compare('a','a',[Globalization.CultureInfo]::CurrentCulture, 'test')
Cannot convert argument "3", with value: "test"..... The possible enumeration values are "None,
IgnoreCase, IgnoreNonSpace, ... Ordinal"."
PS>[string]::Compare('a','a',[Globalization.CultureInfo]::CurrentCulture,'IgnoreCase')

PowerShell will suggest you valid values – so you don't need to look into documentation! This will speed up your productivity if you work with enums.

Note #1: You can of course just use static method of class [regex]:

PS>[regex]::IsMatch('karel@novak.cz', '^\w+@[a-zA-Z_]+\.(?<d>[a-zA-Z]{2,3})$')

Note #2: You know operator -match. There is another very useful operator -replace, that is used for text replacement. Here is just a quick and simple illustration:

PS>(Get-Content c:\test.txt) -replace 'abc','def' | Set-Content c:\test.txt

Function FindR

Later in section about clipboard I use FindR. It's a tiny filter that returns data that match given regular expression.

filter FindR($regex) {
	[Regex]::Matches($_, $regex, 'IgnoreCase') | % { $_.Value }
}

You can pipe almost anything to FindR. PowerShell will try to convert it to string because method Matches expects string as its parameter. If we pipe objects, PowerShell runtime decides how to convert the objects to string.

For example dir | findr '.*gs.*' will work, but it will return only names of files and directories (that match the regex).

But Get-WinEvent -LogName Application -MaxEvents 10 | findr '.*instal.*' will not work, because PowerShell will pass string 'System.Diagnostics.Eventing.Reader.EventLogRecord' to method Matches. This string is probably just a result of ToString() called on objects from Get-WinEvent.

You are able to come up with some realistic scenarios for FindR, I'm sure. I use it when I need to parse logs from our customers. In the last case I had some file sizes in the log and I needed to find out average size and total sum of the sizes. To parse concrete sizes I used look behind.

gc c:\dev\WO\wpdataimport.log | 
  findr -r '(?<=Job content file size: )\d+' | 
  Measure-Object -Sum -Average

First command reads the file, the second one finds only lines that contain 'Job content file size' and returns only numbers that follow the string and the last command does the statistics. Quick and easy. This, Jane, this is PowerShell :)

Encoding, decoding, conversions …

Especially web developers need to work with base64 strings or encode/decode urls. You can do it of course via some online tools or specialized programs. But again, it is much more easier to add this functions to your profile and the conversions will be available all the time.

# imports assembly needed for url stuff
Add-Type -AssemblyName System.Web

function FromBase64([string]$str) {
  [text.encoding]::utf8.getstring([convert]::FromBase64String($str))
}
function ToBase64([string]$str) {
  [convert]::ToBase64String([text.encoding]::utf8.getBytes($str))
}
function UrlDecode([string]$url) {
  [Web.Httputility]::UrlDecode($url)
}
function UrlEncode([string]$url) {
  [Web.Httputility]::UrlEncode($url)
}
function HtmlDecode([string]$url) {
  [Web.Httputility]::HtmlDecode($url)
}
function HtmlEncode([string]$url) {
  [Web.Httputility]::HtmlEncode($url)
}

Admins working solely in command line might use this function that helps with searching on Google:

PS> function Run-GoogleQuery {
  Start-Process ('http://www.google.cz/search?q=' + (UrlEncode ($args -join " ")))
}

PS> Set-Alias qg Run-GoogleQuery
PS> qg this is test # runs default browser and searches for 'this is test'

I used HtmlEncode when I was working on this article and I needed to insert PowerShell code into <pre ..> tag.
With function clip (you will find it later) it was very easy:

HtmlEncode (clip) | clip

Conversion Html2Xml

Sometimes you will need to convert html to xml. Regexes? No! I hope you have heard that parsing HTML with regexes is very tricky. Instead, you may use free library SgmlReader.

function Convert-Html2Xml {
  param(
    [Parameter(ValueFromPipeline=$true)][object[]]$html
  )
  begin   { $sb = new-object Text.StringBuilder(20kb) }
  process { $html | % { $null = $sb.AppendLine($_) } }
  end {
    # no default namespace, thx.
    $str = $sb.ToString().Replace(' xmlns="http://www.w3.org/1999/xhtml"', '')
    Add-Type -Path G:\bin\SgmlReaderDll.dll 
    
    $sr = new-object io.stringreader $str

    $sgml = new-object Sgml.SgmlReader
    $sgml.DocType = 'HTML';
    $sgml.WhitespaceHandling = 'All';
    $sgml.CaseFolding = 'ToLower';
    $sgml.InputStream = $sr;

    $xml = new-object Xml.XmlDocument;
    $xml.PreserveWhitespace = $true;
    $xml.XmlResolver = $null;
    $xml.Load($sgml);

    $sgml.Close()
    $sr.Close()
    
    $xml
  }
}

There are two ways how to work with the function:

PS>$x1 = gc c:\temp\testhtmlsource.delete.html | Convert-Html2Xml
PS>$x1.Save('c:\temp\test1.xml')
PS>$x3 = Convert-Html2Xml (gc c:\temp\testhtmlsource.html)
PS>$x3.Save('c:\temp\test2.xml')

You can use the conversion in cases where given web site doesn't have public API and you need to work with it only through its WEB UI (nightmare!). One example could be translator http://www.slovnik.cz. Complete solution (quick & dirty) uses XPath via cmdlet Select-Xml.
Some other quick & dirty code? I built up one for my colleague who creates from time to time links to movies database CSFD manually. Don't look for beautiful code, you will find only automation.

And last quick example – you found a blog with many posts and all the posts contain full text. It can look like this. Rss feed doesn't help because it returns only some of them. You would like to see headings of the posts because you are just curious. Obviously you can scroll down and catch the headings. But what about this:

$xml = Convert-Html2Xml (download-page 'http://www.nivot.org/CategoryView,category,PowerShell.aspx' )
Select-Xml -Xml $xml -XPath '//div[@class="itemTitle"]/a/text()' | % { $_.Node.Value }

PowerShell 2.0 - About Dynamic Parameters
PowerShell 2.0 – Introducing the PModem File Transfer Protocol
PowerShell 2.0 - Enabling Remoting with Virtual XP Mode on Windows 7
PowerShell 2.0 goes RTM for ALL Platforms
PowerShell 2.0 - Module Initializers
PowerShell 2.0 – Getting and setting text to and from the clipboard
PowerShell 2.0 – Asynchronous Callbacks from .NET
PowerShell – Function Parameters &amp; .NET Attributes
PowerShell 2.0: A Configurable and Flexible Script Logger Module
....

Clipboard

For the first time you will wonder: "Clipboard? Why should I care?". I use it very often as a transport mechanism between an application and PowerShell. You have seen several times that I use clip. One more sample will follow:

I have a problem with deadlocks in sql server. I run sql server from console with switches for deadlock detections. After sql server prints some info about resolved deadlocks, I select the text in console, copy it and go to PowerShell. I would like to find out the SPIDs.
(clip) | FindR -r 'SPID: \d+' | select -unique
This will return IDs of all processes involved.

Take it just as an example, I know that I could use SQL analyzer.

And how the functions for clipboard management look like?

Add-Type –a system.windows.forms
function Set-ClipBoard { 
  param(
    [Parameter(Mandatory=$true,ValueFromPipeline=$true,Position=0)][object]$s
  )
  begin { $sb = new-object Text.StringBuilder }
  process { 
    $s | % { 
      if ($sb.Length -gt 0) { $null = $sb.AppendLine(); }
      $null = $sb.Append($_) 
    }
  }
  end { [windows.forms.clipboard]::SetText($sb.Tostring()) }
}
function Get-ClipBoard { 
  [windows.forms.clipboard]::GetText() 
}
# clip works as Get-Clipboard and Set-Clipboard; it depends on the context:
# gc c:\test.txt | clip 
# clip | sc c:\test.txt
function clip {
  param(
    [Parameter(Mandatory=$false,ValueFromPipeline=$true)][object]$s
  )
  begin { $sb = new-object Text.StringBuilder }
  process {
    $s | % { 
      if ($sb.Length -gt 0) { $null = $sb.AppendLine(); }
      $null = $sb.Append($_) 
    }
  }
  end {
    if ($sb.Length -gt 0) { $sb.Tostring() | Set-ClipBoard} 
    else                  { Get-ClipBoard  }
  }
}

Note that clipboard management is available only if PowerShell is run with -STA switch or in ISE environment. It's because it uses WinForms in the background.
In case you need to have clipboard available in MTA (default mode), look at PowerShell 2.0 – Getting and setting text to and from the clipboard
.

Is there something missing?

Yes, of course. There are some more topics that deserve its own article. There is a great cmdlet New-WebServiceProxy, you can work with SQL server (using .NET classes), you can create GUIs (WinForms or WPF), use remoting (for deployment and continuous integration), … and much more. Hopefully I will cover some of them next time.

You have seen basic features that I find very attractive. However, it's up to you if you will became fan of PowerShell or if you think that the features are not worth learning. But remember, that the most important reason why you should use PowerShell is the first part – quick and effective solution of common tasks. That's where PowerShell is excellent.

Meta: 2010-02-26, Pepa

When skyping with my boss I remembered that I can access Skype through COM interface. What can you do with it? You can start with some fun. You can look at it in action.

$skype = New-Object -ComObject Skype4Com.Skype
$timer  = New-Object Timers.Timer
$timer.Interval = 700
$job = Register-ObjectEvent $timer Elapsed -Action {$skype.ChangeUserStatus((1,2,4)[$global:i++%3])}
$timer.Enabled = $true

I think everything is pretty clear. The script changes each 700 milliseconds your status to Online/Away/DND, so the icon changes from green to yellow and red. Nice effect, isn't it?
It seems that it works only for some time. After several minutes your friends sometimes don't see the changes any more, even though it appears working at your computer.

Have fun with PowerShell!

Meta: 2010-02-22, Pepa

Welcome to another quick & dirty example how you can use PowerShell. Once in a month my friend/colleague @nikdo sends an email about some movies from CSFD site. The email looks something like this (just a part of it):

Code

He writes the code by hand on move after another. In case there are more movies this could be boooring, agree?
Rather than doing it by hand you can create a PowerShell script. You won't be proud of it, but it will do its job.

function Process-Csfd {
    [cmdletbinding()]
    param($name, $dir)
    function downloader {
            $cli = New-Object net.webclient
            $cli.Headers = New-Object net.webheadercollection
            $cli.Headers.Add('User-Agent', 'Mozilla/5.0 (Windows; U; Windows NT 6.0; cs; rv:1.9.1.7) ' +
                'Gecko/20091221 Firefox/3.5.7 (.NET CLR 3.5.30729)')
            $cli.Headers.Add('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8')
            $cli.Headers.Add('Accept-Language', 'cs,en-us;q=0.7,en;q=0.3')
            $cli.Headers.Add('Accept-Encoding', 'gzip,deflate')
            $cli.Encoding = [Text.encoding]::UTF8
            $cli
        }

    function get-csfdImageUrl {
        param($url)
        $cli = (downloader)
        Write-Verbose "Downloading $url"
        $x = convert2xml ($cli.DownloadString($url))
        @{ImageUrl=(Select-Xml $x -xpath //table[`@background]).Node.Background }
    }
    function search-csfdMovie {
        param($what)
        Add-Type -AssemblyName 'system.web'
        $cli = (downloader)
        $url = "http://www.csfd.cz/hledani-filmu-hercu-reziseru-ve-filmove-databazi/?search=" `
            + [system.Web.Httputility]::UrlEncode($what)
        Write-Verbose "Searching csfd, url: $url"
        $x = convert2xml ($cli.DownloadString($url))
        $link = Select-Xml $x -XPath `
            '/html/body/table/tr[2]/td/table/tr/td/table/tr/td/table[2]/tr[2]/td/table/tr/td' |
            select -exp Node | 
            select -exp a | 
            ? { $_.'#text' -eq $what }
        if ($link.Count) {
            Write-Host "there are more links"
            $link | % { write-host $_.OuterXml }
            throw $link
        }
        @{Link='http://www.csfd.cz'+$link.href}
    }
    $where = search-csfdMovie $name
    $imgInfo = get-csfdImageUrl $where.Link
    New-Object PSObject -Property ($where + $imgInfo + @{Name=$what})
}

'Herkules 3D', 'Appaloosa', 'Bílý drak', 'Nerozhodný drak', 'Delfín Filip',`
'Wyatt Earp','Tropická bouře','Naprosto osvětleno','Maratónec' | 
    % { Process-Csfd -name $_ -dir 'c:\temp\dir' -verbose } | 
    % -begin { $r = '<html><body>' } `
      -process { $r += '<a href="{0}" style="display:block;float:left;margin:3px;" title="{1}">
          <img src="{2}" style="width:121px;height:180px;border-style:none" /></a>' `
        -f $_.Link,$_.Name,$_.ImageUrl } `
      -end { $r +'</body></html>' } |
    Set-Content c:\temp\csfd.html

It's very straightforward.

  • Function downloader creates a System.Net.WebClient that mimics browser so that the site doesn't refuses the download.
  • Function search-csfdMovie tries to find a movie and return a link to the movie (e.g. return http://www.csfd.cz/film/8083-maratonec-marathon-man/). Note that I use function convert2xml that you can get from previous post about dictionary. It just converts html to xml.
  • Function get-csfdImageUrl uses the link provided by search-csfdMovie and grabs image url of the movie (e.g. http://img.csfd.cz/posters/0/8083.jpg).
  • In the rest I pipe movies names to the Process-Csfd function, format the output html and store it in a html file.

Can you see the xpath in search-csfdMovie? This is the main reason why this approach is quick&dirty. There are no html classes or ids that can be used when constructing xpath. They maybe just wanted to stop such tools as we created now.
When the site changes its markup, the script will fail. But don't worry, it is very easy to repair it ;).

Saving to disk

In case you need to store the files in a directory, just use the following function and alter the code accordingly.

function download-csfdImage {
    param($url)
    $cli = (downloader)
    Write-Verbose "Downloading $url"
    $x =convert2xml ($cli.DownloadString($url))
    $image = (Select-Xml $x -xpath //table[`@background]).Node.Background
    Write-Verbose "Downloading $image"
    $path = $dir+[io.path]::GetFileName($image)
    $cli.DownloadFile($image, $path)
    @{Path = $path; ImageUrl=$url }
}

And that's all. Keep in mind, that for really simple tasks it is much more efficient to do them by hand then to write the script. However, isn't it fun?

Meta: 2010-02-02, Pepa

Tags: PowerShell