PowerShell Cookbook

Search

Categories

 

On this page

Managing INI files with PowerShell
Using PowerShell and PsExec to invoke expressions on remote computers
Removing Certificates from the Certificate Store
Cmdlets vs Functions
Secret SQL Escape Characters
Invoking Generic Methods on Non-Generic Classes in PowerShell
Obfuscated PowerShell
Why Another 3rd Party Book?
PowerShell: The Definitive Guide Rough Cut now Available
Interested in being a Technical Reviewer?

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: 214
This Year: 14
This Month: 1
This Week: 0
Comments: 522

Sign In

 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 [1] | | # 
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 :")

 

    Write-Error $errorMessage

}

 

## Return the output to the user

$output

 

 

Edit: Thanks to Otto's suggestion, Added the /acceptEula switch to PsExec to accept the EULA automatically. 

Comments [7] | | # 
 Thursday, August 23, 2007
Thursday, August 23, 2007 2:07:47 PM (Pacific Daylight Time, UTC-07:00) ( )

This has come up twice in as many days... how do you remove certificates from the certificate store in PowerShell?

The certificate provider is ultimately a read-only view of your certificates. It does help you retrieve certificates, however, which is an important step in ultimately removing one from a store.
 
To remove one, you'll need to use the .NET APIs:

[cert:\CurrentUser\TrustedPublisher]
PS:200 > dir

    Directory: Microsoft.PowerShell.Security\Certificate::CurrentUser\TrustedPublisher

Thumbprint                                Subject
----------                                -------
FD48FAA9281A657DBD089B5A008FAFE61D3B32FD  CN=PowerShell User
A25800BB7577F5854B3823B82228D94140D0244E  CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington...
564E01066387F26C912010D06BD78D3CF1E845AB  CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington...
 
[cert:\CurrentUser\TrustedPublisher]
PS:201 > $cert = @(dir)[0]
[cert:\CurrentUser\TrustedPublisher]
PS:202 > $store = New-Object System.Security.Cryptography.X509Certificates.X509Store "TrustedPublisher","CurrentUser"
Suggestion: An alias for New-Object is new
[cert:\CurrentUser\TrustedPublisher]
PS:203 > $store.Open("ReadWrite")
[cert:\CurrentUser\TrustedPublisher]
PS:204 > $store.Remove($cert)
[cert:\CurrentUser\TrustedPublisher]
PS:205 > $store.Close()
[cert:\CurrentUser\TrustedPublisher]
PS:206 > dir

    Directory: Microsoft.PowerShell.Security\Certificate::CurrentUser\TrustedPublisher

Thumbprint                                Subject
----------                                -------
A25800BB7577F5854B3823B82228D94140D0244E  CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington...
564E01066387F26C912010D06BD78D3CF1E845AB  CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington...
 

Comments [2] | | # 
 Tuesday, July 24, 2007
Tuesday, July 24, 2007 6:02:06 PM (Pacific Daylight Time, UTC-07:00) ( )

A discussion came up recently in an internal mailing list about the difference between Cmdlets and Functions. What was most interesting was how many hard and fast distinctions and rules came out of the discussion, even though those distinctions don’t really exist.

The key point is that there really isn’t a hard and fast philosophical distinction between cmdlets and functions.

Most differences largely exist because of setup, installation, and supported features. In general, cmdlets (and snapins) are the best way to distribute features, while functions are the easiest way to implement them.

  • It is currently much easier for ISVs and developers to package and deploy cmdlets than it is to package libraries of functions or scripts.
  • It is currently easier to write and package help for cmdlets.
  • Cmdlets are written in a compiled .NET language, while functions (and scripts) are written in the PowerShell language. On the plus side, this makes certain developer tasks (such as P/Invoke calls, working with generics) much easier in a cmdlet. On the minus side, this makes you pay the ‘compilation’ tax — making it slower to implement and evaluate new functionality.
  • In V1,  Cmdlets provide the author a great deal of support for parameter validation, and tentative processing (-WhatIf, -Confirm.) This is an implementation artifact, though, and could go away in the future.
  • [Various technical points] Functions support scoping, different naming guidelines, management through the function drive, etc. See your favourite scripting reference for these details.

