Posts filed under ‘PowerShell’

Converting PowerShell Scripts to HTML for Documentation

There are times when you want to publish a PowerShell script for documentation in a format where having it as HTML is the best option, such as in a blog posting.

I have used a couple of approaches for this.

One is to use an addin for PowerShell-ISE from Gary Lapointe which can be accessed from ISE’s Add-on’s menu and selecting the Add-ons Tools Website option or http://blog.falchionconsulting.com/index.php/2012/10/windows-powershell-v3-ise-copy-as-html-add-on/.

The other is to use PowerShell Studio from Sapien  https://www.sapien.com/software/powershell_studio.

It is handy to have the functionality added in to ISE but is is rather simplistic  as indenting is not maintained where as the Sapien tool, while creating more complex HTML, does produce a more readable result.  To see the difference, here are some renditions of a command I have for trimming strings:

The ISE addin

function ConvertTo-CCTrimmedString

{

[CmdletBinding()]

Param

(

[Parameter(ValueFromPipeLine = $True,

   ValueFromPipelineByPropertyName = $true)]

[String[]]$String

)

Begin

{

}

Process

{

foreach ($s in $String)

{

$processedString = ($S.Trim().Split($null) |

Where-Object { -not [String]::IsNullOrWhiteSpace($PSItem) }) -Join ” “

Write-Output $processedString

}

}

End

{

}

}

 

PowerShell Studio

function ConvertTo-CCTrimmedString
{
    [CmdletBinding()]
    Param
    (
        [Parameter(ValueFromPipeLine = $True,
                   ValueFromPipelineByPropertyName = $true)]
        [String[]]$String
    )
    Begin
    {
    }
    Process
    {
        foreach ($s in $String)
        {
            $processedString = ($S.Trim().Split($null) |
            Where-Object { -not [String]::IsNullOrWhiteSpace($PSItem) }) -Join " "
            Write-Output $processedString
        }
    }
    End
    {
    }
}

To explain the command, unlike the Trim() method on a string which only removes the leading and trailing spaces from the string, my command also reduces multiple instances of white space characters within the string (spaces, tabs, end of line markers etc) to a single space.

I have been using Sapien’s tools for over 16 years now and continue to be impressed with the quality and functionality of their tools.

Advertisements

February 21, 2017 at 2:11 pm Leave a comment

Splitting command lines into command and arguments

Having been inspired by a PowerShell.com PowerTip of the day that looked at a function that would do this

http://s1403.t.en25.com/e/es.aspx?s=1403&e=166808&elq=1882a23c7eeb4ef0aae82b42771499bc

I thought that I would expand the concept to write it as a cmdlet that would take pipeline input.  The initial part of the utility worked well for the results of Get-Process which has a property called CommandLine, but not with service objects.  When I piped the Win32_Service objects into a Select-Object to produce a property called CommandLine I found that there were several instances where the PathName references executable under the Program Files directories without enclosing them in quotation marks so I added a function to process the line and add the quote marks on the basis of the executable being a .exe file.  The catch is that the current version will put a quotation mark after each .exe so if this occurs in the arguments, there may be some spurious quotation marks.  There is a switch to skip the processing of the command line.

