In validateset is it possible to pass the contents of the file? - powershell

In my script , I am passing list of components need to taken build
[ValidateSet("InstalComponent","CoreComponent","SDKComponent","ServiceComponent","TimeComponent")]
$Components=#("InstalComponent","CoreComponent"),
From time to time we are adding new component and the list is too long. If there is a way like passing via file "GEt-content Components.txt". It will be of good. Is there any way to set like this?

One option is to create an [enum] from the file contents, then cast the parameter as that type:
#(
"InstalComponent",
"CoreComponent",
"SDKComponent",
"ServiceComponent",
"TimeComponent") |
set-content Components.txt
Add-Type -TypeDefinition #"
public enum MyComponents
{
$(((Get-Content .\Components.txt) -join ', ').split())
}
"#
function test
{
param ( [MyComponents[]]$Components)
$Components
}
Creating and using enums in Powershell

I may not quite understand the question, but you can validate a parameter using ValidateScript like this:
Create a file "Components.txt" with a line for each option on separate lines, with no quotes or commas. Then validate like this:
param(
[ValidateScript({
$Components = Get-Content .\Components.txt
$Components.Contains($_)
})
]
[String]$Component)
...

Short answer: no.
Longer answer: If you must validate a parameter using a configurable set of values you can still do it the old way by putting the validation at the beginning of the function body:
function Foo {
Param(
[Parameter()]$Bar
)
$validSet = Get-Content 'C:\path\to\components.txt'
if ($validSet -notcontains $Bar) {
throw "Invalid argument for parameter -Bar: $Bar"
}
...
}

Related

Add custom powershell parameter attribute to enable filtering

