PowerShell Cookbook

Search

Categories

 

On this page

Syntax Highlighting in PowerShell
Will it Pipe? Brevity and Readability
PowerShell Cookbook Now Available
The perils of BCC
Build Instructions for the DIY Cat Feeder
Interacting with SQL Databases in PowerShell: Invoke-SqlCommand
Managing INI files with PowerShell
Using PowerShell and PsExec to invoke expressions on remote computers
Removing Certificates from the Certificate Store
Cmdlets vs Functions

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: 220
This Year: 20
This Month: 0
This Week: 0
Comments: 533

Sign In

 Tuesday, November 06, 2007
Wednesday, November 07, 2007 6:22:07 AM (Pacific Standard Time, UTC-08:00) ( )

Since we just released a CTP of PowerShell V2, I thought I'd share a handy little script to demonstrate one of the new APIs we introduced: Show-ColorizedContent.ps1.

This CTP introduces a new tokenizer API that lets you work with PowerShell script content the same way that our parser does -- as a collection of items (tokens) that represent the underlying structure of that script Until now, any tool that works with the data of a PowerShell script needs to parse the script on its own -- usually with fragile regular expressions or other means.

This often works, but usually falls apart on complex scripts:

image

In the first line, "Write-Host" is an argument to the Write-Host cmdlet, but gets parsed as a string. Fair enough, but the second line does not treat the argument the same way. In fact, since it matches a cmdlet name, the argument gets parsed as another cmdlet call. In the here string that follows, the Write-Host cmdlet name gets highlighted again, even though it is really just part of a string.

This is absolutely not a slam on the authors of existing highlighters -- it's just that we've now introduced something that makes life so much easier.

$content = [IO.File]::ReadAllText("c:\temp\ContentTest.ps1")
$errors = [System.Management.Automation.PSParseError[]] @()
[System.Management.Automation.PsParser]::Tokenize($content, [ref] $errors)

This API generates a collection of PSToken objects that give all the information you need to properly dissect a PowerShell script:

PS C:\Temp> [System.Management.Automation.PsParser]::Tokenize($content, [ref] $errors) | ft -auto

Content                           Type Start Length StartLine StartColumn EndLine EndColumn
-------                           ---- ----- ------ --------- ----------- ------- ---------
Write-Host                     Command     0     10         1           1       1        11
Write-Host                      String    11     12         1          12       1        24
...                            NewLine    23      2         1          24       2         1
Write-Host                     Command    25     10         2           1       2        11
Write-Host             CommandArgument    36     10         2          12       2        22
...                            NewLine    46      2         2          22       3         1
...                            NewLine    48      2         3           1       4         1
Write-Host Write-Host           String    50     23         4           1       4        24
...                            NewLine    73      2         4          24       5         1
...                            NewLine    75      2         5           1       6         1
testContent                   Variable    77     12         6           1       6        13
=                             Operator    90      1         6          14       6        15
Write-Host Hello World          String    92     30         6          16       8         3
...                            NewLine   122      2         8           3       9         1

This adds a whole new dimension to the way you can interact with PowerShell. Some natural outcomes are:

  • syntax highlighting
  • preparing a script for production (replacing all aliased commands with their expanded equivalent, etc)
  • script refactoring
  • FxCop / Style guideline checks
  • PowerTab :)

As a starter example, I've attached Show-ColorizedContent.ps1 -- a script to colorize PowerShell scripts in a console window. Its primary goal is to support demonstrations of PowerShell snippets. For that, it adds line numbers to let you easily refer to portions of your script. It also includes a -HighlightRanges parameter to let you highlight specific ranges of the script. The -HighlightRanges parameter is an array of line numbers, which you can easily create using PowerShell's standard array range syntax:


PS C:\Temp> .\Show-ColorizedContent.ps1 Show-ColorizedContent.ps1 ` >> -HighlightRanges (4..5+1+30..33) >> 001 > #requires -version 2.0 002 | 003 | param( 004 > $filename = $(throw "Please specify a filename."), 005 > $highlightRanges = @(), 006 | [System.Management.Automation.SwitchParameter] $excludeLineNumbers) 007 | 008 | # [Enum]::GetValues($host.UI.RawUI.ForegroundColor.GetType()) | % { Write-Host -Fore $_ "$_" } (...)
026 | $highlightColor = "Green" 027 | $highlightCharacter = ">" 028 | 029 | ## Read the text of the file, and parse it 030 > $file = (Resolve-Path $filename).Path 031 > $content = [IO.File]::ReadAllText($file) 032 > $parsed = [System.Management.Automation.PsParser]::Tokenize($content, [ref] $null) | 033 > Sort StartLine,StartColumn 034 | 035 | function WriteFormattedLine($formatString, [int] $line) 036 | { 037 | if($excludeLineNumbers) { return } 038 | 039 | $hColor = "Gray" 040 | $separator = "|"

Enjoy -- you can download it here.

Comments [5] | | # 
 Thursday, November 01, 2007
Friday, November 02, 2007 5:26:52 AM (Pacific Daylight Time, UTC-07:00) ( )

Scott Hanselman and I recently chatted about using PowerShell for a bit of log analysis. The majority of his solution (in green) ended up being quite elegant, flowing into a pipeline nearly as easily as you might speak it:

PS C:\> $re =[regex]"\d{2}(?=[_.])"; import-csv file.csv |
select File, Hits, @{Name="Show";Expression={$re.matches($_.File)[0] } } | sort Show -desc | group Show |
select Name,
{($_.Group | Measure-Object -Sum Hits).Sum }

(Of course, one should never pronounce a regex aloud in polite company.)

The bit in red took a little hammering on, but eventually produced the desired results. At this point, I mentioned that I didn't think the final solution was a good demonstration of PowerShell's pipeline power. 80% of it, absolutely. But since the last bit took some fussing, the solution stopped being an example of how easy the pipeline makes everything, and instead became an example of how you could write a pipeline to do anything.

If I was to blog it with the intent of education, I probably would have written a more scripty function:

function Get-ShowHits
{
    $regex = '/hanselminutes_(\d+).*'
    $shows = Import-CSv File.csv | Select File,Hits | Group { $_.File -replace $regex,'$1' }

    foreach($show in $shows)
    {
        $showOutput = New-Object System.Management.Automation.PsObject
        $showOutput | Add-Member NoteProperty Name $show.Name
        $showOutput | Add-Member NoteProperty Hits ($show.Group | Measure-Object -Sum Hits).Sum
        $showOutput
    }
}
Get-ShowHits | Sort -Desc Hits

This example illustrates a couple great points about PowerShell:

  • You can write functions and scripts to wrap complex functionality into a more usable form
  • You can write pipelines to easily express powerfull object flows (in the $shows line)
  • You can create your own objects with their own properties -- and manipulate them just as easily

But the most important point about these two examples is how easy they are to modify and extend.

Jon Udell keyed in on Scott's post, and in the comments of the two blogs, language comparisons quickly blossomed. Hey, we've been here before!

Specifically,

These types of problems are like mosquito bites for me - I can’t stop itching at them. A better ruby one-liner. Doesn’t print a header but not a big deal…

CSV.read("test.csv").inject(Hash.new(0)) {|h,row| h[row[0][/\d{4}/].to_i] += row[1].to_i;h}.sort.each {|i| puts("#{i[0]}\t#{i[1]}")}

Once you have a solution that works, a natural scripter's passion is to tinker it down to one line. It's no longer educational, intelligible, or extendable, but it's fun. You can do that in PowerShell, too:

$foo = @{}; ipcsv test.csv | % { $foo[0+($_.File -replace '.*?(\d+).*','$1')] += (0+$_.Hits) }; $foo.GetEnumerator() | Sort Value

Mmmm. Pipeline smoke.

Comments [0] | | # 
 Monday, October 29, 2007
Tuesday, October 30, 2007 12:02:20 AM (Pacific Daylight Time, UTC-07:00) ( )

As just announced by O’Reilly, the PowerShell Cookbook is now available!

As your experience grows in any technology, you learn and benefit from the combination of two distinct types of knowledge:

  1. What can I do with the technology?
  2. How do I accomplish a specific task in that technology?

Question #1 focuses on the technology. You learn the answers to #1 from training, exploration, podcasts, and technology-specific books. This is where you immerse yourself in the looping constructs, variable syntax, etc. In PowerShell’s case, that’s (not surprisingly) turned out to be PowerShell in Action, along with the many other excellent PowerShell learning resources out there.

Question #2 focuses on the task. You know what you want to do, and the only think in your way is technology. You learn the answers to #2 from experience, and lots of it. Experience is in short supply when you first start working with a technology, so blog posts and internet searches often tentatively fill that void. Until the O’Reilly cookbook arrives, that is, at which point you have pre-canned solutions to many of your most vexing questions. In PowerShell’s case, we finally have a resource to fill that void – the PowerShell Cookbook.

The PowerShell Cookbook focuses squarely on showing you how to use PowerShell to get your job done. It builds on a huge base of distilled knowledge, and includes:

  • Solutions to the most popular and searched-for TechNet / Script Center topics
  • Scripts that address the most common community, newsgroup, and new user questions
  • Scripts that wrap around and hide the complexity of advanced (but very useful) PowerShell scripting techniques
  • Task-based introduction to all of PowerShell’s major features

In addition to all of this pre-distilled knowledge, the cookbook’s appendix provides a very thorough and complete PowerShell reference. Thorough enough to be a book all of its own, but now expanded and finally in a printed format. It includes:

  • A complete reference to the PowerShell language and environment
  • An exhaustive regular expression reference, along with PowerShell examples
  • A complete listing of all PowerShell automatic variables
  • A guide to PowerShell’s standard verbs, which helps you write scripts that match the PowerShell naming guidelines
  • Hand-picked lists of the .NET classes, WMI classes, and COM objects most useful to system administrators
  • An exhaustive String and DateTime formatting reference, along with PowerShell examples

Since the book focuses so strongly on the same crowd that frequents Microsoft ScriptCenter, I tapped Dean Tsaltas (one of the original Scripting Guys) for feedback and a forward. Perhaps my greatest disappointment about the book is that we didn’t have room for his fantastic cover quote. I would be remiss to relegate his unbiased opinion to the Cavern-Of-Thoughts-Unpublished, so I include it here:

"Must-read of the season!  A stunning achievement! Tantalizing, witty, and entirely satisfying. Oh, wait -- that's the foreword."

Dean Tsaltas, Scripting Guy emeritus, author of the Foreword.

So, enjoy the book  — or at least the foreword :)

Comments [4] | | # 
 Wednesday, October 24, 2007
Wednesday, October 24, 2007 9:36:53 PM (Pacific Daylight Time, UTC-07:00) ( )

When you’ve got to deal with as much information as everybody in a large company does, most people write Inbox rules to redirect DLs to their own sub-folders. They then group the folder “By Conversation,” which lets them quickly read the threads of interest. But most importantly, it lets them ignore the ones that do not.

 

If you don’t yet use these techniques to help you manage your own mail, you will love yourself for starting. I may follow up with a post in the future about that, but it’s a staple in email management in any large company.

 

When you BCC a distribution / discussion list, that mail break the inbox rules of everybody subscribed to the DL. That includes your boss, the person managing the sr. exec’s mailbox, and the person just about to jump off a cliff from email overload.

 

So aside from being rude (people set these rules up for a reason,) it doesn’t actually solve the problem you might think it does. Although it is nearly always done with the intent to minimize "noise," noise in a person's mailbox is much worse than noise on a distribution list.  Inbox rules are the single most effective means that we have to manage the massive stream of information that flows through the company.

 

 

BCC to "take Offline":

Please do not use BCC to take a thread offline.  Instead, a reply-all with "taking this offline" will suffice. The alias knows that the thread is being addressed, and further conversation is unlikely to follow. In a follow-up mail to the person, you might provide the information you planned to originally. For those that batch their reading of the alias, they probably don’t even care about this specific thread, and will skim right past it.

 

If the conversation continues (i.e.: despite the intention to take offline,) then it was clearly of interest to the DL. No amount of BCCing will prevent the conversation from ending on its own schedule.

 

When you've resolved the issue, people often appreciate it when you follow up with the main thread with the details of the resolution.

 

BCC to "Redirect to another Alias":

Please do not use BCC to redirect a thread.  Instead, a reply-all with "please take this up on the <more-appropriate-alias> alias." The alias knows that the thread is being addressed, and further conversation is unlikely to follow. For those that batch their reading of the alias, they probably don’t even care about this specific thread, and will skim right past it. Aaron Lerch recently wrote a cool macro to unfortunately automate exactly this scenario.

 

BCC to prevent "Reply All":

Please do not use BCC to prevent Reply-Alls.  There are very few situations when limiting “Reply All” makes sense on a distribution / discussion list. If a conversation starts or continues (i.e.: despite the intention to prevent Reply-All,) then it was clearly of interest to the DL. No amount of BCCing will prevent the conversation from ending on its own schedule.

 

A DL isn’t there to be people’s internal version of the LazyWeb — so I disagree with attempting to prevent Reply All. A thread will continue until it runs out of steam – in which case no amount of prevention will stop it. If it was a boring question in the first place, you don’t have to worry about a reply-all storm anyways.  DLs that are highly populated involuntarily (i.e.: organizational aliases) should have the send-to permissions restricted. But since techniques exist for managing mail to DLs, a Reply-All storm really doesn’t matter.

 

Case in point: An internal alias recently suffered a meltdown. With rules in place, my only comment was “weird, why do I have 3000 mails in that folder all of a sudden?”

 

In the very unlikely chance that you have a good reason to prevent a Reply-All, Scott Hanselman’s suggestion (or an Outlook NoReplyAll form) is probably the best bet.

 

 

BCC for unknown reasons:

Please do not use BCC unless you have a good reason to.  Instead, send a message saying, "Please reply directly to me," if that was your intent.

 

Comments [3] | | # 
 Monday, October 22, 2007
Tuesday, October 23, 2007 6:17:45 AM (Pacific Daylight Time, UTC-07:00) ( )

A few weeks ago, Design News ran an engineering-centric article on the DIY Cat Feeder that entertained my cats while I was on vacation last year. Everybody loves a good hack, but I've been surprised by the attention it received. If you've been wanting to know more about how it was built, the article provides much more detail.

As part of their article preparation, they asked for mechanical and schematic drawings, along with part numbers from anything in the Allied Electronics catalog that the cat feeder used. Since my main goal was to make something out of random junk laying around the house, I thought the question was a little funny – as the parts list ended up being:

Amt Part Description
1 Big box of Pepsi
1-2 Thin phone books
1 Sturdy cardboard box
1 Elastic band
5-6 Bamboo skewers
1 Sheet of cardboard
1 Bowl
1 Wireless-enabled computer
1 Very large box

A few weeks later, a photographer came by for a photo shoot, and the rest was history.
 
The hardest part of the process, by far, was coming up with the mechanical drawing. I first started drawing in Paint.NET, but got slightly annoyed by my inability to keep the lines consistent in a fake 3D drawing. I gave up after I realized that I wanted to show another perspective – and there would be no way I could ever keep the two consistent. After trying out half a dozen free CAD/CAM programs and 3D modeling applications, I finally settled on Google Sketchup. It ultimately solved my needs perfectly. If you're looking for a good, free, low fidelity 3D modeling program, definitely give it a try.

I finally got the chance to clarify something in the write-up, too. The Ubuntu aspect of this story got fully overblown, as Windows is just as capable of ejecting a CDROM tray :) Here is the PowerShell script that does it:

$mediaPlayer = New-Object -Com WMPlayer.OCX
$mediaPlayer.CDRomCollection.Item(0).eject()
Start-Sleep -Milliseconds 300
$mediaPlayer.cdromCollection.Item(0).eject()
Start-Sleep 1
$mediaPlayer.CDRomCollection.Item(0).eject()
Start-Sleep -Milliseconds 300

And to schedule it:

schtasks /create /tn "Feed Cats" /sc DAILY /st 07:00:00 `
    /tr "powershell c:\users\<user>\feedcats.ps1"