<#
.Synopsis
Get-CommandLine takes an object that has a parameter
called CommandLine and breaks out the command
element and the arguments.
.DESCRIPTION
The utility creates an object that contains the
initial commandline and the command and arguments
on their own. An attempt will be made to process
the command line to deal with a case, such as can occur
with services, where the executable is in a program files
directory but does not enclose it in quotation marks.
.PARAMETER CommandLine
CommandLine is the string to be processed. Input can come from
the pipeline so long as the object has a property called CommandLine.
.PARAMETER IncludeOriginalCommandLine
IncludeOriginalCommandLine is a switch to tell the cmdlet to included
the value of the CammandLine in the output.
.PARAMETER SkipCommandLineProcessing
If this switch is included the utility will not attempt to parse the
command line and add quotation marks. If this switch is used, it is
advisabel to also use the IncludeOriginalCommandLine switch to be
able to check the full syntax of the command line.
.EXAMPLE
Get-WmiObject -Class Win32_Process | Get-CommandLine | Out-GridView
.EXAMPLE
Get-CommandLine -CommandLine ‘”notepad.exe” c:\test’
.EXAMPLE
Get-WmiObject -Class Win32_Process | Get-CommandLine -IncludeOriginalCommandLine
.EXAMPLE
gwmi Win32_Service | Select @{n=”CommandLine”;e={$PSItem.PathName}} | Get-CommandLine -IncludeOriginalCommandLine | Out-GridView
#>
function Get-CommandLine
{
[CmdletBinding()]
[OutputType([int])]
Param
(
# Param1 help description
[Parameter(ValueFromPipelineByPropertyName=$true,
Position=0)]
[String]$CommandLine,
[Switch]$IncludeOriginalCommandLine,
[Switch]$SkipCommandLineProcessing
)

 

Begin
{
}
Process
{
if (-not [string]::IsNullorEmpty($CommandLine))
{
$originalCommandLine = $CommandLine
function Process-CommandLine
{
Param
(
[string]$CommandLineToProcess,
[string]$BaseDirectory
)

if ($CommandLineToProcess.Substring(0,$BaseDirectory.Length + 1) -eq ($BaseDirectory + “\”))
{
$CommandLineToProcess = “`”” + $BaseDirectory + $CommandLine.Substring($BaseDirectory.Length).Replace(“.exe”,”.exe`” “)
}
return $CommandLineToProcess
}
if (-not $SkipCommandLineProcessing)
{
# processing cases where references are made to the program directories without quotation marks and
# based on the assumption that the command referenced is a .exe.

$baseDirectories = @()
$baseDirectories += ${env:ProgramFiles(x86)}
$baseDirectories += $env:ProgramFiles
$baseDirectories += $env:CommonProgramFiles
$baseDirectories += ${env:CommonProgramFiles(x86)}
$baseDirectories += $env:ProgramW6432
$baseDirectories += $env:CommonProgramW6432
foreach ($bd in $baseDirectories)
{
if ($CommandLine.Length -gt $bd.Length)
{
if ($CommandLine.Substring(0,($bd.Length + 1)) -eq ($bd + “\”))
{
$CommandLine = Process-CommandLine $CommandLine $bd
}
}
}
}

$cmdObject = New-Object PSObject -Property @{
Command = “”
Argument = “”
}
if ($IncludeOriginalCommandLine)
{
$cmdObject | Add-Member -Name CommandLine -MemberType NoteProperty -Value “”
}
$firstChar = $CommandLine.SubString(0,1)
$commandString = “”
switch ($firstChar)
{
‘”‘ {$commandString = $CommandLine.Split(‘”‘)[1]}
“`'” {$commandString = $CommandLine.Split(“`'”)[1]}
Default {$commandString = $CommandLine.Split(” “)[0]}
}
if ($IncludeOriginalCommandLine)
{
$cmdObject.CommandLine = $originalCommandLine
}
$cmdObject.Command = $commandString

if ($firstChar -gt “a”)
{
$cmdObject.Argument = $CommandLine.Substring($commandString.Length)
}
else
{
$cmdObject.Argument = $CommandLine.Substring($commandString.Length+2)
}

Write-Output $cmdObject
}
}
End
{
}

}

 

 

 

January 8, 2017 at 9:29 am Leave a comment

Personalising PowerShell Commands, or what seemed like a good idea in Redmond isn’t necessarily the best option for me!

To be fair, when the PowerShell commands are being developed in Redmond they need to be able to work for all people across the globe in all situations as much as possible.  In particular this means that many of the default values for command parameters are very generic so at least they work.  A classic example of this is the parameter ComputerName having a default value of LocalHost.  This will work, but in most cases is not the best value for administrators.

As an example of how you can personalise a command, I will look at the ConvertTo-HTML command. My preference is rather than have to output look like:

converttohtmlsansborders

is to have it look like:

converttohtmlborders

There are a couple of ways to personalise how these commands work so that you can get the default values that make sense for you and your environment.  A very straightforward option is to use the $PSDefaultParameterValues variable that you can set in your profile.  This enables you to define a value for a specific parameter on a specific command.  For example, I can use:

 $PSDefaultParameterValues.Add("ConvertTo-HTML:Head",$style)

