PowerShell - automate comment header - powershell

When writing up a PowerShell script, what options are there to support template insertion that infers your parameters?
e.g. for the below, it'd somehow read you have $Name as a parameter, and then automatically produce something like below?
.SYNOPSIS
.DESCRIPTION
.PARAMETER Name
.PARAMETER Extension
function Add-Extension
{
param ([string]$Name,[string]$Extension = "txt")
$name = $name + "." + $extension
$name
<#
.SYNOPSIS
Adds a file name extension to a supplied name.
.DESCRIPTION
Adds a file name extension to a supplied name.
Takes any strings for the file name or extension.
.PARAMETER Name
Specifies the file name.
.PARAMETER Extension
Specifies the extension. "Txt" is the default.
.INPUTS
None. You cannot pipe objects to Add-Extension.
.OUTPUTS
System.String. Add-Extension returns a string with the extension or file name.
.EXAMPLE
C:\PS> extension -name "File"
File.txt
.EXAMPLE
C:\PS> extension -name "File" -extension "doc"
File.doc
.EXAMPLE
C:\PS> extension "File" "doc"
File.doc
.LINK
Online version: http://www.fabrikam.com/extension.html
.LINK
Set-Item
#>
}

What you're looking for is a feature of a PowerShell-aware editor or IDE.
The actively developed editor that offers the best PowerShell development experience, across platforms, is Visual Studio Code, combined with its PowerShell extension.
Once you have authored a function's implementation, you can place the cursor at either the start or the end of the function body and type ##, which scaffolds comment-based help for the function based on its parameters.
See also: Get-Help about_Comment_Based_Help, which explains the syntax of comment-based help.

If you want to discover the parameters of a defined function, you can do something like this:
# define function
function Add-Extension {
param([string]$Name, [string]$Extension)
}
# get command info for defined function
$cmdInfo = Get-Command Add-Extension
# compile command metadata from command info
$cmdMetadata = [System.Management.Automation.CommandMetadata]::new($cmdInfo)
Now you can access the individual parameters' metadata via $cmdMetadata.Parameters and generate your comment-based help content:
foreach($param in $cmdMetadata.Parameters.GetEnumerator()) {
".PARAMETER {0}" -f $param.Key
"[Insert parameter description for {0}]" -f $param.Key
""
}
There's one thing you might be interesting in that the parameter metadata won't contain though - the default value expression - for that, you'll need to parse the raw source code instead:
# parse script file containing the function
$scriptPath = Resolve-Path .\scriptWithFunctionDefinition.ps1
$AST = [System.Management.Automation.Language.Parser]::ParseFile($scriptPath, [ref]$null, [ref]$null)
# discover all the function definitions at the root level
$functionDefs = $AST.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false)
The Parser.ParseFile() method will return an abstract syntax tree - or AST for short - the elements of which we can inspect.
The FindAll() method will walk the AST and output any subtree that satisfies the condition - in this case we're looking for any [FunctionDefinitionAst], which, as the name implies, describes a function definition.
Now we can iterate the parameters like before:
foreach($functionDefinition in $functionDefs) {
# Since we're effectively dealing with code still,
# we need to handle both `function f(){}` and `function f{param()}` syntaxes
$parameters = $functionDefinition.Parameters
if($functionDefinition.Body.ParamBlock){
$parameters = $functionDefinition.Body.ParamBlock
}
# Now we can go through the actual parameters
foreach($parameter in $parameters){
".PARAMETER {0}" -f $parameter.Name.VariablePath
# ... and extract the default value expression
if($parameter.DefaultValue){
"Default value is {0}" -f $parameter.DefaultValue
}
}
}

Related

Is it possible to implement IValidateSetValuesGenerator for script parameter validation?

