Chris's profileChris Warwick - The Blog...PhotosBlogSkyDrive Tools Help

Chris Warwick - The Blog Edition

PowerShell and Assorted Ramblings and Rants
January 06

ISE Functions – in a Module

The module linked below adds three functions to the ISE editor:

  • Indent
  • Outdent
  • A primitive interpretation of the Xedit “All” macro

To try it out just copy the folder from here: CJW-ISE-Functions.zip  and place it under your Modules folder.  If you don’t have a Modules folder yet, see This Post on the PowerShell Team Blog, or This Post on Thomas’s Blog.

Here’s a screenshot of the “All” function:

image

 

As you’d expect, the Indent and Outdent functions do exactly what they say; default is to in/out-dent by 4 spaces.  Indent is mapped to Ctrl-F12 and Outdent to Ctrl-F11.

I’m never quite sure if SkyDrive is going to work, so here’s the CJW-ISE-Functions.psm1 file.  This was created with the Module Module!:

Cheers,

Chris

 

  1 ################################################################################
  2 
  3 Function Get-ISEMatchingLines {
  4 <#
  5 .Synopsis
  6     Displays lines matching the given pattern from the currently active ISE Editor window
  7 .Description
  8     This is a poor-man's-version of the Xedit "ALL" command.  All lines matching the given parameter
  9     are displayed as output
 10 
 11 .Parameter Pattern
 12 .Example
 13     Get-ISEMatchingLines "# Todo"
 14 .ReturnValue
 15     Output lines matching pattern
 16 .Link
 17 
 18 .Notes
 19  NAME:      Get-ISEMatchingLines
 20  AUTHOR:    Chris Warwick
 21  LASTEDIT:  01/05/2009 22:30:12
 22 #Requires -Version 2.0
 23 #>
 24 
 25 [CmdletBinding(SupportsShouldProcess=$False, SupportsTransactions=$False, ConfirmImpact='None')]
 26 
 27 Param([Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)][String]$Pattern)
 28 
 29     Process{
 30         If ($host.Name -ne 'Windows PowerShell ISE Host'){Throw 'This function only runs in PowerShell ISE'}
 31         $psISE.CurrentOpenedFile.Editor.Text -split "`n"|Select-String -pattern $Pattern
 32     }
 33 } 
 34 
 35 ################################################################################
 36 
 37 Function Indent-ISESelection {
 38 <#
 39 .Synopsis
 40     Indents the current editor selection by the specified number of characters
 41 .Description
 42     The editor current selection is indented by having a given number of spaces (default=4) 
 43     inserted before each line in the selection.  Note that if the selection does not start
 44     on a line-break boundary this cannot be detected and indenting will occur from the start
 45     of the selection irrespective of its actual place in the line.
 46 
 47 .Parameter Spaces
 48 .Example
 49     Indent-ISESelection 8
 50 .ReturnValue
 51     Selected lines indented
 52 .Link
 53 
 54 .Notes
 55  NAME:      Indent-ISESelection
 56  AUTHOR:    Chris Warwick
 57  LASTEDIT:  01/05/2009 22:30:12
 58 #Requires -Version 2.0
 59 #>
 60 
 61 [CmdletBinding(SupportsShouldProcess=$False, SupportsTransactions=$False, ConfirmImpact='None')]
 62 
 63 Param([Parameter(Position=0, Mandatory=$false, ValueFromPipeline=$false)][Int]$indent=4)
 64 
 65     Process{
 66         If ($host.Name -ne 'Windows PowerShell ISE Host'){Throw 'This function only runs in PowerShell ISE'}
 67         $psISE.CurrentOpenedFile.Editor.InsertText(
 68             ($psISE.CurrentOpenedFile.Editor.SelectedText -split "`n"|%{$_ -Replace '^', (' '*$Indent)}) -join "`n"
 69         )
 70     }
 71 } 
 72 
 73 ################################################################################
 74 
 75 Function Outdent-ISESelection {
 76 <#
 77 .Synopsis
 78     Outdents the current editor selection by the specified number of characters
 79 .Description
 80     The editor current selection is outdented by having a given number of spaces (default=4) 
 81     removed before each line in the selection.  Note that if the selection does not start
 82     on a line-break boundary this cannot be detected and outdenting will occur from the start
 83     of the selection irrespective of its actual place in the line.
 84     
 85     The outdenting process will only remove whitespace characters from the start of each line.
 86     If whitespace characters run out then no shifting will occur
 87 
 88 .Parameter Spaces
 89 .Example
 90     Outdent-ISESelection 8
 91 .ReturnValue
 92     Selected lines outdented
 93 .Link
 94 
 95 .Notes
 96  NAME:      Outdent-ISESelection
 97  AUTHOR:    Chris Warwick
 98  LASTEDIT:  01/05/2009 22:30:12
 99 #Requires -Version 2.0
100 #>
101 
102 [CmdletBinding(SupportsShouldProcess=$False, SupportsTransactions=$False, ConfirmImpact='None')]
103 
104 Param([Parameter(Position=0, Mandatory=$false, ValueFromPipeline=$false)][Int]$Outdent=4)
105 
106     Process{
107         If ($host.Name -ne 'Windows PowerShell ISE Host'){Throw 'This function only runs in PowerShell ISE'}
108         $psISE.CurrentOpenedFile.Editor.InsertText(
109             ($psISE.CurrentOpenedFile.Editor.SelectedText -split "`n"|%{$_ -Replace "^\s{0,$Outdent}"}) -join "`n"
110         )
111     }
112 } 
113 
114 ################################################################################
115             
116 $Null=$psISE.CustomMenu.Submenus.Add('Indent', {Indent-ISESelection}, 'Ctrl+F12')
117 $Null=$psISE.CustomMenu.Submenus.Add('Outdent', {Outdent-ISESelection}, 'Ctrl+F11')
118 
119 Export-ModuleMember Get-ISEMatchingLines, Indent-ISESelection, Outdent-ISESelection
120 
121 Set-Alias All Get-ISEMatchingLines -Scope Global
122 Set-Alias Indent Indent-ISESelection -Scope Global
123 Set-Alias Outdent Outdent-ISESelection -Scope Global
124 
125 ################################################################################

PowerShell ISE – Yippee!

No blog entries here for ever, I was thinking this blog would see no life until next year’s Scripting Games:-)  But then CTP3 was released and in particular, the Integrated Scripting Environment (ISE) has got my attention – because it’s scriptable (See PowerShell ISE Can Do a Lot More Than You Think).

The CTP3 Story So Far

I’ve installed CTP3 on 6 machines here at home (3xVista Desktop; 1xVista Laptop; 2xServer 2008 Domain Controllers) – all working as expected; remoting running between all the machines.

I’ve been trying to keep up with the blogs (there have been 33 posts to date on the PowerShell Team Blog alone since CTP3 was released!) and have been toying with Advanced Functions and Modules.  All very, very cool.

My PowerShell Environment (or, Why I Didn’t Use ISE)

