Converting Parts of an Array in powershell - powershell

I'm supposed to tag this as homework, but I'm not sure how. Not asking for an answer, just to be pointed in the right direction of what I'm doing wrong. Basically, this is supposed to make a complete sentence out of ANY user input, with the exception of -AllCaps, which is functioning properly. My problem is the other part.
param ($title, [switch]$AllCaps)
$ex="to|a|the|at|in|of|with|and|but|or"
function Proper($ts)
{
$nt=foreach($word in $ts)
{
$word=$word.ToLower()
if($word -match $ex)
{
if($word -eq $ts[0])
{
$letters=$word -csplit("")
$letters[1]=$letters[1].ToUpper()
$word=$letters -join("")
}
else
{
$word=$word
}
}
else
{
$letters=$word -csplit("")
$letters[1]=$letters[1].ToUpper()
$word=$letters -join("")
}
"$word"
}
$nt=$nt -join(" ")
Write-host $nt
}
if($AllCaps)
{
$title=$title.ToUpper()
"$title"
}
else
{
$ts=$title -split(" ")
$newtitle=Proper $ts
}
So, when I execute the script passing in "the waffle of kings", expected output is "The Waffle of Kings" - instead, the code seems to be completely ignoring the "else" in the first "if else" statement, and the output I'm getting is "The waffle of kings". Since "waffle" doesn't match anything in $ex, it should be moving to the else part, capitalizing the first letter.
Less of note is that it doesn't write to the console without including "$word" within the foreach loop, though the way I have it written, the Write-Host should be doing that for me. If I take out the write-host nothing changes, but if I take out the "$word" in the loop, I get no output at all.

Using the above mentioned
$ex='^(to|a|the|at|in|of|with|and|but|or)$'
I ended up with a good one-liner, but have expanded it out here:
Function Proper($words)
{
$ex='^(to|a|the|at|in|of|with|and|but|or)$'
$nt = $(
foreach ($word in $words)
{
$word = $word.ToLower();
if ($word -match $ex)
{
$word
} else
{
$word = ([String]$word[0]).ToUpper()+$word.Substring(1);
$word
}
}
) -join " "
return $nt
}
I hope that helps.
Thanks, Chris.

Related

Powershell, OOP, Why return $false does not exit Class Method

in Powershell 5
I do not understand, when debugging this code, why when the string " of the" is matched at the end of the main string, the method does not perform exit from the Class Method and continues to iterate.
Seems contra-intuitive to me.
Must I use the auxiliary variable and a label combined with break command?
something like here:
:JUMPASFOX foreach ($i in (1..10)) {
...{break JUMPASFOX}
I thought return $false anywhere causes exit.
I cannot reconcile that my code does not work.
Class EndFinder {
[string[]]$Exclusions_End_Text=#(" of", " of the", " of a", "for a", "for the", " in", " the")
EndFinder(){}
[boolean]Contains_Exclusion_At_The_End ([string]$ClipText) {
#$found=$false
$this.Exclusions_End_Text |foreach {
$exclusion_length=$_.Length
$clip_length=$ClipText.Length
if ($exclusion_length -lt $clip_length) {
$tailing=$ClipText.Substring($clip_length-$exclusion_length,$exclusion_length)
if ($tailing -eq $_) {
#$found=$true
return $true
}
}
}
return $false
#return $found
}
}
$kb=[EndFinder]::New()
$kb.Contains_Exclusion_At_The_End("big problem of the")
As Mathias R. Jessen points out in his comment, the return keyword when used in the scope of a cmdlet such as ForEach-Object will go to the next iteration, similar to what continue would do if it was a loop (for, foreach, while, etc).
Goes to next iteration when the condition is met:
0..4 | ForEach-Object {
if($_ % 2) {
return 'Odd'
}
'Even'
}
Even
Odd
Even
Odd
Even
# Same behavior with script block:
0..4 | & {
process
{
if($_ % 2) {
return 'Odd'
}
'Even'
}
}
Stops the iteration:
foreach($i in 0..4) {
if($i % 2) {
return 'Odd'
}
'Even'
}
Even
Odd
I'm assuming you're trying to see if the string provided to your method ends with any of the $Exclusions_End_Text strings from your class property. If that's the case, this should do it:
Class EndFinder {
hidden [string[]]$Exclusions_End_Text = #(
" of", " of the", " of a", "for a", "for the", " in", " the"
)
# EndFinder(){} => Constructor is not needed here, PS does this by default.
[boolean] Contains_Exclusion_At_The_End ([string]$ClipText)
{
foreach($i in $this.Exclusions_End_Text)
{
if($ClipText.EndsWith($i))
{
return $true
}
}
return $false
}
}
$kb = [EndFinder]::New()
$kb.Contains_Exclusion_At_The_End('big problem of the') # => True
$kb.Contains_Exclusion_At_The_End('not ending with any $Exclusions_End_Text') # => False
I would personally use a static method in this case, I believe it's more appropriate for what you're trying to accomplish:
Class EndFinder {
static [boolean] Contains_Exclusion_At_The_End ([string]$ClipText)
{
$Exclusions_End_Text = #(
" of", " of the", " of a", "for a", "for the", " in", " the"
)
foreach($i in $Exclusions_End_Text)
{
if($ClipText.EndsWith($i))
{
return $true
}
}
return $false
}
}
[EndFinder]::Contains_Exclusion_At_The_End('big problem of the')
[EndFinder]::Contains_Exclusion_At_The_End('not ending with any $Exclusions_End_Text')