schtasks /create /tn "Feed Cats" /sc DAILY /st 17:45:00 `
    /tr "powershell c:\users\<user>\feedcats.ps1"

So now the spare computer has another credit in its already illustrious resume of Cat Feeder and Subversion CVS Source Control Server. That would be, of course, Internet Supermodel.

Comments [0] | | # 
 Thursday, October 18, 2007
Friday, October 19, 2007 6:40:08 AM (Pacific Daylight Time, UTC-07:00) ( )

Jeffrey McManus recently wrote about database queries with PowerShell – a small script that lets you query a SQL data store. This is really powerful. Rather than context switch into SQL Express (or TOAD, or your other favourite administration tool,) you can do what you need from PowerShell.

It goes even further, though. A lot of PowerShell's built-in commands have a set-oriented flavour: Where-Object, Select-Object, Group-Object, and Sort-Object. I also blogged about a set intersection script here: http://www.leeholmes.com/blog/CreatingSQLsJoinlikeFunctionalityInMSH.aspx. After spending a ton of time in Oracle databases during an internship at General Electric, I remember always wishing that DIR supported a WHERE clause. Well, with PowerShell, it does!

One thing Jeffrey's post doesn't fully highlight is that the .NET Framework can also return fully structured objects that represent the results of your query. If your SELECT query returns a recordset with Name and Address columns, you can get back objects with Name and Address properties. With that, you can slice and dice the results even further in the shell.

PS D:\Temp> Invoke-SqlCommand.ps1 -Sql "SELECT TOP 5 * FROM Orders" | Format-Table

    OrderID CustomerID   EmployeeID OrderDate   RequiredDat ShippedDate     ShipVia     Freight
                                                e
    ------- ----------   ---------- ---------   ----------- -----------     -------     -------
      10248 VINET                 5 7/4/1996... 8/1/1996... 7/16/199...           3       32.38
      10249 TOMSP                 6 7/5/1996... 8/16/199... 7/10/199...           1       11.61
      10250 HANAR                 4 7/8/1996... 8/5/1996... 7/12/199...           2       65.83
      10251 VICTE                 3 7/8/1996... 8/5/1996... 7/15/199...           1       41.34
      10252 SUPRD                 4 7/9/1996... 8/6/1996... 7/11/199...           2        51.3

The .NET Framework supports more than just SQL servers, though. It supports Access databases and Excel workbooks, too. Their connection strings are a black-art, so it is natural to wrap all of that magic in a script.

So natural that I include in the upcoming PowerShell Cookbook: Invoke-SqlCommand. (PS: Expect some good news about the book VERY soon.)

##############################################################################
##
## Invoke-SqlCommand.ps1
##
## From Windows PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
## Return the results of a SQL query or operation
##
## ie:
##
##    ## Use Windows authentication
##    Invoke-SqlCommand.ps1 -Sql "SELECT TOP 10 * FROM Orders"
##
##    ## Use SQL Authentication
##    $cred = Get-Credential
##    Invoke-SqlCommand.ps1 -Sql "SELECT TOP 10 * FROM Orders" -Cred $cred
##
##    ## Perform an update
##    $server = "MYSERVER"
##    $database = "Master"
##    $sql = "UPDATE Orders SET EmployeeID = 6 WHERE OrderID = 10248"
##    Invoke-SqlCommand $server $database $sql
##
##    $sql = "EXEC SalesByCategory 'Beverages'"
##    Invoke-SqlCommand -Sql $sql
##
##    ## Access an access database
##    Invoke-SqlCommand (Resolve-Path access_test.mdb) -Sql "SELECT * FROM Users"
##    
##    ## Access an excel file
##    Invoke-SqlCommand (Resolve-Path xls_test.xls) -Sql 'SELECT * FROM [Sheet1$]'
##
##############################################################################

param(
    [string] $dataSource = ".\SQLEXPRESS",
    [string] $database = "Northwind",      
    [string] $sqlCommand = $(throw "Please specify a query."),
    [System.Management.Automation.PsCredential] $credential
  )


## Prepare the authentication information. By default, we pick
## Windows authentication
$authentication = "Integrated Security=SSPI;"

## If the user supplies a credential, then they want SQL
## authentication
if($credential)
{
    $plainCred = $credential.GetNetworkCredential()
    $authentication = 
        ("uid={0};pwd={1};" -f $plainCred.Username,$plainCred.Password)
}

## Prepare the connection string out of the information they
## provide
$connectionString = "Provider=sqloledb; " +
                    "Data Source=$dataSource; " +
                    "Initial Catalog=$database; " +
                    "$authentication; "

## If they specify an Access database or Excel file as the connection
## source, modify the connection string to connect to that data source
if($dataSource -match '\.xls$|\.mdb$')
{
    $connectionString = "Provider=Microsoft.Jet.OLEDB.4.0; Data Source=$dataSource; "

    if($dataSource -match '\.xls$')
    {
        $connectionString += 'Extended Properties="Excel 8.0;"; '

        ## Generate an error if they didn't specify the sheet name properly
        if($sqlCommand -notmatch '\[.+\$\]')
        {
            $error = 'Sheet names should be surrounded by square brackets, and ' +
                       'have a dollar sign at the end: [Sheet1$]'
            Write-Error $error
            return
        }
    }
}

## Connect to the data source and open it
$connection = New-Object System.Data.OleDb.OleDbConnection $connectionString
$command = New-Object System.Data.OleDb.OleDbCommand $sqlCommand,$connection
$connection.Open()

## Fetch the results, and close the connection
$adapter = New-Object System.Data.OleDb.OleDbDataAdapter $command
$dataset = New-Object System.Data.DataSet
[void] $adapter.Fill($dataSet)
$connection.Close()

## Return all of the rows from their query
$dataSet.Tables | Select-Object -Expand Rows


 

Comments [4] | | # 
 Tuesday, October 02, 2007
Tuesday, October 02, 2007 6:24:39 PM (Pacific Daylight Time, UTC-07:00) ( )

The question came up on the newsgroup a few days ago on how to work with INI files from PowerShell.

A lot of great answers came up (using text parsing of the INI files,) but the Windows API actually supports reading and writing of INI file entries directly through its GetPrivateProfileString and WritePrivateProfileString functions. PowerShell doesn’t support P/Invoke to the Win32 API directly, but the Invoke-Win32 script given here does: http://www.leeholmes.com/blog/GetTheOwnerOfAProcessInPowerShellPInvokeAndRefOutParameters.aspx.

Since it’s reusable, that code sits better as a script of its own, named Invoke-WindowsApi (below.)

##############################################################################
##
## Invoke-WindowsApi.ps1
##
## From PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
## Invoke a native Windows API call that takes and returns simple data types.
##
## ie:
##
## ## Prepare the parameter types and parameters for the 
## CreateHardLink function
## $parameterTypes = [string], [string], [IntPtr]
## $parameters = [string] $filename, [string] $existingFilename, [IntPtr]::Zero
## 
## ## Call the CreateHardLink method in the Kernel32 DLL
## $result = Invoke-WindowsApi "kernel32" ([bool]) "CreateHardLink" `
##     $parameterTypes $parameters
##
##############################################################################