I use (and highly recommend) PowerShell Plus (now sold by Idera http://www.idera.com/Products/PowerShell/).  Hopefully the rumoured update due soon will improve the CTP3 interoperability…  Anyway, being a PS+ fan meant I practically ignored ISE all the way through CTP2.

I fired up ISE with CTP3 and thought “same-old, same-old” until I saw Jeffery’s post – at which point I got slightly more interested.  Now this is getting good!  I’ve just written a module containing some ISE functions (to be posted shortly) and thought I’d provide some thoughts…

Where To Next, ISE?

This is a fantastic opportunity!  OK, currently the ISE object model is a little, err, on the light side.  But the potential is there.  I come from a IBM/VM background and was a huge fan of Xedit – the VM programmable editor; there was nothing that couldn’t be done with that editor and I’ve never been 100% happy about my current-fave replacement – whatever that happens to have been (currently EditPad Pro and the PS+ Editor (yeah, I’ve tried the PowerGUI one too…))

ISE could be right up there if the object model is expanded. 

One of the good things in Xedit was the “All” command; this took a pattern and displayed all the lines in the current file that matched the pattern.  All other lines were either hidden entirely or bunches of hidden lines were replaced with a “shadow line” (Set Shadow On/Off).  Global edits to the file would then operate only on the displayed lines (Set Scope Display).  This was incredibly useful (You perhaps don’t miss it until you don’t have it anymore!) – it’s been implemented in other editors (for example GVIM – and Emacs I’m sure) – if you’d like a go get the free Xedit clone by Mark Hessling (here: http://sourceforge.net/project/showfiles.php?group_id=29648) called “The Hessling Editor” or “THE” for short.

I’ve written a very simple, poor-man’s “All” command for ISE (included in the module), but it’s somewhat less than it could be … hopefully that object model will improve!

What about ISE?  Well, check the (extremely comprehensive) Xedit command set and object model here: http://hessling-editor.sourceforge.net/doc/index.html

As a first step – let us display or hide lines in the editor window:-)

Preferably, find out where Mark Hessling is and get him a job on the PowerShell Team:-))

May 04

PowerShell V2 CTP2

I download the latest CTP2 build yesterday and installed it on a couple of machines - all seems to be fine, nothing appears to be broken yet!  The remoting has changed significantly since the last CTP and looks like it's going to be a fantastic facility (shame it's going to be quite a long while before I can rely on V2 being available on all the production machines I come across - oh well!)

Thanks to Shay and Marco on the newsgroup for pointing me in the direction of the WinRM CTP - you'll need to install this too from here before you can run the Configure-WSMan.PS1 script supplied with the CTP2 build.  The WinRM CTP install requires a reboot.

Versioning

I now have PowerShell V1 and V2 to contend with - as well as running different hosts (PowerShell and PowerShell Plus) and the PSCX plug-in.  When writing scripts that rely on a particular feature I usually try to test before-hand to make sure that feature is going to be there.  I was stumped for a while trying to find out if I was running on V1 or V2 (the underlying PowerShell engine version) - Get-Host would normally do the trick, but in PS+ the Get-Host cmdlet only tells me I'm running in PSPlus - nothing about the underlying engine.

PSCX uses "$PSVersionString = (Get-FileVersionInfo "$PSHome\PowerShell.exe").ProductVersion" - but that's no good if PSCX isn't loaded.

Luckily, hunting around in the help files for CTP2 revealed a new $PSVersionTable hash:


PS> $PSVersionTable

Name                           Value
----                           -----
CLRVersion                     2.0.50727.1434
BuildVersion                   6.1.6585.1
PSVersion                      2.0
PSCompatibleVersions           {1.0, 2.0}

 

Just the job:-)

 

Chris

April 29

Spelling and Things

My speeling spelling is pretty awful sometimes.  I can look at a word and know it's spelt incorrectly, but can't for the life of me see why it's wrong.  I'm sure I must have some form of low-grade dyslexia or something!  Luckily, most writing programs (including the excellent Windows Live Writer - highly recommended) have decent spell-checkers built in; but I don't always have such a program open and it can take me a while to decide whether to open Word, go to IE and browse around or just grab a good-old printed dictionary.

I do usually have a PowerShell session open somewhere though (and if I don't, there's always the Sidebar PowerShell Gadget), so I thought I'd write a "Spell" command that would just check a spelling for me.  Although I have an ancient (16-bit, circa Windows 3.1) software version of the Concise Oxford English Dictionary it doesn't have a COM interface - so it seemed a Web Service was the way to go.

A quick search showed Yahoo! and Cdyne to be the most useful candidates (Google also provide a service, but it requires registration - not a big deal, but why do I want to manage a registration key for ever-more when I don't need to?)

Writing the PowerShell script is then a complete pushover, PowerShell just makes this kind of thing so easy.  [Gratuitous praise (no payment required...) It's even more of a  pushover when you use PowerShell Plus (I've just download the latest version and it just keeps getting better and better - well done Karl et al!)].     The code just gets a new System.Net.WebClient object, composes the required URLs and reads back the resulting XML from Yahoo! and Cdyne.  Assuming something useful was returned it's displayed accordingly.  Here's some sample output:

PS> spell advanceed
Yahoo! Suggestions:
advanced
Cdyne Suggestions:
advanced
advance ed
advancer
advance
PS> spell acomoddate
Yahoo! Suggestions:
accommodate
Cdyne Suggestions:
accommodate
accommodated
accommodates
accommodative
PS> speel collaberation
Yahoo! Suggestions:
collaboration
Cdyne Suggestions:
collaboration
collaborations
calibration
collaborative

 

Here's the code (I dot-source this in my profile) - Enjoy!

# Get-Spelling
# Uses Yahoo! and cdyne Web Services to check spelling
# Chris Warwick, April 2008

Function Get-Spelling ([String]$Word = $(Throw "Specify a word to spell-check")) {

	$WebClient = New-Object System.Net.WebClient

	#Create the Yahoo! Service Url, get the result and cast to XML 
	$url = "http://search.yahooapis.com/WebSearchService/V1/spellingSuggestion?appid=CJWGet-Spelling&query=$word"
	$YahooResponse = [xml]$WebClient.DownloadString($url)

	#Create the cdyne Service Url, get the result and cast to XML 
	$url = "http://ws.cdyne.com/SpellChecker/check.asmx/CheckTextBody?BodyText=$word&licensekey="
	$CdyneResponse = [xml]$WebClient.DownloadString($url)

	If ($YahooResponse.ResultSet.Result -ne $null) {
		"Yahoo! Suggestions:"
		$YahooResponse.ResultSet.Result
	}
	else {
		"'$Word' - no alternative suggestions from Yahoo!"
	}

	If ($CdyneResponse -ne $null -and [int]$CdyneResponse.DocumentSummary.MisspelledWord.SuggestionCount -gt 0) {
		"Cdyne Suggestions:"
		$CdyneResponse.DocumentSummary.MisspelledWord.Suggestions[0..3] # (Show only top 4 results)
	}
	else {
		"'$Word' - no alternative suggestions from Cdyne"
	}
}

Set-Alias Spell Get-Spelling
Set-Alias Speel Get-Spelling		#   :-)

Infrastructure Upgrade - Update

So, I went for an AMD Athlon BE-2350 (45W TDP), 4GB Ram, a 750GB SATA Disk (the price break seems to be at this capacity, the 1TB disks are still a bit more expensive).  All built and running Server 2008 64-bit - neat:-) 

