Powershell where-object return code - windows

I'm returning to powershell from bash after a long time, and I've found the where object behavior to be quite confusing.
Why does the following snippet return success? Nothing is found! Why does this not return failure like a grep would?
C:> Get-Process | ?{$_.name -like "laksdjfajsdfkjasdkf"}
C:> echo $?
True

tl;dr
# Run the command and, in addition to outputting to the console,
# collect the results in variable $result, via common parameter -OutVariable / -ov
# If you do NOT need to output to the console, simply use:
# $result = Get-Process | ...
Get-Process | ? { $_.name -like "laksdjfajsdfkjasdkf" } -ov result
# Test if the result is empty (using implicit Boolean conversion)
if (-not $result) { Write-Warning "Nothing matched." }
PowerShell's automatic (Boolean) $? variable in PowerShell is not the (abstract) equivalent of exit codes in traditional shells, as PetSerAl points out.
$? just tells you whether the last statement succeeded and the rules surrounding it are complicated, as the GitHub discussion that Owain Esau links to shows.
Succeeded means that no errors occurred, and a filtering operation not returning anything is success by that definition.
In short: $? is of limited usefulness in PowerShell.
However, the exit code of the most recently executed external program is reflected in automatic variable $LASTEXITCODE, so had you actually invoked grep, its exit code would be reflected there.
(And while $? is set immediately after execution of an external program to reflect $True if the exit code was 0 and $False otherwise, $? may already reflect something else by the time the statement finishes, depending on the specifics of the statement, such as enclosing the call in (...))
In the case at hand you're looking to determine whether the filtering operation performed by the call to the Where-Object cmdlet (invoked via its built-in alias ?) returned any matches, but in PowerShell that status is not reflected anywhere separately.
Therefore, you must examine the output itself to determine whether anything matched, as shown in the snippet at the top.
There are no errors in this scenario, but for the sake of completeness:
PowerShell's error handling is sophisticated, but complex, and again unlike that of traditional shells; you can find an overview here.

Related

How to get a .BIN extension for a file name in a cmd, obtained from a variable off a text file (using powershell) [duplicate]