How do I correctly mock my Function to return a custom property with Pester?

I'm a bit new to PowerShell and specifically Pester testing. I can't seem to recreate a scenario for the function I am making Pester test.
Here is the code:
$State = Get-Status
if(State) {
switch ($State.Progress) {
0 {
Write-Host "Session for $Name not initiated. Retrying."
}
100{
Write-Host "Session for $Name at $($State.Progress) percent"
}
default {
Write-Host "Session for $Name in progress (at $($State.Progress)
percent)."
}
}
I've mocked Get-Status to return true so that the code path would go inside the if block, but then the result doesn't have any value for $State.Progress.
My test would always go into the default block in terms of code path. I tried
creating a custom object $State = [PSCustomObject]#{Progress = 0} to no avail.
Here is part of my Pester test:
Context 'State Progress returns 0' {
mock Get-Status {return $true} -Verifiable
$State = [PSCustomObject]#{Progress = 0}
$result = Confirm-Session
it 'should be' {
$result | should be "Session for $Name not initiated. Retrying."
}
}
There are a couple of issues:
Per 4c's comments, your Mock may not be being called because of scoping (unless you have a describe block around your context not shown). If you change Context to Describe and then use Assert-VerifiableMocks you can see that the Mock does then get called.
You can't verify the output of code that uses Write-Host because this command doesn't write to the normal output stream (it writes to the host console). If you remove Write-Host so that the strings are returned to the standard output stream, the code works.
You can use [PSCustomObject]#{Progress = 0} to mock the output of a .Progress property as you suggested, but I believe this should be inside the Mock of Get-Status.
Here's a minimal/verifiable example that works:
$Name = 'SomeName'
#Had to define an empty function in order to be able to Mock it. You don't need to do this in your code as you have the real function.
Function Get-Status { }
#I assumed based on your code all of this code was wrapped as a Function called Confirm-Session
Function Confirm-Session {
$State = Get-Status
if ($State) {
switch ($State.Progress) {
0 {
"Session for $Name not initiated. Retrying."
}
100{
"Session for $Name at $($State.Progress) percent"
}
default {
"Session for $Name in progress (at $($State.Progress) percent)."
}
}
}
}
#Pester tests for the above code:
Describe 'State Progress returns 0' {
mock Get-Status {
[PSCustomObject]#{Progress = 0}
} -Verifiable
#$State = [PSCustomObject]#{Progress = 0}
$result = Confirm-Session
it 'should be' {
$result | should be "Session for $Name not initiated. Retrying."
}
it 'should call the verifiable mocks' {
Assert-VerifiableMocks
}
}
Returns:
Describing State Progress returns 0
[+] should be 77ms
[+] should call the verifiable mocks 7ms

PowerShell Try/Catch with If Statements

