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.

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

Where does PowerShell look for Modules

One of the great things from PowerShell 3 onwards is that you do not need to import modules to use them so long as they are stored in one of the locations that PowerShell will automatically look for them.  The directories that will be searched are listed in the environment variable PSModulePath.  A quick way to see the directories is:

(Get-Item env:PSModulePath).Value -Split “;”

On my computer, this returns:

C:\Users\Brent\Documents\WindowsPowerShell\Modules
C:\Program Files\WindowsPowerShell\Modules
C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\
C:\Program Files (x86)\Microsoft SQL Server\120\Tools\PowerShell\Modules\

As an aside to this, if you are part of an admin team you can put a UNC path in to this variable so it is possible to have a modules directory on a file share enabling the admin team to easily use and maintain an organisation module.

November 14, 2015 at 11:34 am 1 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

NetComm NP206 Wireless Powerline Kit

I recently wanted to rearrange the WiFi access point in the house to get better overall coverage. Having decided that getting an extra phone point or trying to run cabling was going to be too difficult, I decided to try one of the Ethernet over the power options. I bought the NetComm NP206 units. One of the advantages of this option is that not only does it provide an ethernet connection so I could reposition the AP that I have, it includes a WiFi Access Point as well (802.11n which is better than the AP I already had.)

I plugged the two units in to the power points, they immediately connected to provide me with the ethernet link I needed to connect the ADSL modem to a better positioned Access Point. I plugged the AP I had in to one of the ethernet ports (there are two) and it all worked perfectly.

The I tried to connect to the built in AP. I could see the SSID, just as the manual described. The problem was that when I tried to connect to it it would not accept the password as described in the manual. On one of the devices that I have I received an invalid password message.

The solution – I downloaded the configuration utility, you can find it here:
http://www.netcomm.com.au/sm/drivers/np206

When I ran this, the password displayed for the AP was nothing like that described in the manual.

I reset the password (And the SSID while I was at it) and now have all my WiFi devices connecting to it.

The units seem to be working very well, and, with the exception (and quite a big one at that!) of the wrong password being described in the manual, it worked very easily.

November 13, 2011 at 4:29 pm 3 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

Older Posts


Categories

  • Blogroll

  • Feeds