Now I just need to settle on the best way to migrate my domain.  I don't really want to do a straight upgrade to AD '08 as I have a lot of past development junk hanging around in the current domain - including the E2k3 schema extensions (I've now moved into the cloud and use Windows Live Domains and Windows Live Mail with the Outlook Connector - recommended).  But, I would like to keep the same domain name that I've used for a while now.

I could:

  1. Rename the existing domain, create a new domain with the old name and use ADMT to migrate
  2. Create a new domain with any old name, migrate with ADMT and then rename
  3. Copy data from the old servers to a workgroup; turn off the old domain; promote the workgroup to a domain with the old name and re-permission
  4. Use a different domain name altogether and migrate to that
  5. Upgrade the existing domain and just learn to live with the messy schema

I'm still considering options 1, 3 and 5 - will keep you posted!

Once I've decommissioned the existing domain I can free-up some hardware and continue with the on-going upgrade.

Skiing - Wipe Out!

If you'd like to see me get wiped out on the slopes watch this:-)

Les Arc, earlier in the month.  My eldest daughter is videoing us, my son (who has grown quickly over the last few months!) poses past the camera and - oops!  Luckily I landed on something soft (the lunch in my rucksack!)

 

 

Enjoy!

March 13

Infrastructure Upgrade!

I've been running an AD domain here for a number of years (it's my job OK?!) on a couple of DCs.  I have a bunch of other stuff set up too - including MOM, SQL, WSUS, ISA.  For a long time I also had Exchange - that went last year when the Windows Live team released the Outlook Connector and Window Live Domains (but that's another story...)

Anyway, the time has come to move Server 2008 out of it's VM dev hideaway and into production - trouble is the hardware I have is so old and cranky that I have doubts about its likely longevity.  I'm also fresh out of space - what seemed an unfeasibly large (PATA) 120GB a couple of years ago is now full of the kids videos and photos.  Something else too - I run my servers 24/7 and things have changed over the last few years; these days, every time I look at all the flashing lights I feel guilty about the continuous power drain.

Time to upgrade.  Only problem is - upgrade to what?

Real-world server class hardware is a complete non-starter (power, cost).  Desktops with fast processors and graphics cards are similarly power-hungry and just over-spec.  But low-end, low-power systems don't fit the bill either - often the system boards have a restricted memory capability and off-the-shelf systems aren't customisable to my requirements.

I considered going down the virtualisation route - buying a single, large, fast machine and running everything in VMs - but that just doesn't feel right (yet).  Maybe two large, fast machines would be OK - but that starts to look expensive.

So... self-build it is!

Here are my list of requirements:

  • Low-cost.  Mindful of Scott Hanselman's WAF (Wife-Acceptance-Factor) as well as real-world requirements like the kids' skiing holidays (ouch), the config needs to be, err, realistically priced
  • Low-power.  It doesn't hurt to think a bit green.  On the other hand, I'm not thinking of building solar-powered machines as described here
  • Fast enough.  But that doesn't mean fast by today's standards.  I'm going to be running AD & DFS-R for a small number of users - so fairly leisurely really
  • 64-bit.  Isn't everyone? 

I've just started looking around - I think I'm going to need 4 machines total.  So far I'm looking at AMD Athlon 64, 4GB, 1TB SATA.  If you have any helpful thoughts please post  comments.

March 03

Scripting Games, Advanced Event 10, Blackjack

Disclaimer:  This is modified slightly over the version I submitted.  I got a bit carried away with the description so this is a long post!

First, a screen-shot:-)

When I saw MOW's teaser a couple of weeks' back I decided a text-only version of Blackjack was going to be unacceptable!  I didn't think I'd go as far as drawing cards, but thought it would be good to have a go at writing to the console buffer to display things in a nice tidy fashion:

image

I wrote this in two stages, firstly getting the regular game to work (but using dummy functions to display the output as simple text on the console); secondly, replacing the display functions to use $Host.UI.RawUI calls, changing the program into a full-screen version.  I haven't tried this before (that's the good thing about The Scripting Games!), so there are a few rough edges, noted below.

Here's the code, split into sections (the full code is at the end of the post).  Firstly some setup and pre-amble:

  1 Clear-Host
  2 $script:pos = $Host.ui.rawui.CursorPosition
  3 
  4 $rect = New-Object System.Management.Automation.Host.Rectangle
  5 $rect.Top = $pos.y
  6 $rect.Right = 70
  7 $rect.Left = 0
  8 $rect.Bottom = $pos.y + 25
  9 $script:buf=$Host.UI.RawUI.GetBufferContents($rect)
 10 
 11 $script:tablecolour='darkgreen'
 12 $script:textcolour='black'
 13 
 14 function setTable($colour=$script:tablecolour) {
 15 	$c=New-Object System.Management.Automation.Host.BufferCell
 16 	$c.BackgroundColor=$colour
 17 	$c.ForegroundColor='black'
 18 	$c.Character=' '
 19 
 20 	for ($i=0; $i -le $script:buf.GetLongLength(0)-1; $i++){
 21 		for ($j=0; $j -le $script:buf.GetLongLength(1)-1; $j++){
 22 			$script:buf[$i,$j]=$c
 23 		}
 24 	}
 25 }

The first section defines a playing area and a function to initialise the area.  I'm sure this is unnecessary/can be done in a better way.  See MOW's event 10 post for definitive information:-)

The next section defines a function to write a string to the screen buffer ("writebuf", below) with optional foreground and background colours.  I did this by writing individual characters into the buffer (line 5 below) although, in hindsight, I think using $Host.UI.RawUI.NewBufferCellArray would be the better way to go.

"Writebuf" will potentially be called multiple times, making changes to the screen buffer until function "flush" (line 8 below) is called to display the buffer on the screen.

"ClearTable" (line 10 below) is used to wipe out the previous hand's cards and scores from the screen. 

  1 function writebuf($x,$y,$string,$fore=$script:textcolour,$back=$script:tablecolour) {
  2 	$c=New-Object System.Management.Automation.Host.BufferCell
  3 	$c.BackgroundColor=$back
  4 	$c.ForegroundColor=$fore
  5 	$string.tochararray()|%{$c.Character=$_; $script:buf[$x,$y++]=$c}
  6 }
  7 
  8 function flush {$Host.ui.RawUI.SetBufferContents($script:pos,$script:buf)}
  9 
 10 function clearTable {
 11 	7..18|%{writebuf ($_) 7 (' ').padright(25)}
 12 	7..18|%{writebuf ($_) 36 (' ').padright(25)}
 13 	writebuf 4 18 $(' '.padright(18))
 14 	writebuf 4 51 $(' '.padright(18))
 15 	flush
 16 }
 17 
 18 function status ($string) {writebuf 23 7 $string.padright(60); flush}
 19 
 20 function pause ($milliseconds) {Start-Sleep -Milliseconds $milliseconds}