So, really, the differences are really a function of water finding its level -- people tend to one or the other based on what we've currently made easiest. Aside from the implementation language, all of these factors are transient, though, and could change at any time.

Comments [1] | | # 
 Friday, July 20, 2007
Friday, July 20, 2007 8:40:48 PM (Pacific Daylight Time, UTC-07:00) ( )

I learned of an evil SQL escape sequence today, in the context of a data migration script. The script moves data from one database to another, but the schema changes between databases, so you can't use BCP. As such, the script needs to ensure that it does not modify any of the database content.

The script works well, and it creates insert statements based on the values and the content of the old data. In the VALUES clause, the script single-quotes the data, and then escapes out any single quotes in the data. According to best practices (and  all of the documentation I can find,) that is enough to neutralize any SQL string.

INSERT INTO MyTable ([Column1], [Column2])
VALUES ('Value '' 1', 'Value 2')

But it turns out there were backslash characters getting randomly dropped in the content we were migrating.

After some research, it turns out that only backslash characters before newlines get erased. If you want to include backslash followed by a <cr><lf>, you have to do give the sequence:  \\<cr><lf><cr><lf>. Newlines seem to be the only characters that cause a backslash to become a special character – and the only place I can find that mentions it is http://support.microsoft.com/kb/164291.

As far as I can tell, this must be a relic from ‘Embedded SQL for C and SQL Server’ days: http://msdn2.microsoft.com/En-US/library/aa225198(sql.80).aspx

EXEC SQL INSERT INTO TEXT132 VALUES ('TEST 192 IS THE TEST FOR THE R\
ULE OF THE CONTINUATION OF LINES FROM ONE LINE TO THE NEXT LINE.');