Problem/Details
I am working in PowerShell and trying to figure out how custom Try Catch statements work. My current major issue involves mixing Try/Catch and If statements. So the idea of what I am trying to achieve is something like this:
try {
if (!$someVariable.Text) { throw new exception 0 }
elseif ($someVariable.Text -lt 11) { throw new exception 1 }
elseif (!($someVariable.Text -match '[a-zA-Z\s]')) { throw new exception 2}
}
catch 0 {
[System.Windows.Forms.MessageBox]::Show("Custom Error Message 1")
}
catch 1 {
[System.Windows.Forms.MessageBox]::Show("Custom Error Message 2")
}
catch 2 {
[System.Windows.Forms.MessageBox]::Show("Custom Error Message 3")
}
Now I know the above code is very inaccurate in terms of what the actual code will be, but I wanted to visually display what I'm thinking and trying to achieve.
Question
Does anyone know how to create custom error messages with PowerShell that could assist me with achieving something close to the above idea and explain your answer a bit? Thank you in advance
So far, the link below is the closest thing I have found to what I'm trying to achieve:
PowerShell Try, Catch, custom terminating error message
The Error you throw is stored at $_.Exception.Message
$a = 1
try{
If($a -eq 1){
throw "1"
}
}catch{
if ($_.Exception.Message -eq 1){
"Error 1"
}else{
$_.Exception.Message
}
}
I would suggest using the $PSCmdlet ThrowTerminatingError() method. Here's an example:
Function New-ErrorRecord
{
param(
[String]$Exception,
[String]$ExceptionMessage,
[System.Management.Automation.ErrorCategory] $ErrorCategory,
[String] $TargetObject
)
$e = New-Object $Exception $ExceptionMessage
$errorRecord = New-Object System.Management.Automation.ErrorRecord $e, $ErrorID, $ErrorCategory, $TargetObject
return $ErrorRecord
}
Try
{
If (not condition)
{
$Error = #{
Exception = 'System.Management.Automation.ParameterBindingException'
ExceptionMessage = 'Error text here'
ErrorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
TargetObject = ''
}
$PSCmdlet.ThrowTerminatingError((New-ErrorRecord #Error))
}
} Catch [System.Management.Automation.ParameterBindingException]
{
'do stuff'
}

PowerShell Try/Catch and Retry

I have a fairly large powershell scripts with many (20+) functions which perform various actions.
Right now all of the code doesn't really have any error handling or retry functionality. If a particular task/function fails it just fails and continues on.
I would like to improve error handling and implement retries to make it more robust.
I was thinking something similar to this:
$tries = 0
while ($tries -lt 5) {
try{
# Do Something
# No retries necessary
$tries = 5;
} catch {
# Report the error
# Other error handling
}
}
The problem is that I have many many steps where I would need to do this.
I don't think it make sense to implement the above code 20 times. That seems really superfluous.
I was thinking about writing an "TryCatch" function with a single parameter that contains the actual function I want to call?
I'm not sure that's the right approach either though. Won't I end up with a script that reads something like:
TryCatch "Function1 Parameter1 Parameter2"
TryCatch "Function2 Parameter1 Parameter2"
TryCatch "Function3 Parameter1 Parameter2"
Is there a better way to do this?
If you frequently need code that retries an action a number of times you could wrap your looped try..catch in a function and pass the command in a scriptblock:
function Retry-Command {
[CmdletBinding()]
Param(
[Parameter(Position=0, Mandatory=$true)]
[scriptblock]$ScriptBlock,
[Parameter(Position=1, Mandatory=$false)]
[int]$Maximum = 5,
[Parameter(Position=2, Mandatory=$false)]
[int]$Delay = 100
)
Begin {
$cnt = 0
}
Process {
do {
$cnt++
try {
# If you want messages from the ScriptBlock
# Invoke-Command -Command $ScriptBlock
# Otherwise use this command which won't display underlying script messages
$ScriptBlock.Invoke()
return
} catch {
Write-Error $_.Exception.InnerException.Message -ErrorAction Continue
Start-Sleep -Milliseconds $Delay
}
} while ($cnt -lt $Maximum)
# Throw an error after $Maximum unsuccessful invocations. Doesn't need
# a condition, since the function returns upon successful invocation.
throw 'Execution failed.'
}
}
Invoke the function like this (default is 5 retries):
Retry-Command -ScriptBlock {
# do something
}
or like this (if you need a different amount of retries in some cases):
Retry-Command -ScriptBlock {
# do something
} -Maximum 10
The function could be further improved e.g. by making script termination after $Maximum failed attempts configurable with another parameter, so that you can have have actions that will cause the script to stop when they fail, as well as actions whose failures can be ignored.
I adapted #Victor's answer and added:
parameter for retries
ErrorAction set and restore (or else exceptions do not get caught)
exponential backoff delay (I know the OP didn't ask for this, but I use it)
got rid of VSCode warnings (i.e. replaced sleep with Start-Sleep)
# [Solution with passing a delegate into a function instead of script block](https://stackoverflow.com/a/47712807/)
function Retry()
{
param(
[Parameter(Mandatory=$true)][Action]$action,
[Parameter(Mandatory=$false)][int]$maxAttempts = 3
)
$attempts=1
$ErrorActionPreferenceToRestore = $ErrorActionPreference
$ErrorActionPreference = "Stop"
do
{
try
{
$action.Invoke();
break;
}
catch [Exception]
{
Write-Host $_.Exception.Message
}
# exponential backoff delay
$attempts++
if ($attempts -le $maxAttempts) {
$retryDelaySeconds = [math]::Pow(2, $attempts)
$retryDelaySeconds = $retryDelaySeconds - 1 # Exponential Backoff Max == (2^n)-1
Write-Host("Action failed. Waiting " + $retryDelaySeconds + " seconds before attempt " + $attempts + " of " + $maxAttempts + ".")
Start-Sleep $retryDelaySeconds
}
else {
$ErrorActionPreference = $ErrorActionPreferenceToRestore
Write-Error $_.Exception.Message
}
} while ($attempts -le $maxAttempts)
$ErrorActionPreference = $ErrorActionPreferenceToRestore
}
# function MyFunction($inputArg)
# {
# Throw $inputArg
# }
# #Example of a call:
# Retry({MyFunction "Oh no! It happened again!"})
# Retry {MyFunction "Oh no! It happened again!"} -maxAttempts 10
Solution with passing a delegate into a function instead of script block:
function Retry([Action]$action)
{
$attempts=3
$sleepInSeconds=5
do
{
try
{
$action.Invoke();
break;
}
catch [Exception]
{
Write-Host $_.Exception.Message
}
$attempts--
if ($attempts -gt 0) { sleep $sleepInSeconds }
} while ($attempts -gt 0)
}
function MyFunction($inputArg)
{
Throw $inputArg
}
#Example of a call:
Retry({MyFunction "Oh no! It happend again!"})
Error handling is always going to add more to your script since it usually has to handle many different things. A Try Catch function would probably work best for what you are describing above if you want to have each function have multiple tries. A custom function would allow you to even set things like a sleep timer between tries by passing in a value each time, or to vary how many tries the function will attempt.

Pipeline parameter interferes with $args array

I'm trying to make use of the $args array with a pipeline parameter.
The function expects an arbitrary number of parameters (e.g. param0) following the first, pipelined parameter:
function rpt-params {
param (
[Parameter(ValueFromPipeline=$true,Position=0,Mandatory=$true)][CrystalDecisions.CrystalReports.Engine.ReportDocument]$reportDocument
)
try {
write-host "count: " $args.count
#TODO process args
}
catch [Exception] {
write-host $_.Exception
}
finally {
return $reportDocument
}
}
Attempts to call the function produce an error that reads "rpt-params : A parameter cannot be found that matches parameter name 'param0'.":
...
# syntax 0
rpt-params $rpt -param0 "mb-1" -param1 "me-1"
...
...
# syntax 1; explicitly naming the first parameter
rpt-params -reportDocument $rpt -param0 "mb-1" -param1 "me-1"
...
Is my syntax the issue or is it related to using a pipelined parameter?
Create another parameter, called it something like $rest and decorate it with [Parameter(ValueFromRemainingArguments = $true)].
When you use "[cmdletbinding()]" or "[Parameter()]", which is the case here, your Function turns into an Advanced Function. An Advanced Function can only take the Arguments that are specified under "Param" and no more. To make your Function act like before, like Keith recommends, you'll need to add [Parameter(ValueFromRemainingArguments = $true)]
For Example:
function rpt-params {
param (
[Parameter(ValueFromPipeline=$true,Position=0,Mandatory=$true)]
[CrystalDecisions.CrystalReports.Engine.ReportDocument]$reportDocument,
[Parameter(ValueFromRemainingArguments=$true)]$args
)
try {
write-host "count: " $args.count
#TODO Now args can have all remaining values
}
catch [Exception] {
write-host $_.Exception
}
finally {
return $reportDocument
}
}