The next function ("choice", below) display a message and uses $Host.UK.RawUI.ReadKey to get an answer from the player.  The function will keep reading characters until a valid key is pressed. A list of valid keys is passed to the function in $set:

  1 function choice ($message, $set) {
  2 	status $message
  3 	do {$answer=[string]($Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")).character}
  4 	until ($set -match $answer)
  5 	return $answer
  6 }

So, that's the screen taken care of.  The rest of the code described below plays the Blackjack game calling on the "writebuf" and "status" functions to display the results.  But first, some support functions for the game, starting with the all-important shuffle:

  1 # Start of Blackjack code
  2 
  3 
  4 function shuffle {
  5 	$script:dealt = 0 	# Number of cards dealt so far
  6 	$script:pack = 0..51
  7 	
  8 	# Shuffle the pack.  This is the Fisher-Yates shuffle once more!
  9 	$r = New-Object system.random
 10 	0..51| % {$j = $r.Next($_,51); $x = $pack[$_]; $pack[$_] = $pack[$j]; $pack[$j] = $x}
 11 }

So, "shuffle" creates a new pack/deck of cards.  The script-scoped variable $script:dealt remembers how many cards have been taken from the pack while the $script:pack variable is the pack itself, represented as a 52-element array of numbers between 0 and 51.

Here we see the inevitable Fisher-Yates shuffle again.  I've seen lots of other very weird and wonderful shuffle techniques used throughout the scripting games, but this is really the way to randomly mix the elements of an array.  See the Wikipedia article and don't use any other way OK!

The shuffle function merely provides 52 numbers in a random sequence.  To turn these into playing cards and to assign them to a player's hand we need some more plumbing.  Function "Deal" does what is required, but before describing that we need to know something about the data structures it manipulates.  First of all, the structure of a card.  A card is described by a custom PowerShell object with two properties - one is the value of the card as according to the rules of the Scripting Guys' Blackjack (from 2 to 11, or 1 for Aces if required); the other is a string naming the card ("Queen of Clubs" and so on).

The card is created on line 6 of function deal (below).  The name, suit and value is then calculated from the card number (0..51) on lines 7-16; these lines map the 52 unique numbers from the pack to the corresponding four suits of cards we need.  The $card object will end up having $card.name = "Queen of Clubs" and $card.points = 10 (for example).

"Deal" now has to assign this card to a hand.  For the purposes of this script a hand is another custom PowerShell object with the following properties:

  • Points: The value of the entire hand.  In Blackjack this value will not exceed 21 for a valid hand
  • Soft: A boolean value indicating whether there are any Aces in the hand.  If there are Aces they initially have a value of 11 points each; however, if the value of the overall hand should exceed 21 any Aces in the hand can, instead, be counted as only one point.  If the hand currently contains Aces that are still being counted as 11 points then this property will have the value $True
  • Cards: An array of the card objects already described that make up the hand

As an example, here's how a hand looks at the PowerShell console:

PS> $hand|ft -auto

points  soft cards
------  ---- -----
    13 False {Jack of Clubs, Three of Spades}


PS> $hand.cards|ft -auto

Name            Points
----            ------
Jack of Clubs       10
Three of Spades      3

"Deal" adds the new card to the current hand (line 19, below); calculates the new points total for the hand (line 20) and checks to see if the hand now contains any soft Aces (line 21).  That's the data structures dealt with (so to speak), but there's one more task for "Deal" to take care of.

The addition of another card to the current hand may have caused the hand's value to exceed 21 points.  If this has happened, lines 23-34 check to see if there are any soft Aces in the hand that can have their values changed from 11 points to 1 point.  If there are soft Aces they're downgraded one at a time until the hand's value drops below 21.

Here's the complete "Deal" function:

  1 function deal ([ref] $hand) {
  2 	# Add a card to the specified hand.  First, get the next card from the pack as a custom object
  3 
  4 	if ($script:dealt -ge 51) {Throw "Pack is empty"}
  5 	$c = $script:pack[$script:dealt ++ ]			# Pick next card from pack (if not all dealt)
  6 	$card = [object]|Select Name, Points			# convert card to custom object
  7 	$suit = ('Clubs','Diamonds','Hearts','Spades')[[math]::truncate($c / 13)]		# Calculate suit
  8 	$value = $c % 13 + 1
  9 	$card.name, $card.points = $(switch ($value) {	# Calculate name and points
 10 		1 {"Ace",11}
 11 		11 {"Jack",10}
 12 		12 {"Queen",10}
 13 		13 {"King",10}
 14 		Default {('Two','Three','Four','Five','Six','Seven','Eight','Nine','Ten')[$value - 2],$value}
 15 	})
 16 	$card.name=$card.name + " of $suit"
 17 	
 18 	# Now add the custom card object to the hand...
 19 	$hand.value.cards+=$card
 20 	$hand.value.points=($hand.value.cards|Measure-Object -Sum points).sum
 21 	$hand.value.soft=[bool]($hand.value.cards|?{$_.points -eq 11})
 22 	
 23 	# If hand is now worth more than 21 points, minimize by checking for soft Aces
 24 	while ($hand.value.points -gt 21 -and $hand.value.soft) {
 25 		# Check for soft aces
 26 		foreach ($card in $hand.value.cards) {
 27 			if ($card.points -eq 11) {
 28 				$card.points=1			# Convert soft ace to hard
 29 				break;				# Just change one Ace at a time
 30 			}
 31 		}
 32 		$hand.value.points=($hand.value.cards|Measure-Object -Sum points).sum
 33 		$hand.value.soft=[bool]($hand.value.cards|?{$_.points -eq 11})	# Check if any more soft Aces
 34 	}
 35 }

There's one final subtlety with the "Deal" function.  We want to be able to deal cards to the player or the dealer.  The obvious way to do this is to pass the player's hand or the dealer's hand to the "Deal" function and have it manipulate the appropriate set of cards.  In order to do this we have to use a rather obscure (at least in PowerShell) function - passing parameters by reference.  See the last paragraph of this post on the PowerShell Blog.  I can't find any other references to this technique in a PowerShell article (or in Bruce's book!) - so it really is obscure!

Pass by reference is indicated on the parameter declaration by using the [ref] type.  This must also be specified on the line calling the function (e.g. deal ([ref] $playerhand), see code below).  In addition to using the [ref] type to pass the parameter it is necessary, within the function, to refer to the contents of passed variable by using "$parametername.value".  See, for example, line 19 above, where we use "$hand.value.cards+=..." rather than the more likely "$hand.cards+=...".

Only one more function before we actually start to play (!), "display-hands", below, is mostly mechanical - just writing text at given parts of the screen.  A couple of bits of logic suppress the initial display of the dealer's second card and prevent the dealer's score from being shown until the player has finished (or busted).

  1 function display-hands {
  2 	$playerhand.cards|%{$y=7}{writebuf ($y++) 7 $_.Name.padright(25)}
  3 	$dealerhand.cards|%{$y=7}{writebuf ($y++) 36 $_.Name.padright(25)}
  4 	if ($playerhand.soft) {$soft=', Soft'} else {$soft=''}
  5 	$points="($($playerhand.points) points$soft)"
  6 	writebuf 4 18 $points.padright(18)
  7 	if (!$dealerdown -and $dealerhand.cards.count -gt 1) {
  8 		writebuf 8 36 ('?'.padright(25))
  9 	} 
 10 	if ($dealerdown) {
 11 		if ($dealerhand.soft) {$soft=', Soft'} else {$soft=''}
 12 		$points="($($dealerhand.points) points$soft)"
 13 		writebuf 4 51 $points.padright(18)
 14 	}
 15 	flush
 16 }

Finally, we use all these functions to play the game!  Display some headers, initialise the hands, shuffle the pack and deal the first two cards each:

  1 setTable 
  2 writebuf 1 22 ' Welcome to Blackjack! ' 'red' 'black'
  3 
  4 writebuf 4 7 'Your Cards'
  5 writebuf 5 7 '~~~~~~~~~~'
  6 
  7 writebuf 4 36 "Dealer's Cards"
  8 writebuf 5 36 '~~~~~~~~~~~~~~'
  9 
 10 
 11 do {
 12 	# Play a new hand...
 13 
 14 	$dealerhand=,[object]|select points, soft, cards
 15 	$dealerhand.cards=@()
 16 	$playerhand=,[object]|select points, soft, cards
 17 	$playerhand.cards=@()
 18 
 19 	$dealerdown=$FALSE		# True when dealer reveals second card in hand
 20 	clearTable
 21 	status "Shuffling Cards..."; shuffle; pause 1000
 22 	status "Dealing Cards..."; pause 800
 23 
 24 	# Deal first two cards
 25 	1..2| % {
 26 		deal ([ref]$playerhand); display-hands; pause 600
 27 		deal ([ref]$dealerhand); display-hands; pause 600
 28 	}
 29 

After all that, the code that takes care of playing the player's hand is trivial, thankfully.  Every time the payer chooses "Hit" we deal a new card into the player's hand (line 7, below) and update the display:

  1 $next = ''
  2 # Play player's hand...
  3 while (($playerhand.points -lt 21) -and ($next -ne 's')) {
  4 $next = Read "You have $($playerhand.points) points.  Stay (S) or Hit (H)?" 'SHQ'
  5 if ($next -eq 'Q') {Clear-Host; exit}
  6 if ($next -eq 'H') {
  7 deal ([ref]$playerhand)
  8 status ("You draw the $($playerhand.cards[-1].name)")
  9 pause 1000
10 display-hands
11 }
12 }

The player has finished; that's either because they stuck (on less than 21) or got 21 or more points, check these possibilities and then move on to the dealer's hand, played on lines 12-17 below:

  1 	# Check if result, otherwise play dealer's hand...
  2 	switch ($playerhand.points) {
  3 		21 {status '21, You Win!'; break}
  4 		{$_ -gt 21} {status 'Over 21.  Sorry, you lose'; break}
  5 		default {
  6 			# Play dealer's hand...
  7 			$dealerdown=$TRUE
  8 			status "You stick on $($playerhand.points) points"
  9 			pause 1400
 10 			display-hands
 11 			pause 1200
 12 			while ($dealerhand.points -lt $playerhand.points) {
 13 				deal ([ref]$dealerhand)
 14 				status ("The Dealer draws the $($dealerhand.cards[-1].name)")
 15 				pause 2000
 16 				display-hands
 17 			}
 18 			status "The Dealer has $($dealerhand.points) points"
 19 			pause 1600
 20 			switch ($dealerhand.points) {
 21 				{$_ -gt 21} {status 'You Win!'; break}
 22 				{$_ -ge $playerhand.points} {status 'The Dealer Wins'}
 23 			}
 24 		}
 25 	}
 26 
 27 	pause 1600
 28 	$next = choice 'Play again (Y/N)?' 'YNQ'
 29 } until ($next -ne 'y')
 30 
 31 Clear-Host

 

 

Now, when I get bored I must convert the display functions to use MOW's fantastic cards!

Here's the whole program:

 

 

  1 # Blackjack, Winter Scripting Games 2008, Advanced Event 10, Chris Warwick, Get-UKPSUG
  2 
  3 # Screen Buffer Code and auxiliary functions
  4 
  5 Clear-Host
  6 $script:pos = $Host.ui.rawui.CursorPosition
  7 
  8 $rect = New-Object System.Management.Automation.Host.Rectangle
  9 $rect.Top = $pos.y
 10 $rect.Right = 70
 11 $rect.Left = 0
 12 $rect.Bottom = $pos.y + 25
 13 $script:buf=$Host.UI.RawUI.GetBufferContents($rect)
 14 
 15 $script:tablecolour='darkgreen'
 16 $script:textcolour='black'
 17 
 18 function setTable($colour=$script:tablecolour) {
 19 	$c=New-Object System.Management.Automation.Host.BufferCell
 20 	$c.BackgroundColor=$colour
 21 	$c.ForegroundColor='black'
 22 	$c.Character=' '
 23 
 24 	for ($i=0; $i -le $script:buf.GetLongLength(0)-1; $i++){
 25 		for ($j=0; $j -le $script:buf.GetLongLength(1)-1; $j++){
 26 			$script:buf[$i,$j]=$c
 27 		}
 28 	}
 29 }
 30 
 31 function clearTable {
 32 	7..18|%{writebuf ($_) 7 (' ').padright(25)}
 33 	7..18|%{writebuf ($_) 36 (' ').padright(25)}
 34 	writebuf 4 18 $(' '.padright(18))
 35 	writebuf 4 51 $(' '.padright(18))
 36 	flush
 37 }
 38 
 39 function writebuf($x,$y,$string,$fore=$script:textcolour,$back=$script:tablecolour) {
 40 	$c=New-Object System.Management.Automation.Host.BufferCell
 41 	$c.BackgroundColor=$back
 42 	$c.ForegroundColor=$fore
 43 	$string.tochararray()|%{$c.Character=$_; $script:buf[$x,$y++]=$c}
 44 }
 45 
 46 function flush {$Host.ui.RawUI.SetBufferContents($script:pos,$script:buf)}
 47 
 48 function status ($string) {writebuf 23 7 $string.padright(60); flush}
 49 
 50 function pause ($milliseconds) {Start-Sleep -Milliseconds $milliseconds}
 51 
 52 function choice ($message, $set) {
 53 	status $message
 54 	do {$answer=[string]($Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")).character}
 55 	until ($set -match $answer)
 56 	return $answer
 57 }
 58 
 59 # Start of Blackjack code
 60 
 61 
 62 function shuffle {
 63 	$script:dealt = 0 	# Number of cards dealt so far
 64 	$script:pack = 0..51
 65 	
 66 	# Shuffle the pack.  This is the Fisher-Yates shuffle once more!
 67 	$r = New-Object system.random
 68 	0..51| % {$j = $r.Next($_,51); $x = $pack[$_]; $pack[$_] = $pack[$j]; $pack[$j] = $x}
 69 }
 70 
 71 function deal ([ref] $hand) {
 72 	# Add a card to the specified hand.  First, get the next card from the pack as a custom object
 73 
 74 	if ($script:dealt -ge 51) {Throw "Pack is empty"}
 75 	$c = $script:pack[$script:dealt ++ ]			# Pick next card from pack (if not all dealt)
 76 	$card = [object]|Select Name, Points			# convert card to custom object
 77 	$suit = ('Clubs','Diamonds','Hearts','Spades')[[math]::truncate($c / 13)]		# Calculate suit
 78 	$value = $c % 13 + 1
 79 	$card.name, $card.points = $(switch ($value) {		# Calculate name and points
 80 		1 {"Ace",11}
 81 		11 {"Jack",10}
 82 		12 {"Queen",10}
 83 		13 {"King",10}
 84 		Default {('Two','Three','Four','Five','Six','Seven','Eight','Nine','Ten')[$value - 2],$value}
 85 	})
 86 	$card.name=$card.name + " of $suit"
 87 	
 88 	# Now add the custom card object to the hand...
 89 	$hand.value.cards+=$card
 90 	$hand.value.points=($hand.value.cards|Measure-Object -Sum points).sum
 91 	$hand.value.soft=[bool]($hand.value.cards|?{$_.points -eq 11})
 92 	
 93 	# If hand is now worth more than 21 points, minimize by checking for soft Aces
 94 	while ($hand.value.points -gt 21 -and $hand.value.soft) {
 95 		# Check for soft aces
 96 		foreach ($card in $hand.value.cards) {
 97 			if ($card.points -eq 11) {
 98 				$card.points=1		# Convert soft ace to hard
 99 				break;			# Just change one Ace at a time
100 			}
101 		}
102 		$hand.value.points=($hand.value.cards|Measure-Object -Sum points).sum
103 		$hand.value.soft=[bool]($hand.value.cards|?{$_.points -eq 11})	# Check if any more soft Aces
104 	}
105 }
106 
107 function display-hands {
108 	$playerhand.cards|%{$y=7}{writebuf ($y++) 7 $_.Name.padright(25)}
109 	$dealerhand.cards|%{$y=7}{writebuf ($y++) 36 $_.Name.padright(25)}
110 	if ($playerhand.soft) {$soft=', Soft'} else {$soft=''}
111 	$points="($($playerhand.points) points$soft)"
112 	writebuf 4 18 $points.padright(18)
113 	if (!$dealerdown -and $dealerhand.cards.count -gt 1) {
114 		writebuf 8 36 ('?'.padright(25))
115 	} 
116 	if ($dealerdown) {
117 		if ($dealerhand.soft) {$soft=', Soft'} else {$soft=''}
118 		$points="($($dealerhand.points) points$soft)"
119 		writebuf 4 51 $points.padright(18)
120 	}
121 	flush
122 }
123 
124 
125 
126 setTable 
127 writebuf 1 22 ' Welcome to Blackjack! ' 'red' 'black'
128 
129 writebuf 4 7 'Your Cards'
130 writebuf 5 7 '~~~~~~~~~~'
131 
132 writebuf 4 36 "Dealer's Cards"
133 writebuf 5 36 '~~~~~~~~~~~~~~'
134 
135 
136 do {
137 	# Play a new hand...
138 
139 	$dealerhand=,[object]|select points, soft, cards
140 	$dealerhand.cards=@()
141 	$playerhand=,[object]|select points, soft, cards
142 	$playerhand.cards=@()
143 
144 	$dealerdown=$FALSE		# True when dealer reveals second card in hand
145 	clearTable
146 	status "Shuffling Cards..."; shuffle; pause 1000
147 	status "Dealing Cards..."; pause 800
148 
149 	# Deal first two cards
150 	1..2| % {
151 		deal ([ref]$playerhand); display-hands; pause 600
152 		deal ([ref]$dealerhand); display-hands; pause 600
153 	}
154 
155 	$next = ''
156 	# Play player's hand...
157 	while (($playerhand.points -lt 21) -and ($next -ne 's')) {
158 		$next = Read "You have $($playerhand.points) points.  Stay (S) or Hit (H)?" 'SHQ'
159 		if ($next -eq 'Q') {Clear-Host; exit}
160 		if ($next -eq 'H') {
161 			deal ([ref]$playerhand)
162 			status ("You draw the $($playerhand.cards[-1].name)")
163 			pause 1000
164 			display-hands
165 		}
166 	}
167 
168 	# Check if result, otherwise play dealer's hand...
169 	switch ($playerhand.points) {
170 		21 {status '21, You Win!'; break}
171 		{$_ -gt 21} {status 'Over 21.  Sorry, you lose'; break}
172 		default {
173 			# Play dealer's hand...
174 			$dealerdown=$TRUE
175 			status "You stick on $($playerhand.points) points"
176 			pause 1400
177 			display-hands
178 			pause 1200
179 			while ($dealerhand.points -lt $playerhand.points) {
180 				deal ([ref]$dealerhand)
181 				status ("The Dealer draws the $($dealerhand.cards[-1].name)")
182 				pause 2000
183 				display-hands
184 			}
185 			status "The Dealer has $($dealerhand.points) points"
186 			pause 1600
187 			switch ($dealerhand.points) {
188 				{$_ -gt 21} {status 'You Win!'; break}
189 				{$_ -ge $playerhand.points} {status 'The Dealer Wins'}
190 			}
191 		}
192 	}
193 
194 	pause 1600
195 	$next = choice 'Play again (Y/N)?' 'YNQ'
196 } until ($next -ne 'y')
197 
198 Clear-Host
 
February 29

Scripting Games, Advanced Event 8, Making Music

A list of songs are provided in a CSV file.  Produce a CD's worth, i.e. between 75-80 minutes worth of music (selecting a maximum of two tracks from any one artitst).

  1 # The input CSV file doesn't have headers so can't use import-csv. Create custom objects instead...
  2 $s=$(foreach ($line in Get-Content 'c:\scripts\songlist.csv') {
  3 	$song=[object]|select Artist, Title, Time
  4 	$song.artist, $song.title, $song.time = $line.Split(',')
  5 	$song
  6 })
  7 
  8 # Now select a playlist.  Shuffle the tracks and select songs until we've got a CD's worth...
  9 
 10 do {
 11 	# Randomise the songlist to make the resulting CD more interesting!
 12 	# This is the Fisher-Yates shuffle again...
 13 	$c=$s.count-1
 14 	$r=New-Object system.random
 15 	0..$c|%{$j=$r.Next($_,$c); $x=$s[$_]; $s[$_]=$s[$j]; $s[$j]=$x}
 16 
 17 	# Pick songs from the list until we have > 75 minutes worth...
 18 	$CDtime=[timespan]0;
 19 	$playlist=@()
 20 	for ($i=0; ($CDtime -lt [timespan]"01:15:00") -and ($i -le $c); $i++) {
 21 		# Only add this track if we don't have two songs by this artist already
 22 		if (($playlist|?{$_.artist -eq $s[$i].artist}).count -lt 2) {
 23 			$playlist+=$s[$i]
 24 			$CDtime+=[timespan]::Parse("00:$($s[$i].time)")
 25 		}
 26 	}
 27 	$mins=[int]$CDtime.totalminutes
 28 
 29 } until ($mins -lt 80)		# If the playlist is too long, shuffle the tracks and try again
 30 
 31 
 32 $playlist|sort artist|ft -auto		# Display the list of songs in the playlist
 33 
 34 "Total music time: {0}:{1,2:00}" -f $mins, $($CDtime.seconds)

We'd normally use import-csv to read a CSV file, but this isn't very useful here because the Scripting Guys forgot to add a header line to their music list!  Instead we read the CSV using Get-Content and create custom objects for each track (lines 2-6) - storing the resulting array of songs in $s.

Songs need to be added to a CD until the resulting list is greater than 75 minutes long.  But - the list has to fit on to the CD, so it has to be less than 80 minutes long.

There were two ways to go here.  I could've used a back-tracking technique (no pun intended!) - adding songs until >75 minutes worth, then if the result is >80 minutes long, remove a track and try others.  If none of the combinations work, remove two tracks and try others - etc etc.  But this seemed like overkill, so I decided to simply randomise the list of songs and select greater than 75 minutes worth.  If the result won't fit (>80 minutes) then I throw away the entire list, randomise again and pick a new list.  The do..until loop in lines 10 and 29 keeps trying until the desired result is obtained.  For lists of this length this is good enough IMO - I never saw this take more than two attempts to generate a suitable list of tracks.

To randomise the list of songs we use the Fisher-Yates shuffle again (as seen in event 7).  This time the algorithm (lines 13-15) is written as a pipeline and counts from the beginning of the array.

To add up the minutes and seconds for each track we use the PowerShell [timespan] type in $CDtime (line 18).   The loop in line 20 iterates through the array of songs until the total timespan is greater than 75 minutes (while $CDtime -lt [timespan]"01:15:00").

The check at line 22 ensures that no more than two tracks by a single artist make it onto the CD.  If there's less than two songs by the current artist the track is added and the total time updated accordingly.  The resulting playlist is displayed, sorted by artist, along with the final total track time.

Scripting Games, Advanced Event 7, Play Ball

Work out a rota for games between 6 teams; randomise the list to avoid one team playing too many consecutive games.

  1 $games=@()
  2 $teams='A','B','C','D','E','F'
  3
  4 for ($i=0; $i -lt $teams.length; $i++) {
  5     $iTeam=$teams[$i]
  6     for ($j=$i+1; $j -lt $teams.length; $j++) {
  7         $jTeam=$teams[$j]
  8         # Following line distributes home and away games :-)
  9         if (($i+$j)%2) {$games+="$iTeam vs. $jTeam"} else {$games+="$jTeam vs. $iTeam"}
10     }
11 }
12
13 # Mix the games up by repeatedly swapping array elements with other random elements
14 $element= New-Object System.Random
15
16 for ($i=$games.length-1; $i -ge 0; $i--) {
17     $index=$element.next(0,$i)
18     # Swapping element $i with $index...
19     $x=$games[$i]; $games[$i]=$games[$index]; $games[$index]=$x
20 }
21
22 $games
23

So, the script is in two sections: the first part (lines 4-11) works out the list of possible unique games between the teams (and saves them in $games).  By taking even- and odd-numbered fixtures (line 9) we distribute home and away games (not required, but seemed fair:-)

Lines 14-20 use an algorithm known as a Fisher-Yates shuffle to mix up the games in a random way by swapping (in place) elements of the array with random other elements.  Item one is swapped with an item randomly selected between 1..n.  Item 1 is, in effect, now an element randomly selected from the whole array.  Item 1 is now left alone and item 2 is randomly selected from the remaining items (2..n).  And so on...  (Note: in this script, for some reason, I started at the last element and worked backwards - not sure why but normality is returned the next time this algorithm appears!)

The resulting fixtures are displayed (line 20) and that's it.

February 27

Scripting Games, Advanced Event 6, Prime Time

When I first saw this one on the list I grinned a bit because one of the first real scripts I wrote in PowerShell was a translation of a procedure to calculate prime factors. I say "real" script here in the sense of "More than a couple of lines long" rather than "Of use in a real world situation"!

But this was actually a different problem - in fact just a simple prime sieve.

Here it is:

  1 param ([int]$max=200)	# Find primes up to $max
  2 
  3 $sieve=,1*($max+1)	# Initialise sieve, assume all primes to start
  4 
  5 for ($i=2; $i -le $max; $i++) {
  6 	# Find next prime entry in sieve
  7 	if ($sieve[$i]) {
  8 		$i
  9 		# Blank out higher multiples of current prime in sieve
 10 		for ($j=$i*$i; $j -le $max; $j+=$i) {$sieve[$j]=0}
 11 	}
 12 }

The "sieve" in a prime sieve algorithm is just a list of numbers that are either possible primes, or which have been shown to have factors.  A couple of things I like about PowerShell (among many things!): initialising an array of number from 1 to 12 (or whatever) is just $a=1..12; Initialising an array of a single item and giving it a value of one is: $a=,1; But if you apply the multiplication operator to an array PowerShell will duplicate the array by the number of times specified.  To create a 200 element array of integers with all array entries set to 1, use: $a=,1*200.  Nice!  This is used at line 3 to create the sieve.  The "1" in this case is just a boolean value to indicate that the number at array position [x] is a prime.

{Note: I could've used $true and $false rather than 0 and 1 in the sieve, but old habits die hard, sorry!}

We then start at 2 and count up to $max looking for the next prime - in other words, where sieve[i] is true (line 7).  Initially the whole sieve is indicating that every number is a prime (because no factors have been found yet), so $sieve[2] is true.

Every time a match is found, the script displays the match (line 8) and then does the sieving thing.  Multiples of the current prime (in the first case 2, 4, 6, 8, .....) are marked in the sieve by looping up to $max, in increments of the current prime, setting the corresponding sieve elements to false (0) along the way (line 10).  There's a small optimisation here; elements in the sieve less than the square of the current prime can be skipped because they have already been set by previous factors - so we start setting entries at ($i*$i).

/\/\o\/\/ mentioned performance in his post; I don't think it was me on the #powershell IRC channel (I can't find a good IRC client - can anyone recommend one??)  But here's a sample run on the first 100,000 primes (faster than a brute-force attack to 200!):

PS > (Measure-Command {./primes.ps1 100000}).tostring()
00:00:02.7081285
PS >

And on a cool million:-)

PS > (Measure-Command {./primes.ps1 1000000}).tostring()
00:01:01.5248231
PS >

So, this was about prime numbers - I'll post my prime factor script (a translation from Rexx, S/370 Assembler, C and Basic in reverse chronological order:-) in a future blog post.

Scripting Games, Advanced Event 5, Strong Password

Check a given password against various specified criteria and rate the password accordingly.  This was mostly an exercise in pattern matching.  I chose to use a Switch block (actually three switch blocks, just because I could:-)

  1 param ($p=$(Read-Host "Enter Password to check"))
  2 $Score=13; $dictionary='c:\scripts\wordlist.txt'
  3 
  4 switch ($p) {
  5 	{Select-String "^$_$" -Quiet -Path $dictionary}
  6 		{"Password is in dictionary"; $Score--}
  7 	{Select-String "^$($_.substring(0,$_.length-1))$" -Quiet -Path $dictionary}
  8 		{"Password minus last character is in dictionary"; $Score--}
  9 	{Select-String "^$($_.substring(1))$" -Quiet -Path $dictionary}
 10 		{"Password minus first character is in dictionary"; $Score--}
 11 	{($_ -match '0') -and (Select-String "^$($_.replace('0','o'))$" -Quiet -Path $dictionary)}
 12 		{"Password (replacing zero for O) is in dictionary"; $Score--}
 13 	{($_ -match '1') -and (Select-String "^$($_.replace('1','l'))$" -Quiet -Path $dictionary)}
 14 		{"Password (replacing one for l) is in dictionary"; $Score--}
 15 	{($_.length -lt 10) -or ($_.length -gt 20)}
 16 		{"Password must be between 10 and 20 characters long"; $Score--}
 17 	{!($_ -match '[0-9]')}
 18 		{"Password must include at least one digit"; $Score--}
 19 	{!($_ -cmatch '[A-Z]')}
 20 		{"Password must include at least one uppercase letter"; $Score--}
 21 	{!($_ -cmatch '[a-z]')}
 22 		{"Password must include at least one lowercase letter"; $Score--}
 23 	{!($_ -match '[^a-z0-9]')}
 24 		{"Password must include at least one symbol"; $Score--}
 25 }
 26 
 27 switch -case -regex ($p) {
 28 	'[a-z]{4,}'		{"Password includes 4 or more consecutive lowercase characters"; $Score--}
 29 	'[A-Z]{4,}'		{"Password includes 4 or more consecutive uppercase characters"; $Score--}
 30 	'(.).*\1'		{"Password includes duplicate characters"; $Score--}
 31 }
 32 
 33 $Strength=$(switch ($score) {
 34 	{$_ -le 6}	{"weak"}
 35 	{$_ -ge 11}	{"strong"}
 36 	default		{"moderately-strong"}
 37 });"Password score $score, indicating a $strength password"
 

First thing is to read the word list file.  Since we're going to be using this more than once we cache the content (in $dictionary).  The first five tests are handled by select-string.  Next is a simple length check.  Then some regex checking. 

The second switch block does some further regex pattern matching, including a simple regex to check for duplicate characters:

(.).*\1

Although this looks a bit like ascii-art (or a text-message smiley of some kind!) this actually says "Match any single character and remember what is was '(.)'; then match zero or more further characters '.*'; then match another occurrence of whatever character we matched initially '\1'".

This will scan along the string starting with the first character and walk up along the string checking and matching each character in turn until a character is found that occurs at least twice.  At this point, if there are duplicate characters, the regex succeeds.  Otherwise the end of the string is reached without a match and the regex fails.

The final switch block displays the result.

Scripting Games, Beginner Event 6, Coffee Break

Read a file containing coffee orders for a bunch of offices and add up the total number of coffees of each type.

Lines in the order file corresponding to the office number ("Office 100" etc) are dropped from the pipeline, other entries from the file are split into coffee type and number and used to populate a hash table.  Line 6 displays the resulting hash table.

  1 $Orders=@{}
  2 Get-Content 'c:\scripts\coffee.txt'|?{!($_ -match '^Office')}|%{
  3 	$coffee, $number = $_.split()
  4 	$Orders[$coffee]+=[int]$number
  5 } 
  6 $Orders.psbase.keys|%{"$($_): $($Orders[$_])"}

Scripting Games, Beginner Event 5, What's the Difference

Take a date parameter in the form "March 3, 2008" and display the difference from today's date in various contrived formats...

Although this worked OK for me apparently the Scripting Guys didn't like it much as I got a 0 - ho hum!  Maybe it doesn't work in US timezones - or maybe I just messed up the submission?  Or maybe it's just wrong?!  If you can see why it might be broken please post a comment - thanks!

  1 # Collect all the supplied arguments and join them together as strings...
  2 # We're hoping to be passed "March 3, 2009" (but without the quotes, unfortunately!)
  3 $Args|%{$s=""}{$s+=[string]$_+" "}
  4 
  5 $d=Get-Date($s)	# Hopefully the resulting parameter string will turn out to be a valid date
  6 
  7 $now=Get-Date
  8 
  9 $MonthsDifference=($d.year-$now.Year)*12+$d.Month-$now.Month-1
 10 $MonthDaysDifference=($d-$now.AddMonths($MonthsDifference)).days
 11 
 12 "Days: $(($d-$now).days)"
 13 "Months: $(($d.year-$now.year)*12+$d.month-$now.month)"
 14 "Month/Days: $($MonthsDifference)/$MonthDaysDifference"

This does illustrate one of the foibles of PowerShell's argument handling; while it is undoubtedly a Good Thing to have unified parameter parsing that's handled for you automatically, it can be a nuisance when the specific requirements need something else.  In this case we need to take the complete parameter string and pass it to Get-Date to convert it to a date type*.  This would be fine if the whole parameter string were enclosed in quotes, but since we can't rely on that we have to piece together the individual chunks from $args[].  In a good-old batch file we can get the full argument list in a single variable, but not in PS!

We could, instead, get the line used to invoke the script from $MyInvocation.line and split off the script name from the start of the line, but this fails if the script was called from a line like (for example):

> $a=script parm1 parm2; $a.length

In this case the information from $MyInvocation.line includes the WHOLE line (including the second statement after the ";").  Once again, we could do a bit more (potentially complex) parsing to eliminate all but the section we are interested in, but this starts to get very messy.

Thankfully this situation is improved (somewhat) with V2 (although maybe I should get onto Connect and request an $AllArgs variable:-)

 

* <rant> Oh, BTW, we could use a cast to [datetime] but in general casts to this type are best avoided (even if you do live in the US) thanks to the horribly broken format the PS team sprung on us at the very end of the V1 beta :-(   </rant>

February 26

Scripting Games, Sudden Death Challenge 5

First, some excuses!  I haven't done much playing around with WMI so far - and was also pushed for time to get this in; it works fine but maybe a little rough around the edges!

Problem was to list properties of WMI Win32 classes with names starting from A-Z (actually from A-Y since as of today there aren't any property names in the Win32 classes starting with Z)

  1 $w=Get-WmiObject -List|?{$_.Name -like 'Win32_*'}
  2 
  3 for ($c='A'; $c -le 'Z'; [char]$c=[int][char]$c+1) {
  4 	foreach ($name in $w) {
  5 		if ($found=$name.PSBase.properties |?{$_.name -like "$($c)*"}|Select-Object -First 1) {
  6 			"{0,-30}{1}" -f $($found.name), $($name.__CLASS)
  7 			break
  8 		}
  9 	}
 10 }

So, the script initially gets a list of all the Win32 WMI classes (in $w) and then loops through initial characters from A to Z.  For each initial character it iterates over the WMI classes checking the properties of each class for a property name beginning with the current initial letter ($name.PSBase.properties |?{$_.name -like "$($c)*"}).  When an entry is found it's displayed and the script moves on to the next initial character.  Here's the output on my (Vista) machine:

AdditionalDescription         Win32_JobObjectStatus
Bias                          Win32_TimeZone
ControlFlags                  Win32_SecurityDescriptor
Description                   Win32_PrivilegesStatus
ExitStatus                    Win32_ProcessStopTrace
FileName                      Win32_ModuleLoadTrace
GuidInheritedObjectType       Win32_ACE
HostingModel                  Win32_OsBaselineProvider
ImageBase                     Win32_ModuleLoadTrace
JobMemoryLimit                Win32_NamedJobObjectLimitSetting
KeepAliveInterval             Win32_NetworkAdapterConfiguration
Logfile                       Win32_NTLogEvent
MachineName                   Win32_ComputerSystemEvent
Name                          Win32_Trustee
Operation                     Win32_PrivilegesStatus
ParameterInfo                 Win32_PrivilegesStatus
Quarter                       Win32_CurrentTime
RecordNumber                  Win32_NTLogEvent
StatusCode                    Win32_PrivilegesStatus
TIME_CREATED                  Win32_Trustee
UserStackBase                 Win32_ThreadStartTrace
Version                       Win32_OsBaselineProvider
Win32ErrorCode                Win32_JobObjectStatus
XOffCharacter                 Win32_SerialPortConfiguration
YResolution                   Win32_PrinterConfiguration
February 23

Scripting Games, Sudden Death Challenge 4

Very Straightforward, the key piece is the [char][int] cast.

$n=Get-Content 'c:\scripts\numbers.txt'
0..($n.length/2-1)|%{$s=""}{$s+=[char][int]($n.substring($_*2,2))}{$s}

Just index along the string of digits, converting pairs to the equivalent character

Scripting Games, Advanced Event 4, Image is Everything

Display a calendar for the specified month (...tidy formatting counts!)

Years' ago one of the of the tasks we were set at uni was to write a compiler and an intermediate-code interpreter for a subset of Pascal.  When my solution was up a running one of the test programs I complied and ran produced a calendar in a similar way (although I've long since lost the source, unfortunately, thanks to frequent occurrences of media obsolescence; maybe XML saved to the web will stand the test of time!)

Back then, calculating the day of the week for a given date involved using an algorithm known as Zellar's congruence.  These days just use the .Net .DayOfWeek property:-)

  1 $month, $year=(Read-Host "Enter month/year").split('/')
  2 
  3 $days=[datetime]::DaysInMonth($year,$month)
  4 
  5 " "; get-date("$year/$month") -f "MMMM yyyy"; " "
  6 "Sun  Mon  Tue  Wed  Thu  Fri  Sat"
  7 
  8 $line="     "*[int](get-date("$year/$month/1")).dayofweek
  9 for ($i=1; $i -le $days; $i++) {
 10   $line+="{0,3:##}  " -f $i
 11   if ($line.Length -ge 35){$line; $line=""}
 12 }
 13 if ($line) {$line}

All standard stuff.  Line 1 reads the month and year ("3/2008") in the format specified.  5-6 display a couple of header lines.

The only line of note is line 8.  This takes the day of the week for the 1st of the month in question (Sunday..Saturday as an integer 0..6) and uses this value to calculate in which column day 1 should appear.  A number of spaces are multiplied by the ordinal of the day of the week to give the correct offset.

Lines 9-11 then produce output lines, adding on days until the end of the month.

The formatting for a two digit integer displayed in a space 3 character wide (0,3:##) is vaguely reminiscent of COBOL (anyone else remember those PICTURE entries in the DATA DIVISION?  Shiver!)

Scripting Games, Advanced Event 3, Instant Runoff Election

An interesting problem, although the resulting script (well, my resulting script!) isn't that thrilling...

Basic recap: votes are cast for candidates in order of preference (1st choice, 2nd choice etc).  Count all the 1st choice votes for each candidate; if any of the candidates have greater than 50% of the total 1st choice votes they win.  Otherwise, ignore votes for the candidate in last place and recount, promoting other candidates accordingly.

Here's my entry:

  1 $results=Get-Content 'c:\scripts\votes.txt'
  2 $candidates = 4
  3 $eliminated = @{}
  4 
  5 for ($i = $candidates - 1; $i -ge 0; $i -- ) {
  6 	$votes = @{}   # Accumulated count of votes for each candidate
  7 	$votecount = 0    # Tally total number of votes cast
  8 	foreach ($vote in $results) {
  9 		$votecount ++ 
 10 		foreach ($name in $vote.split(',')) {
 11 			if ( ! $eliminated[$name]) {
 12 				$votes[$name] ++ ; break
 13 			}
 14 		}
 15 	}
 16 	$names = $votes.psbase.keys|sort -Descending {$votes[$_]}
 17 
 18 	if ($votes[$names[0]] / $votecount -gt 0.5) { 
 19 		"The winner is $($names[0]) with {0:p1} of the vote" -f ($votes[$names[0]] / $votecount)
 20 		break
 21 	}
 22 	else {
 23 		$eliminated[$names[$i]]=$TRUE   # Eliminate loser and count again...
 24 	}
 25 }

The basic vote counting part of the script is between lines 6-24.  This is broken down into two main sections: a scan of the votes collecting counts; and a decision about a winner.  Lines 8-15 actually count the votes - take the first place from each ballot paper and if the candidate has not been eliminated add a first place vote to his count (in hash $votes, indexed by candidate name).  If the candidate has been eliminated check the next candidate and so on.

Once all counted, line 16 sorts the hash table by total votes and saves the result in $names (1st place candidate in $names[0])

Line 18 checks if the candidate with the highest result actually achieved more the 50% of the vote - if so the result is announced and the script ends.  If this isn't the case the candidate with the lowest score is added to a hash of eliminated candidates at line 23.

The process is then repeated up to "$candidate" times at line 5.  The rather odd looking sequence $i=3..0 is to enable the last place candidate to be identified (in the first round of counting the last place candidate will be in $names[3] - see line 23).

Not very elegant I'm afraid.

Scripting Games, Beginner Event 4, Count Me In

Display the number of characters in the current script (irrespective of the path to the script)

(get-content $MyInvocation.MyCommand.Definition|Measure-Object -Character).Characters

The only trick here is knowing that $MyInvocation.MyCommand.Definition holds the path to the currently running script.

Scripting Games, Beginner Event 3, Let's Get Together

Not much to say about this one - takes the first line from all the text files in c:\scripts and concatenates them into c:\temp\newfile.txt:

get-childitem 'c:\scripts\*.txt'|%{(get-content $_)[0]}>>'c:\temp\newfile.txt'

The Scripting Guy's solution is better!

 
Public folders

Chris