I'm writing a function that's basically a convenience wrapper around an external program that it'll invoke. Several of the parameters will be forwarded to the external program, but not all.
I was writing
$ArgsToFwd = #()
switch ($PSBoundParameters.Keys) {
'--server' {$ArgsToFwd += #('--server',$server)}
'--userid' {$ArgsToFwd += #('--userid',$userid)}
...
}
but then I thought it might be better to define a custom parameter attribute that could let me do something like:
params(
[Parameter()]
[IsExternal()]
[string]$Server
)
#...
foreach ($key in $PSBoundParameters.Keys) {
if (<#a test for the custom [IsExternal] attribute#>) {
$ArgsToFwd += #($key, $PSBoundParameters[$key])
}
}
But I can't quite figure it out. Can that be done?
If you want to use custom attributes, there are 3 things you need to do:
Define a custom Attribute type
Decorate your function's parameters with instances of said attribute
Write a mechanism to discover the attribute decorations and do something with them at runtime
Let's start with step 1, by defining a custom class that inherits from System.Attribute:
class ProxyParameterAttribute : Attribute
{
[string]$Target
ProxyParameterAttribute([string]$Target){
$this.Target = $Target
}
}
Attribute annotations map directly back to the target attribute's constructor, so in this case, we'll be using it like [ProxyParameter('someValue')] and 'someValue' will then be stored in the $Target property.
Now we can move on to step 2, decorating our parameters with the new attribute. You can omit the Attribute part of the name when applying it in the param block, PowerShell is expecting all annotations to be attribute-related anyway:
function Invoke-SomeProgram
{
param(
[ProxyParameter('--server')]
[Parameter()]
[string]$Server
)
# ... code to resolve ProxyParameter goes here ...
}
For step 3, we need a piece of code that can discover the attribute annotations on the parameters of the current command, and use them to map the input parameter arguments to the appropriate target names.
To discover declared parameter metadata for the current command, the best entry point is $MyInvocation:
# Create an array to hold all the parameters you want to forward
$argumentsToFwd = #()
# Create mapping table for parameter names
$paramNameMapping = #{}
# Discover current command
$currentCommandInfo = $MyInvocation.MyCommand
# loop through all parameters, populate mapping table with target param names
foreach($param in $currentCommandInfo.Parameters.GetEnumerator()){
# attempt to discover any [ProxyParameter] attribute decorations
$proxyParamAttribute = $param.Value.Attributes.Where({$_.TypeId -eq [ProxyParamAttribute]}, 'First') |Select -First 1
if($proxyParamAttribute){
$paramNameMapping[$param.Name] = $proxyParamAttribute.Target
}
}
# now loop over all parameter arguments that were actually passed by the caller, populate argument array while taking ProxyParameter mapping into account
foreach($boundParam in $PSBoundParameters.GetEnumerator()){
$name = $boundParam.Name
$value = $boundParam.Value
if($paramNameMapping.ContainsKey[$name]){
$argumentsToFwd += $paramNameMapping[$name],$value
}
}
Now that the parameters how been filtered and renamed appropriately, you can invoke the target application with the correct arguments via splatting:
.\externalApp.exe #argumentsToFwd
Putting it all together, you end up with something like:
class ProxyParameterAttribute : Attribute
{
[string]$Target
ProxyParameterAttribute([string]$Target){
$this.Target = $Target
}
}
function Invoke-SomeProgram
{
param(
[ProxyParameter('--server')]
[Parameter()]
[string]$Server
)
# Create an array to hold all the parameters you want to forward
$argumentsToFwd = #()
# Create mapping table for parameter names
$paramNameMapping = #{}
# Discover current command
$currentCommandInfo = $MyInvocation.MyCommand
# loop through all parameters, populate mapping table with target param names
foreach($param in $currentCommandInfo.Parameters.GetEnumerator()){
# attempt to discover any [ProxyParameter] attribute decorations
$proxyParamAttribute = $param.Value.Attributes.Where({$_.TypeId -eq [ProxyParamAttribute]}, 'First') |Select -First 1
if($proxyParamAttribute){
$paramNameMapping[$param.Name] = $proxyParamAttribute.Target
}
}
# now loop over all parameter arguments that were actually passed by the caller, populate argument array while taking ProxyParameter mapping into account
foreach($boundParam in $PSBoundParameters.GetEnumerator()){
$name = $boundParam.Name
$value = $boundParam.Value
if($paramNameMapping.ContainsKey[$name]){
$argumentsToFwd += $paramNameMapping[$name],$value
}
}
externalApp.exe #argumentsToFwd
}
You can add other properties and constructor arguments to the attributes to store more/different data (a flag to indicate whether something is just a switch, or a scriptblock that transforms the input value for example).
If you need this for multiple different commands, extract the step 3 logic (discovering attributes and resolving parameter names) to a separate function or encapsulate it in a static method on the attribute class.

Powershell call class on a string?

Trying to call this class to reverse a string in powershell, but can't figure out how to call it.. very simple question but not sure how to do it
class Reverse{
[string]$Stringy
[string]Reversi(){
return $(this.Stringy[-1..-$Stringy.length] -join "")
}
}
Then i am not sure how to call this class. By itself, the return statement does the job, but i need to call the class, please help, no hints just point to the code there is no help online.
My poor attempt to answer this question, as Abraham pointed out in his comment, you need to create an instance of the class:
class ReverseClass {
[string]$Stringy
ReverseClass([string]$String){
$this.Stringy = $String
}
[string]Reverse(){
return -join $this.Stringy[-1..-$this.Length()]
}
[int]Length(){
return $this.Stringy.Length
}
}
$z = [ReverseClass]'Hello World!'
$z.Reverse() # => !dlroW olleH
Again, I insist, a class is an overkill for what you need to do:
function Reverse-String([string]$String){
-join $String[-1..-$String.Length]
}
Reverse-String 'Hello world!' # => !dlrow olleH
If this class was stored on a file like C:\users\user\my documents\script.ps1 and you wanted to load the class or function to your current session you would need to dot source it (simply add a dot before the path). This will load all everything, classes, functions, variables, etc to your current scope.
For example:
If your current directory is not where the script is stored:
. "C:\users\user\my documents\script.ps1"
If you changed directory to my documents in this case:
. .\script.ps1
Are you sure you need a class with an instance method?
If you change the method to be static instead, you can pass whatever string value as a method parameter and get your result without having to create an instance of the class:
class ReverseClass {
static [string] Reverse([string]$string){
return -join $string[-1..-$string.Length]
}
}
Now you can call it using the static member operator :::
PS ~> [ReverseClass]::Reverse("hello")
olleh
Continuing from my comment.
This...
class Reverse{
[string]$Stringy
[string]Reversi(){
return $(this.Stringy[-1..-$Stringy.length] -join "")
}
}
...is not a PowerShell function. This is how you declare a function in PowerShell.
<#
.Synopsis
Short description
.DESCRIPTION
Long description
.EXAMPLE
Example of how to use this
.EXAMPLE
Another example of how to use this
#>
function Verb-Noun
{
[CmdletBinding()]
[Alias()]
[OutputType([int])]
Param
(
[string]$Param1,
[int]$Param2
)
}
# calling the function
Verb-Noun -Param1 '' -Param2 ''
For strings, there is no reason to explicitly cast them as strings. Anything thing in quotes (single or double), is automatically seen as a string.
This is all documented in the PowerShell help files...
Get-Help -Name About_functions
... and MS Docs.
Though you can do classes in PowerShell v5 and higher. As documented in the Powershell help files...
Get-Help -Name About_classes
... and MS Docs.
class Reverse{
[string]$Stringy
[string]Reversi(){
return $this.Stringy[-1..-$this.Stringy.length] -join ""
}
}
PS C:> $reverse=New-Object Reverse -Property #{Stringy="Hello World"}
PS C:> $reverse.Reversi()
dlroW olleH

How to define the return type / OutputType of a function

Why is the following changing type?
function SomeFunction($SomeParameter){
return $SomeParameter
}
I guess I need to set a return type, but how?
An example is using:
$NewFolder=Join-Path $CurrentFolder -ChildPath $FolderName
$Tmp=SomeFunction($NewFolder)
Now $Tmp is an array and not just a path
While this answer explains the behavior you're seeing, here I will attempt to answer the actual question: how to declare the expected output type of a function!
You do so by adding an [OutputType] attribute to the param() block of your function - so the first thing you'll want to do is to skip the C#-style param list and declare a proper param block instead:
function SomeFunction
{
param($SomeParameter)
return $SomeParameter
}
Now we just need to add the [OutputType] attribute decorator:
function SomeFunction
{
[OutputType([string])]
param($SomeParameter)
return $SomeParameter
}
since we're just returning the parameter argument value as-is in this example, we should play nice and make sure it's actually also a string:
function SomeFunction
{
[OutputType([string])]
param(
[string]$SomeParameter
)
return $SomeParameter
}
Worth noting that [OutputType()] makes no guarantees as to the type of objects emitted during execution, it's simply a way for the author of a function to indicate the intended output type.
Read more about [OutputType] in the about_Functions_OutputTypeAttribute help file
Your issue is per "design". PowerShell will return an array in chunks so that it can be forwarded the PowerShell pipeline.
Example:
SomeFunction -SomeParameter #(1,2,3,4) | Where-Object { $_ -gt 2 }
Without this behavior pipelining the output of the function to another function/cmdlet won't be possible.
If you want to return an array you can change to code to:
function SomeFunction($SomeParameter){
<#
# Through the unary operator we can return an array with one entry.
# This entry contains the original array.
#>
,$SomeParameter
}
Another option would be the use of #() when at the calling side:
function SomeFunction($SomeParameter){
# return to pipelin
$SomeParameter
}
$array = #(SomeFunction -SomeParameter 1,2,3,4)
There is also this reddit answer explaining the behavior in more detail.
Hope that helps.

PowerShell parameter sets or dynamic parameters

I'm trying to figure out which method would work best for the following situation.
Example function:
Set-APICredentials {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$APIUser,
[Parameter(Mandatory)]
[string]$APIKey,
[Parameter(Mandatory)]
[string]$PFXFile,
[Parameter(Mandatory)]
[string]$PFXPassword,
[switch]$AsVariable
)
begin{
$PFXPath = (Get-ChildItem -Name $PFXFile).FullName
}
process{
#create basic auth header from APIUser and APIKey
$basicAuthHeader
#create certificate object, verify private key, convert back into PFX Collection as bytes string variable
$clientAuthCertRaw
#create hashtable with credentials
$credentials = #{
basicAuthHeader = $basicAuthHeader
clienAuthCertRaw = $clientAuthCertRaw
}
}
end{
if ($AsVariable) {
Sglobal:APICreds = $credentials
} else {
Export-Clixml -InputObject $credentials -Path $PSScriptRoot\APICredentials.xml
}
}
}
If (Test-Path -Path $PSScriptRoot\APICredentials.xml) is true and -AsVariable is specified then no other parameters are needed/used.
Otherwise if (Test-Path -Path $PSScriptRoot\APICredentials.xml) is false then everything previously stated as mandatory are required.
Is there some way to create a conditional parameter set?
Should I just create two parameter sets and error out if the previously stated logic is false? Or should I set -AsVariable as a parameter and handle the rest with dynamic parameters?
Because in most cases everything is mandatory and its only under special circumstances that -AsVariable is used on its own. I figured that configuring everything else as dynamic parameters would be wrong.
Dynamic Parameters are the strict way to handle exactly what you want to do, but I don't think they're worth the effort in most cases.
The most straightforward way to handle this is not to have the string params mandatory and just do the check at the beginning of the function.
You have a process block but the example params don't take pipeline input. If the function is taking pipeline input consider where you put that check; probably in the begin block but it will depend on the params maybe?
I was also toying around with abusing [ValidateScript({})] for this, but it's not quite going to work, because if you add it on the [switch], you can't access/check the other params to check their values, and if you put it on the conditionally mandatory values (to check for the existence of the file) it will only run the validation when the parameter is bound.
This also feels a little bit like a code smell? Curious about your use case.

