Powershell String Length Validation - powershell

I created a really simple HelloWorld.ps1 Power-shell script which accepts a Name parameter, validates its length and then prints a hello message, for example if you pass John as Name, it's supposed to print Hello John!.
Here is the Power-shell script:
param (
[parameter(Mandatory=$true)]
[string]
$Name
)
# Length Validation
if ($Name.Length > 10) {
Write-Host "Parameter should have at most 10 characters."
Break
}
Write-Host "Hello $Name!"
And here is the command to execute it:
.\HelloWorld.ps1 -Name "John"
The strange behavior is every time that I execute it:
It doesn't perform validation, so it accepts Name parameters longer than 10 characters.
Every time I execute it, it creates and updates a file named 10 without any extension.
What's the problem with my script and How can I validate string length in PowerShell?

The problem - Using wrong operator
Using wrong operators is a common mistake in PowerShell. In fact > is output redirection operator and it sends output of the left operand to the specified file in the right operand.
For example $Name.Length > 10 will output length of Name in a file named 10.
How can I validate string length?
You can use -gt which is greater than operator this way:
if($Name.Length -gt 10)
Using ValidateLength attribute for String Length Validation
You can use [ValidateLength(int minLength, int maxlength)] attribute this way:
param (
[ValidateLength(1,10)]
[parameter(Mandatory=$true)]
[string]
$Name
)
Write-Host "Hello $Name!"

Related

Passing $args[x] in an if statement in .ps1 (PowerShell) does not seem to work when it does not exist

Suppose I have the following testArgsX.ps1 file:
$ARG_X_PARAMETER=1
If ($args[0] -ne "") {
echo ARGS[X]="`"$args[0]`""
$ARG_X_PARAMETER=$args[0]
}
echo ARG_X_PARAMETER=$ARG_X_PARAMETER
When I execute it either in a command prompt (C:\Test> powershell .\testArgsX.ps1) or in PowerShell prompt (PS C:\Test> .\testArgsX.ps1), it outputs the following:
ARGS[X]="[0]"
ARG_X_PARAMETER=
It seems that $args[0], in the if-condition, is not interpreted as a scalar value. Although I understand there are 2 ways to circumvent this problem (shown below). I want to know why it does not interpret it as a scalar value and if there is a way to fix it so that it does.
Take out the entire if statement (If ( ... ) { ... }) and replace it with Param ( [Int]$RUN_JUST_ONCE = 1 )
Change the if condition from If ($args[0] -ne "") { to If ($args.Length -gt 0) {
$args[0] is a scalar, but accessing a non-existent array element in PowerShell returns $null[1], and $null is indeed not equal to the empty string ("" or '').
Therefore, you should test for $null.
Note that testing for $null is best performed by placing $null on the LHS ($null -ne $args[0]), because on the RHS it would act as a filter instead of returning a Boolean in case the other operand happens to be an array.
Also, note that in order to use an expression such as $args[0] inside "...", an expandable (interpolating) string, you must use $(), the subexpression operator; without it, $args as a whole is expanded, and [0] is used verbatim - see this answer
echo is a built-in alias for the Write-Output cmdlet, whose explicit use is rarely needed; simply using a command or expression whose output is neither captured nor redirected is implicitly output.
To put it all together:
$ARG_X_PARAMETER=1
If ($null -ne $args[0]) {
"ARGS[X]=`"$($args[0])`""
$ARG_X_PARAMETER=$args[0]
}
"ARG_X_PARAMETER=$ARG_X_PARAMETER"
Alternatively, declare parameters for your script, which also makes it easy to assign default values:
param(
# This implicitly becomes the 1st positional parameter.
$ARG_X_PARAMETER = 1
)
"ARG_X_PARAMETER=$ARG_X_PARAMETER"
By default, parameters are positional, which means they can be used unnamed (e.g. testArgsX.ps1 42), though using the name explicitly is always an option (e.g.
testArgsX.ps1 -ARG_X_PARAMETER 42) - though a better name is the probably called for.
PowerShell optionally allows you to assign a data type to parameters and use a variety of attributes, notably ones for argument validation.
See about_Functions and about_Functions_Advanced_Parameters.
[1] With Set-StrictMode -Version 3 or higher in effect, accessing a non-existent element causes a statement-terminating error instead.

