Win10 Powershell - Simple If/Elseif Depends on Condition Order? - powershell

I'm attempting to write a deployment script that checks the OS major version, then runs command based on that. I can grab that just fine with [System.Environment]::OSVersion.Version.Major, but when I attempt to use that in an if/elseif statement, I always get the first condition, and somehow the variable changes.
So the code I'm working with to test is this (using a Windows 10 machine):
$OS_VERSION=$([System.Environment]::OSVersion.Version.Major)
if ($OS_VERSION = 6) {
Write-Output "OS Version is $OS_VERSION"
# Do stuff...
} elseif ($OS_VERSION = 10) {
Write-Output "OS Version is $OS_VERSION"
# Do different stuff..
}
I noticed that when I switch the order of the conditions it runs as expected, and even more frustrating, is that the statement works perfectly fine on a Windows 7 machine.
Is this a bug/quirk of Powershell I'm missing, or am I doing something stupid?

I thought I'd write this out since I can explain it better than my comment. When you enter an expression without assigning it, it gets output to the pipeline. In this case
if ($OS_VERSION = 6) {
is an expression (since the if statement evaluates expressions for a boolean value to take action). If you wrap this in parentheses when entered at an interactive prompt, it'll output what the variable assigns at the same time as assigning the variable, much like
6 | Tee-Object -Variable OS_VERSION
would, or a -PassThru switch on some cmdlets:
> ($Test = 6)
>> 6
> $Test
>> 6
So what you're doing here is always assigning that variable to 6 which evaluates to true since if is truthy and non-zero is true. What you need is the -eq comparison operator:
if ($OS_VERSION -eq 6) {
More information can be found from the following command:
Get-Help -Name about_Comparison_Operators

PowerShell does not use = as a comparison operator.
If you want to compare for equality, the operator is -eq:
if ($OS_VERSION -eq 6) {
Write-Output "OS Version is $OS_VERSION"
# Do stuff...
} elseif ($OS_VERSION -eq 10) {
Write-Output "OS Version is $OS_VERSION"
# Do different stuff..
}
This will correct your problem. You should take a close look at Get-Help -Name about_Comparison_Operators (or read the link).

Related

Can I use regular expressions to search PowerShell command history via CTRL+R back-search?

I have run a series of PowerShell commands, and I'd like to search through the history, using the CTRL + R back search feature using regular expressions. Either it's not implemented right now, or I'm doing something wrong.
Imagine that you have a command in your history: ssh username#host.name
I would like to press CTRL+R and type ssh.*host to return this command onto my PowerShell prompt. Right now, this doesn't seem to work.
Question: Can I use regular expressions to search PowerShell command history via CTRL+R back-search?
Expected Result
The back search returns a result based on a regular expression.
back-i-search: <expression>
Actual Result
The back search fails.
failed-bck-i-search: <expression>
Yyyyeach, you can. The shell does not give you much aid on it, but nothing stops you from developing your own script that would emulate this behavior.
PSReadLine can come handy here. Check this out:
Set-PSReadlineKeyHandler -Chord Ctrl+r -ScriptBlock {
$currentInput = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref] $currentInput, [ref] $null)
$entries = [Microsoft.PowerShell.PSConsoleReadLine]::GetHistoryItems() | Where-Object CommandLine -match $currentInput | Select-Object CommandLine | ForEach-Object {$_.CommandLine.ToString()}
$pointer = $entries.Count - 1
while($pointer -ge 0 -and $pointer -lt $entries.Count) {
[Microsoft.PowerShell.PSConsoleReadLine]::BeginningOfLine()
[Microsoft.PowerShell.PSConsoleReadLine]::KillLine()
[Microsoft.PowerShell.PSConsoleReadLine]::Replace(0, 0, $entries[$pointer])
$userInp = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown").VirtualKeyCode
if($userInp -eq 38) {
$pointer -= 1;
} elseif($userInp -eq 40) {
$pointer += 1;
} elseif($userInp -eq 13) {
[Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
break;
} else {
break;
}
}
}
Usage
Type your expression
PS> ssh.*host
Press ctrl+r. The input should be replaced with the last entry matching the regex.
PS> ssh cobolcourse#mainserver.host
Now you can use up and down arrows to navigate the history. Press enter to accept or anything else to cancel.
How it works
The script just reads all of the PSReadLine history, filters it with what you need and feeds it to an array $entries. Then it scans for keys pressed to catch arrows to navigate through it and replaces the current prompt with the last visited entry.
It's very simple and most likely prone to edge-cases, but I think you can tinker on it further on. If you want to improve the performance, you can try to utilize pipelines, but then making it interactive will become more challenging. Anyways, good luck!

Powershell Switch Parameter to add to end of expression

Here's what I'm trying to do:
param([Switch]$myparameter)
If($myparamter -eq $true) {$export = Export-CSV c:\temp\temp.csv}
Get-MyFunction | $export
If $myparameter is passed, export the data to said location. Else, just display the normal output (in other words, ignore the $export). What doesn't work here is setting $export to the "Export-csv...". Wrapping it in quotes does not work.
I'm trying to avoid an if, then statement saying "if it's passed, export this. If it's not passed, output data"
I have a larger module that everything works in so there is a reason behind why I am looking to do it this way. Please let me know if any additional information is needed.
Thank you everyone in advance.
tl;dr:
param([Switch] $myparameter)
# Define the core command as a *script block* (enclosed in { ... }),
# to be invoked later, either with operator . (no child variable scope)
# or & (with child variable scope)
$scriptBlock = { Get-MyFunction }
# Invoke the script block with . (or &), and pipe it to the Export-Csv cmdlet,
# if requested.
If ($myparameter) { # short for: ($myparameter -eq $True), because $myparameter is a switch
. $scriptBlock | Export-Csv c:\temp\temp.csv
} else {
. $scriptBlock
}
TessellatingHeckler's answer is concise, works, and uses a number of advanced features cleverly - however, while it avoids an if statement, as requested, doing so may not yield the best or most readable solution in this case.
What you're looking for is to store a command in a variable for later execution, but your own attempt to do so:
If ($myparameter -eq $true) { $export = Export-CSV c:\temp\temp.csv }
results in immediate execution, which is not only unintended, but fails, because the Export-Csv cmdlet is missing input in the above statement.
You can store a snippet of source code for later execution in a variable via a script block, simply by enclosing the snippet in { ... }, which in your case would mean:
If ($myparameter -eq $true) { $export = { Export-Csv c:\temp\temp.csv } }
Note that what you pass to if is itself a script block, but it is by definition one that is executed as soon as the if condition is found to be true.
A variable containing a script block can then be invoked on demand, using one of two operators:
., the "dot-sourcing" operator, which executes the script block in the current scope.
&, the call operator, which executes the script block in a child scope with respect to potential variable definitions.
However, given that you only need the pipeline with an additional command if switch $myparameter is specified, it's better to change the logic:
Store the shared core command, Get-MyFunction, in a script block, in variable $scriptBlock.
Invoke that script block in an if statement, either standalone (by default), or by piping it to Export-Csv (if -MyParameter was specified).
I'm trying to avoid an if, then statement
Uh, if you insist...
param([Switch]$myparameter)
$cmdlet, $params = (('Write-output', #{}),
('Export-Csv', #{'LiteralPath'='c:\temp\temp.csv'}))[$myparameter]
Get-MyFunction | & $cmdlet #params

Converting VBScript's Imp Operator

If you google for PowerShell Imp you currently will find the VBScript-to-Windows PowerShell Conversion Guide on top of the list. Yet the answer is much of technical help:
Imp Operator
Definition: Performs a logical implication on two expressions.
What do we know about the Imp operator? Two things: 1) it’s not supported in Visual Basic .NET, and 2) we have never heard of anyone actually using it. Therefore, we’re going to say that it doesn’t really matter whether or not there’s a Windows PowerShell equivalent.
In fact, I used the Imp operator (also written as: A→B) a number of times in VBScript and likely would have used it in PowerShell if it existed.
Example 1
I want to copy any file that is not hidden except if it is set to be archived (it also needs to be copied).
A command for this would have been something like:
If (($File.Attributes -match "Hidden") -imp ($File.Attributes -match "Archive")) {Copy $File}
Example 2
I have a Log-Entry cmdlet with a Log-Debug alias. Similar to the native Write-Host/Write-Debug functions; when the Log-Debug alias is used, the information not be revealed but only be recorded when the common -Debug is supplied.
A command for this would have been something like:
If (($Noun -eq "Debug") -imp $MyInvocation.BoundParameters.Debug.IsPresent) {Add-content $LogFile $$Entry}
How can I build a logical implication operator with a minimum of code?
If you take the definition quiet literally, you will probably come to a statement like for a logical implication:
If ($Expression1) {If ($Expression2) {DoThis}} Else {DoThis}
Which might get quiet excessive as you need to execute twice the same function.
So, it is preferable to have a single If condition as replacement for the VBScript's Imp operator which made me come up with:
If (($True, [Bool]$Expression2)[[Bool]$Expression1]) {...}
But a little further investigation led me even to a much simpler replacement for the VBScript's Imp operator:
If (!$Expression1 -or $Expression2) {...}
Check
0..3 | ForEach {
$Expression1, $Expression2 = [Int]($_ / 2), [Int]($_ % 2)
New-Object PSObject -Property #{
Expression1 = [Bool]$Expression1
Expression2 = [Bool]$Expression2
Implication = !$Expression1 -or $Expression2
}
} | Format-Table -AutoSize
Truth Table
Expression1 Expression2 Implication
----------- ----------- -----------
False False True
False True True
True False False
True True True
Note: In this solution $Null expressions are considered $False. This differs from the VBScript Imp implementation but is consistent with other PowerShell operators that contain $Null expressions. e.g. The VBScript statement: If 1 And vbNull Then msgbox "True" Else msgbox "False", returns True where the PowerShell statement If (1 -And $Null) {"True"} Else {"False"}, returns False.
Bitwise
If you looking for a bitwise Imp operator (which should probably be called -bImp, if it existed), then it would be:
$Implication = -bNot Expression1 -bOr Expression2 # e.g.: -bNot 3 -bOr 5 = -3 (-bAnd 0xF = 13)

$null/empty interactive vs script difference

Why do the following 3 lines run without error from the PowerShell prompt, but return an error when run in a script (foo.ps1)? In both cases, $b -eq $null returns $true and $b.GetType() returns an error for invoking on $null, but there is something different about the $b in the interactive session.
$a = 1,2,3
[array]$b = $a | where {$false}
$b | where {$_.GetType()}
When run as script, the last line returns
You cannot call a method on a null valued expression.
I ran into this during ill-fated attempts to prevent array unrolling. Removing [array] makes the error go away, and I'll move on to trying to better understand the unrolling rules (I want $b to be an empty array, not $null), but I'd like to understand the reason for the difference here.
There is a perfect explanation
By typing [array] you tell the variable to be strongly typed. I suspect this line in .NET code, triggers the exception as it needs a type as a variable...
http://referencesource.microsoft.com/#mscorlib/system/array.cs,72
If you're running this from the ISE or from interactive, the variables are being saved. In your examples, I'm not sure why you're using Where-Object instead of %/ForEach-Object. Working on what I think you're attempting to do:
$a = #(1, 2, 3)
[Array]$b
$a | % { $b += $_ }
$b | % { $_.GetType() }

Is it possible to terminate or stop a PowerShell pipeline from within a filter

I have written a simple PowerShell filter that pushes the current object down the pipeline if its date is between the specified begin and end date. The objects coming down the pipeline are always in ascending date order so as soon as the date exceeds the specified end date I know my work is done and I would like to let tell the pipeline that the upstream commands can abandon their work so that the pipeline can finish its work. I am reading some very large log files and I will frequently want to examine just a portion of the log. I am pretty sure this is not possible but I wanted to ask to be sure.
It is possible to break a pipeline with anything that would otherwise break an outside loop or halt script execution altogether (like throwing an exception). The solution then is to wrap the pipeline in a loop that you can break if you need to stop the pipeline. For example, the below code will return the first item from the pipeline and then break the pipeline by breaking the outside do-while loop:
do {
Get-ChildItem|% { $_;break }
} while ($false)
This functionality can be wrapped into a function like this, where the last line accomplishes the same thing as above:
function Breakable-Pipeline([ScriptBlock]$ScriptBlock) {
do {
. $ScriptBlock
} while ($false)
}
Breakable-Pipeline { Get-ChildItem|% { $_;break } }
It is not possible to stop an upstream command from a downstream command.. it will continue to filter out objects that do not match your criteria, but the first command will process everything it was set to process.
The workaround will be to do more filtering on the upstream cmdlet or function/filter. Working with log files makes it a bit more comoplicated, but perhaps using Select-String and a regular expression to filter out the undesired dates might work for you.
Unless you know how many lines you want to take and from where, the whole file will be read to check for the pattern.
You can throw an exception when ending the pipeline.
gc demo.txt -ReadCount 1 | %{$num=0}{$num++; if($num -eq 5){throw "terminated pipeline!"}else{write-host $_}}
or
Look at this post about how to terminate a pipeline: https://web.archive.org/web/20160829015320/http://powershell.com/cs/blogs/tobias/archive/2010/01/01/cancelling-a-pipeline.aspx
Not sure about your exact needs, but it may be worth your time to look at Log Parser to see if you can't use a query to filter the data before it even hits the pipe.
If you're willing to use non-public members here is a way to stop the pipeline. It mimics what select-object does. invoke-method (alias im) is a function to invoke non-public methods. select-property (alias selp) is a function to select (similar to select-object) non-public properties - however it automatically acts like -ExpandProperty if only one matching property is found. (I wrote select-property and invoke-method at work, so can't share the source code of those).
# Get the system.management.automation assembly
$script:smaa=[appdomain]::currentdomain.getassemblies()|
? location -like "*system.management.automation*"
# Get the StopUpstreamCommandsException class
$script:upcet=$smaa.gettypes()| ? name -like "*StopUpstreamCommandsException *"
function stop-pipeline {
# Create a StopUpstreamCommandsException
$upce = [activator]::CreateInstance($upcet,#($pscmdlet))
$PipelineProcessor=$pscmdlet.CommandRuntime|select-property PipelineProcessor
$commands = $PipelineProcessor|select-property commands
$commandProcessor= $commands[0]
$ci = $commandProcessor|select-property commandinfo
$upce.RequestingCommandProcessor | im set_commandinfo #($ci)
$cr = $commandProcessor|select-property commandruntime
$upce.RequestingCommandProcessor| im set_commandruntime #($cr)
$null = $PipelineProcessor|
invoke-method recordfailure #($upce, $commandProcessor.command)
if ($commands.count -gt 1) {
$doCompletes = #()
1..($commands.count-1) | % {
write-debug "Stop-pipeline: added DoComplete for $($commands[$_])"
$doCompletes += $commands[$_] | invoke-method DoComplete -returnClosure
}
foreach ($DoComplete in $doCompletes) {
$null = & $DoComplete
}
}
throw $upce
}
EDIT: per mklement0's comment:
Here is a link to the Nivot ink blog on a script on the "poke" module which similarly gives access to non-public members.
As far as additional comments, I don't have meaningful ones at this point. This code just mimics what a decompilation of select-object reveals. The original MS comments (if any) are of course not in the decompilation. Frankly I don't know the purpose of the various types the function uses. Getting that level of understanding would likely require a considerable amount of effort.
My suggestion: get Oisin's poke module. Tweak the code to use that module. And then try it out. If you like the way it works, then use it and don't worry how it works (that's what I did).
Note: I haven't studied "poke" in any depth, but my guess is that it doesn't have anything like -returnClosure. However adding that should be easy as this:
if (-not $returnClosure) {
$methodInfo.Invoke($arguments)
} else {
{$methodInfo.Invoke($arguments)}.GetNewClosure()
}
Here's an - imperfect - implementation of a Stop-Pipeline cmdlet (requires PS v3+), gratefully adapted from this answer:
#requires -version 3
Filter Stop-Pipeline {
$sp = { Select-Object -First 1 }.GetSteppablePipeline($MyInvocation.CommandOrigin)
$sp.Begin($true)
$sp.Process(0)
}
# Example
1..5 | % { if ($_ -gt 2) { Stop-Pipeline }; $_ } # -> 1, 2
Caveat: I don't fully understand how it works, though fundamentally it takes advantage of Select -First's ability to stop the pipeline prematurely (PS v3+). However, in this case there is one crucial difference to how Select -First terminates the pipeline: downstream cmdlets (commands later in the pipeline) do not get a chance to run their end blocks.
Therefore, aggregating cmdlets (those that must receive all input before producing output, such as Sort-Object, Group-Object, and Measure-Object) will not produce output if placed later in the same pipeline; e.g.:
# !! NO output, because Sort-Object never finishes.
1..5 | % { if ($_ -gt 2) { Stop-Pipeline }; $_ } | Sort-Object
Background info that may lead to a better solution:
Thanks to PetSerAl, my answer here shows how to produce the same exception that Select-Object -First uses internally to stop upstream cmdlets.
However, there the exception is thrown from inside the cmdlet that is itself connected to the pipeline to stop, which is not the case here:
Stop-Pipeline, as used in the examples above, is not connected to the pipeline that should be stopped (only the enclosing ForEach-Object (%) block is), so the question is: How can the exception be thrown in the context of the target pipeline?
Try these filters, they'll force the pipeline to stop after the first object -or the first n elements- and store it -them- in a variable; you need to pass the name of the variable, if you don't the object(s) are pushed out but cannot be assigned to a variable.
filter FirstObject ([string]$vName = '') {
if ($vName) {sv $vName $_ -s 1} else {$_}
break
}
filter FirstElements ([int]$max = 2, [string]$vName = '') {
if ($max -le 0) {break} else {$_arr += ,$_}
if (!--$max) {
if ($vName) {sv $vName $_arr -s 1} else {$_arr}
break
}
}
# can't assign to a variable directly
$myLog = get-eventLog security | ... | firstObject
# pass the the varName
get-eventLog security | ... | firstObject myLog
$myLog
# can't assign to a variable directly
$myLogs = get-eventLog security | ... | firstElements 3
# pass the number of elements and the varName
get-eventLog security | ... | firstElements 3 myLogs
$myLogs
####################################
get-eventLog security | % {
if ($_.timegenerated -lt (date 11.09.08) -and`
$_.timegenerated -gt (date 11.01.08)) {$log1 = $_; break}
}
#
$log1
Another option would be to use the -file parameter on a switch statement. Using -file will read the file one line at a time, and you can use break to exit immediately without reading the rest of the file.
switch -file $someFile {
# Parse current line for later matches.
{ $script:line = [DateTime]$_ } { }
# If less than min date, keep looking.
{ $line -lt $minDate } { Write-Host "skipping: $line"; continue }
# If greater than max date, stop checking.
{ $line -gt $maxDate } { Write-Host "stopping: $line"; break }
# Otherwise, date is between min and max.
default { Write-Host "match: $line" }
}