PowerShell Cookbook

Search

Categories

 

On this page

HTML Agility Pack Rocks Your Screen Scraping World
Responding to USB Devices in PowerShell
PowerShell Hotkey to Pause Pandora
Looking for Reviewers for PowerShell Cookbook v2
Delayed Screen Captures in PowerShell
Where is Rename-Computer?
An Exercise in De-Obfuscation
Holiday Wishes
Hex Dumper in PowerShell
Testing for PowerShell Remoting: Test-PsRemoting

Archive

Blogroll

Disclaimer
I work for Microsoft.

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

RSS 2.0 | Atom 1.0 | CDF

Send mail to the author(s) E-mail

Total Posts: 256
This Year: 7
This Month: 1
This Week: 0
Comments: 781

Sign In

 Friday, March 05, 2010
Friday, March 05, 2010 8:24:20 AM (Pacific Standard Time, UTC-08:00) ( )

I had to do some deep data extraction from a web page today, and naturally leaned on PowerShell for some assistance. PowerShell is a great language for text munging, and web content is no different. There are tons of examples online, but here’s an example from earlier in this blog: http://www.leeholmes.com/blog/PowerShellTheOracleInstantAnswersFromYourPrompt.aspx.

As I looked at the underlying HTML of this page, though, my heart sank. I cared about four pieces of data, and they were arranged without much structure on the web page. The information I cared about was in a couple of different tables, a couple of different table rows, and sometimes in different columns. You can parse your way around this, but it’s simply error-prone and annoying.

At that point, I remembered something called the HTML Agility Pack that I’ve been meaning to experiment with for some time. The HTML Agility Pack lets you navigate an HTML document as though it were well-formed XML, even though the underlying HTML usually isn’t. It doesn’t leverage PowerShell’s XML adapter, but the .NET objects act just like the XML classes from the .NET Framework.

On the down-side, data navigation and selection in XML comes via the XPath language. Like Regular Expressions, XPath queries are an esoteric art and difficult to get right. Luckily, you don’t need much knowledge of XPath for simple XML navigation.

This whole experience gives a great example of the “admin development model.” 15 minutes after thinking about parsing the web page with the HTML Agility Pack, I had a working version. PowerShell’s Get-Member cmdlet was all I used for discovery – no documentation was harmed in the making of this script. Here is the literal text of my history buffer, experimentation and all. On line 251 and 252, I put the history into the ISE so that I can hack out the experimentation bits and keep the stuff that worked.