in my profile.ps1 file to set the default value for the Head parameter in my ConvertTo-HTML to embed a css specification for the table layout as defined in the $style variable.

Ref: Get-Help about_PSDefaultParameterValues -ShowWindow

This approach works very well but has the limitation that it is set for me on one computer.  If there are commands that are commonly used by administrators in your organisation and you all have a common need it would be better to have an organisational solution.

My approach to this is to create a wrapper or proxy command.  What I mean by this is that you effectively write your own version of the standard command and set you own default values and/or other functionality.  This command can then be put in to an organisational module (you can set PowerShell to look for modules on a UNC path so that it is shared by all staff.)

Ref: https://bchallis.wordpress.com/2015/11/14/where-does-powershell-look-for-modules/

One way to do this is to write your own command collecting all the parameters used in the original command and then use these values to make the call to the original command.  An easier way is to use PowerShell to do it for you.  I have a command New-CCCommandWrapper which is:

function New-CCCommandWrapper
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $True)]
        [String]$CommandName,
        [String]$NounPrefix = "CC",
        [Switch]$CopyToClipboard,
        [Switch]$RawCode
    )
    [System.Text.StringBuilder]$sb = New-Object System.Text.StringBuilder | Out-Null
    $functionName = $CommandName.Replace("-","-$NounPrefix")
    $sb = "function " + $functionName + "`n{`n`t"
    $command = Get-Command $CommandName
    $metadata = New-Object System.Management.Automation.CommandMetaData($command)
    [String]$code = [System.Management.Automation.ProxyCommand]::Create($metadata)
    [String]$formattedCode = $code.Replace("`n","`n`t")
    if ($formattedCode.EndsWith("`t"))
    {
        $sb.Append($formattedCode.Substring(0,($formattedCode.Length - 1))) | Out-Null
    }
    else
    {
        $sb.Append($formattedCode) | Out-Null
    }
    
    $sb.Append("}") | Out-Null
    
    if ($RawCode)
    {
        $codeToReturn = $code
    }
    else
    {
        $codeToReturn = $sb.ToString()
    }
    if ($CopyToClipboard)
    {
        Set-Clipboard -Value $codeToReturn
        Write-Verbose "$functionName copied to clipboard"
    }
    else
    {
        Write-Output $codeToReturn
    }
}
As an aside, I like the way Sapien’s PowerShell Studio creates Comment Based Help by reading the parameter definition to produce a useful template.

ref: https://www.sapien.com/software/powershell_studio

psstudiogeneratecommenthelp

Using this gave me a template for the help that provides tags for all of the parameters.

New-CCComandWrapper writes the following code:

function ConvertTo-CCHtml
{
    [CmdletBinding(DefaultParameterSetName = 'Page', HelpUri = 'http://go.microsoft.com/fwlink/?LinkID=113290', RemotingCapability = 'None')]
    param (
        [Parameter(ValueFromPipeline = $true)]
        [psobject]${InputObject},
        [Parameter(Position = 0)]
        [System.Object[]]${Property},
        [Parameter(ParameterSetName = 'Page', Position = 3)]
        [string[]]${Body},
        [Parameter(ParameterSetName = 'Page', Position = 1)]
        [string[]]${Head},
        [Parameter(ParameterSetName = 'Page', Position = 2)]
        [ValidateNotNullOrEmpty()]
        [string]${Title},
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Table', 'List')]
        [string]${As},
        [Parameter(ParameterSetName = 'Page')]
        [Alias('cu', 'uri')]
        [ValidateNotNullOrEmpty()]
        [uri]${CssUri},
        [Parameter(ParameterSetName = 'Fragment')]
        [ValidateNotNullOrEmpty()]
        [switch]${Fragment},
        [ValidateNotNullOrEmpty()]
        [string[]]${PostContent},
        [ValidateNotNullOrEmpty()]
        [string[]]${PreContent})
    
    begin
    {
        try
        {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
            {
                $PSBoundParameters['OutBuffer'] = 1
            }
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\ConvertTo-Html', [System.Management.Automation.CommandTypes]::Cmdlet)
            $scriptCmd = { & $wrappedCmd @PSBoundParameters }
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        }
        catch
        {
            throw
        }
    }
    
    process
    {
        try
        {
            $steppablePipeline.Process($_)
        }
        catch
        {
            throw
        }
    }
    
    end
    {
        try
        {
            $steppablePipeline.End()
        }
        catch
        {
            throw
        }
    }
    <#          .ForwardHelpTargetName Microsoft.PowerShell.Utility\ConvertTo-Html     .ForwardHelpCategory Cmdlet          #>
}