Can I use a constant in the ValidateSet attribute of a PowerShell function parameter?

I am using the ValidateSet attribute on one of my PowerShell function parameters like so:
[ValidateSet('Development','Test','Production')]
[string]$Context
I have repeated this is many places throughout a scripting project. Can these literal strings be replaced with a constant?
No, it has to be a literal or a scriptblock. The scriptblock option seems pointless since it seems to use the literal (string) value of the scriptblock instead of executing it.
So effectively, from my testing, you must use literals.
If you use a dynamic parameter instead you could achieve this, but that's way overkill just to be DRY.
If you try to use a variable, it won't work (and ISE will give you the red squiggly). The help text erroneously says it must be a constant, but it means literal.
I created a constant with:
Set-Variable -Option Constant
And it still does not work.
Adding this to help others searching for a similar solution. I was looking for a way to validate parameters against the keys of a global hash table. This is what I ended up doing:
$global:MyHash = #{
"anyitem" = #{"name" = "somename1"; "count" = 42 };
"someitem" = #{"name" = "another name"; "count" = 1337 };
}
function Test-Hash
{
param
(
[Parameter(mandatory = $true)] [ValidateScript( { $_ -in $global:MyHash.Keys } )] [string[]] $items
)
}
Test-Hash -items anyitem, someitem
I ended up replacing ValidateSet with ValidateScript as I realized (as mentioned in this thread as well) that the code block in ValidateSet does not work at all. Instead validating against the keys of a hash table one could easily use something like
$validParams = #('opt1', 'opt2')
and in the ValidateScript codeblock
{ $_ -in $validParams }
This is basically what I assume should answer the question.