221 cd C:\temp\HtmlAgilityPack.1.4.0.beta2.binaries
222 dir
223 add-type -Path .\HtmlAgilityPack.dll
224 $types = add-type -Path .\HtmlAgilityPack.dll -PassThru
225 $types
226 $types | ? { $_.IsPublic }
227 $doc = new-object HtmlWeb
228 ($types | ? { $_.IsPublic })[1]
229 ($types | ? { $_.IsPublic })[1].FullName
230 $doc = New-Object HtmlAgilityPack.HtmlDocument
231 $doc
232 $doc | gm
233 $result = $doc.Load("C:\temp\texts.html")
234 $result
235 $doc
236 $doc | gm
237 $doc.DocumentNode
238 $doc.DocumentNode | gm
239 $doc.DocumentNode.SelectNodes("//h1")
240 $doc.DocumentNode.SelectNodes("//table[@class='table-gen']")
241 $doc.DocumentNode.SelectNodes("//table[@class='table-gen']/tr[2]")
242 $doc.DocumentNode.SelectNodes("//table[@class='table-gen']")
243 $texts = $doc.DocumentNode.SelectNodes("//table[@class='table-gen']")
244 $texts[0]
245 $testText = $texts[0]
246 $testText | clip
247 $testText.SelectSingleNode("/tr[1]/td")
248 $testText.SelectSingleNode("tr[1]/td")
249 $testText.SelectSingleNode("tr[1]/td").InnerTExt
250 $testText.SelectSingleNode("tr[1]/td").InnerText.Trim()
251 ise
252 h
253 $time = [DateTime] $testText.SelectSingleNode("tr[1]/td").InnerText.Trim()
254 $testText.SelectSingleNode("tr[2]/td").InnerText.Trim()
255 $testText.SelectSingleNode("tr[2]/td").InnerText.Replace('Description:','').Trim()
256 $testText.SelectSingleNode("tr[6]/td").InnerText
257 $testText.SelectSingleNode("tr[5]/td").InnerText
258 $testText.SelectSingleNode("tr[4]/td").InnerText
259 $testText.SelectSingleNode("tr[5]/td").InnerText
260 $testText.SelectSingleNode("tr[5]/td[1]")
261 $testText.SelectSingleNode("tr[5]/td[2]")
262 $time = $testText.SelectSingleNode("tr[1]/td").InnerText.Trim()
263 $inOut = $testText.SelectSingleNode("tr[2]/td").InnerText.Replace('Description:',...
264 $to = $testText.SelectSingleNode("tr[5]/td").InnerText.Replace('Number Called:','...
265 $from = $testText.SelectSingleNode("tr[5]/td[2]").InnerText.Replace('Calling Numb...
266 New-Object PsObject -Property @{ Time = $time; Type = $inOut; From = $from; To = ...
267 New-Object PsObject -Property @{ Time = $time; Type = $inOut; From = $from; To = ...
268 $texts | % {...
269 C:\temp\textparser.ps1

The final script:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
cd C:\temp\HtmlAgilityPack.1.4.0.beta2.binaries
add-type -Path .\HtmlAgilityPack.dll
$doc = New-Object HtmlAgilityPack.HtmlDocument
$result = $doc.Load("C:\temp\texts.html")
$texts = $doc.DocumentNode.SelectNodes("//table[@class='table-gen']")

$result = $texts | % {
    $testText = $_
    $time = $testText.SelectSingleNode("tr[1]/td").InnerText.Trim()
    $time = $time.TrimEnd(" CST")
    $time = ([DateTime] $time).AddHours(-2)
    $inOut = $testText.SelectSingleNode("tr[2]/td").InnerText.Replace('Description:','').Trim()
    $to = $testText.SelectSingleNode("tr[5]/td").InnerText.Replace('Number Called:','').Trim()
    $from = $testText.SelectSingleNode("tr[5]/td[2]").InnerText.Replace('Calling Number:','').Trim()

    New-Object PsObject -Property @{ Time = $time; Type = $inOut; From = $from; To = $to } |
        Select From,To,Type,Time
}

$result | Sort Time | ft -auto | out-string -width 75

All in all, the HTML Agility Pack is a very attractive approach that I plan to start using more often.

Comments [0] | | # 
 Monday, March 01, 2010
Tuesday, March 02, 2010 6:02:24 AM (Pacific Standard Time, UTC-08:00) ( )

I recently got a cool USB MIDI keyboard (M-Audio Axiom 49) and software (Propellerhead Reason) combo that lets me play keyboard with all kinds of cool sounds effects and general fun. Despite how cool the system is, it’s not quite as spontaneous as a plain ol’ electronic keyboard. Just to piddle around for a bit, you have to turn on the keyboard, launch Reason, create a new song, add a virtual instrument, and then start playing.

How can PowerShell of all things help the anguish of a struggling keyboard hack?

The answer comes in two parts – creating a template song, and then having PowerShell launch it when you turn on the keyboard.

It turns out that you can resolve a lot of the issues in Reason by just creating a good starter song. Add in the mixer, a synthesizer, and save it to disk – I called it “Default.rns.” When you double-click on this file, Reason starts up with all of the presets you had configured.

The second part is more tricky, unless of course you happen to have Real Ultimate Power.

The core of this solution comes in three parts:

  1. WMI lets you enumerate USB devices on the system.
  2. The Register-WmiEvent cmdlet lets you respond to WMI events.
  3. WMI has a default __InstanceCreationEvent class that lets you respond to ANY new instance being created (Services, Processes, etc.)

The answer to #1 has come up a few times on the Team Blog:

http://blogs.msdn.com/powershell/archive/2007/02/24/displaying-usb-devices-using-wmi.aspx
http://blogs.msdn.com/powershell/archive/2009/01/10/get-usb-using-wmi-association-classes-in-powershell.aspx

I had no idea what the Axiom keyboard was being recognized as, so I did the easy thing. Ran the query once, turned off the keyboard, and then ran it again. Compare-Object to the rescue:

001
002
003
$before = gwmi Win32_USBControllerDevice |% { [wmi] $_.Dependent }
$after = gwmi Win32_USBControllerDevice |% { [wmi] $_.Dependent }
Compare-Object $before $after -PassThru

That gives an object like:

Availability                :
Caption                     : USB Axiom 49
ClassGuid                   : {4d36e96c-e325-11ce-bfc1-08002be10318}
CompatibleID                : {USB\Class_01&SubClass_01&Prot_00, USB\Class_01&SubClass_01, USB\Class_01}
ConfigManagerErrorCode      : 0
ConfigManagerUserConfig     : False
CreationClassName           : Win32_PnPEntity
Description                 : USB Audio Device
DeviceID                    : USB\VID_0763&PID_0199&MI_00\6&2F841C5F&0&0000
ErrorCleared                :
ErrorDescription            :
HardwareID                  : {USB\VID_0763&PID_0199&REV_0105&MI_00, USB\VID_0763&PID_0199&MI_00}
InstallDate                 :
LastErrorCode               :
Manufacturer                : (Generic USB Audio)
Name                        : USB Axiom 49
PNPDeviceID                 : USB\VID_0763&PID_0199&MI_00\6&2F841C5F&0&0000

There’s the ticket. It’s actually a Win32_PnpEntity. While the previous WMI query helped us discover USB entities, now that we know how the keyboard is identified to the system, we can drop the complicated WMI dependency traversal, and turn this specific goal into a simpler query:

Get-WmiObject Win32_PnPEntity –Filter "Name='USB Axiom 49'"

Now that we know which WMI class represents the keyboard, and the query that selects it specifically, we can use WMI’s __InstanceCreationEvent to monitor for that instance as it comes and goes. The Register-WmiEvent cmdlet makes this a snap:

001
002
003
004
005
006
007
008
009
010
function Register-KeyboardEvent
{
    $query = "SELECT * FROM __InstanceCreationEvent " +
         "WITHIN 5 " +
         "WHERE TargetInstance ISA 'Win32_PnPEntity' " +
         "AND TargetInstance.Name = 'USB Axiom 49' "

    $null = Register-WmiEvent -Query $query -SourceIdentifier KeyboardMonitor -Action { E:\lee\reason\Default.rns }
}

After calling this function, PowerShell automatically launches Reason with your favourite presets already loaded whenever you turn on the USB keyboard.

Comments [0] | | # 
 Friday, February 26, 2010
Friday, February 26, 2010 5:25:54 PM (Pacific Standard Time, UTC-08:00) ( )

I’ve got a neat application that I use to map keystrokes to background PowerShell functions / script blocks. I recently started using Pandora more often, and the killer problem is when somebody drops by the office to ask a quick question. You dig around all of your open Explorer windows until you find the one for Pandora, then find the Pause button.

Here’s a function that does all of that for you, and maps it to Control+Alt+P:

001
002
003
004
005
006
007
008
009
010
011
012
013
function PausePandora
{
    $ie = New-Object -Com Shell.Application
    $pandora = $ie.Windows() | ? { $_.LocationName -like "*Pandora Radio*" }
    if($pandora)
    {
        $pandora.Navigate2("http://www.pandora.com/#/paused")
    }
    $ie = $null
}

## Pause Pandora
$keyMapping['Control,Alt,P'] = @{ Action =  { PausePandora } }
Comments [2] | | # 
 Wednesday, February 24, 2010
Wednesday, February 24, 2010 4:09:42 PM (Pacific Standard Time, UTC-08:00) ( )

We're getting close to "content complete" of the Windows PowerShell Cookbook, 2nd edition. The next step is technical review, where we look for both high-level and low-level feedback on the content and structure.

Also, this is a book focused on administrators. While PowerShell uber-hackers are always appreciated, inexperience with PowerShell is extremely valuable, as well.

If you're interested in being a Technical Reviewer of the book, please send me an email (click on the mail icon in the sidebar to the right) and I'll forward your information to O'Reilly.

Comments [0] | | # 
 Monday, January 18, 2010
Monday, January 18, 2010 8:53:33 PM (Pacific Standard Time, UTC-08:00) ( )

Here’s one that’s short and sweet. A lot of programs exist for the sole purpose of taking a screen shot after a certain amount of time. For example, delay for 10 seconds before taking a screen shot so that you have the time to open some menus.

This is incredibly simple with PowerShell:

Add-Type -Assembly System.Windows.Forms
Sleep 10

## Capture the entire screen
[System.Windows.Forms.Sendkeys]::SendWait("{PrtSc}")

## Capture the current window
[System.Windows.Forms.Sendkeys]::SendWait("%{PrtSc}")

Comments [0] | | # 
 Wednesday, January 13, 2010
Wednesday, January 13, 2010 9:10:49 PM (Pacific Standard Time, UTC-08:00) ( )

In PowerShell V2, we added a bunch of computer management cmdlets:

PS >Get-Command -Noun Computer | Format-Table -Auto

CommandType Name                Definition
----------- ----                ----------
Cmdlet      Add-Computer        Add-Computer [-DomainName] <String> [-Credent
Cmdlet      Checkpoint-Computer Checkpoint-Computer [-Description] <String> [
Cmdlet      Remove-Computer     Remove-Computer [[-Credential] <PSCredential>
Cmdlet      Restart-Computer    Restart-Computer [[-ComputerName] <String[]>]
Cmdlet      Restore-Computer    Restore-Computer [-RestorePoint] <Int32> [-Ve
Cmdlet      Stop-Computer       Stop-Computer [[-ComputerName] <String[]>] [[

Astute followers of our CTP process might notice a missing entry in this list when compared to earlier builds: the Rename-Computer cmdlet.

During testing and additional validation, we realized two things:

  • 1) The API used to rename computers is fairly “forgiving”
    If you specify an invalid computer name, the API often doesn’t reject it – but instead transforms it. For example, supplying a computer name that is too long will simply truncate the computer name and complete the operation. There’s nothing like losing a computer in your domain! Similarly, the API supports full Unicode input, while many applications break if the computer name is not the same as its NetBIOS name (which does not support full Unicode.)
  • 2) Our implementation failed to detect some higher-level scenarios that the existing NetDom.exe command detects
    NetDom.exe is a tool that already lets you rename computers, and users might expect some degree of compatibility with the safety it ensures. For example: in the machine a CA server? In the middle of a DC promo? In the middle of a role change?

Since both netdom.exe and WMI (http://msdn.microsoft.com/en-us/library/aa393056(VS.85).aspx) are easily accessible alternatives, we decided to remove the cmdlet rather than ship something that didn’t meet expectations.

Comments [0] | | # 
 Monday, January 04, 2010
Monday, January 04, 2010 10:46:18 PM (Pacific Standard Time, UTC-08:00) ( )

I don’t like to post line noise very often, but I did break that exception in the Holiday Wishes post a few weeks ago. I hate quoting entire blog posts, but, 177 characters isn’t too bad:

'(18029799997931744,139752119745773792|%{"{0,60}"-f [Convert]::ToString($_, 2).Replace("0"," ")})-split''(.{12}|Mh)''|?{$_}'|%{iex $_;-join[char[]]$_[[char[]]"nOBB7[4oBCaenRa"]}

When it runs, you get this for output:

PS >'(18029799997931744,139752119745773792|%{"{0,60}"-f [Convert]::ToString
($_, 2).Replace("0"," ")})-split''(.{12}|Mh)''|?{$_}'|%{iex $_;-join[char[]
]$_[[char[]]"nOBB7[4oBCaenRa"]}                                            
     1                                                                     
    111                                                                    
   11111                                                                   
  1111111                                                                  
    111                                                                    
   11111                                                                   
 111111111                                                                 
11111111111                                                                
    111                                                                    
    111                                                                    
Merrv ChristMas                                                            

While it may seem like magic, it in fact was not – just some terribly obtuse PowerShell scripting. Let’s take a look at what makes it tick. Rather than deconstruct it, we’ll build it up from scratch.

In a text editor, I drew the tree you see in the main output. In thinking how to compress this down into a shorter script, the essence of the solution is that the tree is really just a set of ON (“1”) cells, and OFF (“ “) cells. Binary is a system that describes ON and OFF really well, so perhaps we can store the tree’s pattern of ON and OFF in a binary number?

Converting this to more realistic binary, we get:

000001000000
000011100000
000111110000
001111111000
000011100000
000111110000
011111111100
111111111110
000011100000
000011100000

Now, NUMBERS do a great job of encoding binary patterns. For example, “1234” is 10011010010 in binary. What if we just concatenate all those ones and zeros together to find the number it represents?

PS >$result = [Convert]::ToInt64("00000100000000001110000000011111000000111
111100000001110000000011111000001111111110011111111111000001110000000001110
0000", 2)                                                                  
Exception calling "ToInt64" with "2" argument(s): "Value was either too la 
rge or too small for a UInt64."                                            
At line:1 char:29                                                          
+ $result = [Convert]::ToInt64 <<<< ("000001000000000011100000000111110000 
00111111100000001110000000011111000001111111110011111111111000001110000000 
0011100000", 2)                                                            
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationExcep  
   tion                                                                    
    + FullyQualifiedErrorId : DotNetMethodException                        

Unfortunately, there are too many bits (120) to fit in a single 64-bit number. While there are some “big number” libraries about, this should stand on its own. Instead, lets break it up into two smaller 60-bit chunks, and convert those:

PS >[Convert]::ToInt64("000001000000000011100000000111110000001111111000000
011100000", 2)                                                             
18029799997931744                                                          
PS >[Convert]::ToInt64("000111110000011111111100111111111110000011100000000
011100000", 2)                                                             
139752119745773792                                                         

Perfect. There are the magic numbers you see at the beginning of the script. We should be able to recreate our greeting by converting them back to binary, and outputting them:

PS >18029799997931744,139752119745773792 | Foreach-Object {                
>>     [Convert]::ToString($_, 2)                                          
>> }                                                                       
>>                                                                         
1000000000011100000000111110000001111111000000011100000                    
111110000011111111100111111111110000011100000000011100000                  

Hmm. These don’t match up any longer. Since leading zeroes are not important when converting numbers to binary, the .NET Framework doesn’t put any. After all, how many would it put? We can get around this problem by right-aligning the strings back to 60 characters:

PS >18029799997931744,139752119745773792 | Foreach-Object {                
>>     "{0,60}" -f [Convert]::ToString($_, 2)                              
>> }                                                                       
>>                                                                         
     1000000000011100000000111110000001111111000000011100000               
   111110000011111111100111111111110000011100000000011100000               

That’s got the correct width again, but those zeroes are going to get in the way. Let’s convert them back to spaces:

PS >18029799997931744,139752119745773792 | Foreach-Object {                
>>     "{0,60}" -f [Convert]::ToString($_, 2).Replace("0"," ")             
>> }                                                                       
>>                                                                         
     1          111        11111      1111111       111                    
   11111     111111111  11111111111     111         111                    

That’s getting pretty close. In fact, many trees look exactly like this when they come from a box. However, it’s not stacked properly – our original tree rows were 12 wide, and now they are 60. Busting out a gnarly word wrapping regular expression I blogged out a while ago, let’s apply it:

PS >(                                                                      
>>     18029799997931744,139752119745773792 | Foreach-Object {             
>>         "{0,60}" -f [Convert]::ToString($_, 2).Replace("0"," ")         
>>     }                                                                   
>> ) -split '(.{12})' | Where-Object { $_ }                                
>>                                                                         
     1                                                                     
    111                                                                    
   11111                                                                   
  1111111                                                                  
    111                                                                    
   11111                                                                   
 111111111                                                                 
11111111111                                                                
    111                                                                    
    111                                                                    

Hey, that rings a bell!

The next step is to add the “Merry Christmas” greeting at the end. Rather than write it directly or invent some cool scheme to encode the characters, what if we picked characters from the script itself, so far? If we have access to the script as a string, we can use PowerShell’s array slicing to pick whatever letters we want out. For example:

PS >"ABCDEF"[3,4,0,3,1,4,4,5]                                              
D                                                                          
E                                                                          
A                                                                          
D                                                                          
B                                                                          
E                                                                          
E                                                                          
F                                                                          

We don’t actually have access to the script as a string, so instead – let’s pack it into a string first, and then run it with Invoke-Expression. We’ll drop a bunch of spaces and use a bunch of aliases to compress things further:

'(18029799997931744,139752119745773792|%{"{0,60}"-f [Convert]::ToString($_, 2).Replace("0"," ")})-split''(.{12})''|?{$_}'|iex

Now, how can we invoke that string, but also have it available for the letter picking process we still want to do? We could store it in a variable (and reuse that variable,) but it might be easier to use the Foreach-Object cmdlet to loop over the (single) string, and then reuse the implicit $_ variable.

PS >'"Hello"' | Foreach-Object { Invoke-Expression $_; $_[3,2,2] }         
Hello                                                                      
l                                                                          
e                                                                          
e                                                                          

PowerShell naturally emits those last objects one-by-one, so let’s join them into a string:

PS >'"Hello"' | Foreach-Object { Invoke-Expression $_; -join $_[3,2,2] }   
Hello                                                                      
lee                                                                        

That looks like the approach we need to pick the letters out of our original script. I generated a table and reviewed its output:

PS >$script = '(18029799997931744,139752119745773792|%{"{0,60}"-f [Convert]
::ToString($_, 2).Replace("0"," ")})-split''(.{12})''|?{$_}'               
PS >$x = 0; $script.ToCharArray() | % { "$x : $_"; $x++ }                  
0 : (                                                                      
1 : 1                                                                      
2 : 8                                                                      
3 : 0                                                                      
4 : 2                                                                      
5 : 9                                                                      
6 : 7                                                                      
51 : [                                                                     
52 : C                                                                     
53 : o                                                                     
54 : n                                                                     
55 : v                                                                     
56 : e                                                                     
57 : r                                                                     
58 : t                                                                     
59 : ]                                                                     
116 : }                                                                    

Unfortunately, the script output is missing a few chararcters: “M”, and “h”. It’s also missing a “y”, but using a “v” is a common trick due to its visual similarity. Where can we pack those letters in, though?

The regular expression looks like the perfect place. It splits on sequences of 12 characters, so we could also ask it to split on the non-existent string “Mh”. It would have no impact, but would give us the characters we need. Some readers thought that the “v” instead of a “y” was a typo, so Robert (in the comments) fixed the script by adding the “y” to this throwaway piece of the regular expression.

'(18029799997931744,139752119745773792|%{"{0,60}"-f [Convert]::ToString($_, 2).Replace("0"," ")})-split''(.{12}|Mhy)''|?{$_}

After adding the extra text to the script, review the output again and get a big list of numbers:

… –join $_[110,79,66,66,55,91,52,111,66,67,97,101,110,82,97]

That’s unbecoming of an obfuscated script. Luckily, those numbers are in the regular ASCII range, and we can convert them to a string:

PS >[char[]] (110,79,66,66,55,91,52,111,66,67,97,101,110,82,97)            
n                                                                          
O                                                                          
B                                                                          
B                                                                          
7                                                                          
[                                                                          
4                                                                          
o                                                                          
B                                                                          
C                                                                          
a                                                                          
e                                                                          
n                                                                          
R                                                                          
a                                                                          
PS >[int[]] [char[]] "nOBB7[4oBCaenRa"                                     
110                                                                        
79                                                                         
66                                                                         
66                                                                         
55                                                                         
91                                                                         
52                                                                         
111                                                                        
66                                                                         
67                                                                         
97                                                                         
101                                                                        
110                                                                        
82                                                                         
97                                                                         

PowerShell’s array slicing does automatic integer casting if needed, so we are fine just converting the string to a sequence of characters:

… $_[[char[]]"nOBB7[4oBCaenRa"]}

And there you have it: 1200 words to describe 177 characters.

Comments [1] | | # 
 Friday, December 18, 2009
Friday, December 18, 2009 10:17:11 PM (Pacific Standard Time, UTC-08:00) ( )

'(18029799997931744,139752119745773792|%{"{0,60}"-f [Convert]::ToString($_, 2).Replace("0"," ")})-split''(.{12}|Mh)''|?{$_}'|%{iex $_;-join[char[]]$_[[char[]]"nOBB7[4oBCaenRa"]}

Comments [5] | | # 
 Monday, November 23, 2009
Monday, November 23, 2009 10:05:11 PM (Pacific Standard Time, UTC-08:00) ( )

Marcel has been posting some interesting articles on using PowerShell to generate the MD5 hashes of files.  Now, an MD5 hash of a file is just an array of bytes.  Typical hashing programs display this in a more friendly manner:

PS:15 C:\Temp >md5sum 71-59-B7.bmp
a05805e638741bb767f97c0e88962952 *71-59-B7.bmp

Although the output of Marcel’s scripts could definitely be crafted to display this output, they currently output the string representation of a byte array:

PS:19 C:\Temp >get-md5 (get-childitem 71-59-B7.bmp)
160 88 5 230 56 116 27 183 103 249 124 14 136 150 41 82

One of the comments in response to Marcel’s post was that PowerShell should, by default, output byte arrays as hex.  This is a good suggestion, and we can go even further with it.  Let’s write a script to give us a full hex editor-like view of a byte array:

PS:20 C:\Temp >get-md5 (get-childitem 71-59-B7.bmp) | format-hex


            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F


00000000   A0 58 05 E6 38 74 1B B7 67 F9 7C 0E 88 96 29 52   X.æ8t.•gù|.??)R

Or even better, let’s use it to dump out a very small bitmap – 10 pixels of the colour (R=0x71 G=0x59 B=0xB7)

PS:21 C:\Temp >Format-Hex 71-59-B7.bmp


            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F


00000000   42 4D 5E 00 00 00 00 00 00 00 36 00 00 00 28 00  BM^.......6...(.
00000010   00 00 0A 00 00 00 01 00 00 00 01 00 20 00 00 00  ............ ...
00000020   00 00 00 00 00 00 C4 0E 00 00 C4 0E 00 00 00 00  ......Ä...Ä.....
00000030   00 00 00 00 00 00 B7 59 71 FF B7 59 71 FF B7 59  ......•Yq.•Yq.•Y
00000040   71 FF B7 59 71 FF B7 59 71 FF B7 59 71 FF B7 59  q.•Yq.•Yq.•Yq.•Y
00000050   71 FF B7 59 71 FF B7 59 71 FF B7 59 71 FF        q.•Yq.•Yq.•Yq.

To make it easier to determine byte offsets, files are usually broken down into 16-byte rows.  The left-hand section gives the offset of the 16-byte chunk.  The middle section gives the hex representation of the data at that location.  These pieces of data are aligned in columns also, corresponding to their location within the 16-byte chunk.  So column “E” in row 0x40 means a file offset of (0x40 + 0x0E) = 0x4E.  The last section gives an ASCII representation of the data.

In this representation, it becomes possible to see some of the underlying structure of the bitmap format:

Offset Length Comment
0x00 2 “BM,” the magic bitmap header
0x02 4 “0x5E,” the length of the file. Notice that our last data byte is at 0x5D.  Since we started counting from zero, this means that we have 0x5E bytes of data.
(...) (...) (...)
0x0A 4 “0x36”, specifies the absolute start of the bitmap data. Notice that the data begins at offset (0x30 + 0x06).
0x36 40 10 4-byte pixel representations. In Bitmaps, they are laid out as (B=0xB7 G=0x59 R=0x71 <reserved>)

Now, for the script:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
##############################################################################
##
## Format-Hex
##
## From Windows PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS
Outputs a file or pipelined input as a hexadecimal display. To determine the
offset of a character in the input, add the number at the far-left of the row
with the the number at the top of the column for that character.
 
.EXAMPLE
PS >"Hello World" | Format-Hex

            0 1 2 3 4 5 6 7 8 9 A B C D E F
 
00000000 48 00 65 00 6C 00 6C 00 6F 00 20 00 57 00 6F 00 H.e.l.l.o. .W.o.
00000010 72 00 6C 00 64 00 r.l.d.
 
.EXAMPLE
PS >Format-Hex c:\temp\example.bmp
  
#>


[CmdletBinding(DefaultParameterSetName = "ByPath")]
param(
    ## The file to read the content from
    [Parameter(ParameterSetName = "ByPath", Position = 0)]
    [string] $Path,
   
    ## The input (bytes or strings) to format as hexadecimal
    [Parameter(
        ParameterSetName = "ByInput", Position = 0,
        ValueFromPipeline = $true)]
    [Object] $InputObject
)

begin 
{
    ## Create the array to hold the content. If the user specified the
    ## -Path parameter, read the bytes from the path.
    [byte[]] $inputBytes = $null
    if($Path) { $inputBytes = [IO.File]::ReadAllBytes( (Resolve-Path $Path) ) }
   
    ## Store our header, and formatting information
    $counter = 0
    $header = " 0 1 2 3 4 5 6 7 8 9 A B C D E F"
    $nextLine = "{0} " -f  [Convert]::ToString(
        $counter, 16).ToUpper().PadLeft(8, '0')
    $asciiEnd = ""

    ## Output the header
    "`r`n$header`r`n"
}

process
{
    ## If they specified the -InputObject parameter, retrieve the bytes
    ## from that input
    if($InputObject)
    {
        ## If it's an actual byte, add it to the inputBytes array.
        if($InputObject -is [Byte])
        {
            $inputBytes = $InputObject
        }
        else
        {
            ## Otherwise, convert it to a string and extract the bytes
            ## from that.
            $inputString = [string] $InputObject
            $inputBytes = [Text.Encoding]::Unicode.GetBytes($inputString)
        }
    }
   
    ## Now go through the input bytes
    foreach($byte in $inputBytes)
    {
        ## Display each byte, in 2-digit hexidecimal, and add that to the
        ## left-hand side.
        $nextLine += "{0:X2} " -f $byte

        ## If the character is printable, add its ascii representation to
        ## the right-hand side. Otherwise, add a dot to the right hand side.
        if(($byte -ge 0x20) -and ($byte -le 0xFE))
        {
           $asciiEnd += [char] $byte
        }
        else
        {
           $asciiEnd += "."
        }

        $counter++;

        ## If we've hit the end of a line, combine the right half with the
        ## left half, and start a new line.
        if(($counter % 16) -eq 0)
        {
           
            "$nextLine $asciiEnd"
            $nextLine = "{0} " -f [Convert]::ToString(
                $counter, 16).ToUpper().PadLeft(8, '0')
            $asciiEnd = "";
        }
    }
}

end
{
    ## At the end of the file, we might not have had the chance to output
    ## the end of the line yet. Only do this if we didn't exit on the 16-byte
    ## boundary, though.
    if(($counter % 16) -ne 0)
    {
       while(($counter % 16) -ne 0)
       {
          $nextLine += " "
          $asciiEnd += " "
          $counter++;
       }
       "$nextLine $asciiEnd"
    }

    ""
}

 

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]

Comments [0] | | # 
 Friday, November 20, 2009
Friday, November 20, 2009 7:42:09 PM (Pacific Standard Time, UTC-08:00) ( )

When you’re writing a script that depends on PowerShell Remoting, it’s often helpful to know that the remoting channel is open and will support the activities of your script.

PowerShell has a Test-WSMan command, but that only verifies that a WSMan connection is possible. There are other scenarios you could be impacted by:

  • Not having permission on the remote machine
  • Misconfiguration of the PowerShell endpoint
  • Corrupted installation
  • (etc)

As you dig deeper, you realize that the only way to really test the viability of the remoting channel is to just do something on it, and verify that you got the results you expected to. Since the implementation was so simple, we didn’t write a cmdlet for it. In retrospect, the concept is more difficult than the implementation, so we probably should have written it anyways. Here’s an example function that tests the remoting connection to a specific machine.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028

function Test-PsRemoting
{
    param(
        [Parameter(Mandatory = $true)]
        $computername
    )
   
    try
    {
        $errorActionPreference = "Stop"
        $result = Invoke-Command -ComputerName $computername { 1 }
    }
    catch
    {
        Write-Verbose $_
        return $false
    }
   
    ## I've never seen this happen, but if you want to be
    ## thorough....
    if($result -ne 1)
    {
        Write-Verbose "Remoting to $computerName returned an unexpected result."
        return $false
    }
   
    $true   
}

Comments [0] | | #