The wrapper function uses the $PSBoundParameters dictionary object to feed the values provided when the command is called to the wrapped command.  My code checks to see if there is a value for the Head parameter, and if not, passes the default table style in to that parameter at the start of the begin block to produce the following command:

function ConvertTo-CCHTML
{
    [CmdletBinding(DefaultParameterSetName = 'Page', HelpUri = 'http://go.microsoft.com/fwlink/?LinkID=113290', RemotingCapability = 'None')]
    param
    (
        [Parameter(ValueFromPipeline = $true)]
        [psobject]${InputObject},
        [Parameter(Position = 0)]
        [System.Object[]]${Property},
        [Parameter(ParameterSetName = 'Page', Position = 3)]
        [string[]]${Body},
        [Parameter(ParameterSetName = 'Page', Position = 1)]
        [string[]]${Head},
        [Parameter(ParameterSetName = 'Page', Position = 2)]
        [ValidateNotNullOrEmpty()]
        [string]${Title},
        [ValidateSet('Table', 'List')]
        [ValidateNotNullOrEmpty()]
        [string]${As},
        [Parameter(ParameterSetName = 'Page')]
        [Alias('cu', 'uri')]
        [ValidateNotNullOrEmpty()]
        [uri]${CssUri},
        [Parameter(ParameterSetName = 'Fragment')]
        [ValidateNotNullOrEmpty()]
        [switch]${Fragment},
        [ValidateNotNullOrEmpty()]
        [string[]]${PostContent},
        [ValidateNotNullOrEmpty()]
        [string[]]${PreContent}
    )
    
    begin
    {
        try
        {
            $defaultTableStyle = '<style>
            th {
                  VERTICAL-ALIGN:  TOP; 
                  COLOR:  #018AC0; 
                  TEXT-ALIGN:  left;
                  background-color:LightSteelBlue;
                  color:Black;
                  BORDER: 1px  solid black;
                  }
            table, td 
            {
                border: 1px solid black;
            }
            td 
            {
                padding: 5px;
            }
            tr:nth-child(even) {background-color:White;}
            tr:nth-child(odd) {background-color:AliceBlue;}
            </style>'
            if ([String]::IsNullOrWhiteSpace($Head))
            {
                $PSBoundParameters['Head'] = $defaultTableStyle
            }
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
            {
                $PSBoundParameters['OutBuffer'] = 1
            }
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\ConvertTo-Html', [System.Management.Automation.CommandTypes]::Cmdlet)
            $scriptCmd = { & $wrappedCmd @PSBoundParameters }
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        }
        catch
        {
            throw
        }
    }
    
    process
    {
        try
        {
            $steppablePipeline.Process($_)
        }
        catch
        {
            throw
        }
    }
    
    end
    {
        try
        {
            $steppablePipeline.End()
        }
        catch
        {
            throw
        }
    }
}

This approach means I can include my version of the command in a module that can be deployed as widely as needed.

I have included the code for both commands here commands.

In my next post I will discuss two of the other command wrappers that I have created which alter the functionality of the commands they wrap.

January 8, 2017 at 9:28 am Leave a comment

Creating a Windows Form in PowerShell

The script here is a demonstration of how PowerShell can be used to create .NET objects such as Windows forms.  This code creates and builds a GUI along with button click events.  As a demonstration it is designed to read a csv file (using the Open File Dialog box to locate it) and work with controls on the page.

 

# Demonstration code to create a .NET Windows Form, populate it with controls and use it
# to read and process a file.

# Load the .NET Assemblies
[void][System.Reflection.Assembly]::LoadWithPartialName(“System.windows.forms”)
[void][reflection.assembly]::LoadWithPartialName(“System.Drawing”)

