Powershell psexec wrapper

From Svendsen Tech Powershell Wiki

Jump to: navigation, search

Contents






A Simple Example Of Running A PowerShell Command With PsExec

I figure some might just be looking for a way to execute PowerShell scripts, cmdlets, commands, scriptblocks or whatever, so here's an example at the top of the article:

PS C:\> .\PsExec.exe \\winxpssdtemp cmd /c 'echo . | powershell.exe -command "$env:PROCESSOR_ARCHITECTURE; exit 100"'

PsExec v1.98 - Execute processes remotely
Copyright (C) 2001-2010 Mark Russinovich
Sysinternals - www.sysinternals.com


x86
cmd exited on winxpssdtemp with error code 100.
PS C:\>

Getting the quoting right can be tricky. The point here is that you need to pipe something to PowerShell.exe, using cmd.exe. Also check out the example with my wrapper below. Lee Holmes has an article where he addresses some of the quoting issues.


Capturing The Error Code And Storing Output In A Variable

Here's a quick demonstration where I assign the PsExec command's output to a variable. The "rest" of the output is written to STDERR (file descriptor 2), not STDOUT (file descriptor 1). I'm too ignorant to figure out a way of avoiding a temporary file, so that's what I ended up using. I parse it with a regular expression to extract the error code.

PS C:\PS> $Output = .\PsExec.exe \\winxpssdtemp cmd /c 'echo. |
powershell.exe -command "$env:PROCESSOR_ARCHITECTURE; exit 100"' 2> error.tmp

