Explain Bizarre Function Call Processing/Results - powershell

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!!!

Related

powershell: concatenate an extension to each element of an array [duplicate]

I have an array and when I try to append a string to it the array converts to a single string.
I have the following data in an array:
$Str
451 CAR,-3 ,7 ,10 ,0 ,3 , 20 ,Over: 41
452 DEN «,40.5,0,7,0,14, 21 ,  Cover: 4
And I want to append the week of the game in this instance like this:
$Str = "Week"+$Week+$Str
I get a single string:
Week16101,NYG,42.5 ,3 ,10 ,3 ,3 , 19 ,Over 43 102,PHI,- 1,14,7,0,3, 24 ,  Cover 4 103,
Of course I'd like the append to occur on each row.
Instead of a for loop you could also use the Foreach-Object cmdlet (if you prefer using the pipeline):
$str = "apple","lemon","toast"
$str = $str | ForEach-Object {"Week$_"}
Output:
Weekapple
Weeklemon
Weektoast
Another option for PowerShell v4+
$str = $str.ForEach({ "Week" + $Week + $_ })
Something like this will work for prepending/appending text to each line in an array.
Set array $str:
$str = "apple","lemon","toast"
$str
apple
lemon
toast
Prepend text now:
for ($i=0; $i -lt $Str.Count; $i++) {
$str[$i] = "yogurt" + $str[$i]
}
$str
yogurtapple
yogurtlemon
yogurttoast
This works for prepending/appending static text to each line. If you need to insert a changing variable this may require some modification. I would need to see more code in order to recommend something.
Another solution, which is fast and concise, albeit a bit obscure.
It uses the regex-based -replace operator with regex '^' which matches the position at the start of each input string and therefore effectively prepends the replacement string to each array element (analogously, you could use '$' to append):
# Sample array.
$array = 'one', 'two', 'three'
# Prepend 'Week ' to each element and create a new array.
$newArray = $array -replace '^', 'Week '
$newArray then contains 'Week one', 'Week two', 'Week three'
To show an equivalent foreach solution, which is syntactically simpler than a for solution (but, like the -replace solution above, invariably creates a new array):
[array] $newArray = foreach ($element in $array) { 'Week ' + $element }
Note: The [array] cast is needed to ensure that the result is always an array; without it, if the input array happens to contain just one element, PowerShell would assign the modified copy of that element as-is to $newArray; that is, no array would be created.
As for what you tried:
"Week"+$Week+$Str
Because the LHS of the + operation is a single string, simple string concatenation takes place, which means that the array in $str is stringified, which by default concatenates the (stringified) elements with a space character.
A simplified example:
PS> 'foo: ' + ('bar', 'baz')
foo: bar baz
Solution options:
For per-element operations on an array, you need one of the following:
A loop statement, such as foreach or for.
Michael Timmerman's answer shows a for solution, which - while syntactically more cumbersome than a foreach solution - has the advantage of updating the array in place.
A pipeline that performs per-element processing via the ForEach-Object cmdlet, as shown in Martin Brandl's answer.
An expression that uses the .ForEach() array method, as shown in Patrick Meinecke's answer.
An expression that uses an operator that accepts arrays as its LHS operand and then operates on each element, such as the -replace solution shown above.
Tradeoffs:
Speed:
An operator-based solution is fastest, followed by for / foreach, .ForEach(), and, the slowest option, ForEach-Object.
Memory use:
Only the for option with indexed access to the array elements allows in-place updating of the input array; all other methods create a new array.[1]
[1] Strictly speaking, what .ForEach() returns isn't a .NET array, but a collection of type [System.Collections.ObjectModel.Collection[psobject]], but the difference usually doesn't matter in PowerShell.

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.

How to return multiple values without it being cast as an array?

In powershell, I have a function where I want to return multiple values and use said values as positional arguments to a second function. Unfortunately, it is returning the group of values as an array. How do I avoid this?
Additionally, can I avoid this behavior without introducing new variables? I understand I could pass the returned array to a variable and splat it as arguments to the function, but I would like to avoid that if possible.
Code that demonstrates the issue I'm having is as follows:
function Return-Values{
return "One", "Two", "Three"
}
function Print-Args{
param($One,$Two,$Three)
Write-Host "1" $One
Write-Host "2" $Two
Write-Host "3" $Three
}
Print-Args (Return-Values)
The output is:
1 One Two Three
2
3
I expect the output to be:
1 One
2 Two
3 Three
You could use About Splatting. I was not able to get the function work as you wished. The following example does what you wish with one line more code. Maybe someone else knows another way.
function Return-Values{
return "One", "Two", "Three"
}
[System.Array]$InputArray = Return-Values #Get the input values as an array
function Print-Args{
param($One,$Two,$Three)
Write-Host "1" $One
Write-Host "2" $Two
Write-Host "3" $Three
}
Print-Args #InputArray #Use splatting for the input parameters

Powershell skip element in array, if it blank