param(
    [string] $dllName
    [Type$returnType
    [string] $methodName,
    [Type[]] $parameterTypes,
    [Object[]] $parameters
    )

## Begin to build the dynamic assembly
$domain = [AppDomain]::CurrentDomain
$name = New-Object Reflection.AssemblyName 'PInvokeAssembly'
$assembly = $domain.DefineDynamicAssembly($name'Run')
$module = $assembly.DefineDynamicModule('PInvokeModule')
$type = $module.DefineType('PInvokeType'"Public,BeforeFieldInit")

## Go through all of the parameters passed to us.  As we do this,
## we clone the user's inputs into another array that we will use for
## the P/Invoke call.  
$inputParameters = @()
$refParameters = @()

for($counter = 1$counter -le $parameterTypes.Length; $counter++)
{
   ## If an item is a PSReference, then the user 
   ## wants an [out] parameter.
   if($parameterTypes[$counter - 1] -eq [Ref])
   {
      ## Remember which parameters are used for [Out] parameters
      $refParameters += $counter

      ## On the cloned array, we replace the PSReference type with the 
      ## .Net reference type that represents the value of the PSReference, 
      ## and the value with the value held by the PSReference.
      $parameterTypes[$counter - 1] = 
         $parameters[$counter - 1].Value.GetType().MakeByRefType()
      $inputParameters += $parameters[$counter - 1].Value
   }
   else
   {
      ## Otherwise, just add their actual parameter to the
      ## input array.
      $inputParameters += $parameters[$counter - 1]
   }
}

## Define the actual P/Invoke method, adding the [Out]
## attribute for any parameters that were originally [Ref] 
## parameters.
$method = $type.DefineMethod($methodName'Public,HideBySig,Static,PinvokeImpl',
    $returnType$parameterTypes)
foreach($refParameter in $refParameters)
{
   [void] $method.DefineParameter($refParameter"Out"$null)
}

## Apply the P/Invoke constructor
$ctor = [Runtime.InteropServices.DllImportAttribute].GetConstructor([string])
$attr = New-Object Reflection.Emit.CustomAttributeBuilder $ctor$dllName
$method.SetCustomAttribute($attr)

## Create the temporary type, and invoke the method.
$realType = $type.CreateType()

$realType.InvokeMember($methodName'Public,Static,InvokeMethod'$null$null
    $inputParameters)

## Finally, go through all of the reference parameters, and update the
## values of the PSReference objects that the user passed in.
foreach($refParameter in $refParameters)
{
   $parameters[$refParameter - 1].Value = $inputParameters[$refParameter - 1]
}

 

Using this script, you can easily wrap these APIs in a more friendly PowerShell scripts.

##############################################################################
##
## Get-PrivateProfileString.ps1
##
## Get an entry from an INI file.
##
## ie:
##
##  PS >Get-PrivateProfileString.ps1 C:\winnt\system32\ntfrsrep.ini text DEV_CTR_24_009_HELP
##
##############################################################################

param(
    $file,
    $category,
    $key)

## Prepare the parameter types and parameter values for the Invoke-WindowsApi script
$returnValue = New-Object System.Text.StringBuilder 500
$parameterTypes = [string], [string], [string], [System.Text.StringBuilder], [int], [string]
$parameters = [string] $category, [string] $key, [string] ""
   [System.Text.StringBuilder] $returnValue, [int] $returnValue.Capacity, [string] $file

## Invoke the API
[void] (Invoke-WindowsApi "kernel32.dll" ([UInt32]) "GetPrivateProfileString" `
   $parameterTypes $parameters)

## And return the results
$returnValue.ToString()

and

##############################################################################
##
## Set-PrivateProfileString.ps1
##
## Set an entry from an INI file.
##
## ie:
##
##  PS >copy C:\winnt\system32\ntfrsrep.ini c:\temp\
##  PS >Set-PrivateProfileString.ps1 C:\temp\ntfrsrep.ini text `
##  >> DEV_CTR_24_009_HELP "New Value"
##  >>
##  PS >Get-PrivateProfileString.ps1 C:\temp\ntfrsrep.ini text DEV_CTR_24_009_HELP
##  New Value
##  PS >Set-PrivateProfileString.ps1 C:\temp\ntfrsrep.ini NEW_SECTION `
##  >> NewItem "Entirely New Value"
##  >>
##  PS >Get-PrivateProfileString.ps1 C:\temp\ntfrsrep.ini NEW_SECTION NewItem
##  Entirely New Value
##
##############################################################################

param(
    $file,
    $category,
    $key,
    $value)

## Prepare the parameter types and parameter values for the Invoke-WindowsApi script
$parameterTypes = [string], [string], [string], [string]
$parameters = [string] $category, [string] $key, [string] $value, [string] $file

## Invoke the API
[void] (Invoke-WindowsApi "kernel32.dll" ([UInt32]) "WritePrivateProfileString" $parameterTypes $parameters)

Comments [5] | | # 
Tuesday, October 02, 2007 4:02:52 PM (Pacific Daylight Time, UTC-07:00) ( )

While eagerly awaiting PowerShell’s upcoming remoting functionality, many people turn to Sysinternals’ PsExec tool to build their own version. However, PowerShell seems to hang when called via PsExec on the remote machine. This has come up on the SysInternal forum (http://forum.sysinternals.com/forum_posts.asp?TID=10823) among other places, and is caused by the same issue outlined here: http://www.leeholmes.com/blog/UsingMshexeInteractivelyFromWithinOtherPrograms.aspx.

 

To work around this problem, you can give some input to the Powershell process. But to give it input, you need to use cmd.exe:

 

psexec \\server cmd /c "echo . | powershell dir 'c:\program files'"

 

Now, working around quote encoding and two levels of escape characters (cmd.exe and PowerShell) can be quite painful when crafting the PowerShell command this way. For that, you can use the –EncodedCommand parameter, which accepts a Base64-encoded version of your command.

 

$expression = "dir 'c:\program files'"

$commandBytes = [System.Text.Encoding]::Unicode.GetBytes($expression)

$encodedCommand = [Convert]::ToBase64String($commandBytes)

psexec \\server cmd /c "echo . | powershell -EncodedCommand $encodedCommand"

 

And to make it even more PowerShelly, the –OutputFormat of XML lets you get back an XML representation of your command’s output. On your local system, PowerShell converts your output back to deserialized objects. From there, you can continue to manipulate the output with the object-oriented goodness you’ve come to expect of us J In the example below, the server processes the Where-Object query, but the client sorts the result on Handles.

 

PS >$command = { Get-Process | Where-Object { $_.Handles -gt 1000 } }

PS >Invoke-RemoteExpression \\LEE-DESK $command | Sort Handles

 

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName

-------  ------    -----      ----- -----   ------     -- -----------

   1025       8     3780       3772    32   134.42    848 csrss

   1306      37    50364      64160   322   409.23   4012 OUTLOOK

   1813      39    54764      36360   321   340.45   1452 iTunes

   2316     273    29168      41164   218   134.09   1244 svchost

 

Here’s a script that automates all of this for you:

 

Program: Start a Process on a Remote Machine

Example 21-4 lets you invoke PowerShell expressions on remote machines. It uses PsExec (from http://www.microsoft.com/technet/sysinternals/utilities/psexec.mspx) to support the actual remote command execution.

This script offers more power than just remote command execution, however. As Example 21-3 demonstrates, it leverages PowerShell’s capability to import and export strongly structured data, so you can work with the command output using many of the same techniques you use to work with command output on the local system. Example 21-3 demonstrates this power by filtering command output on the remote system but sorting it on the local system.

Example 21-3. Invoking a PowerShell expression on a remote machine

PS >$command = { Get-Process | Where-Object { $_.Handles -gt 1000 } }

PS >Invoke-RemoteExpression \\LEE-DESK $command | Sort Handles

 

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName

-------  ------    -----      ----- -----   ------     -- -----------

   1025       8     3780       3772    32   134.42    848 csrss

   1306      37    50364      64160   322   409.23   4012 OUTLOOK

   1813      39    54764      36360   321   340.45   1452 iTunes

   2316     273    29168      41164   218   134.09   1244 svchost

Since this strongly structured data comes from objects on another system, PowerShell does not regenerate the functionality of those objects (except in rare cases). For more information about importing and exporting structured data, see “Easily Import and Export Your Structured Data.”

Example 21-4. Invoke-RemoteExpression.ps1

##############################################################################

##

## Invoke-RemoteExpression.ps1

##

## Invoke a PowerShell expression on a remote machine. Requires PsExec from

## http://www.microsoft.com/technet/sysinternals/utilities/psexec.mspx

##

## ie:

##

##  PS >Invoke-RemoteExpression \\LEE-DESK { Get-Process }

##  PS >(Invoke-RemoteExpression \\LEE-DESK { Get-Date }).AddDays(1)

##  PS >Invoke-RemoteExpression \\LEE-DESK { Get-Process } | Sort Handles

##

##############################################################################

 

param(

  $computer = "\\$ENV:ComputerName",

  [ScriptBlock] $expression = $(throw "Please specify an expression to invoke."),

  [switch] $noProfile

  )

 

## Prepare the command line for PsExec. We use the XML output encoding so

## that PowerShell can convert the output back into structured objects.

$commandLine = "echo . | powershell -Output XML "

 

if($noProfile)

{

    $commandLine += "-NoProfile "

}

 

## Convert the command into an encoded command for PowerShell

$commandBytes = [System.Text.Encoding]::Unicode.GetBytes($expression)

$encodedCommand = [Convert]::ToBase64String($commandBytes)

$commandLine += "-EncodedCommand $encodedCommand"

 

## Collect the output and error output

$errorOutput = [IO.Path]::GetTempFileName()

$output = psexec /acceptEula $computer cmd /c $commandLine 2>$errorOutput

 

## Check for any errors

$errorContent = Get-Content $errorOutput

Remove-Item $errorOutput

if($errorContent -match "Access is denied")

{

    $OFS = "`n"

    $errorMessage = "Could not execute remote expression. "

    $errorMessage += "Ensure that your account has administrative " +

        "privileges on the target machine.`n"

    $errorMessage += ($errorContent -match "psexec.exe :")