I'm a PowerShell user for some time, but im not sure how to implement a -Help switch for my PoSh script(s).
The scripts have already a built-in help which can be retrieved by using Get-Help myscript.ps1.
The scripts have multiple parameters which are documented. I would like to add another parameter -Help to call Get-Help on the script itself.
I would like to offer an additional interface to get the help page(s). It's also common to use -h/--help on Linux systems in alternative to man <cmd>.
I already tested it with functions but the function's name is fixed and known; a script can be renamed ...
#
# .SYNOPSIS
# help text
#
[CmdLetBinding()]
param(
[switch]$Help = $false
)
if ($Help)
{ Get-Help <me>
return
}
Write-Host "other statements"
What should I insert for <me>?
I think you can make good use of $MYINVOCATION.InvocationName here :
function Test-Function {
Param([Switch]
$Help = $false
)
if($Help) {
"help mode : function name is $($MYINVOCATION.InvocationName)"
} else {
"normal process"
}
}
Test-Function -Help
Get-Help has yet to be called with the function name, and I'll let you lose some hair on this (failed with .) !
Related
I've just come across the doctest module in Python, which helps you perform automated testing against example code that's embedded within Python doc-strings. This ultimately helps ensure consistency between the documentation for a Python module, and the module's actual behavior.
Is there an equivalent capability in PowerShell, so I can test examples in the .EXAMPLE sections of PowerShell's built-in help?
This is an example of what I would be trying to do:
function MyFunction ($x, $y) {
<#
.EXAMPLE
> MyFunction -x 2 -y 2
4
#>
return $x + $y
}
MyFunction -x 2 -y 2
You could do this, although I'm not aware of any all-in-one built-in way to do it.
Method 1 - Create a scriptblock and execute it
Help documentation is an object, so can be leveraged to index into examples and their code. Below is the simplest example I could think of which executes your example code.
I'm not sure if this is what doctest does - it seems a bit dangerous to me but it might be what you're after! It's the simplest solution and I think will give you the most accurate results.
Function Test-Example {
param (
$Module
)
# Get the examples
$examples = Get-Help $Module -Examples
# Loop over the code of each example
foreach ($exampleCode in $examples.examples.example.code) {
# create a scriptblock of your code
$scriptBlock = [scriptblock]::Create($exampleCode)
# execute the scriptblock
$scriptBlock.Invoke()
}
}
Method 2 - Parse the example/function and make manual assertions
I think a potentially better way to this would be to parse your example and parse the function to make sure it's valid. The downside is this can get quite complex, especially if you're writing complex functions.
Here's some code that checks the example has the correct function name, parameters and valid values. It could probably be refactored (first time dealing with [System.Management.Automation.Language.Parser]) and doesn't deal with advanced functions at all.
If you care about things like Mandatory, ParameterSetName, ValidatePattern etc this probably isn't a good solution as it will require a lot of extension.
Function Check-Example {
param (
$Function
)
# we'll use this to get the example command later
# source: https://vexx32.github.io/2018/12/20/Searching-PowerShell-Abstract-Syntax-Tree/
$commandAstPredicate = {
param([System.Management.Automation.Language.Ast]$AstObject)
return ($AstObject -is [System.Management.Automation.Language.CommandAst])
}
# Get the examples
$examples = Get-Help $Function -Examples
# Parse the function
$parsedFunction = [System.Management.Automation.Language.Parser]::ParseInput((Get-Content Function:$Function), [ref]$null, [ref]$null)
# Loop over the code of each example
foreach ($exampleCode in $examples.examples.example.code) {
# parse the example code
$parsedExample = [System.Management.Automation.Language.Parser]::ParseInput($exampleCode, [ref]$null, [ref]$null)
# get the command, which gives us useful properties we can use
$parsedExampleCommand = $parsedExample.Find($commandAstPredicate,$true).CommandElements
# check the command name is correct
"Function is correctly named: $($parsedExampleCommand[0].Value -eq $Function)"
# loop over the command elements. skip the first one, which we assume is the function name
foreach ($element in ($parsedExampleCommand | select -Skip 1)) {
"" # new line
# check parameter in example exists in function definition
if ($element.ParameterName) {
"Checking parameter $($element.ParameterName)"
$parameterDefinition = $parsedFunction.ParamBlock.Parameters | where {$_.Name.VariablePath.Userpath -eq $element.ParameterName}
if ($parameterDefinition) {
"Parameter $($element.ParameterName) exists"
# store the parameter name so we can use it to check the value, which we should find in the next loop
# this falls apart for switches, which have no value so they'll need some additional logic
$previousParameterName = $element.ParameterName
}
}
# check the value has the same type as defined in the function, or can at least be cast to it.
elseif ($element.Value) {
"Checking value $($element.Value) of parameter $previousParameterName"
$parameterDefinition = $parsedFunction.ParamBlock.Parameters | where {$_.Name.VariablePath.Userpath -eq $previousParameterName}
"Parameter $previousParameterName has the same type: $($element.StaticType.Name -eq $parameterDefinition.StaticType.Name)"
"Parameter $previousParameterName can be cast to correct type: $(-not [string]::IsNullOrEmpty($element.Value -as $parameterDefinition.StaticType))"
}
else {
"Unexpected command element:"
$element
}
}
}
}
Method 3 - Use Pester (maybe out of scope)
I think this one is a bit off topic, but worth mentioning. Pester is the test framework for PowerShell and has features that could be helpful here. You could have a generic test that takes a script/function as argument and runs tests against the parsed examples/functions.
This is could involve executing the script like in method 1 or checking the parameters like in method 2. Pester has a HaveParameter assertion that allows you to check certain things about your function.
HaveParameter documenation, copied from link above:
Get-Command "Invoke-WebRequest" | Should -HaveParameter Uri -Mandatory
function f ([String] $Value = 8) { }
Get-Command f | Should -HaveParameter Value -Type String
Get-Command f | Should -Not -HaveParameter Name
Get-Command f | Should -HaveParameter Value -DefaultValue 8
Get-Command f | Should -HaveParameter Value -Not -Mandatory
I have two PowerShell scripts, which have switch parameters:
compile-tool1.ps1:
[CmdletBinding()]
param(
[switch]$VHDL2008
)
Write-Host "VHDL-2008 is enabled: $VHDL2008"
compile.ps1:
[CmdletBinding()]
param(
[switch]$VHDL2008
)
if (-not $VHDL2008)
{ compile-tool1.ps1 }
else
{ compile-tool1.ps1 -VHDL2008 }
How can I pass a switch parameter to another PowerShell script, without writing big if..then..else or case statements?
I don't want to convert the parameter $VHDL2008 of compile-tool1.ps1 to type bool, because, both scripts are front-end scripts (used by users). The latter one is a high-level wrapper for multiple compile-tool*.ps1 scripts.
You can specify $true or $false on a switch using the colon-syntax:
compile-tool1.ps1 -VHDL2008:$true
compile-tool1.ps1 -VHDL2008:$false
So just pass the actual value:
compile-tool1.ps1 -VHDL2008:$VHDL2008
Try
compile-tool1.ps1 -VHDL2008:$VHDL2008.IsPresent
Assuming you were iterating on development, it is highly likely that at some point you are going to add other switches and parameters to your main script that are going to be passed down to the next called script. Using the previous responses, you would have to go find each call and rewrite the line each time you add a parameter. In such case, you can avoid the overhead by doing the following,
.\compile-tool1.ps1 $($PSBoundParameters.GetEnumerator() | ForEach-Object {"-$($_.Key) $($_.Value)"})
The automatic variable $PSBoundParameters is a hashtable containing the parameters explicitly passed to the script.
Please note that script.ps1 -SomeSwitch is equivalent to script.ps1 -SomeSwitch $true and script.ps1 is equivalent to script.ps1 -SomeSwitch $false. Hence, including the switch set to false is equivalent to not including it.
According to a power shell team's blog (link below,) since V2 there is a technique called splatting. Basically, you use the automatic variable #PsBoundParameters to forward all the parameters. Details about splatting and the difference between # and $ are explained in the Microsoft Docs article (link below.)
Example:
parent.ps1
#Begin of parent.ps1
param(
[Switch] $MySwitch
)
Import-Module .\child.psm1
Call-Child #psBoundParameters
#End of parent.ps1
child.psm1
# Begin of child.psm1
function Call-Child {
param(
[switch] $MySwitch
)
if ($MySwitch){
Write-Output "`$MySwitch was specified"
} else {
Write-Output "`$MySwitch is missing"
}
}
#End of child.psm1
Now we can call the parent script with or without the switch
PS V:\sof\splatting> .\parent.ps1
$MySwitch is missing
PS V:\sof\splatting> .\parent.ps1 -MySwitch
$MySwitch was specified
PS V:\sof\splatting>
Update
In my original answer, I sourced the children instead of importing it as a module. It appears sourcing another script into the original just makes the parent's variables visible to all children so this will also work:
# Begin of child.ps1
function Call-Child {
if ($MySwitch){
Write-Output "`$MySwitch was specified"
} else {
Write-Output "`$MySwitch is missing"
}
}
#End of child.ps1
with
#Begin of parent.ps1
param(
[Switch] $MySwitch
)
. .\child.ps1
Call-Child # Not even specifying #psBoundParameters
#End of parent.ps1
Maybe, this is not the best way to make a program, nevertheless, this is the way it works.
About Splatting(Microsoft Docs)
How and Why to Use Splatting (passing [switch] parameters)
Another solution. If you declare your parameter with a default value of $false:
[switch] $VHDL2008 = $false
Then the following (the -VHDL2008 option with no value) will set $VHDL2008 to $true:
compile-tool1.ps1 -VHDL2008
If instead you omit the -VHDL2008 option, then this forces $VHDL2008 to use the default $false value:
compile-tool1.ps1
These examples are useful when calling a Powershell script from a bat script, as it is tricky to pass a $true/$false bool from bat to Powershell, because the bat will try to convert the bool to a string, resulting in the error:
Cannot process argument transformation on parameter 'VHDL2008'.
Cannot convert value "System.String" to type "System.Management.Automation.SwitchParameter".
Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0.
Is there an easy way to make the -Verbose switch "passthrough" to other function calls in Powershell?
I know I can probably search $PSBoundParameters for the flag and do an if statement:
[CmdletBinding()]
Function Invoke-CustomCommandA {
Write-Verbose "Invoking Custom Command A..."
if ($PSBoundParameters.ContainsKey("Verbose")) {
Invoke-CustomCommandB -Verbose
} else {
Invoke-CustomCommandB
}
}
Invoke-CustomCommandA -Verbose
It seems rather messy and redundant to do it this way however... Thoughts?
One way is to use $PSDefaultParameters at the top of your advanced function:
$PSDefaultParameterValues = #{"*:Verbose"=($VerbosePreference -eq 'Continue')}
Then every command you invoke with a -Verbose parameter will have it set depending on whether or not you used -Verbose when you invoked your advanced function.
If you have just a few commands the do this:
$verbose = [bool]$PSBoundParameters["Verbose"]
Invoke-CustomCommandB -Verbose:$verbose
I began using KeithHill's $PSDefaultParameterValues technique in some powershell modules. I ran into some pretty surprising behavior which I'm pretty sure resulted from the effect of scope and $PSDefaultParameterValues being a sort-of global variable. I ended up writing a cmdlet called Get-CommonParameters (alias gcp) and using splat parameters to achieve explicit and terse cascading of -Verbose (and the other common parameters). Here is an example of how that looks:
function f1 {
[CmdletBinding()]
param()
process
{
$cp = &(gcp)
f2 #cp
# ... some other code ...
f2 #cp
}
}
function f2 {
[CmdletBinding()]
param()
process
{
Write-Verbose 'This gets output to the Verbose stream.'
}
}
f1 -Verbose
The source for cmdlet Get-CommonParameters (alias gcp) is in this github repository.
How about:
$vb = $PSBoundParameters.ContainsKey('Verbose')
Invoke-CustomCommandB -Verbose:$vb
I am trying to use the Get-Help cmdlet to display comment-based help in the same format in which it displays the cmdlet help topics that are generated from XML files. The ability to do this is documented in about_Comment_based_Help on TechNet, but when I execute the get-help cmdlet against my script I only get the script name returned. Any help would be appreciated!
PS C:\Admin> Get-Help .\checksystem.ps1 -full
checksystem.ps1
checksystem.ps1 script:
function IsAlive {
<#
.DESCRIPTION
Checks to see whether a computer is pingable or not.
.PARAMETER computername
Specifies the computername.
.EXAMPLE
IsAlive -computername testwks01
.NOTES
This is just an example function.
#>
param (
$computername
)
Test-Connection -count 1 -ComputerName $computername -TimeToLive 5 |
Where-Object { $_.StatusCode -eq 0 } |
Select-Object -ExpandProperty Address
}
IsAlive -computername 192.168.1.1
It will work, but you are trying to run get help on the script. You have added the help to the function. If you dot source your script, and then type get-help isalive, you will see your help for the function.
. .\checksystem.ps1 ; get-help isalive -full
It works, you just gotta make sure you have the right headings. I've always put the comment block right above the function too. I'm not sure if it's supposed to work inside of the function or not.
Below is an example of one of my functions that has a working doc help.
##############################################################################
#.SYNOPSIS
# Gets a COM object from the running object table (ROT) similar to GetObject
# in Visual Basic.
#
#.DESCRIPTION
# To maintain consistency with New-Object this cmdlet requires the -ComObject
# parameter to be provided and the TypeName parameter is not supported.
#
#.PARAMETER TypeName
# Not supported, but provided to maintain consistency with New-Object.
#
#.PARAMETER ComObject
# The ProgID of a registered COM object, such as MapPoint.Application.
#
#.PARAMETER Force
# If an existing object is not found, instead of writing an error, a new
# instance of the object will be created and returned.
#
#.EXAMPLE
# $olMailItem = 0
# Get-Object -ComObject Outlook.Application | %{$_.CreateItem($olMailItem).Display()}
##############################################################################
function Get-Object {
[CmdletBinding(DefaultParameterSetName='Net')]
param (
[Parameter(ParameterSetName='Net', Position=1, Mandatory=$true)]
[String]$TypeName,
[Parameter(ParameterSetName='Com', Mandatory=$true)]
[String]$ComObject,
[Parameter()]
[Switch]$Force
)
if ( $TypeName ) { throw '-TypeName is not supported. Use -ComObject instead.' }
if ( $ComObject ) {
try {
[System.Runtime.InteropServices.Marshal]::GetActiveObject($ComObject)
}
catch [System.Management.Automation.MethodInvocationException] {
if ( $Force ) { New-Object -ComObject $ComObject }
else { Write-Error "An active object of type $ComObject is not available." }
}
}
}
Note - If you forget to add the name of a parameter after .PARAMETER, none of your custom help text will show when you run get-help
Similarly, if you misspell any of the keywords the custom help will not be displayed.
By default, any named function that has the [CmdletBinding()] attribute accepts the -debug and -verbose (and a few others) parameters and has the predefined $debug and $verbose variables. I'm trying to figure out how to pass them on to other cmdlet's that get called within the function.
Let's say I have a cmdlet like this:
function DoStuff() {
[CmdletBinding()]
PROCESS {
new-item Test -type Directory
}
}
If -debug or -verbose was passed into my function, I want to pass that flag into the new-item cmdlet. What's the right pattern for doing this?
$PSBoundParameters isn't what you're looking for. The use of the [CmdletBinding()] attribute allows the usage of $PSCmdlet within your script, in addition to providing a Verbose flag. It is in fact this same Verbose that you're supposed to use.
Through [CmdletBinding()], you can access the bound parameters through $PSCmdlet.MyInvocation.BoundParameters. Here's a function that uses CmdletBinding and simply enters a nested prompt immediately in order examine the variables available inside the function scope.
PS D:\> function hi { [CmdletBinding()]param([string] $Salutation) $host.EnterNestedPrompt() }; hi -Salutation Yo -Verbose
PS D:\>>> $PSBoundParameters
____________________________________________________________________________________________________
PS D:\>>> $PSCmdlet.MyInvocation.BoundParameters
Key Value
--- -----
Salutation Yo
Verbose True
So in your example, you would want the following:
function DoStuff `
{
[CmdletBinding()]
param ()
process
{
new-item Test -type Directory `
-Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
}
}
This covers -Verbose, -Verbose:$false, -Verbose:$true, and the case where the switch is not present at all.
Perhaps it sounds strange, but there isn't any easy way for a cmdlet to know its verbose or debug mode. Take a look at the related question:
How does a cmdlet know when it really should call WriteVerbose()?
One not perfect, but practically reasonable, option is to introduce your own cmdlet parameters (for example, $MyVerbose and $MyDebug) and use them in the code explicitly:
function DoStuff {
[CmdletBinding()]
param
(
# Unfortunately, we cannot use Verbose name with CmdletBinding
[switch]$MyVerbose
)
process {
if ($MyVerbose) {
# Do verbose stuff
}
# Pass $MyVerbose in the cmdlet explicitly
New-Item Test -Type Directory -Verbose:$MyVerbose
}
}
DoStuff -MyVerbose
UPDATE
When we need only a switch (not, say, a verbosity level value) then the approach with $PSBoundParameters is perhaps better than proposed in the first part of this answer (with extra parameters):
function DoStuff {
[CmdletBinding()]
param()
process {
if ($PSBoundParameters['Verbose']) {
# Do verbose stuff
}
New-Item Test -Type Directory -Verbose:($PSBoundParameters['Verbose'] -eq $true)
}
}
DoStuff -Verbose
It's all not perfect anyway. If there are better solutions then I would really like to know them myself.
There is no need. PowerShell already does this as the code below proves.
function f { [cmdletbinding()]Param()
"f is called"
Write-Debug Debug
Write-Verbose Verbose
}
function g { [cmdletbinding()]Param()
"g is called"
f
}
g -Debug -Verbose
The output is
g is called
f is called
DEBUG: Debug
VERBOSE: Verbose
It is not done as direct as passing -Debug to the next cmdlet though. It is done through the $DebugPreference and $VerbrosePreference variables. Write-Debug and Write-Verbose act like you would expect, but if you want to do something different with debug or verbose you can read here how to check for yourself.
Here's my solution:
function DoStuff {
[CmdletBinding()]
param ()
BEGIN
{
$CMDOUT = #{
Verbose = If ($PSBoundParameters.Verbose -eq $true) { $true } else { $false };
Debug = If ($PSBoundParameters.Debug -eq $true) { $true } else { $false }
}
} # BEGIN ENDS
PROCESS
{
New-Item Example -ItemType Directory #CMDOUT
} # PROCESS ENDS
END
{
} #END ENDS
}
What this does different from the other examples is that it will repsect "-Verbose:$false" or "-Debug:$false". It will only set -Verbose/-Debug to $true if you use the following:
DoStuff -Verbose
DoStuff -Verbose:$true
DoStuff -Debug
DoStuff -Debug:$true
You could build a new hash table based on the bound debug or verbose parameters and then splat it to the internal command. If you're just specifying switches (and aren't passing a false switch, like $debug:$false) you can just check for the existence of debug or verbose:
function DoStuff() {
[CmdletBinding()]
PROCESS {
$HT=#{Verbose=$PSBoundParameters.ContainsKey'Verbose');Debug=$PSBoundParameters.ContainsKey('Debug')}
new-item Test -type Directory #HT
}
}
If you want to pass the parameter value it's more complicated, but can be done with:
function DoStuff {
[CmdletBinding()]
param()
PROCESS {
$v,$d = $null
if(!$PSBoundParameters.TryGetValue('Verbose',[ref]$v)){$v=$false}
if(!$PSBoundParameters.TryGetValue('Debug',[ref]$d)){$d=$false}
$HT=#{Verbose=$v;Debug=$d}
new-item Test -type Directory #HT
}
}
The best way to do it is by setting the $VerbosePreference. This will enable the verbose level for the entire script. Do not forget to disable it by the end of the script.
Function test
{
[CmdletBinding()]
param($param1)
if ($psBoundParameters['verbose'])
{
$VerbosePreference = "Continue"
Write-Verbose " Verbose mode is on"
}
else
{
$VerbosePreference = "SilentlyContinue"
Write-Verbose " Verbose mode is Off"
}
# <Your code>
}
You can set the VerbosePreference as a global variable on starting your script and then check for the global variable in your custom cmdlet.
Script:
$global:VerbosePreference = $VerbosePreference
Your-CmdLet
Your-CmdLet:
if ($global:VerbosePreference -eq 'Continue') {
# verbose code
}
Checking explicitly for 'Continue' allows the script to be equal to -verbose:$false when you call the CmdLet from a script that doesn't set the global variable (in which case it's $null)
You do not have to do any checks or comparisons. Even though -Verbose (and -Debug) are of type [switch], they seem to understand not just $true and $false but also their preference variable. The preference variable also gets inherited correctly to all child functions that are called. I tried this on Powershell version 7.3.2 and it works as expected.
function Parent {
[CmdletBinding()]param()
Child
}
function Child {
[CmdletBinding()]param()
New-Item C:\TEST\SomeDir -Force -ItemType Directory -Verbose:$VerbosePreference -Debug:$DebugPreference
}
Parent -Verbose
Parent -Debug
I think this is the easiest way:
Function Test {
[CmdletBinding()]
Param (
[parameter(Mandatory=$False)]
[String]$Message
)
Write-Host "This is INFO message"
if ($PSBoundParameters.debug) {
Write-Host -fore cyan "This is DEBUG message"
}
if ($PSBoundParameters.verbose) {
Write-Host -fore green "This is VERBOSE message"
}
""
}
Test -Verbose -Debug