How to perform conditional looping using powershell - powershell

I want to execute test cases using commands using PowerShell scripts.
How many time I want to execute those tests depend upon the $HowManyTimes variable.
It's possible that I get the success result before the $HowManyTimes
e.g As of now I set it to 3, but what if I get the successful result in the 2nd attempt itself.
I want to ignore that specific test case that got passed and proceed to execute the new test case.
I have copied the output of the command in $OutPut variable which looks like below
Not Imp Info
Errors, Failures and Warnings
Failed : UnitTestProject1.TestClass.AddTest()
Expected: 30
But was: 45
Not Imp Info
Test Run Summary
Overall result: Failed
Not Imp Info
Below if the script which I have written.
$XMLfile = 'Results.xml'
[XML]$Results = Get-Content $XMLfile
$HowManyTimes = 3 # How many time the script should run.
$TestCaseToExecute
$OutPut # Storing the result of command
$StopOperation = 0
for ($i=1; $i -le $HowManyTimes; $i++)
{
#I am reading the xml file which can have n test cases.
#For each loop on all the test case which are failed.
foreach($Result in $Results.'test-run'.'test-suite'.'test-suite'.'test-suite'.'test-suite'.'test-case' | Where-Object {$_.result -eq "Failed"})
{
Write-Host "Failed Test Case -- fullname :" ($Result).fullname
$TestCaseToExecute = ($Result).fullname
Write-Host " Executing ====>" $TestCaseToExecute
Write-Host "-----------------------------------------"
$OutPut = nunit3-console.exe UnitTestProject1\bin\Debug\UnitTestProject1.dll --test=$TestCaseToExecute | Out-String
if($OutPut -Like "*Overall result: Failed*"){
Write-Host "If Block"
continue
}else{
Write-Host "else block Stop : "
break
}
}
}
Please let me know what changes I need to make to achieve this.

Related

In powershell, Have Out-Host with conditional statement

I have a requirement to append | Out-Host with each Powershell execution.
But the response with out-host changes as below
>> some crap
>> $? --> returns false
>> some crap | Out-Host
>> $? --> returns false as expected
>> $(some crap) | Out-Host
>> $? --> returns true not expected
I understand that the true return might be because of the subexpression that I have introduced. But I see it needed in scenarios where I have a conditional script. There simple appending Out-Host doesn't work. For example,
$condition = $true; if ( $condition ) {echo "The condition was true"} | Out-Host
The above fails that an empty pipe is not allowed
If I change it to the below, it works
$($condition = $true; if ( $condition ) {echo "The condition was true"} )| Out-Host
I basically want to append Out-Host such that my output/response of run doesn't get affected. Since Out-Host is said to be the default, there should be a way to handle it for conditional statements as well.
Any help is appreciated.
It's still not entirely clear to me why you need to add Out-Host all over the place, but the safest option for programmatically adding | Out-Host to all your pipelines is to parse the existing script using the Parser class:
function Add-TrailingOutHost {
param(
[string]$Script
)
# Start by parsing the script
$parserErrors = #()
$AST = [System.Management.Automation.Language.Parser]::ParseInput($Script, [ref]$null, [ref]$parserErrors)
if($parserErrors){
Write-Error 'Errors encountered parsing script'
return
}
# Locate all pipeline expressions in the AST returned by the parser
$pipelines = $AST.FindAll({
param($Tree)
# We only want "free-standing" pipelines - ignore pipelines in flow control statements and assignments
$Tree -is [System.Management.Automation.Language.PipelineAst] -and $Tree.Parent -isnot [System.Management.Automation.Language.StatementAst]
}, $true)
# We'll be injecting code into the script, thereby modifying the offsets of the existing statements
# To avoid inject code at the wrong offset we need to traverse the script bottom-up
for($i = $pipelines.Count - 1; $i -ge 0; $i--){
$pipeline = $pipelines[$i]
# Check if the last command in the pipeline is already `Out-*` something - ignore those
$lastCommand = $pipeline.PipelineElements[-1]
if($lastCommand.CommandElements[0] -like 'Out-*'){
continue
}
# Otherwise, inject the string `| Out-Host` at the end of the pipeline
$Script = $Script.Insert($lastCommand.Extent.EndOffset, ' | Out-Host')
}
# return modified script
return $Script
}
Now you can add Out-Host to the relevant parts of any script with:
Add-TrailingOutHost -Script #'
echo "Hello there"
if(Test-Path .){
echo "Something else"
}
'#
The output of which is:
echo "Hello there" | Out-Host
if(Test-Path .){
echo "Something else" | Out-Host
}

Output of Get-content as variable?