Test-NetConnection returns TRUE when run manually but when in a looping script, only some of the ports returns TRUE.
I wrote a powershell script that loops through port numbers to do a Test-NetConnection:
$machine = '[targetmachinename]'
$this_machine = $env:COMPUTERNAME
$port_arr = #(8331, 8332, 8333, 8334, 8335, 8310, 8311)
foreach ($port in $port_arr) {
Test-NetConnection $machine.domain.name.com -port $port -InformationLevel Quiet
}
When I run the script, it always returns TRUE on the same two port numbers and returns FALSE on the other ports.
When I manually run the code for each port, they each come back as TRUE for all ports.
I have tried messing around with the port numbers by removing, adding, and moving them around but it always gives the same results with only the same two port numbers returning TRUE.
I suspected maybe the variable, array, foreach loop or something might be bad, but if that was the case, why would it work for the same two ports and not for the others even when I change up the array?
I was thinking about putting a delay or wait in between loops but have not tested it yet.
This script works fine when run locally from the target machine. Having this issue when running the script from another machine.
UPDATE:
Looking at the powershell log:
Command start time: 20191111121539
**********************
PS>TerminatingError(New-Object): "Exception calling ".ctor" with "2" argument(s): "No connection could be made because the target machine actively refused it [IPADDRESS]:[PORT]""
I noticed that the IPADDRESS does not match up with the target machine name, but instead matches up with the source machine.
I replaced the $machine.domain.name.com to the actual ip address of the machine and that got the script working as expected.
Why does $machine.domain.name.com resolve to the source machine? Even if I concatenate that incorrectly, wouldn't that normally become an unresolved address and error? Shouldn't all port checks have failed at that point?
tl;dr
Replace argument
$machine.domain.name.com
with
"$machine.domain.name.com"
While unquoted command arguments in PowerShell are typically treated as expandable strings - i.e., as if they were implicitly enclosed in "...", this is not the case if your argument starts with a variable reference such as $machine.
In that case, PowerShell tries to evaluate the argument as an expression, and since [string] variable $machine has no .domain property (and subsequent nested properties), the entire argument effectively evaluates to $null[1] - resulting in inadvertent targeting of the local machine by Test-NetConnection.
The subtleties around how PowerShell parses unquoted command arguments:
are explored in this answer.
what the design rationale behind these subtleties may be is the subject of this GitHub issue.
Conversely, to learn about how expandable strings (string interpolation) - variable references and expressions embedded in "..." - work in PowerShell,
see this answer.
Additionally, BACON observes the following regarding the use of -InformationLevel Quiet with Test-NetConnection:
I think passing -InformationLevel Quiet was actively impairing debugging in this case. Given $machine = 'foo', compare the output (particularly the ComputerName property) of:
Test-NetConnection $machine.domain.name.com -InformationLevel Quiet
vs.
Test-NetConnection $machine.domain.name.com
vs.
Test-NetConnection "$machine.domain.name.com".
In other words, [it's best to] ensure that the cmdlet (and its parameters) is behaving as expected before passing the parameter that says "I don't care about all that information. Just tell me if it passed or failed."
[1] $null is the effective result by default or if Set-StrictMode -Version 1 is in effect; with Set-StrictMode -Version 2 or higher, you would actually get an error.
A common mistake I've seen people make (myself included) is in your variable name and usage in powershell. For example I forgot $ all the time. This is just looping through my machine as an example, but it tests all these ports correctly.
$port_arr = #(139,3389,5040)
$mac = #("myComputer")
foreach ($mc in $mac){
foreach ($i in $port_arr) {
Test-NetConnection $mc -port $i
}
}
Do you have an example of your powershell code? Also, have you stepped through to determine that it's working as expected?

Repeating character for divider line in PowerShell

This is just a question of curiosity to better learn how PowerShell works. Why can I put the repeating string into a variable, but not put it in a Write-Host statement? The purpose is to print a row of dashes as a divider.
$divider = "-"*25
Write-Host $divider
Write-Host "-"*25
Write-Host $("-"*25) #is this the best way to do it with a var?
Results Window:
-------------------------
- *25
-------------------------
In, Write-Host "-"*25, it is evaluated as an argument to the function instead of an expression. So, parenthesis need to be used to get it as an expression first and then result of this expression passed as an argument to Write-Host.
Check the about Parsing

PowerShell command fails after variable is used

I'm trying to run a command to gather use profiles with certain requirements, here's my code:
#$numberOfDays = 30
#$numberOfDays
$profileStructsToRemove = Get-CimInstance Win32_UserProfile |
Where-Object {$_.LastUseTime -lt $(Get-Date).Date.AddDays(-$numberOfDays) } |
Where-Object {$_.LocalPath.ToUpper() -ne 'C:\USERS\ADMINISTRATOR'} |
Where-Object {$_.LocalPath.ToUpper() -ne 'C:\USERS\SOME_PROFILE_TO_KEEP'} |
Where-Object {$_.LocalPath.ToUpper() -ne 'C:\USERS\PUBLIC'}
$profileStructsToRemove #print
I use the $numberOfDays variable to determine the number of days subtracted from today's date that I want to use as the filter. Right now it's commented out, and the command succeeds, although since $numberOfDays isn't defined I assume it's using a null value? I'm not really sure but it works that way...
However, when I assign $numberOfDays to 30, it fails to populate the variable $profileStructsToRemove with ANYTHING at all. It just utterly fails. I could really use some input on why this is happenening.
How is the command working when $numberOfDays isn't defined? Is it just a null value, or treating it as 0 for the AddDays function?
Why is this command failing once $numberOfDays is assigned a value?
gms0ulman's helpful answer answers question #1 well (converting $null to an [int] yields 0).
As for question #2:
At least on Windows 10 on a non-domain machine, the .LastUseTime property seemingly always returns the current date and time, which (a) makes it useless and (b) explains why you didn't see any results.
You could try to check the .LastDownloadTime instead, if that field has a value in your case - I presume it would only have a value if the profiles are roaming profiles.
For reference, here's the full list of available time stamps:
LastAttemptedProfileDownloadTime, LastAttemptedProfileUploadTime, LastBackgroundRegistryUploadTime, LastDownloadTime, LastUploadTime, LastUseTime.
As for how to optimize the code in your question in general:
PowerShell string operators are case-insensitive by default, so there's no need for .toUpper().
You could combine your multiple Where-Object calls into a single one, and you can use
-notin with an array of paths on the RHS, instead of using -ne with individual paths.
To put it all together (PSv3+; keeping in mind that comparing against .LastUsedTime may be pointless):
$profileStructsToRemove = Get-CimInstance win32_userprofile | Where-Object {
$_.LastUseTime -lt $(Get-Date).Date.AddDays(-$numberOfDays) -and
$_.LocalPath -notin 'C:\USERS\ADMINISTRATOR',
'C:\USERS\SOME_PROFILE_TO_KEEP',
'C:\USERS\PUBLIC'
}
Yes, it's null, which added zero days. This is fine - you can test with:
$(Get-Date).Date.AddDays($null)
Are you sure there are profiles that match that data? Check the data when $numberOfDays is null to confirm.

How to run a PowerShell script with verbose output?

I'm wondering if there's a way to run a PowerShell script such that both the commands and the output of each line of the script are printed. For example, in Bash you would write bash -x myscript or place a set -x at the top of your script. In Batch, you would omit the #echo off traditionally left at the top of your script. Does PowerShell have an equivalent of these constructs?
Things I've tried: Running powershell -? | sls verbose, which turned up nothing.
Just goes to show, #JamesKo, if you ask the wrong question you get the wrong answer :-(. Several people put forth good-faith answers here based on (a) lack of Linux exposure and (b) your use of the term verbose. In the following I will walk you through how Linux relates to PowerShell on this topic, but feel free to jump to the answer at the end if you are in a hurry. :-)
Background
In PowerShell, verbose has a very specific meaning which the PowerShell man page is even rather vague about:
Displays detailed information about the operation performed by the
command. This information resembles the information in a trace or in a
transaction log. This parameter works only when the command generates
a verbose message.
It even sounds like what you want... but let's compare that to the Linux documentation for set -x which, depending on your flavor of Linux, could be this (from man-pages project)...
The shell shall write to standard error a trace for each command after
it expands the command and before it executes it.
or this (from gnu)...
Print a trace of simple commands, for commands, case commands, select
commands, and arithmetic for commands and their arguments or
associated word lists after they are expanded and before they are
executed.
The very first line of your question clearly and concisely agrees with these. But verbose in PowerShell is different. In a nutshell, turning on verbose mode (be it with the -Verbose command line switch or the $VerbosePreference variable) simply enables output from the verbose stream to the console. (Just like Linux offers two streams, stdout and stderr, PowerShell offers multiple streams: output stream, error stream, warning stream, verbose stream, and debug stream. You work with these streams in an identical fashion to that of Linux--you can even use, e.g., commands 4>&1 to merge the verbose stream to stdout, for example. (You can read more about PowerShell's multiple output streams in the Basic Writing Streams section of PowerShell One-Liners: Accessing, Handling and Writing Data and a good quick reference is the Complete Guide to PowerShell Punctuation.)
The Answer
The Set-PSDebug command will give you bash-equivalent tracing. You can even adjust the tracing detail with the -Trace parameter. First, here's the control, before using Set-PSDebug:
PS> Get-PSDepth
0
With a value of 1 you get each line of code as it executes, e.g.:
PS> Set-PSDebug -Trace 1
PS> Get-PSDepth
DEBUG: 1+ >>>> Get-PSDepth
DEBUG: 141+ >>>> {
DEBUG: 142+ >>>> $nest = -1
DEBUG: 143+ >>>> $thisId = $pid
DEBUG: 144+ while ( >>>> (ps -id $thisId).Name -eq 'powershell') {
DEBUG: 145+ >>>> $thisId = (gwmi win32_process -Filter "processid='$thisId'").ParentProcessId
DEBUG: 146+ >>>> $nest++
DEBUG: 144+ while ( >>>> (ps -id $thisId).Name -eq 'powershell') {
DEBUG: 148+ >>>> $nest
0
DEBUG: 149+ >>>> }
With a value of 2 you also get variable assignments and code paths:
PS> Set-PSDebug -Trace 2
PS> Get-PSDepth
DEBUG: 1+ >>>> Get-PSDepth
DEBUG: ! CALL function '<ScriptBlock>'
DEBUG: 141+ >>>> {
DEBUG: ! CALL function 'Get-PSDepth' (defined in file 'C:\Users\msorens\Documents\WindowsPowerShell\profile.ps1')
DEBUG: 142+ >>>> $nest = -1
DEBUG: ! SET $nest = '-1'.
DEBUG: 143+ >>>> $thisId = $pid
DEBUG: ! SET $thisId = '9872'.
DEBUG: 144+ while ( >>>> (ps -id $thisId).Name -eq 'powershell') {
DEBUG: 145+ >>>> $thisId = (gwmi win32_process -Filter "processid='$thisId'").ParentProcessId
DEBUG: ! SET $thisId = '10548'.
DEBUG: 146+ >>>> $nest++
DEBUG: ! SET $nest = '0'.
DEBUG: 144+ while ( >>>> (ps -id $thisId).Name -eq 'powershell') {
DEBUG: 148+ >>>> $nest
0
DEBUG: 149+ >>>> }
Those are traces of a simple cmdlet I wrote called Get-PSDepth. It prints the commands, assignments, etc. with the DEBUG prefix, intermixed with the actual output, which in this case is the single line containing just 0.
You can always use the below in your script.
$VerbosePreference="Continue"
Note: You have to open the shell in elevated mode.
Below screenshot is for reference.
Hope it helps.
To let PowerShell script able to receive parameters from arguments/command line, need to add [CmdletBinding()] param () even though do not have any parameter.
Example script: Test-Output.ps1
[CmdletBinding()] param ()
Write-Host "Test output on OS $($Env:OS)"
Write-Verbose "Test VERBOSE output on OS $($Env:OS)"
Execute the script in PowerShell:
PS C:\> .\Test-Output.ps1 -Verbose
Execute the script in PowerShell on Linux:
/$ pwsh
PS /> ./Test-Output.ps1 -Verbose
Execute the script using PowerShell.exe on Windows:
C:\> powershell.exe Test-Output.ps1 -Verbose
Execute the script using pwsh.exe PowerShell Core on Windows:
C:\> pwsh.exe Test-Output.ps1 -Verbose
Execute the script using pwsh PowerShell Core on Linux:
/$ pwsh Test-Output.ps1 -Verbose
Sample output on Windows:
Test output on OS Windows_NT
VERBOSE: Test VERBOSE output on OS Windows_NT
Sample output on Linux:
Test output on OS
VERBOSE: Test VERBOSE output on OS
This actually is very easy every PowerShell CMDLET has a built in Verbose tag. All you have to do for example:
Test-Connection -ComputerName www.google.com -Verbose
That is it. I hope this helps
Note: This answer was originally posted in response to the duplicate question at Windows Powershell needs to print out information for a particular command regardless of whether it successfully executed or Not.
To complement and elaborate on Michael Sorens' helpful answer:
With limitations, you can use Set-PSDebug -Trace 1 to let PowerShell echo script statements for you before they are executed; however, this is more like bash's set -o verbose / set -v option rather than set -o xtrace / set -x, in that the commands echoed are unexpanded; details follow:
# Start tracing statements.
Set-PSDebug -Trace 1
try
{
# Sample command
cmd /c echo 'hi there' $HOME
}
finally {
Set-PSDebug -Trace 0 # Turn tracing back off.
}
The above yields:
DEBUG: 4+ >>>> cmd /c echo 'hi there' $HOME
"hi there" C:\Users\jdoe
DEBUG: 6+ >>>> Set-PSDebug -Trace 0 # Turn tracing off.
While this approach requires little extra effort, its limitations are:
You don't get to control the prefix of the tracing statements. (e.g. DEBUG: 4+ >>>> , where 4 is the line number.
Turning tracing back off invariably also produces a tracing statement.
There's no way to capture or suppress the tracing output - it invariably prints to the host (console); however, you can capture it from outside PowerShell, via PowerShell's CLI - see this answer.
As of PowerShell 7.2, commands that span multiple lines using line continuation only have their first line echoed (see GitHub issue #8113).
Perhaps most importantly, the statements being echoed are their literal source-code representations and can therefore contain unexpanded variable references and expressions, such as $HOME in the example above.
GitHub issue #9463 proposes that expanded values (i.e., variables and expressions replaced by their values), as you would get with set -x in bash, for instance.
While this is feasible for calls to external programs - whose arguments are invariably strings anyway, the challenge is that calls to PowerShell commands supports arguments of arbitrary .NET types, not all of which have a faithful string-literal representation; that said, even a for-human-eyeballs-only string representation is arguably preferable to unexpanded values.
If you need to see expanded argument values and/or control the output format / target:
Note:
While Invoke-Expression (iex) should generally be avoided, due to its inherent security risks and because better, safer options are usually available, it does offer a solution here - as always, be sure that you fully control what goes into the string passed to Invoke-Expression, to avoid potential injection of unwanted commands.
The solution requires you to construct the string to pass to Invoke-Expression with up-front expansion (string-interpolation), so that the resulting command line, when executed, only contains literal arguments, so that echoing the command line paints the full picture of what executable is invoked and what its arguments are.
As noted above, this is only robustly possible if you're calling an external program, such as msbuild.
First, define a helper function that accepts a command-line string, echoes it, and then executes it via Invoke-Expression:
# !! IMPORTANT:
# !! Only pass *trusted* strings to this function - the
# !! argument is blindly passed to Invoke-Expression.
function Invoke-AfterEchoing {
param([string] $commandLine)
# Echo the command line to be executed,
# using the verbose output stream in this example:
Write-Verbose -Verbose "Executing: $commandLine"
Invoke-Expression $commandLine
}
Now you can construct your command-line strings and pass them to the helper function:
Invoke-AfterEchoing #"
& "$msbuild" .\blabBlah.csproj /t:Clean
"#
Invoke-AfterEchoing #"
& "$msbuild" .\blabBlah.csproj /t:Build /p:Configuration=Dev
"#
Note:
A expandable here-string (#"<newline>...<newline>"#) is used to simplify the string-internal quoting.
The expandable form is chosen to ensure that the executable path and all arguments are expanded up front and therefore embedded as their literal values in the resulting string, so that echoing the string will show all actual values.
&, the call operator, is used to invoke msbuild, which is syntactically necessary, given that its path is passed quoted, which in turn is necessary if $msbuild contains a path with spaces.
The output will look something like this:
VERBOSE: Executing: & "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin" .\blabBlah.csproj /t:Clean
# ... (output)
VERBOSE: Executing: & "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin" .\blabBlah.csproj /t:Build /p:Configuration=Dev
# ... (output)
If you're using write-verbose in your scripts this will happen automatically,
However, if you need to manually write verbose output from your functions, you'll need to manually check that each function is called with the verbose flag. This can be done by checking$PSCmdlet.MyInvocation.BoundParameters["Verbose"]
from inside your function.

Let bash treat error inside condition as error

I want to write a robust bash script and I use set -e to make sure the script will stop running whenever something goes wrong. It works great but there is still one hole: conditions. If I write if f; then .... fi and function f fails on executing some command, the script will not be terminated. Therefore I need to check return code of every command in f, as well as in all the subroutines f invoke, recursively. This is annoying.
Is there something, e.g. some flag or option in bash, that makes it fail even inside a condition. The only exception is return statement directly inside f. If f calls g and g returns 1, then it is still considered as error, with the exception that g is also called as condition, i.e. if g; then ... fi, then return statement inside g is allowed. So on so forth.
Succinctly, No.
If you want the shell to exit on failure, don't test f; simply run it. The code in the then clause should simply follow the invocation of f because you'll only ever get to execute it if f succeeded.
Old code:
set -e
if f; then echo "f succeeded"; fi
New code:
set -e
f
echo "f succeeded"
You'll only see "f succeeded" if it does succeed; if it fails, the set -e ensures the script exits.
What you're asking to do is slightly odd. I can imagine two possibilities for why you're asking:
You want the script to exit if anything goes wrong inside the body of the function f, even if the function itself still returns success.
You want to distinguish between the function successfully computing what turns out to be a legitimate false value, and failing.
For case 1 - don't do that. If anything goes wrong inside f, the function should absolutely return false (if not exit the whole script itself). And if it returns false, you're good - see #JonathanLeffler's answer.
For case 2, you're effectively trying to make $? serve two duties. It can be either an error indicator or a Boolean result; it can't be both.
If you want your script to exit if something goes wrong, you could just have the function itself call exit when that happens, so the caller doesn't have to worry about that case.
Alternatively, you could have the function echo its computed Boolean value instead of returning it, and reserve $? for success/failure. Something like this, maybe:
f() {
if that thing is true; then
echo 1
else
echo 0
fi
if something went wrong; then
return 1
else
return 0
fi
}
result=$(f) # we'll blow up and exit if f returns nonzero in `$?`
if (( result )); then
...
fi
You would have to distinguish between f being false and f encountering an error.
Traditionally both falsehood and error conditions are signaled in the same way: by setting $? to a non-zero value.
Some commands do make such a distinction. For example, the grep command sets $? to 0 if the pattern was found, 1 if it wasn't, and 2 if there was an error (such as a missing file). So for the grep command, you could do something like:
grep ...
case $? in
0) # ok
;;
1) # pattern not found
;;
*) # error
;;
esac
But that's specific to grep. There is no universal convention for distinguishing between a command yielding a "false" result and failing, and in many cases there is no such distinction to make.
To do what you want, you'll have to define just what constitutes an error, and then determine for each command you might execute how to detect an error condition.

Resources