In the example in the powershell documentation you need to implement a class, like:
Class SoundNames : System.Management.Automation.IValidateSetValuesGenerator { ... }
But, how can you do this in a script based cmdlet? (See "Parameters in scripts" in about_Scripts).
In a script cmdlet the first line must be param( ... ).
Unless "SoundNames" is already defined, then it fails with Unable to find type [SoundNames]
If this type is already defined in the powershell session, this works ... but I'd like a stand-alone script that doesn't require the user do something first to define this type (e.g. dot-sourcing some other file).
I want to define a parameter that accepts only *.txt filenames (for existing files in a specific directory).
You can't do this in a (stand-alone) script, for the reason you state yourself:
The param block (possibly preceded by a [CmdletBinding()] attribute, must be at the very start of the file (leaving using statements aside).
This precludes defining a custom class that implements the IValidateSetValuesGenerator interface for use in your param block.
To work around this limitation, use a [ValidateScript()] attribute:
param(
[ValidateScript({
if (-not (Get-Item -ErrorAction Ignore "$_.txt")) {
throw "$_ is not the (base) name of a *.txt file in the current dir."
}
return $true
})]
[string] $FileName
)
Note that this doesn't give you tab-completion the way that IValidateSetValuesGenerator-based validation would automatically give you.
To also provide tab-completion, additionally use an [ArgumentCompleter()] attribute:
param(
[ValidateScript({
if (-not (Get-Item -ErrorAction Ignore "$_.txt")) {
throw "$_ is not the (base) name of a *.txt file in the current dir."
}
return $true
})]
[ArgumentCompleter({
param($cmd, $param, $wordToComplete)
(Get-Item "$wordToComplete*.txt").BaseName
})]
[string] $FileName
)

Powershell command Start-BitsTransfer fails when source file paths contain square brackets [] [duplicate]

Is there a function in PowerShell for escaping characters in paths?
NB: I'm aware that most cmdlets which provide a Path parameter also provide a LiteralPath parameter which resolves this issue. This question is more from curiosity than need, as in most use cases I can think of, switching to LiteralPath makes sense. However there are some genuine use-cases (e.g. Start-BitsTransfer has a Source parameter, but no literal equivalent).
Detail
If I have a file c:\temp\test[0123].txt, instead of Get-Item 'c:\temp\test[0123].txt', I'd have to use Get-Item 'c:\temp\test`[0123`].txt' to get a result (or make use of the LiteralPath parameter).
Even the path returned by another PowerShell command returns an unescaped string; i.e. Get-ChildItem 'c:\temp\' -Filter 'test*.txt' | Convert-Path | Get-Item fails (NB: if we pass the actual FileSystemInfo object all works, but that object has no properties with the correctly escaped string path).
We can easily escape this using code such as below:
$path = 'c:\temp\test[0123].txt'
$path = $path -replace '([][[])', '`$1' # escape square brackets by adding back ticks
Get-Item $path
However when escaping strings, standard advice is to avoid rolling your own solution / to make use of the language's solutions to these issues.
Is there any pre-existing function in PowerShell for this purpose, or any recommended way of approaching this; or is the only option to use a bespoke function (e.g. below)?
function ConvertFrom-LiteralPath {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]$LiteralPath
)
process {
(New-Object -TypeName 'PSObject' -Property #{
LiteralPath = $LiteralPath
Path = $LiteralPath -replace '([][[\*\?])', '`$1' # escapes ], [, *, and ?
})
}
}
Info on special characters:
Wildcard Queries: https://technet.microsoft.com/en-us/library/ee692793.aspx
File Paths: https://technet.microsoft.com/en-us/library/ff730956.aspx
There is the following method you can use:
[Management.Automation.WildcardPattern]::Escape('test[1].txt')
Returns:
test`[1`].txt
Documented here:
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.wildcardpattern.escape

Why does pipeline parameter cause error when combined with PSDefaultParameterValues?