function LoadCSV(){
# Clear any items that may exist in the lists.
$count = $form.Controls[“lstNames”].Items.Count – 1
for ($i = $count;$i -ge 0;$i–)
{
$form.Controls[“lstNames”].Items.RemoveAt($i)
$form.Controls[“lstEmails”].Items.RemoveAt($i)
$form.Controls[“lstphones”].Items.RemoveAt($i)
}

[System.Windows.Forms.OpenFileDialog]$ofd = New-Object System.Windows.Forms.OpenFileDialog
$ofd.ShowHelp = $True
$ofd.Filter = “CSV Files (*.csv)|*.csv|All Files (*.*)|*.*”
if ($ofd.ShowDialog() -eq “OK”)
{
$details = Import-Csv -Path $ofd.FileName
foreach ($line in $details)
{
$form.Controls[“lstNames”].Items.Add($line.Name)
$form.Controls[“lstEmails”].Items.Add($line.Email)
$form.Controls[“lstphones”].Items.Add($line.Phone)
}
}
}

function ProcessDetails()
{
Clear-Host
for ($i = 0;$i -lt $form.Controls[“lstNames”].Items.Count;++$i)
{
Write-Host $form.Controls[“lstNames”].Items[$i] ” – ” $form.Controls[“lstEmails”].Items[$i] ” – ” $form.Controls[“lstphones”].Items[$i]
}
}

function BuildForm()
{
#Create the form
$form = new-object Windows.Forms.form
$form.Size = new-object System.Drawing.Size @(800,300)
$form.text = “Demo Form created with PowerShell”

#Create the buttons
$btnLoadCSV = New-Object System.Windows.Forms.Button
$btnLoadCSV.Width=100
$btnLoadCSV.Location = New-Object System.Drawing.Size(350, 10)
$btnLoadCSV.Text = “Load CSV File”

$btnProcessCSV = New-Object System.Windows.Forms.Button
$btnProcessCSV.Width = 100
$btnProcessCSV.Text = “Process CSV Data”
$btnProcessCSV.Location = New-Object System.Drawing.Size(350, 35)

#Create the list boxes and their assosciated labels.
#Names
$lblNames = New-Object System.Windows.Forms.Label
$lblNames.Text = “Names”
$lblNames.Location = New-Object System.Drawing.Size(50, 75)

$lstNames = New-Object System.Windows.Forms.ListBox
$lstNames.Name = “lstNames”
$lstNames.Width = 200
$lstNames.Height = 100
$lstNames.Location = New-Object System.Drawing.Size(50, 100)

#Email addresses
$lblEmails = New-Object System.Windows.Forms.Label
$lblEmails.Text = “Email Addresses”
$lblEmails.Location = New-Object System.Drawing.Size(300, 75)

$lstEmails = New-Object System.Windows.Forms.ListBox
$lstEmails.Name = “lstEmails”
$lstEmails.Width = 200
$lstEmails.Height = 100
$lstEmails.Location = New-Object System.Drawing.Size(300, 100)

#Phone numbers
$lblPhones = New-Object System.Windows.Forms.Label
$lblPhones.Text = “Phone Numbers”
$lblPhones.Location = New-Object System.Drawing.Size(550, 75)

$lstPhones = New-Object System.Windows.Forms.ListBox
$lstPhones.Name = “lstPhones”
$lstPhones.Width = 200
$lstPhones.Height = 100
$lstPhones.Location = New-Object System.Drawing.Size(550, 100)

#Add the controls to the form
$form.Controls.Add($btnLoadCSV)
$form.Controls.Add($btnProcessCSV)
$form.Controls.Add($lstNames)
$form.Controls.Add($lstEmails)
$form.Controls.Add($lstPhones)
$form.Controls.Add($lblNames)
$form.Controls.Add($lblEmails)
$form.Controls.Add($lblPhones)

$btnLoadCSV.add_click({LoadCSV})
$btnProcessCSV.add_click({ProcessDetails})
return $form
}

$form = BuildForm;
$form.ShowDialog();

February 16, 2012 at 8:36 am 5 comments

Finding PowerShell cmdlets that use a particular parameter

There are times when I have wanted to know what cmdlets use a particular parameter, such as ComputerName or PassThru, so I have added the following function to my profile:

function Get-CommandByParameter ([string]$Parameter)
{
    Get-Command | Where-Object {try {$_.Parameters.Keys -contains $Parameter} catch {}}
}

May 3, 2011 at 11:27 am Leave a comment

More on PowerShell Common Parameter

Following up on how to use the Common Parameters with your own functions utlising the cmdlets such as Write-Verbose I went exploring the preference variables to see how they reacted to the switches to see if I could detect the use of the switches for my own code.  The following script is what I used to see the variables’ values change and to show that I could detect the switches being used.

function Test-CommonParameterVariables
{
[CmdletBinding()]
Param
(
[string]$switches
)
“Commandline switches: $switches`n”
“Preference Variables:”
“———————”
dir variable:\*Preference | Sort-Object -Property Name
if ($VerbosePreference -ne ‘SilentlyContinue’)
{
Write-Warning “In the VerbosePreference block”
}
if ($DebugPreference -ne ‘SilentlyContinue’)
{
Write-Warning “In the DebugPreference block”
}
}

Clear-Host
Test-CommonParameterVariables ‘No switches’
Write-Host “—————————————-”
Test-CommonParameterVariables ‘-Verbose’ -Verbose

Write-Host “—————————————-”
Test-CommonParameterVariables ‘-Debug’ -Debug

Write-Host “—————————————-”
Test-CommonParameterVariables ‘-Verbose -Debug’ -Verbose -Debug

Write-Host “—————————————-”
Test-CommonParameterVariables ‘-ErrorAction Continue’ -ErrorAction Continue

Write-Host “—————————————-”
Test-CommonParameterVariables ‘-ErrorAction Inquire’ -ErrorAction Inquire
Write-Host “—————————————-”
Test-CommonParameterVariables ‘-ErrorAction Stop’ -ErrorAction Stop
Write-Host “—————————————-“

February 25, 2011 at 4:35 pm Leave a comment

Using Common Parameters in PowerShell

I have been exploring how the Common Parameter functionality gets used in custom code.  Basically it looks as though you need to use the CmdletBinding attribute to have the Common Parameters bound to your function, which means that you need to have parameters for this to work.  Then you have to use cmdlets such as Write-Verbose to hook in the the in built support for the Common Parameters.

The following code is what I have been using to check the functionality:

###############################################
# Script to explore common parameters
# Brent Challis
# Dimension Data Learning Solutions
# Melbourne, Victoria, Australia
###############################################
function Test-CommonParametersWithCmdletBinding
{
[CmdletBinding()]
Param
(
[string]$FunctionMessage = “Initial Value”
)
Write-Output “Starting Function with CmdletBinding.”
Write-Host “Writing to the host.”
Write-Output $FunctionMessage
Write-Verbose “Verbose message.”
Write-Debug “Debug message.”
Write-Warning “Warning message.”
Write-Output “Function Complete.”
}

function Test-CommonParametersWithoutCmdletBinding
{
Param
(
[string]$FunctionMessage = “Initial Value”
)
Write-Output “Starting Function without CmdletBinding.”
Write-Host “Writing to the host.”
Write-Output $FunctionMessage
Write-Verbose “Verbose message.”
Write-Debug “Debug message.”
Write-Warning “Warning message.”
Write-Output “Function Complete.”
}

Clear-Host
Write-Host “Starting.”
Write-Host “==========================================================”
Write-Host “Getting help info”
Get-Help Test-CommonParametersWithCmdletBinding
Write-Host “———————————————————-”
Get-Help Test-CommonParametersWithoutCmdletBinding
Write-Host “==========================================================”
Write-Host “Run with CmdletBinding.”
Test-CommonParametersWithCmdletBinding “Test Message” -Verbose -Debug
Write-Host “==========================================================”
Write-Host “Run without CmdletBinding.”
Test-CommonParametersWithoutCmdletBinding “Test Message” -Verbose -Debug
Write-Host “==========================================================”
Write-Output “Test complete.”

February 23, 2011 at 10:30 am Leave a comment

Older Posts


Calendar

October 2017
M T W T F S S
« Feb    
 1
2345678
9101112131415
16171819202122
23242526272829
3031  

Posts by Month

Posts by Category