PS C:\PS> type .\error.tmp
PsExec.exe :
At line:1 char:24
+ $Output = (.\PsExec.exe <<<<  \\winxpssdtemp cmd /c 'echo. | powershell.exe -command "$env:PROCESSOR_ARCHITECTURE; ex
it 100"') 2> error.tmp
    + CategoryInfo          : NotSpecified: (:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

PsExec v1.98 - Execute processes remotely
Copyright (C) 2001-2010 Mark Russinovich
Sysinternals - www.sysinternals.com


Connecting to winxpssdtemp...Starting PsExec service on winxpssdtemp...Connecting with PsExec service on winxpssdtemp..
.Starting cmd on winxpssdtemp...
cmd exited on winxpssdtemp with error code 100.


PS C:\PS> ((gc .\error.tmp) -join "`n") -match 'cmd exited on \S+ with error code (\d+)\.'
True

PS C:\PS> $Matches[1]
100

PS C:\PS> $Output
x86

General Information About The PsExec Wrapper

Presented here is a generic Sysinternals PsExec wrapper, written in PowerShell. Sysinternals was purchased by Microsoft, and their web site redirects to microsoft.com. You can use this script to wrap and parse PsExec output from a command that's run against a list of computers. Rudimentary regexp parsing support and capturing against the PsExec output is available.

Note that in most cases, WMI will be what you want. See this article for an example of a WMI wrapper similar to this one, and also this article for some information about WMI timeouts. Also see "Get-Help Get-WmiObject" at your PowerShell prompt. PowerShell remoting, introduced in PowerShell v2, is also a far more sophisticated and robust option - and it has encrypted authentication, for what it's worth.

Also be aware that PsExec transmits the user credentials, including the password, in plain text. NB! Some time shortly before April 1st, 2014, PsExec was changed so that it now encrypts the credentials, so make sure you get the latest version.

Output will be in either CSV or XML format (you decide).

The CSV consists of a first, double-quoted field with the computer name, then the PsExec output is joined with -MultiLineJoinString (default " | "), and added as a second field. You can specify your own join string if you want, to suit your parsing needs. You can also specify a regexp with the -ExtractionRegex parameter, where the stored CSV or XML output from PsExec will be the content of the first capture group. The default regular expression is "(.+)" (capture everything). Read more about PowerShell regular expressions here.

I decided to surround the second CSV field in double quotes since I think double quotes in the data will be more uncommon than commas, and with double quotes around the second CSV field, you get it as one field if you pass the output file to Import-Csv afterwards. I think it's what most people will want most of the time. Maybe I should have added it as an option.

See the examples below to see what the XML output looks like.

It has occurred to me that I should add an option where "cmd /c " is also included in the default command, to avoid one level of nested quoting.

Script Options

The examples below probably provide the most immediate understanding. A good tip is to specify the -ExtractionRegex parameter last, if you do specify it, since the regexp seems to have a tendency to mess up parameter name completion with tab after it's been specified. I'd think balanced single quotes around it would do, but alas.

  • -PsExecCommand (required): This is the actual command you would pass to psexec. The script prepends "PsExec.exe \\$Computer " and the rest is up to you to specify. See examples.
  • -ComputerList (required): Comma-separated list of computers: comp1,comp2,comp3. To specify a file containing target computer names, just use "(gc .\computerfile.txt)", including the parentheses (and without the quotes).
  • -OutputFile (required): Output destination file name. NB! If you use the optional -XmlOutput parameter, you will need to specify a full path to the output file, or you will probably find it in your default profile directory.
  • -MultiLineJoinString (optional): If you are not using the optional -XmlOutput option, output will be a CSV file with possible multi-line output joined together with this string as the second CSV data field (surrounded by double quotes). The first CSV field being the computer name. The default join string is " | " (space, pipe, space).
  • -DelimiterJoinString (optional): If you use the optional -ExtractionRegex parameter, the output will be joined together using this string if it spans multiple lines, before the regexp match is performed, so you need to "anchor", typically with "Something: (.+?)( \| |$)". See the examples for an explanation.
  • -ExtractionRegex (optional): The default is "(.+)", which means "everything", or the first line with the "no single-line" option. A regexp to run against the lines joined together with the -DelimiterJoinString (default: " | "). If the output is only one line, the -DelimiterJoinString is not used.
    • You need to have at least one set of capturing parentheses in the regexp, because the content of $Matches[1] is what is saved to file when you specify the -ExtractionRegex parameter. You need to anchor against " | ", typically with "Something: (.+?)(?: \| |$)". There's more information about this in the examples section. Don't forget to escape the pipe with a backslash. Or use a different -DelimiterJoinString to anchor against.
    • Also be aware that the single-line regexp option is active by default, so the regexp meta character "." will also match newlines, so ".+" might match more than you would expect. You can disable single-line matching with the -RegexOptionNoSingleLine parameter.
  • -RegexOptionNoSingleLine (optional): Disables single-line regexp matching against the PsExec output.
  • -RegexOptionCaseSensitive (optional): Enables case sensitivity for the regexp.
  • -Clobber (optional): Overwrite output file if it exists without asking. Otherwise you will be prompted to overwrite (default: yes, so you can just press enter).
  • -XmlOutput (optional): Rather than CSV output, you will get XML output. See examples.

Examples And Screenshots

Some examples and screenshots.

First Simple Example

Let's start with something simple, using only the required parameters. Have PsExec simply "cmd /c echo %PROCESSOR_ARCHITECTURE%" and use the default CSV format, against two computers specified on the command line.

PS C:\prog\Powershell> .\PsExec-Wrapper.ps1 -PsExecCommand 'cmd /c echo %PROCESSOR_ARCHITECTURE%'
-ComputerList winxpesxi,win7esxi -OutputFile arch.csv -Clobber
Script start time: 11/13/2011 08:16:06
Found PsExec.exe in current working directory. Using: .\PsExec.exe
Processing winxpesxi... Regex matched. Captured: x86
Processing win7esxi... Regex matched. Captured: AMD64
Done!
Script start time: 11/13/2011 08:16:06
Script end time:   11/13/2011 08:16:28
Output file: arch.csv

# Type the file to look at it:
PS C:\prog\Powershell> type .\arch.csv
"ComputerName","Output"
"winxpesxi","x86"
"win7esxi","AMD64"

# Try Import-Csv on the file:
PS C:\prog\Powershell> Import-Csv .\arch.csv

ComputerName                                                Output
------------                                                ------
winxpesxi                                                   x86
win7esxi                                                    AMD64

It looks right.

You'll notice the PowerShell console window title gets "stuck" with the last PsExec stuff. If you want to set it back to the default, you can run this command:

(Get-Host).UI.RawUI.WindowTitle = 'Windows PowerShell'

Or this:

$Host.UI.RawUI.WindowTitle = 'Windows PowerShell'


Second Example With Regexp Extraction

Now let's look at something a little more complicated where we deal with multi-line output from a command. For some reason, I want the serial number on the system drives of a range of computers. It's quick and dirty, and I'll just parse "dir %SYSTEMDRIVE%\", looking for "Volume Serial Number is <CAPTURE_REST_OF_LINE>". A "line" in the output from the PsExec command is user-defined with the -DelimiterJoinString parameter, which by default is " | ". I will use a regular expression to extract what I want.

The command will be:

.\PsExec-Wrapper.ps1 -PsExecCommand "cmd /c dir %SYSTEMDRIVE%\"
-ComputerList (gc computers.txt) -OutputFile c:\prog\powershell\disk-serial.csv
-clobber -extractionregex 'volume serial number is (.+?)(?: \| |$)'

The trickiest part here might be understanding the -DelimiterJoinString part, which joins all the lines together, separated by " | " (by default), so often you will be "anchoring" on " | " to indicate the end of a line, and using the non-greedy ".+?" or ".*?" regular expressions before it to capture. Also see example 5 for more information.

The regular expression in the command is "volume serial number is (.+?)(?: \| |$)". It will be case-insensitive by default. There you anchor on "volume serial number is", followed by a space, then you non-greedily capture everything with "(.+?)", until you hit the first instance of " | ", or the end of the text.

"(?: \| |$)" might look cryptic, but the "?:" construct means it's a non-capturing group, which doesn't matter here, since only the first capture is used, but it's considered best practices by most - although it's slightly more unreadable to the untrained eye. You could have used "( \| |$)" instead and it would not have mattered in this case. The first pipe, escaped with a backslash ("\"), means a literal pipe, here surrounded by spaces, then there's a non-escaped pipe ("|"), which means OR in regular expressions.

So it means either " | " or "$", and the regexp meta-character "$" means the end of the text (or line with the multi-line option, which you can add with an inline mode modifier). If you know there's a line after what you're grabbing, you can just use " \| " instead (such as in this example case). I'm describing what's most likely to work flexibly in diverse scenarios.

PS C:\prog\Powershell> .\PsExec-Wrapper.ps1 -PsExecCommand "cmd /c dir %SYSTEMDRIVE%\"
-ComputerList (gc computers.txt) -OutputFile c:\prog\powershell\disk-serial.csv
-clobber -extractionregex 'volume serial number is (.+?)(?: \| |$)'
Script start time: 11/13/2011 08:31:19
Found PsExec.exe in current working directory. Using: .\PsExec.exe
Processing winxpesxi... Regex matched. Captured: D8B2-1634
Processing win7esxi... Regex matched. Captured: 847E-4BE4
Processing notexisting... Regex did not match and no output.
Processing 2008r2esxi... Regex matched. Captured: 88C4-00FE
Done!
Script start time: 11/13/2011 08:31:19
Script end time:   11/13/2011 08:32:04
Output file: c:\prog\powershell\disk-serial.csv

# Type the file to inspect
PS C:\prog\Powershell> type .\disk-serial.csv
"ComputerName","Output"
"winxpesxi","D8B2-1634"
"win7esxi","847E-4BE4"
"notexisting","ERROR: No output"
"2008r2esxi","88C4-00FE"

# Check it out with Import-Csv
PS C:\prog\Powershell> Import-Csv .\disk-serial.csv

ComputerName                                                Output
------------                                                ------
winxpesxi                                                   D8B2-1634
win7esxi                                                    847E-4BE4
notexisting                                                 ERROR: No output
2008r2esxi                                                  88C4-00FE


If we use a regexp that doesn't match, we will get the full output separated by -MultiLineJoinString, which by default is " | " (space, pipe, space). If the output is single-line, the join can be disregarded. Test on a few computers before running this against the masses, so you can weed out errors. Here I use "serial number WAS" rather than "serial number is" in the regexp, otherwise it is identical to the previous example. This is to demonstrate what kind of output you get if there isn't a regexp match and multiple-line output.

PS C:\prog\Powershell> .\psexec-Wrapper.ps1 -PsExecCommand "cmd /c dir %SYSTEMDRIVE%\"
-ComputerList (gc computers.txt) -OutputFile c:\prog\powershell\disk-serial.csv -clobber
-ExtractionRegex 'volume serial number WAS (.+?)(?: \| |$)'
Script start time: 11/13/2011 08:33:18
Found PsExec.exe in current working directory. Using: .\PsExec.exe
Processing winxpesxi... Regex did not match. Using all output in lines joined with ' | '.
Processing win7esxi... Regex did not match. Using all output in lines joined with ' | '.
Processing notexisting... Regex did not match and no output.
Processing 2008r2esxi... Regex did not match. Using all output in lines joined with ' | '.
Done!
Script start time: 11/13/2011 08:33:18
Script end time:   11/13/2011 08:34:01
Output file: c:\prog\powershell\disk-serial.csv

# If you look at it with notepad or Import-Csv, you will see that the entire output
# from "cmd /c dir %SYSTEMDRIVE%\" is there, joined together with " | ":

PS C:\prog\Powershell> Import-Csv .\disk-serial.csv

ComputerName                    Output
------------                    ------
winxpesxi                        Volume in drive C has no label. |  Volume Serial Number...
win7esxi                         Volume in drive C has no label. |  Volume Serial Number...
notexisting                     ERROR: No output
2008r2esxi                       Volume in drive C has no label. |  Volume Serial Number...

Third Example With XML Output

Now, we'll capture either the boot time or up time from the output from the application systeminfo. Again, there are better ways to do this, it's just an example (I'm not the one looking for a PsExec wrapper, I just wrote one :-). I have written an article about how to find when remote computers were last booted up, and put it here; this uses WMI.

I've added the parameter "-XmlOutput". NB! It's important to remember that you need to specify a FULL path to the output file when you use the -XmlOutput parameter! Otherwise, it will probably end up in your default profile directory if you just use ".\output.xml".

PS C:\prog\Powershell> .\PsExec-Wrapper.ps1 -PsExecCommand "cmd /c systeminfo"
-ComputerList (gc computers.txt) -OutputFile c:\prog\powershell\uptime.xml -Clobber
-ExtractionRegex '((?:boot|up) time:\s+.+?)(?: \| |$)' -XmlOutput

Script start time: 11/13/2011 09:25:28
Found PsExec.exe in current working directory. Using: .\PsExec.exe
Processing winxpesxi... Regex matched. Captured: Up Time:            13 Days, 10 Hours, 10 Minutes, 24 Seconds
Processing win7esxi... Regex matched. Captured: Boot Time:          09.11.2011, 03:30:01
Processing notexisting... Regex did not match and no output.
Processing 2008r2esxi... Regex matched. Captured: Boot Time:          09.11.2011, 03:44:59
Done!
Script start time: 11/13/2011 09:25:28
Script end time:   11/13/2011 09:26:21
Output file: c:\prog\powershell\uptime.xml
PS C:\prog\Powershell> .\uptime.xml
PS C:\prog\Powershell>

And this is what the resulting XML looks like in Internet Explorer:

Image:PsExec-Wrapper-XML-Output-Example-Smaller.png

Fourth Example Where We Run a PowerShell Command Using PsExec

While we're rolling around in the filth...: Let's look at how we can execute PowerShell commands with the PsExec-Wrapper.ps1 script. Getting the quoting right can be a little tricky, and also, you need to know about a little trick: The trick is piping something to PowerShell, such as in: "cmd /c echo . | powershell ...". You can find more about that elsewhere on the web.

We're currently in quote hell; I really miss Perl's q() and qq() operators. Check this out, and notice how I enclose the entire -PsExecCommand parameter in single quotes, and then use a doubled-up single quote inside to represent a literal single quote inside the string, along with double quotes for PowerShell's -Command parameter:

PS C:\prog\Powershell> .\PsExec-Wrapper.ps1
-PsExecCommand 'cmd /c ''echo . | PowerShell -Command "$env:windir"'''
-ComputerList (gc .\computers.txt) -OutputFile test.csv -Clobber

Script start time: 11/13/2011 10:22:38
Found PsExec.exe in current working directory. Using: .\PsExec.exe
Processing winxpesxi... Regex matched. Captured: C:\WINDOWS
Processing win7esxi... Regex matched. Captured: C:\Windows
Processing notexisting... Regex did not match and no output.
Processing 2008r2esxi... Regex matched. Captured: C:\Windows
Done!
Script start time: 11/13/2011 10:22:38
Script end time:   11/13/2011 10:23:24
Output file: test.csv

# Look at the text file:
PS C:\prog\Powershell> type .\test.csv
"ComputerName","Output"
"winxpesxi","C:\WINDOWS"
"win7esxi","C:\Windows"
"notexisting","ERROR: No output"
"2008r2esxi","C:\Windows"

# Try Import-Csv on it:
PS C:\prog\Powershell> Import-Csv .\test.csv

ComputerName                                                Output
------------                                                ------
winxpesxi                                                   C:\WINDOWS
win7esxi                                                    C:\Windows
notexisting                                                 ERROR: No output
2008r2esxi                                                  C:\Windows

Of course, you can also output stuff to XML, like the memory size in GB, with this command:

PS C:\prog\Powershell> .\PsExec-Wrapper.ps1
-PsExecCommand 'cmd /c ''echo . | powershell -command
"[Math]::Round((((gwmi Win32_ComputerSystem).TotalPhysicalMemory)/1GB), 2)"'''
-ComputerList (gc .\computers.txt) -OutputFile C:\prog\Powershell\memory.xml
-XmlOutput -Clobber

I really wanted to use "(((gwmi Win32_ComputerSystem).TotalPhysicalMemory)/1GB).ToString('N')", which I normally use for formatting numbers nicely and in a way that respects system locale settings, but I simply couldn't figure out an easy way of getting quotes to be correctly parsed in that mess. Nothing seemed to work. You might find the magic incantation required. Instead, I found the [Math]::Round($Value, $Precision) function, which I wrapped it in. Post-data-collection processing might be required, or easier.

The command above gives XML output like this:

Image:PsExec-Wrapper-XML-Output-Example-Memory-Smaller.png


Fifth Example Demonstrating "Advanced" Options

For a more "normal" regexp experience, you might expect "$" to match before every newline in the output from PsExec, rather than having to use " \| " or "(?: \| |$)" as mentioned in previous examples. Also, having "." match newlines might not be what you want. I had single-line CSV output in mind when I created this script. I might have made poor decisions for some use cases.

There are two, or possibly three, things you need to do to have "normal", multi-line behavior:

  • Add "(?m)" at the start of your regexp.
  • Use the -DelimiterJoinString "`n" parameter
  • Use the -RegexOptionNoSingleLine parameter

The last parameter mentioned above would not be needed in the example below, if you made ".+" non-greedy with ".+?", since "$" takes precedence with the multi-line flag, (?m). See the PowerShell regexp article for more information about regular expressions in general. If you do add the parameter -RegexOptionNoSingleLine, you can, on the other hand, leave out the question mark that makes ".+" non-greedy. There are several ways to do things; understanding, and experimenting, helps.

Here's one way to do it:

PS C:\prog\Powershell> .\PsExec-Wrapper.ps1 -PsExecCommand 'cmd /c systeminfo'
-ComputerList (gc .\computers.txt)
-OutputFile C:\prog\Powershell\uptime.xml
-Clobber -XmlOutput -DelimiterJoinString "`n"
-RegexOptionNoSingleLine
-ExtractionRegex '(?m)((?:boot|up) time:\s+.+)$'

Script start time: 11/19/2011 09:25:28
Found PsExec.exe in current working directory. Using: .\PsExec.exe
Processing winxpesxi... Regex matched. Captured: Up Time:            1 Days, 7 Hours, 21 Minutes, 34 Seconds
Processing win7esxi... Regex matched. Captured: Boot Time:          09.11.2011, 03:30:01
Processing notexisting... Regex did not match and no output.
Processing 2008r2esxi... Regex matched. Captured: Boot Time:          09.11.2011, 03:44:59
Done!
Script start time: 11/19/2011 09:25:28
Script end time:   11/19/2011 09:26:21
Output file: C:\prog\Powershell\uptime.xml

I'm tossing in a quick example of how to peek at the XML with PowerShell.

PS C:\prog\Powershell> $Xml = [xml] (gc .\uptime.xml)
PS C:\prog\Powershell> $Xml.computers.computer

name                                                        output
----                                                        ------
winxpesxi                                                   Up Time:            1 Days, 7 Hours, 21 Minutes, 34 Seconds
win7esxi                                                    Boot Time:          09.11.2011, 03:30:01
notexisting                                                 ERROR: No output
2008r2esxi                                                  Boot Time:          09.11.2011, 03:44:59

PS C:\prog\Powershell> type .\uptime.xml | select-string 'output'

    <output>Up Time:            1 Days, 7 Hours, 21 Minutes, 34 Seconds</output>
    <output>Boot Time:          09.11.2011, 03:30:01</output>
    <output>ERROR: No output</output>
    <output>Boot Time:          09.11.2011, 03:44:59</output>

PS C:\prog\Powershell> $Xml.computers.computer | %{ $_.output }
Up Time:            1 Days, 7 Hours, 21 Minutes, 34 Seconds
Boot Time:          09.11.2011, 03:30:01
ERROR: No output
Boot Time:          09.11.2011, 03:44:59
PS C:\prog\Powershell> $Xml.computers.computer | %{ $_.output -replace '\s+', ' ' }
Up Time: 1 Days, 7 Hours, 21 Minutes, 34 Seconds
Boot Time: 09.11.2011, 03:30:01
ERROR: No output
Boot Time: 09.11.2011, 03:44:59

The Integrated Help Text

Tip: What you'll usually want to list is the parameter list with brief descriptions, or to check out this page again. To do this, you can type "Get-Help .\PsExec-Wrapper.ps1 -Detailed":


NAME
    C:\PowerShell\PsExec-Wrapper.ps1

SYNOPSIS
    Svendsen Tech's generic PowerShell PsExec wrapper. Also look into PowerShell
    remoting which came with PowerShell v2, and consider WMI, which are better
    solutions in most cases.

    Author: Joakim Svendsen


SYNTAX
    C:\PowerShell\PsExec-Wrapper.ps1 [-PsExecCommand] <String> [-ComputerList] <String[]> [-OutputFi
    le] <String> [[-MultiLineJoinString] <String>] [[-DelimiterJoinString] <String>] [[-ExtractionRegex] <Regex>] [-Reg
    exOptionNoSingleLine] [-RegexOptionCaseSensitive] [-Clobber] [-XmlOutput] [<CommonParameters>]


DESCRIPTION
    See the online documentation for comprehensive documentation and examples at:
    http://www.powershelladmin.com/wiki/Powershell_psexec_wrapper


PARAMETERS
    -PsExecCommand <String>
        The command to pass to PsExec after "PsExec.exe \\<computer> ".

    -ComputerList <String[]>
        List of computers to process. Use a file with "(gc computerfile.txt)".

    -OutputFile <String>
        Output file name. You will be asked to overwrite unless -Clobber is specified.

    -MultiLineJoinString <String>
        The string to join multi-line output with in the second CSV field or XML field, if necessary.

    -DelimiterJoinString <String>
        The string that separates lines in the PsExec output. You can specify a newline with "`n".
        Also see -RegexoptionNoSingleLine

    -ExtractionRegex <Regex>
        The first capture group, indicated by parentheses, in the specified regexp, will be extracted
        instead of the entire output. If there is no match, it will fall back to -MultiLineJoinString
        and store the entire output. By default, it's "(.+)", which means "one or more of any character".

    -RegexOptionNoSingleLine [<SwitchParameter>]
        Makes the regexp meta-character "." NOT match newlines.

    -RegexOptionCaseSensitive [<SwitchParameter>]
        Makes the regexp case sensitive.

    -Clobber [<SwitchParameter>]
        Overwrite output file if it exists without prompting.

    -XmlOutput [<SwitchParameter>]
        Output to XML instead of CSV. Remember to use a full path!

Download and Source Code

  • Source code:
<#
.SYNOPSIS
Svendsen Tech's generic PowerShell PsExec wrapper. Also look into PowerShell
remoting which came with PowerShell v2, and consider WMI, which are better
solutions in most cases.

Author: Joakim Svendsen

.DESCRIPTION
See the online documentation for comprehensive documentation and examples at:
http://www.powershelladmin.com/wiki/Powershell_psexec_wrapper

.PARAMETER PsExecCommand
The command to pass to PsExec after "PsExec.exe \\<computer> ".
.PARAMETER ComputerList
List of computers to process. Use a file with "(gc computerfile.txt)".
.PARAMETER OutputFile
Output file name. You will be asked to overwrite unless -Clobber is specified.
.PARAMETER MultiLineJoinString
The string to join multi-line output with in the second CSV field or XML field, if necessary.
.PARAMETER DelimiterJoinString
The string that separates lines in the PsExec output. You can specify a newline with "`n".
Also see -RegexoptionNoSingleLine
.PARAMETER ExtractionRegex
The first capture group, indicated by parentheses, in the specified regexp, will be extracted
instead of the entire output. If there is no match, it will fall back to -MultiLineJoinString
and store the entire output. By default, it's "(.+)", which means "one or more of any character".
.PARAMETER RegexOptionNoSingleLine
Makes the regexp meta-character "." NOT match newlines.
.PARAMETER RegexOptionCaseSensitive
Makes the regexp case sensitive.
.PARAMETER Clobber
Overwrite output file if it exists without prompting.
.PARAMETER XmlOutput
Output to XML instead of CSV. Remember to use a full path!
#>

param(
        [Parameter(Mandatory=$true)][string]   $PsExecCommand,
        [Parameter(Mandatory=$true)][string[]] $ComputerList ,
        [Parameter(Mandatory=$true)][string]   $OutputFile,
        [string] $MultiLineJoinString = ' | ',
        [string] $DelimiterJoinString = ' | ',
        [regex]  $ExtractionRegex     = '(.+)' ,
        [switch] $RegexOptionNoSingleLine,
        [switch] $RegexOptionCaseSensitive,
        [switch] $Clobber,
        [switch] $XmlOutput
      )

Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'

# Store script start time. Will be displayed at the end along with the end time.
$StartTime = Get-Date
"Script start time: $StartTime"

# Prompt to overwrite unless -Clobber is specified.
if ( -not $Clobber -and (Test-Path $OutputFile) ) {
    
    $Answer = Read-Host 'Output file already exists and -Clobber not specified. Overwrite? (y/n) [yes]'
    if ($Answer -imatch 'n') { Write-Output 'Aborted.'; exit; }
    
}

# Remove it if it exists, and just suppress errors if it doesn't.
Remove-Item $OutputFile -ErrorAction SilentlyContinue

# Some minor sanity checking if the user supplied an extraction regex other than '(.+)'. Make sure they capture something.
if ( $ExtractionRegex.ToString() -ne '(.+)' -and ($ExtractionRegex.ToString() -inotmatch '\(' -or $ExtractionRegex.ToString() -inotmatch '\)') ) {
    
    Write-Output "The supplied regex '$($ExtractionRegex.ToString())' is missing either a '(' or a ')'.`nYou must capture something.`nSee Get-Help $($MyInvocation.MyCommand.Name).`nWill now exit with exit code 1"
    exit 1
    
}

# 45 lines to check if we have psexec.exe in current working dir or path... Should write a generic function for this.
$PsExecFile = 'PsExec.exe'

if ( Test-Path $PsExecFile ) {
    
    $PsExecFullPath = '.\' + $PsExecFile
    Write-Output "Found $PsexecFile in current working directory. Using: $PsExecFullPath"
    
}

elseif ($PsExecFileObject = Get-Command $PsExecFile) {
    
    if ($PsExecFileObject -is [System.Array]) {
        
        $PsexecFullPath = $PsExecFileObject[0].Definition
        Write-Output "Found multiple $PsExecFile instances in path. Using this path: $PsexecFullPath"
        
    }
    
    elseif ($PsExecFileObject -is [System.Management.Automation.ApplicationInfo]) {
        
        $PsExecFullPath = $PsExecFileObject.Definition
        Write-Output "Found one instance of $PsExecFile in path. Using this path: $PsexecFullPath"
        
    }
    
    else {
        
        Write-Output "Unknown object type returned from 'Get-Command $PsExecFile'.`nWill now exit with status code 3"
        exit 3
        
    }
    
}

else {
    
@"
You need to download PsExec from www.sysinternals.com (redirects to microsoft.com)
in order to use this PsExec wrapper script. It needs to be in the current working
directory, or in a directory in the current PATH environment variable.

Will now exit with status code 2.
"@
    
    exit 2
    
}

# Output array
$OutputArray = @()

# Temporary file name
$TempFileName = '.\Svendsen-Tech-PsExec-wrapper.tmp'

# Add the option that makes "." match newlines unless -RegexOptionNoSingleLine is passed
if (-not $RegexOptionNoSingleLine) {
    
    $ExtractionRegex = [regex] ('(?s)' + $ExtractionRegex.ToString())
    
}

# Make the regex case-insensitive by default, unless -RegexOptionCaseSensitive is passed
if (-not $RegexOptionCaseSensitive) {
    
    $ExtractionRegex = [regex] ('(?i)' + $ExtractionRegex.ToString())

}

foreach ($Computer in $ComputerList) {
    
    Write-Host -NoNewline "Processing ${Computer}... "
    
    # It returns some odd error, but it's redirected if you set $ErrorActionPreference to "Continue"...
    $ErrorActionPreference = 'Continue'
    $Output = Invoke-Expression "$PsExecFullPath \\$Computer $PsExecCommand 2> $TempFileName"
    $ErrorActionPreference = 'Stop'
    
    if (($Output -join $DelimiterJoinString) -imatch $ExtractionRegex) {
        
        Write-Host "Regex matched. Captured: $($Matches[1])"
        $ExtractedOutput = $Matches[1]
        
    }
    
    else {
        
        if ($Output) {
            
            $ExtractedOutput = $Output
            Write-Host "Regex did not match. Using all output in $(if ($XmlOutput) { 'XML field' } else { "lines joined with '$($MultiLineJoinString)'" })."
            
        }
        
        else {
            
            $ExtractedOutput = 'ERROR: No output'
            Write-Host "Regex did not match and no output."
            
        }
        
    }
    
    $OutputArray += ,$ExtractedOutput
    
}

if (Test-Path $TempFileName) {
    
    Remove-Item $TempFileName -ErrorAction 'Continue'
    
}

if ($XmlOutput) {
    
    $XmlString = '<computers>'
    
    foreach ( $i in 0..($ComputerList.Length - 1) ) {
        
        $XmlString += '<computer><name>' + $ComputerList[$i] + '</name><output>' + $OutputArray[$i] + '</output></computer>'
        
    }
    
    $XmlString += '</computers>'
    
    $Xml = [xml] $XmlString
    
    $Xml.Save($OutputFile)
    
}

else {
    
    # Create CSV headers manually
    '"ComputerName","Output"' | Out-File $OutputFile
    
    foreach ( $i in 0..($ComputerList.Length - 1) ) {
        
        # Create CSV manually
        '"' + $ComputerList[$i] + '","' + ($OutputArray[$i] -join $MultiLineJoinString) + '"' | Out-File -Append $OutputFile
        
    }
    
}

@"
Done!
Script start time: $StartTime
Script end time:   $(Get-Date)
Output file: $OutputFile
"@


Personal tools