So, now you know – escaping single quotes isn’t enough to neutralize a SQL string.  (And it should be pointed out that you should use SQLCommand and prepared statements whenever possible. In this case, it's not possible :) )

Comments [0] | | # 
 Monday, June 18, 2007
Tuesday, June 19, 2007 3:39:37 AM (Pacific Daylight Time, UTC-07:00) ( )

A question recently came in asking, "How do you invoke a generic method on a non-generic class in PowerShell?"

In an earlier post (http://www.leeholmes.com/blog/CreatingGenericTypesInPowerShell.aspx), we talked about how to create generic types in PowerShell, but classes can contain generic methods even when the classes themselves don't represent generic types.

It is possible to call the generic methods on this type of class, with the complexity of the solution depending on the complexity of the class.

Take, for example, the following class definition:

// csc /target:library GenericClass.cs
// [Reflection.Assembly]::LoadFile("c:\temp\GenericClass.dll")
using System;

public class NonGenericClass
{
    public void SimpleGenericMethod<T>(T input)
    {
        Console.WriteLine("Hello Simple World: " + input);
    }

    public void GenericMethod<T>(T input)
    {
        Console.WriteLine("Hello World: " + input);
    }

    public void GenericMethod<T>(T input, int count)
    {
        for(int x = 0; x < count; x++)
            Console.WriteLine("Hello Multi Partially Generic World: " + input);
    }

    public void GenericMethod<T,U>(T input, U secondInput) where T : IEquatable<T> where U : IEquatable<U>
    {
        Console.WriteLine("Hello Multi Generic World: " + input.Equals(secondInput));
    }

    public static void GenericStaticMethod<T>(T input)
    {
        Console.WriteLine("Hello Static World: " + input);
    }
}

It gives an example of the breadth of the problem. We have a simple generic method, a generic method with overrides, and finally, a static generic method.

If you don't need to worry about naming conflicts (as in the SimpleGenericMethod and GenericStaticMethod methods,) the solution is just a few lines of code:

$nonGenericClass = New-Object NonGenericClass
$method = [NonGenericClass].GetMethod("SimpleGenericMethod")
$closedMethod = $method.MakeGenericMethod([string])
$closedMethod.Invoke($nonGenericClass, "Welcome!")

If you do have to worry about multiple methods with the same name, then the GetMethod() method can no longer help you. In that case, we need to call the GetMethods() method, and then find which method shares the proper parameter types.

To abstract all of this logic, use the following Invoke-GenericMethod script:

## Invoke-GenericMethod.ps1 
## Invoke a generic method on a non-generic type: 
## 
## Usage: 
## 
##   ## Load the DLL that contains our class
##   [Reflection.Assembly]::LoadFile("c:\temp\GenericClass.dll")
##
##   ## Invoke a generic method on a non-generic instance
##   $nonGenericClass = New-Object NonGenericClass
##   Invoke-GenericMethod $nonGenericClass GenericMethod String "How are you?"
##
##   ## Including one with multiple arguments
##   Invoke-GenericMethod $nonGenericClass GenericMethod String ("How are you?",5)
##
##   ## Ivoke a generic static method on a type
##   Invoke-GenericMethod ([NonGenericClass]) GenericStaticMethod String "How are you?"
## 

param( 
    $instance = $(throw "Please provide an instance on which to invoke the generic method"), 
    [string] $methodName = $(throw "Please provide a method name to invoke"), 
    [string[]] $typeParameters = $(throw "Please specify the type parameters"), 
    [object[]] $methodParameters = $(throw "Please specify the method parameters")
    ) 

## Determine if the types in $set1 match the types in $set2, replacing generic
## parameters in $set1 with the types in $genericTypes
function ParameterTypesMatch([type[]] $set1, [type[]] $set2, [type[]] $genericTypes)
{
    $typeReplacementIndex = 0
    $currentTypeIndex = 0

    ## Exit if the set lengths are different
    if($set1.Count -ne $set2.Count)
    {
        return $false
    }

    ## Go through each of the types in the first set
    foreach($type in $set1)
    {
        ## If it is a generic parameter, then replace it with a type from
        ## the $genericTypes list
        if($type.IsGenericParameter)
        {
            $type = $genericTypes[$typeReplacementIndex]
            $typeReplacementIndex++
        }

        ## Check that the current type (i.e.: the original type, or replacement
        ## generic type) matches the type from $set2
        if($type -ne $set2[$currentTypeIndex])
        {
            return $false
        }
        $currentTypeIndex++
    }

    return $true
}

## Convert the type parameters into actual types
[type[]] $typedParameters = $typeParameters

## Determine the type that we will call the generic method on. Initially, assume
## that it is actually a type itself.
$type = $instance

## If it is not, then it is a real object, and we can call its GetType() method
if($instance -isnot "Type")
{
    $type = $instance.GetType()
}

## Search for the method that:
##    - has the same name
##    - is public
##    - is a generic method
##    - has the same parameter types
foreach($method in $type.GetMethods())
{
    # Write-Host $method.Name
    if(($method.Name -eq $methodName) -and 
       ($method.IsPublic) -and 
       ($method.IsGenericMethod))
    {
        $parameterTypes = @($method.GetParameters() | % { $_.ParameterType })
        $methodParameterTypes = @($methodParameters | % { $_.GetType() })
        if(ParameterTypesMatch $parameterTypes $methodParameterTypes $typedParameters)
        {
            ## Create a closed representation of it
            $newMethod = $method.MakeGenericMethod($typedParameters)

            ## Invoke the method
            $newMethod.Invoke($instance, $methodParameters)

            return
        }
    }
}

## Return an error if we couldn't find that method
throw "Could not find method $methodName"


 

Comments [3] | | # 
 Tuesday, June 05, 2007
Wednesday, June 06, 2007 6:06:48 AM (Pacific Daylight Time, UTC-07:00) ( )

For some reason, a surprisingly common (albeit half-joking) remark about PowerShell is that it hasn't "arrived" until you can write obfuscated one-liners like you do in C or Perl. 

They may have missed Adam's obfuscated script (http://www.proudlyserving.com/archives/2005/11/obfuscated_mona.html,) or this email quoting / wrapping one-liner: http://www.leeholmes.com/blog/EmailQuotingAndWrappingIn59Bytes.aspx.

While I normally try to clarify and educate, here's another script to make your eyes burn:

$ofs="";
'"$(0'+ '..(0'+ 'xa*['+ 'Math'+ ']::R'+ 'ound'+ '([Ma'+ 'th]:'+ ':Pi/'+ '2,1)'+ ')|%{'+ '[cha'+ 'r][i'+ 'nt]"'+ '"$($'+ '("""'+ '"0$('+ '1838'+ '1589'+ '*726'+ '371*'+ '60)$'+ '(877'+ '7365'+ '981*'+ '263*'+ '360)'+ '$(22'+ '2330'+ '793*'+ '1442'+ '99)$'+ '(310'+ '9*37'+ ') ""'+ '"")[' + '($_*' + '3)..' + '($_*'+ '3+2)' + '])""' + ' })"'|iex
 

[Edit: Added a missing something-or-other]
 

Comments [0] | | # 
 Friday, May 18, 2007
Friday, May 18, 2007 4:31:56 PM (Pacific Daylight Time, UTC-07:00) ( )

Gerd recently posed a good question on the PowerShell blog:

I'm really surprised to read about another 3rd party book from a member of the PS team. To me PS is the most intriguing MS innovation for years and Bruce's book is really excellent and answered almost all my questions, but maybe I'm not the only one wondering why PS team members write that busy for the bookstore shelf, as long as the official MS documentation on PS is less than adequate. I would expect the essential information needed to master PS from MSDN and not from Manning or O'Reilly, sorry.
http://blogs.msdn.com/powershell/archive/2007/05/12/windows-powershell-the-definitive-guide.aspx#2618740

The answer is that we're trying to tackle this from all angles.

The question considers MSDN as a sole learning resource, but it's just one avenue for most people. PowerShell installs tutorial-style documentation with the product itself, and we are continually working to improve both user-focused and developer-focused help content. But that’s just the start.

What we write goes into the product help, MSDN, and Script Center. It also goes onto blogs, newsgroups, and books. There’s also Podcasts, screen casts, LiveMeetings! And, of course, there’s also training sessions, keynotes, conferences, interviews, and mailing lists.

Books are the preferred information delivery vehicle for a lot of people. They travel well, and have an aesthetic appeal that many desire. In fact, I've heard from several people that they have literally been waiting for the O'Reilly book — filling that need with quality content is important to us as well.

That said, if you see holes in the documentation, please let us know. We’re continually working to improve it, but we may miss areas.  We use the Microsoft Connect website (http://blogs.msdn.com/powershell/archive/2006/05/09/filing-bugs.aspx) to help gauge priority, so we would really appreciate your input (via document bugs and suggestions) there.

 

Comments [4] | | # 
 Thursday, May 10, 2007
Thursday, May 10, 2007 4:10:13 PM (Pacific Daylight Time, UTC-07:00) ( )

With the book getting close to completion, O'Reilly has now posted a "Rough Cut" version of the book here: http://www.oreilly.com/catalog/9780596528492/. The Rough Cuts program gives you early access to the majority of the book's content before publication, with the option for an online and print bundle for a discounted price.

Comments [0] | | # 
 Wednesday, May 09, 2007
Thursday, May 10, 2007 1:15:20 AM (Pacific Daylight Time, UTC-07:00) ( )

[Edit: Thanks for all of the interest -- the review period is now closed.]

We're getting close to "content complete" of Windows PowerShell: The Definitive Guide. 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 let me know and I'll forward your information to O'Reilly. Feel free to leave a comment here, or click on the mail icon in the sidebar to the right.

Comments [12] | | #