Can Powershell return complete interpolated values using convertfrom-stringdata

I have a .properties file with the following properties in them:
repository.host=hostname.com/nexus
repository.api.url=https://${repository.host}/service/rest/v1
repository.url=https://${repository.host}/repository
I am able to return the values using the following powershell function:
static [string] getProperty( [string] $property ){
$properties = "../resources/vars/$([jenkins]::PROPERTIES_FILE)"
$properties = get-content $properties | convertfrom-stringdata
return $properties.$property
}
When attempting to return the property repository.url powershell return this string: https://${repository.host}/repository/
My question is: Is it possible through features that already exist in powershell for the returned string to be https://hostname.com/nexus/repository/?
By design, for security reasons, ConvertFrom-StringData does not perform string expansion (interpolation) on its input.
Assuming you trust the input string[1], you can perform the expansion on demand, after having read the values from the file.
Note that use of ConvertFrom-StringData is problematic, as you've discovered, because the hashtable it creates invariably has unordered keys; that is, the order of the entries does not reflect the order in which the properties are defined in the file.
Therefore, processing the hashtable entries can make the on-demand expansion fail, if an out-of-order entry is processed before another entry whose value it needs for the expansion.
The solution is to roll your own ConvertFrom-StringData variant[2] that reads the properties into an ordered hashtable.
This additionally allows you to combine the read-from-file and expansion-on-demand tasks:
# Create a sample properties file.
#'
repository.host=hostname.com/nexus
repository.api.url=https://${repository.host}/service/rest/v1
repository.url=https://${repository.host}/repository
'# > sample.properties
# Parse the file and build an *ordered* hashtable from it.
$orderedHash = [ordered] #{}
switch -Regex -File sample.properties {
'^\s*#|^\s*$' { continue } # skip comment and blank lines.
default {
# Determine the key and value...
$key, $value = $_ -split '=', 2
# ... and create the entry while expanding ${...} references to previous
# entries.
$orderedHash[$key.Trim()] = $ExecutionContext.InvokeCommand.ExpandString((
$value.Trim() -replace '\$\{([^}]+)\}', '$$($$orderedHash[''$1''])'
))
}
}
# Output the result.
$orderedHash
Note the use of method $ExecutionContext.InvokeCommand.ExpandString to perform on-demand string expansion (interpolation); since this method isn't easy to discover, GitHub issue #11693 proposes that this functionality be surfaced as a proper, easily discoverable cmdlet named something like Expand-String or Expand-Template.
Note: In order to be able to use $ExecutionContext from the method of a PS custom class, you must explicitly reference it in the global scope via $global:ExecutionContext.
For more information about the regex-based -replace operator, see this answer.
The above yields (note that the input order was maintained):
Name Value
---- -----
repository.host hostname.com/nexus
repository.api.url https://hostname.com/nexus/service/rest/v1
repository.url https://hostname.com/nexus/repository
[1] Via $(), the subexpression operator, it is possible to embed arbitrary commands in the input strings.
[2] The code below does not replicate all features of ConvertFrom-String data, but it works with the sample input. While it does support skipping comment lines (those whose first non-whitespace character is a #) and blank lines, treating \ as escape characters and supporting escape sequences such as \n for a newline is not implemented.
The original solution provided by #mklement0 was very useful, and has guided me towards a more complete solution. This solution accomplishes/corrects a couple of things:
The ability to create the hashtable from a file source.
The ability to access the $ExecutionContext variable from within a class method, using the $global: scope.
The ability to thoroughly parse all keys within the hashtable.
static [string] getProperties ( [string] $file, [string] $property ){
$properties = get-content $file -raw | convertfrom-stringdata
while ( $properties.values -match '\$\{([^}]+)\}' ){
foreach ($key in #($properties.Keys)) {
$properties[$key] = $global:ExecutionContext.InvokeCommand.ExpandString( ($properties[$key] -replace '\$\{([^}]+)\}', '$$($$properties[''$1''])') )
}
}
return $properties[$property]
}
Note: When the while loop is not present and searching matches of ${*}, any given returned value may not be completely interpolated or expanded. As an example without the while loop present output from a file may look like this:
/nexus
${nexus.protocol}://${nexus.hostname}:${nexus.port}${nexus.context}
${nexus.protocol}://${nexus.hostname}:${nexus.port}${nexus.context}/repository/installers/com/amazon/java/8.0.252/java-1.8.0-amazon-corretto-devel-1.8.0_252.b09-1.x86_64.rpm
${nexus.protocol}://${nexus.hostname}:${nexus.port}${nexus.context}
${nexus.protocol}://${nexus.hostname}:${nexus.port}${nexus.context}/repository/installers/com/oracle/tuxedo/12.1.3.0.0/p30596495_121300_Linux-x86-64.zip
443
https://hostname.com:443/nexus
https://hostname.com:443/nexus/repository/installers/com/oracle/java/jdk/8u251/jdk-8u251-linux-x64.rpm
https://hostname.com:443/nexus/repository/installers/com/oracle/weblogic/12.2.1.3.0/p30965714_122130_Generic.zip
hostname.com
https
https://hostname.com:443/nexus/repository/installers/com/oracle/weblogic/12.2.1.3.0/p30965714_122130_Generic.zip
And if you were to run the same script again (still without the while loop) would look like this:
hostname.com
https://hostname.com:443/nexus
/nexus
https://hostname.com:443/nexus
https://hostname.com:443/nexus
https://hostname.com:443/nexus/repository/installers/com/oracle/weblogic/12.2.1.3.0/p30965714_122130_Generic.zip
https://${nexus.hostname}:443/nexus/repository/installers/com/oracle/java/jdk/8u251/jdk-8u251-linux-x64.rpm
https://hostname.com:443/nexus/repository/installers/com/oracle/tuxedo/12.1.3.0.0/p30596495_121300_Linux-x86-64.zip
https://hostname.com:443/nexus/repository/installers/com/amazon/java/8.0.252/java-1.8.0-amazon-corretto-devel-1.8.0_252.b09-1.x86_64.rpm
443
https
https://${nexus.hostname}:443/nexus/repository/installers/com/oracle/weblogic/12.2.1.3.0/p30965714_122130_Generic.zip
The reason for the sometimes incompletely interpolated/expanded strings is because hashtables are naturally unordered. With the introduction of the while loop, results will not be returned until all interpolated/expanded strings are resolved.
The official output would look as such:
hostname.com
https://hostname.com:443/nexus
/nexus
https://hostname.com:443/nexus
https://hostname.com:443/nexus
https://hostname.com:443/nexus/repository/installers/com/oracle/weblogic/12.2.1.3.0/p30965714_122130_Generic.zip
https://hostname.com:443/nexus/repository/installers/com/oracle/java/jdk/8u251/jdk-8u251-linux-x64.rpm
https://hostname.com:443/nexus/repository/installers/com/oracle/tuxedo/12.1.3.0.0/p30596495_121300_Linux-x86-64.zip
https://hostname.com:443/nexus/repository/installers/com/amazon/java/8.0.252/java-1.8.0-amazon-corretto-devel-1.8.0_252.b09-1.x86_64.rpm
443
https
https://hostname.com:443/nexus/repository/installers/com/oracle/weblogic/12.2.1.3.0/p30965714_122130_Generic.zip

Explain Bizarre Function Call Processing/Results

Can someone please explain how/why this code:
function DoIt($one, $two)
{
"`$one is $one"
"`$two is $two"
return $one * $two
}
$sum = DoIt 5 4
$sum
works exactly as expected/intended - i.e., it produces the following output:
$one is 5
$two is 4
20
However, this (seemingly almost identical) code:
function DoIt($one, $two)
{
"`$one is $one"
"`$two is $two"
return $one * $two
}
$sum = DoIt 5 4
"`$sum is $sum"
bends my brain and breaks my understanding of reality by producing the following output:
$sum is $one is 5 $two is 4 20
The reason for the odd behavior is that you are polluting the output stream. PowerShell doesn't explicitly have a pure "return" value like a compiled program, and relies on streams to pass data between functions. See about_Return for more information, as well as an example that illustrates this very behavior.
Basically the $sum variable gets all the output of the function, and as #TheMadTechnician says, it outputs differently depending on how it is being used.
The "correct" way in PowerShell to return values is in general not to use return but to use Write-Output to explicitly define which stream you want to output to. The second thing is that you have to use Write-Host when writing messages to the Host console, otherwise it gets returned on the Output stream as well.
function DoIt($one, $two)
{
Write-Host "`$one is $one"
Write-Host "`$two is $two"
Write-Output ($one * $two)
}
$sum = DoIt 5 4
"`$sum is $sum"
$one is 5
$two is 4
$sum is 20
There's good information in the existing answers, but let me try to bring it all together:
function DoIt($one, $two)
{
"`$one is $one"
"`$two is $two"
return $one * $two
}
produces three outputs ("return values") to PowerShell's success [output] stream (see about_Redirection), which are the evaluation results of:
expandable string "`$one is $one"
expandable string "`$two is $two"
expression $one * $two
The expandable strings are implicitly output - due to producing a result that is neither captured, sent to another command, nor redirected .
Similarly, the return in return $one * $two is just syntactic sugar for:
$one * $two # implicit output
return # control flow: exit the scope
Note:
return is unnecessary here and never required.
While Write-Output could be used in lieu of implicit output:
it is only ever helpful if you want output a collection as a single object, via its -NoEnumerate switch.
it is otherwise not only needlessly verbose, it slows things down.
If you want to print status information from a function without polluting the output stream, you have several choices:
Write-Host prints to the host's display (the console, if you're running in a console window); in PSv4-, such output could neither be captured nor suppressed; in PSv5+, Write-Host now writes to the information stream (number 6), which means that 6> can be used to redirect/suppress the output.
Write-Verbose is for opt-in verbose output, which you can activate by setting preference variable $VerbosePreference to 'Continue' or by using the -Verbose switch.
Write-Debug is for opt-in debugging output via $DebugPreference or -Debug, though note that in the latter case a prompt is displayed whenever a Write-Debug statement is encountered.
Because these outputs are captured in a variable - $sum = DoIt 5 4 - and there is more than 1 output, they are implicitly collected in an array (of type [object[]]).
Using implicit output to print this variable's value to the display - $sum - enumerates the array elements, which prints them each on their own line:
$one is 5
$two is 4
20
By contrast, using implicit output with expandable string "`$sum is $sum" results in a single output string in which the reference to variable $sum, which contains an array, is expanded as follows:
each element of the array is stringified
loosely speaking, this is like calling .ToString() on each element, but it's important to note that PowerShell opts for a culture-invariant string representation, if available - see this answer.
the results are joined with the value of (the rarely used automatic variable) $OFS as the separator to form a single string; $OFS defaults to a single space.
Thus, if you join array elements $one is 5, $two is 4, and 20 with a single space between them, you get:
$one is 5 $two is 4 20
To put it differently: the above is the equivalent of executing '$one is 5', '$two is 4', '20' -join ' '
This is due to how you are outputting an array, and has nothing to do with the function. You can replicate the same behavior with the following:
$sum='$one is 5','$two is 4',20
"`$sum is $sum"
Putting the variable in double quotes performs string expansion, which performs the .ToString() method on each item in the array, and then joins them with a space to convert the array into a string.
After playing around more (using the info I learned from #HAL9256), I discovered the problem isn't actually with using return vs Write-Output but rather with just how command-less strings are handled in functions. For example, this too works perfectly:
function DoIt($one, $two)
{
Write-Host "`$one is $one"
Write-Host "`$two is $two"
return ($one * $two)
}
$sum = DoIt 5 4
Write-Host "`$sum is $sum"
that is, it produces the following expected output:
$one is 5
$two is 4
$sum is 20
(And, apparently, the mathematical computation without explicit parentheses works just fine within a return command - unlike within a Write-Output command.)
In other words, I guess a static string in a function is treated as if you're building an array to be returned (as #TheMadTechnician was referring) as opposed to shorthand for a Write-Host command. Live and learn - "explicit is better than implicit". :-)
Granted, I still don't understand why the final output wasn't exactly the same (right or wrong) between the two code blocks in my original question ... but, hey, my brain is tired enough for one day. :-P
thanks again guys!!!

Expand a function in a here-string in powershell

I need to use the result of a function in some output, and i would like to streamline the script by expanding the function into the here-strings of powershell
# instead of
$result = myfunction
"The result is $result"
# syntax that would let me call the function inside the string
"the result is ?myfunction()"
I don't see any documentation regarding this behavior but i'd really appreciate having missed it rather than it not existing.
If it is not doable what alternatives do i have?
"the result is also " myfunction
...?
Use a subexpression for that:
"the result is $(myfunction)"
Other options are concatenating string and function output:
"the result is " + (myfunction)
or using the format operator:
"the result is {0}" -f (myfunction)

Is there a concise way to pass a few existing variables to a Powershell function as parameters?

I seem often to find myself in a situation where I have a bunch of variables set up and I want to pass them in to a function (for example, passing parameters to another function without modification). Currently I end up doing something like this:
Get-RelatedThing -CompanyTag $CompanyTag -ProjectTag $ProjectTag -EnvName $EnvName
This seems verbose and not hugely readable. I could use positional parameters, but that sacrifices clarity and compatibility. Precreating an array and splatting just creates vertical clutter rather than horizontal (except in the unusual case where I can do something like Get-RelatedThing #PSBoundParameters, but this has pitfalls).
What I'd really like is to be able to do something like:
Get-RelatedThing #(Get-Variable CompanyTag,ProjectTag,EnvName)
but Get-Variable returns an Object[] and the splatting operator can only parse a hashtable of named params (or an array of positional params). I stole a function to solve this problem:
function Get-VariablesAsHashtable {
[CmdletBinding()]
param
(
[Parameter(Position=0)]
[string[]] $Name
)
$ObjectArray = Get-Variable -Name #($Name)
$Hashtable = #{}
$ObjectArray | Foreach { $Hashtable[$_.Name] = $_.Value }
return $Hashtable
}
which works when run "in advance":
# works
$calculatedVars = (Get-VariablesAsHashtable ProjectTag,EnvName,ComponentTag);
(Get-RelatedThing #calculatedVars);
but NOT when run "inline":
# fails
(Get-RelatedThing #(Get-VariablesAsHashtable ProjectTag,EnvName,ComponentTag));
Get-InitialThing : Cannot process argument transformation on parameter 'ProjectTag'. Cannot convert value to type System.String.
Am I missing something that would fix this approach? Or is there a better way of doing this?
Actually, you are wrong in assumption that splatting works only for hashtables. It works perfectly fine with arrays too, but it uses each element of an array as positional parameter to called command. If you are sure that all values are there in the first place, you can simply splat the array:
function Test-Binding {
param (
$Foo,
$Bar
)
"Foo: $Foo"
"Bar: $Bar"
}
$Foo = 'first value'
$Bar = 'other value'
$Splat = #(Get-Variable -ValueOnly Foo, Bar)
Test-Binding #Splat
The inline syntax you've tried fails for very simple reason: you are NOT splatting in this case. What you really do in this line:
(Get-RelatedThing #(Get-VariablesAsHashtable ProjectTag,EnvName,ComponentTag));
... is passing single argument, that you force to be an array (that what #() stands for in this case), array of single hashtable (one that your function produces). There is no way to splat hashtable you are building "ad hoc". You need to build it first, store in variable x, and than pass this variable as your "splat" using #x literal, instead of usual $x.
I've heard suggestion to fill this gap (with syntax like : command ##{ foo = 'bar'}) but until than, you are forced to build your hashtable/ array first, and splat it using variable name and # prefix.
See if this doesn't work:
Get-RelatedThing #($(Get-Variable CompanyTag,ProjectTag,EnvName))
And you can actually simplify that some more with a different function. This shows the effect of splatting a hash table:
$Params = #{
CompanyTag = 'Company1'
ProjectTag = 'Project1'
EnvName = 'Env1'
}
[Scriptblock]::Create("$(&{$args}#Params)")
-EnvName: Env1 -CompanyTag: Company1 -ProjectTag: Project1
Modifying your original function to reproduce that:
function Get-VariablesAsParameters {
[CmdletBinding()]
param
(
[Parameter(Position=0)]
[string[]] $Name
)
$ObjectArray = Get-Variable -Name #($Name)
[string]($ObjectArray | Foreach { '-{0}: {1}' -f $_.Name,$_.Value})
}
function Get-RelatedThing {$args}
Get-RelatedThing $(Get-VariablesAsParameters CompanyTag,ProjectTag,EnvName)
-CompanyTag: Company1 -ProjectTag: Project1 -EnvName: Env1