I have a powershell script, where I receive names of elements as a variables from Jenkins:
$IISarray = #("$ENV:Cashier_NAME", "$ENV:Terminal_NAME", "$ENV:Content_Manager_NAME", "$ENV:Kiosk_BO_NAME")
foreach ($string in $IISarray){
"some code goes here"
}
Sometimes random elements can be blank. How can I add a check to see if the current element in array is blank, skip it and go to next element?
It's easiest to use -ne '' to created a filtered copy of the array that excludes empty entries, courtesy of the ability of many PowerShell operators to act as a filter with an array-valued LHS.
Note: I'm assuming you mean to filter out empty strings, not also blank (all-whitespace) ones, given that undefined environment variables expand to an empty string.
# Sample array with empty elements.
# Note: No need for #(...), unless there's just *one* element.
$IISarray = "foo", "", "bar", "baz", ""
# Note the `-ne ''`, which filters out empty elements.
foreach ($string in $IISarray -ne ''){
$string # echo
}
The above yields:
foo
bar
baz
soundstripe's answer offers a Where-Object solution, which potentially provides added flexibility via the ability to specify an arbitrary filter script block, but the use of a pipeline is a bit heavy-handed for this use case.
Fortunately, PSv4+ offers the .Where() collection method, which performs noticeably better.
Let me demonstrate it with a solution that also rules out blank (all-whitespace) elements:
# Note the all-whitespace element, which we want to ignore too.
PS> ("foo", " ", "bar", "baz", "").Where({ $_.Trim() })
foo
bar
baz
Similar to the Where-Object cmdlet, you pass a script block to the .Where() method, inside of which the automatic $_ variable represents the input element at hand.
The .Trim() method trims leading and trailing whitespace from a string and returns the result.
An all-whitespace string therefore results in the empty string.
In a Boolean context (as the .Where() method script block implicitly is), the empty string evaluates to $false, whereas any non-empty string is $true.
You can choose to be explicit, however ($_.Trim() -ne ''), or even use a .NET method ([string]::IsNullOrWhiteSpace($_)).
You can use Where-Object to filter out null or empty values. It is very commonly used, so ? is shorthand for Where-Object.
$IISarray = #("$ENV:Cashier_NAME", "$ENV:Terminal_NAME", "$ENV:Content_Manager_NAME", "$ENV:Kiosk_BO_NAME")
foreach ($string in ($IISarray | ? {$_})){
"some code goes here"
}
The $_ is an automatic variable representing each incoming object in the pipeline. Both $null and the empty string '' are falsy in Powershell, so only non-null values with length > 0 will be passed in to your for loop.
# you can skip the `#` and brackets as well as the quotation marks
$IISarray = $ENV:Cashier_NAME, $ENV:Terminal_NAME, $ENV:Content_Manager_NAME, $ENV:Kiosk_BO_NAME
foreach($String in $IISarray) {
# trim the strings and check the length
if($String.Trim().Length -gt 0) {
"some code goes here"
}
}

Why it needs an extra pair of bracket?

The following powershell script
"one","two","three" | % { "$(($l++)): $_" }
will print
1: one
2: two
3: three
However, after remove the bracket around $l++
"one","two","three" | % { "$($l++): $_" }
it will print
: one
: two
: three
This is because $l++ is a voidable statement. In powershell certain types of
expressions, when used as statements, are not displayed.
Voidable statements include assignments and the increment/decrement operators. When they are used in an expression, they return a value, but when they’re used as a standalone statement, they return no value. It is very well explained in Windows Powershell in Action by Bruce Payette:
The increment and decrement operators were almost not included in PowerShell
because they introduced a problem. In languages such as C and C#, when you use
one of these operators as a statement:
$a++
nothing is displayed. This is because statements in C and C# don’t return values. In
PowerShell, however, all statements return a value. This led to confusion. People
would write scripts like this:
$sum=0
$i=0
while ($i -lt 10) { $sum += $i; $i++ }
$sum
and be surprised to see the numbers 1 through 10 displayed. This was because $a++
returned a value and PowerShell was displaying the results of every statement. This
was so confusing that we almost removed these operators from the language. Then we
hit on the idea of a voidable statement. Basically, this means that certain types of
expressions, when used as statements, are not displayed. Voidable statements include
assignments and the increment/decrement operators. When they are used in an
expression, they return a value, but when they’re used as a standalone statement, they
return no value. Again, this is one of those details that won’t affect how you use PowerShell other than to make it work as you expect. (source: Windows Powershell in Action)
I believe this was a design decision made by the PowerShell team to avoid surprises due to PowerShell outputting return values. Many C/C# folks would expect the following function to only output 1 not #(0,1).
function foo {
$i = 0
$i++
$i
}
So the statement form $i++ doesn't output the value of $i before it is incremented. If you want that behavior, PowerShell allows you to get that behavior by putting the increment (or decrement) statement directly inside an expression e.g.:
function foo {
$i = 0
($i++)
$i
}
This will output #(0,1).
Bruce Payette discusses this in Chapter 5 of Windows PowerShell in Action 2nd Edition. The increment and decrement operators are "voidable statements". Quoting from the book:
Basically, this means that certain types of expressions, when used as
statements, are not displayed. Voidable statements include assignment
statements and the increment/decrement operators. When increment and
decrement are used in an expression, they return a value, but when
they’re used as a standalone statement, they return no value.
That is because $l++ doesn't return anything, :
$l = 0
$l++ #nothing
$l #gives 1
$l++ #nothing
($l++) #gives 2
This is done so that there is no confusion when you are returning to pipeline. Effectively,
$l++ is $l = $l+ 1, so it doesn't return anything.
What you want to see is $l = $l + 1; $l, which is why you have to do ($l++).
I think there are two tasks it needs to do:
Execute $l++ (increment the variable) which happens on the inner parenthesis
Retrieve $l's value which happens in the outer parenthesis.
Here is a illustration of this:
$l = 0
"$($l++ ; $l)"
Outputs: 1 where
$l = 0
"$($l++)"
Doesn't output anything.
In order to receive output I need two statements inside the sub-expression. Otherwise the only thing that happens is that $l is incremented but its value is not retrieved.