I am attempting to run a foreach loop on a get-content and convertfrom-json cmd. Now im aware this potentially has issues being multiple value results in the variable, im wondering how i can continue to pass this info to the rest of the script.
$testconv = Get-device * |select ID
$testid = $testconv.id
$conv = foreach ($id in $testid)
{
get-content "\\HDC-PRTG-03\System Information Database\Services\Device$id.Services" | Convertfrom-json
}
$rpccheck =$conv.message
$snmpcheck = $conv.message
$svcname = $conv.data.displayname
$svcstate=$conv.data.properties.state
if($RPCon = $rpccheck |select-string -pattern RPC -AllMatches){
write-host RPC Not enabled
}else{
write-host No RPC Enabled - Moving to Services List
Now when i run that with out the $conv= making it a variable it returns
kind : Services
recievetime : 29-01-2018 14:43:32
error : 106
Message : SNMP Channels Not Available.
Which is what i expect. However when i define it a variable with $conv= it just starts to say it cannot find the file paths which i find an odd error to throw but hey ho.
Do any of you smart guys have any pointers for how i can keep these fromjson objects in memory so i can continue to run foreach loops against them. The ultiumate function of this script is to query a local .services file for what services are running on the device and then create sensors to monitor them within our PRTG installation. Therefore i need to be able to ref the deviceID and apply things to it.
I suspect i may be using too many foreach loops in the whole script but frankly i am 100% out of my depth
any guidance hugely hugely appreciated
Sam
If i understand correctly you should have json files for all device ID's. If a file with the name of a particular device is missing you will get the 'File not found' error.
As for the code, you can try this:
$testconv = Get-Device * | select ID
$testid = $testconv.id
$oldErrorAction = $ErrorActionPreference
$ErrorActionPreference = 'Stop'
foreach ($id in $testid) {
try {
$conv = Get-Content -Path "\\HDC-PRTG-03\System Information Database\Services\Device$id.Services" -Raw | ConvertFrom-Json
$rpccheck = $conv.message # These look the same to me...
$snmpcheck = $conv.message # These look the same to me...
$svcname = $conv.data.displayname
$svcstate = $conv.data.properties.state
$Matches = ($rpccheck | Select-String -Pattern "RPC*" -AllMatches)
if ($Matches.Matches.Count) {
Write-Host "RPC Not enabled"
}
else {
Write-Host "No RPC Enabled - Moving to Services List "
}
}
catch {
Write-Warning $_.Exception.Message
}
}
$ErrorActionPreference = $oldErrorAction
Instead of the try{}..catch{} you could also first test if a file with that name is present using Test-Path directly before doing the Get-Content.

How to redirect Access Denied and all other errors in Powershell to a variable (Loop until command doesnt fail)

I am trying to create a bootstrap script to setup servers and add them to the domain. The problem is some of the networking changes do not get implemented for a varying amount of time causing the command to join the domain to fail (always with the same error). TO get around this all I have to do is run the command again a minute later but this is not practical as part of a long script.
My solution is along the lines of:
$exit = 1
while ($exit -ne 0){
$exit = 0
join-domain-command
$exit = $?
sleep 20
}
However $exit is always 0/false even when I put in some non existent command that is bound to fail.
My other idea was to redirect all errors to a variable and search the variable for text containing the error I commonly come across and if that error is not present, stop the loop.
Trying to redirect the stderr in powershell doesn't work for me either
$exit = & (dummy-command -confirm:$false) 2>$exit
echo "exit = $exit"
SO here I deliberately set the ExecPol in an un-elevated prompt:
you can use -errorvariable
$exit = 1
while ($exit -ne 0){
$exit = 0
join-domain-command -errorvariable $error
$exit = $?
sleep 20
}
Or if you want to do something with the error:
try {
join-domain-command -errorvariable $error
}
catch{
$error | out-file C:\errors.txt -append
#or if $error -contains "something"
#...do something
}
then search the text file for your errors
EDIT
So a few things the actual correct use of errorVariable doesnt use the $ so it would be -errorvariable myError
If you want to search an error a better way to do it would be this:
while ($exit -ne 0)
{
try {
join-domain-command
}
catch{
if(!($error[0].tostring().contains("specific error text"))) #error text is not your specific error
{$exit = 1}
}
sleep 20
}
All errors can be found in $error and if you want to check that last error you use $error[0] which give you the last error that was received.
I usually do something like the following and I put it in a separate function to keep the main code path clean. A counter can be added to limit the number of retries.
$Succeeded = $false
while($Succeeded -eq $false){
try{
#commands that may fail
$Succeeded = $true
}
catch{
#write a message or log something
}
start-sleep -s 60
}

echo in while loop get's added to my return value

I wasn't sure how to describe this problem in the title so here goes.
I call a function from a script in another script. In that function i have a while loop that basically keeps looping through a set of ip's and looks up their hostname. when the while loop times out or we have all the host names.
it returns the hostnames.
My problem is that the return value contains every single Write-Host i'm doing in that function.
i know it's because Write-Host puts stuff on the pipeline and the return just returns whatever it has.
How do i go about fixing this?
The entire script i run get's logged in a log file which is why i want to have some verbose logging.
| out-null on write-host fixes the issue but it doesn't print the write-host values in the script.
in main.psm1 i have a function like so:
$nodes = #("ip1", "ip2", "ip3", "ip4")
$nodesnames = DoStuff -nodes $nodes
then in functions.psm1 i have functions like:
Function DoStuff
{
param($nodes)
$timeout = 300
$timetaken = 0
$sleepseconds = 5
$nodenames = #("$env:COMPUTERNAME")
while(($nodenames.count -lt $nodes.count) -and ($timetaken -lt $timeout))
{
try
{
Write-Host "Stuff"
foreach($node in $nodes)
{
$nodename = SuperawesomeFunction $node
Write-Host "$nodename"
if($nodenames -notcontains $nodename)
{
$nodenames += #($nodename)
}
}
}
catch
{
Write-Host "DoStuff Failed because $_"
}
Start-Sleep $sleepseconds
$timetaken += $sleepseconds
}
return $nodenames
}
Function SuperawesomeFunction
{
param($node)
$nodename = [System.Net.Dns]::GetHostEntry("$node")
return $nodename
}
Thanks.
So the answer is, your function is working like it is by design. In PowerShell a function will return output in general to the pipeline, unless specifically directed otherwise.
You used Echo before, which is an alias of Write-Output, and output is passed down the pipe as I mentioned before. As such it would be collected along with the returned $nodenames array.
Replacing Echo with Write-Host changes everything because Write-Host specifically tells PowerShell to send the information to the host application (usually the PowerShell Console or PowerShell ISE).
How do you avoid this? You could add a parameter specifying a path for a logfile, and have your function update the logfile directly, and only output the relevant data.
Or you can make an object with a pair of properties that gets passed back down the pipe which has the DNS results in one property, and the errors in another.
You could use Write-Error in the function, and set it up as an advanced function to support -errorvariable and capture the errors in a separate variable. To be honest, I'm not sure how to do that, I've never done it, but I'm 90% sure that it can be done.

How can you continue executing a PowerShell script, only if it has been called 3 times in 1 minute?

I have a script that is being called via a Windows Scheduled Task, and that task is triggered based on a certain Windows Application Event. It is only critical to execute the script, though, if the event occurs 3 or more times in 1 minute; if the event occurs once a minute, no action should be taken.
I know this can be handled in the script itself. Let's say there are at least 2 new variables I will need:
# time window, in seconds
$maxTime = 60
# max number of times this script needs to be called, within $maxTime window,
# before executing the rest of the script
$maxCount = 3
I started outlining an algorithm using a temp file as tracking, but thought there might be a simpler solution that someone can show me. Thanks
You could store your execution times in an environment variable.
Before this script will work, you must create the LastExecutionTimes environment variable.
$maxTime = 60
$maxCount = 3
$now = Get-Date
# Get execution times within the time limit.
$times = #($env:LastExecutionTimes -split ';'|
Where-Object {$_ -and $now.AddSeconds(-1 * $maxTime) -lt $_})
$times += '{0:yyyy-MM-dd HH:mm:ss}' -f $now
$env:LastExecutionTimes = $times -join ';'
if($times.Length -lt $maxCount) {return}
# Reset the execution times
$env:LastExecutionTimes =''
Write-Host 'Continue Script' -ForegroundColor Yellow
I would write a text file and a secondary script or function to check it. Where essentially it will call it each time, and then writes the information writes to a text file at call time.
The something like this:
if(!((Get-Date).AddMinutes(-1) -lt $oldTime))
{
$CurDate = Get-Date
"$CurDate, 1" | out-File "TheCheck.txt"
}
else
{
$counter++
if($counter -ge 3) {Call WorkerFunction}
else{
"$oldTime, $counter" | Out-File "TheCheck.txt"
}
Its missing some variables, but overall should be functional as a supplemental script. Then what your scheduled task actually does is call this, if the time since the $oldTime is over 1 minute, then it over writes the file with the current time and 1 for a $counter variable. If its less than a minute since the first call it then checks the $counter and if it is 3 or higher (could also do -eq ) to 3 then it calls your main script.