My powershell function should accept a list of valid paths of mixed files and/or directories either as a named parameter or via pipeline, filter for files that match a pattern, and return the list of files.
$Paths = 'C:\MyFolder\','C:\MyFile'
This works: Get-Files -Paths $Paths This doesn't: $Paths | Get-Files
$PSDefaultParameterValues = #{
"Get-Files:Paths" = ( Get-Location ).Path
}
[regex]$DateRegex = '(20\d{2})([0-1]\d)([0-3]\d)'
[regex]$FileNameRegex = '^term2-3\.7_' + $DateRegex + '\.csv$'
Function Get-Files {
[CmdletBinding()]
[OutputType([System.IO.FileInfo[]])]
[OutputType([System.IO.FileInfo])]
param (
[Parameter(
Mandatory = $false, # Should default to $cwd provided by $PSDefaultParameterValues
ValueFromPipeline,
HelpMessage = "Enter filesystem paths that point either to files directly or to directories holding them."
)]
[String[]]$Paths
)
begin {
[System.IO.FileInfo[]]$FileInfos = #()
[System.IO.FileInfo[]]$SelectedFileInfos = #()
}
process { foreach ($Path in $Paths) {
Switch ($Path) {
{ Test-Path -Path $Path -PathType leaf } {
$FileInfos += (Get-Item $Path)
}
{ Test-Path -Path $Path -PathType container } {
foreach ($Child in (Get-ChildItem $Path -File)) {
$FileInfos += $Child
}
}
Default {
Write-Warning -Message "Path not found: $Path"
continue
}
}
$SelectedFileInfos += $FileInfos | Where-Object { $_.Name -match $FileNameRegex }
$FileInfos.Clear()
} }
end {
Return $SelectedFileInfos | Get-Unique
}
}
I found that both versions work if I remove the default parameter value. Why?
Why does passing a parameter via the pipeline cause an error when that parameter has a default defined in PSDefaultParameterValues, and is there a way to work around this?
Mathias R. Jessen provided the crucial pointer in a comment:
A parameter that is bound via an entry in the dictionary stored in the $PSDefaultParameterValues preference variable is bound before it is potentially bound via the pipeline, just like passing a parameter value explicitly, as an argument would.
Once a given parameter is bound that way, it cannot be bound again via the pipeline, causing an error:
The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.
As you can see, the specific problem at hand - a parameter already being bound - is unfortunately not covered by this message. The unspoken part is that once a given parameter has been bound by argument (possibly via $PSDefaultParameterValues), it is removed from the set of candidate pipeline-binding parameters the input could bind to, and if there are no candidates remaining, the error occurs.
The only way to override a $PSDefaultParameterValue preset value is to use an (explicit) argument.
This comment on a related GitHub issue provides details on the order of parameter binding.
A simplified way to reproduce the problem:
& {
# Preset the -Path parameter for Get-Item
# In any later Get-Item calls that do not use -Path explicitly, this
# is the same as calling Get-Item -Path /
$PSDefaultParameterValues = #{ 'Get-Item:Path' = '/' }
# Trying to bind the -Path parameter *via the pipeline* now fails,
# because it has already been bound via $PSDefaultParameterValues.
# Even without the $PSDefaultParameterValues entry in the picture,
# you'd get the same error with: '.' | Get-Item -Path /
'.' | Get-Item
# By contrast, using an *argument* allows you to override the preset.
Get-Item -Path .
}
What's happening here?!
This is a timing issue.
PowerShell attempts to bind and process parameter arguments in roughly* the following order:
Explicitly named parameter arguments are bound (eg. -Param $value)
Positional arguments are bound (abc in Write-Host abc)
Default parameter values are applied for any parameter that wasn't processed during the previous two steps - note that applicable $PSDefaultParameterValues always take precedence over defaults defined in the parameter block
Resolve parameter set, validate all mandatory parameters have values (this only fails if there are no upstream command in the pipeline)
Invoke the begin {} blocks on all commands in the pipeline
For any commands downstream in a pipeline: wait for input and then start binding it to the most appropriate parameter that hasn't been handled in previous steps, and invoke process {} blocks on all commands in the pipeline
As you can see, the value you assign to $PSDefaultParameterValues takes effect in step 3 - long before PowerShell even has a chance to start binding the piped string values to -Paths, in step 6.
*) this is a gross over-simplification, but the point remains: default parameter values must have been handled before pipeline binding starts.
How to work around it?
Given the procedure described above, we should be able to work around this behavior by explicitly naming the parameter we want to bind the pipeline input to.
But how do you combine -Paths with pipeline input?
By supplying a delay-bind script block (or a "pipeline-bound parameter expression" as they're sometimes called):
$Paths | Get-Files -Paths { $_ }
This will cause PowerShell to recognize -Paths during step 1 above - at which point the default value assignment is skipped.
Once the command starts receiving input, it transforms the input value and binds the resulting value to -Paths, by executing the { $_ } block - since we just output the item as-is, the effect is the exact same as when the pipeline input is bound implicitly.
Digging deeper
If you want to learn more about what happens "behind the curtain", the best tool available is Trace-Command:
$PSDefaultParameterValues['Get-Files:Paths'] = $PWD
Trace-Command -Expression { $Paths |Get-Files } -Name ParameterBinding -PSHost
I should mention that the ParameterBinding tracer is very verbose - which is great for surmising what's going on - but the output can be a bit overwhelming, in which case you might want to replace the -PSHost parameter with -PSPath .\path\to\output.txt to write the trace output to a file

What is the proper way to define a dynamic ValidateSet in a PowerShell script?

I have a PowerShell 7.1 helper script that I use to copy projects from subversion to my local device. I'd like to make this script easier for me to use by enabling PowerShell to auto-complete parameters into this script. After some research, it looks like I can implement an interface to provide valid parameters via a ValidateSet.
Based on Microsoft's documentation, I attempted to do this like so:
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidateSet([ProjectNames])]
[String]
$ProjectName,
#Other params
)
Class ProjectNames : System.Management.Automation.IValidateSetValuesGenerator {
[string[]] GetValidValues() {
# logic to return projects here.
}
}
When I run this, it does not auto-complete and I get the following error:
❯ Copy-ProjectFromSubversion.ps1 my-project
InvalidOperation: C:\OneDrive\Powershell-Scripts\Copy-ProjectFromSubversion.ps1:4
Line |
4 | [ValidateSet([ProjectNames])]
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Unable to find type [ProjectNames].
This makes sense since the class isn't defined until after the parameters. So I moved the class above the parameters. Obviously this is a syntax error. So how do I do this? Is it not possible in a simple PowerShell script?
Indeed, you've hit a catch-22: for the parameter declaration to work during the script-parsing phase, class [ProjectNames] must already be defined, yet you're not allowed to place the class definition before the parameter declaration.
The closest approximation of your intent using a stand-alone script file (.ps1) is to use the ValidateScript attribute instead:
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidateScript(
{ $_ -in (Get-ChildItem -Directory).Name },
ErrorMessage = 'Please specify the name of a subdirectory in the current directory.'
)]
[String] $ProjectName # ...
)
Limitations:
[ValidateScript] does not and cannot provide tab-completion: the script block, { ... }, providing the validation is only expected to return a Boolean, and there's no guarantee that a discrete set of values is even involved.
Similarly, you can't reference the dynamically generated set of valid values (as generated inside the script block) in the ErrorMessage property value.
The only way around these limitations would be to duplicate that part of the script block that calculates the valid values, but that can become a maintenance headache.
To get tab-completion you'll have to duplicate the relevant part of the code in an [ArgumentCompleter] attribute:
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidateScript(
{ $_ -in (Get-ChildItem -Directory).Name },
ErrorMessage = 'Please specify the name of a subdirectory in the current directory.'
)]
[ArgumentCompleter(
{
param($cmd, $param, $wordToComplete)
# This is the duplicated part of the code in the [ValidateScipt] attribute.
[array] $validValues = (Get-ChildItem -Directory).Name
$validValues -like "$wordToComplete*"
}
)]
[String] $ProjectName # ...
)

Is there a PowerShell equivalent for Python's doctest module?

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