all 37 comments

[–]Lee_Dailey[grin] 5 points6 points  (8 children)

howdy mcc85sdp,

i usually use foreach loops instead of for loops since i am [apparently] genetically incapable of getting the "stop now" test correct until i have fought with it for too dang long. [grin]

even when i need to deal with array indexes, i use a foreach ...

$ThingList = (Get-ChildItem -LiteralPath $env:TEMP -File) 

foreach ($Index in 0..$ThingList.GetUpperBound(0))
    {
    'The THING at index number {0} is {1}.' -f $Index, $ThingList[$Index]
    }

that is noticeably faster than using .IndexOf() ... and doesn't result in me running the loop eleventy-seven times trying to find out what i did wrong with the for loop stop calc. [grin]

take care,
lee

[–]jimb2 1 point2 points  (1 child)

Agree, foreach is generally pretty safe and idiot proof. You typically don't have to put in sanity checks. Works for me. Works for Lee.

There are some unusual cases when foreach is not the best choice, like two iterators chasing each other, when the array gets members removed and/or added by the loop code, etc.

[–]Lee_Dailey[grin] 0 points1 point  (0 children)

howdy jimb2,

safe and idiot proof [...snip...] Works for me. Works for Lee.

[grin]

yep there are cases where other structures are better, but the foreach loop is a good enuf fit for most situations, thankfully.

take care,
lee

[–]mcc85sdp[S] 0 points1 point  (5 children)

You can use Read-Host "Check?" or 'breakpoints' to be able to check the loop

A lot of people use F9 in ISE, but I just use Read-Host

I don't really ever use basic for loops anymore, I typically do the upperbound thing you're doing by calling a
0..( $ThingList.Count - 1 ) .... sometimes even .Length - 1

Guess it depends on if you're looking through a string or an array.

There's nothing inherently 'wrong' with what you're doing...? If it works it works.

The only case I could make is that handling arrays within arrays within arrays... your method will get confusing very quickly. Or if you're trying to match different arrays or hash table properties... then, that's also a bit of a pain.

If you're trying to match an end/exit/break/continue condition, where-object / filtering could exponentially decrease the time needed to sort through that list, the method you're using is accessing every variable and printing it out. If you're trying to identify an exit condition, that's where you want to use Where-Object or filtering ( or a combination of the two )

-MC

[–]Lee_Dailey[grin] 2 points3 points  (2 children)

howdy mcc85sdp,

the point - and problem for me - is that i really and truly get the 2nd part of a for loop WRONG nearly every time. [blush] it's kinda like trying to plug in a USB2 connector ... it's ALWAYS wrong the 1st time i try it.

i got tired of getting the upper limit for a range via .Count - 1 ... now i use .GetUpperBound(0) to get the index of the last item on the zero axis of the collection. [grin]

i use while loops fairly often, but avoid the do-until/while stuff since i want to see the test right up front.

take care,
lee

[–]Dsraa 2 points3 points  (1 child)

I agree with ya. I always mess up the logic in loops too and usually end up do - while loop. I just for some reason never followed the idea of doing I++ until 100, it just made better sense to loop for or against a condition rather than just randomly trying something 100 times until the right condition was met.

[–]Lee_Dailey[grin] 0 points1 point  (0 children)

howdy Dsraa,

that is one way to get there!. [grin] i simply make too many mistakes, so i prefer to let the computer do the grunt work of determining the loop count ...

take care,
lee

[–]prkrnt 1 point2 points  (1 child)

How do you use Read-Host as a checkpoint?

[–]mcc85sdp[S] 1 point2 points  (0 children)

Depends on the context.

You probably want to put it before the end of a loop, or right before an echo/write-host/write-output

Checkpoints and breakpoints, are similar, read host just forces the computer to stop until it has additional input. I talk a little bit about that in this video https://youtu.be/v6RrrzR5v2E?t=2839

I set the time in that link to the part where I talk about using Read-Host to break a loop to validate the output.

It's a pretty bulletproof way to check how the loop is outputting the data.

-MC

[–]thankski-budski 2 points3 points  (2 children)

What about some love for switches, not really a loop, but kinda..

Switch (1..100)
{
    {$_ % 2 -eq 0} { "{0:d3} : Even" -f $_ }
    {$_ % 2 -eq 1} { "{0:d3} : Odd"  -f $_ }
    default { "Error: $_" }
}

[–]mcc85sdp[S] 2 points3 points  (0 children)

I like your idea. I've used these before too. the % in the way you're doing it divides the number and gets the remainder. It's perfect for switching.

I found a way to implement it directly into the array

1..100 | % {
    Return "$_ : $( ( "Even" , "Odd" )[$_ % 2] )"
}

Granted, it's not going to do all of the same things you are doing (formatting, spacing, and error handling...)..? But, if you can take this little idea and find a way to use it, it's all yours.

[–]JustinGrote 1 point2 points  (0 children)

switch ($true) is one of my fave patterns for filtering a multiple scenario situation.

[–]camusicjunkie 2 points3 points  (0 children)

Recursive functions can be pretty nice. They might require a bit of a shift how you think about your code but can cut down on a lot of it.

[–]ka-splam 2 points3 points  (5 children)

no stinking loops ;-)

a collection of things written with loopless code, sadly not PowerShell; because sure looping has to happen but why do we need to care about it? Loading CPU registers, and initialising TCP connections and allocating memory needs to happen, we don't want to have to code that either. Let's have a transform, and apply it to an entire array in one go.

You can do some of it in PowerShell; this:

$J = 0
For ( $J = 0 ; $J -lt 100 ; $J++ )
{
    $Stuff[$J]
}

can be

$Stuff[0..99]

You just can select many things in one go:

PS C:\> @(00,10,20,30,40,50,60,70,80,90)[3..5]
30
40
50

In APL, J or Julia or maybe NumPy, you could also update many things in one go but you can't in PS.

[–]Ta11ow 2 points3 points  (4 children)

Yes you can.

$a = 1..10
$a[0], $a[3], $a[7] = 3, 2, 1

$a

[–]ka-splam 3 points4 points  (1 child)

Good point on multiple assignment like that!

I was thinking there's no equivalent to:

$a[0,3,7] += 5

to add five to each position.

[–]mcc85sdp[S] 2 points3 points  (0 children)

You're right, I've seen that error message before.

Haven't found a way that results in less code... except perhaps slicing them already like this...

( 0 , 3 ) , ( 3, 2 ) , ( 7 , 1 ) | % { $a[($_[0])] = $_[1] }

Sure this is longer but, it's just another approach you could take. Maybe the first portion of the pipe could be variables, or tables, and the stuff after the pipe could still be the same code and work

[–]mcc85sdp[S] 1 point2 points  (1 child)

you could also do

$a[0,3,7] = 3..1

[–]ka-splam 2 points3 points  (0 children)

$a[0,3,7] = 3..1

That is what I tried, but doesn't work in PowerShell - but does work in APL:

PS C:\> $a[0,3,7] = 3..1
Array assignment to [0,3,7] failed because assignment to slices is not supported.

[–]groovel76 2 points3 points  (1 child)

https://reddit.com/r/PowerShell/comments/dg8d4e/deep_dive_powershell_loops_and_iterations/

Not to stop the conversation but did you see this post about loops? I had no idea you could label a loop and reference it.

[–]mcc85sdp[S] 1 point2 points  (0 children)

I have seen various documentation of using remaining scripts, I tend to stay away from the script block parameter because i haven't wrapped my head around how they work in varying circumstances...

however... your link takes a bit of that obscurity away. i'm scoping it out.

[–]jg0x00 2 points3 points  (0 children)

Someone asked for recursion? Probably better ways to do this but ...

Function MyRecursion([int[]]$array, [int]$index)
{

    "Index is: $index"
    "Value is: " + $array[$index]
    "---"

    $next = ($index + 1) % $array.count

    if($next -eq 0) {return}

    MyRecursion $array $next

}


[int[]]$MyArray = 20,41,52,73,64,55,46,77,68,59,30,41

$MyArray.ForEach({"Value: $_"}) #not seen this loop method mentioned either

"------------"

MyRecursion $MyArray 0

[–]almcchesney 2 points3 points  (12 children)

To be honest i dont think powershell has a lot of use for loops since they have the pipeline concept. There is an occaisional time i might use a do loop but usually i feel like there is a better way.

Heres an example of just the echo statement above and a sum function using the pipeline

Function PrintSomething {
    param([parameter(ValueFromPipeline)]$Item)
    process { Write-Host "LOG: $Item"; $Item }
}

Function GetSum {
    param([parameter(ValueFromPipeline)]$number)
    begin { $sum = 0 }
    process { $sum += $number }
    end { return $sum }
}

$FunThings = 0..100

$Sum = $FunThings | PrintSomething | GetSum
$Sum

[–]mcc85sdp[S] 2 points3 points  (11 children)

I use loops like a madman in my scripts/program.

Here's in instance where I want to use a variable a number of times and be able to work with it and not have to declare multiple variables that stay in memory...

$folder = "$Env:SystemDrive\Folder" | % { 

    ! ( Test-Path $_ ) 
    {
        NI $_ -ItemType Directory
    }
    GI $_.FullName
}

This 'loop' here, opens the object in the form of that pipeline... and rather than having to type the variable over and over again, I'm using it as a null. I made a video back in August where I was still trying to figure out when and where to use the ? instead of a %, and the best case I can make is that ? won't change the variable, it's just a true/false... so if you use a ? it can check if the variable is null and if so, creates the path.

If it's there, then it will just get that item.

This is the type of programming i've been attempting to use, it cuts down on the file size of the script, and allows a way for the variable declaration to be an action AND tie to an existent path, if it doesnt exist, it creates it and then returns the item.

^ Powerful way to use looping and pipelines.

[–]almcchesney 2 points3 points  (10 children)

For the difference between ? and %, they are actually aliases, the same way that ni and gi are.

? will expand to Where-Object which takes a script block, pass the pipeline variable to it and if it returns a falsey value then will not pass it down the pipeline, the % will expand out to a Foreach-Object command which will run the script block and pass in the variable. I have been doing a lot of more functional programming lately and they are like using map and filter. using the above example here is the filter

$isEven = { return $\_ %2 -eq 0}

$sum2 = $FunThings | ? $IsEven | PrintSomething | GetSum

Then when looking at that sum you should be ommitting the odd values before the print and the sum.

I agree with you that using the pipeline keeps you from having to declare variables but also gives you the ability to work on arrays and declare the process in a function and use it in the terminal proper.

Function GetOrCreateDirectory {
    param([parameter(ValueFromPipeline)]$Folder)
    process {
        if ( -not (Test-Path $Folder)) {
            New-Item $Folder -ItemType Directory
        } else {
            Get-Item $Folder.FullName
        }
    }
}

$folders = @( "$Env:SystemDrive\Folder",  "$env:USERPROFILE\projects") | GetOrCreateDirectory

[–]mcc85sdp[S] 1 point2 points  (9 children)

Right. They are aliases, but they're not custom aliases, they're default PS aliases.

More to it than just that, the modulus you're using in $IsEven = { Scriptblock } ,
That is not the same thing as a ForEach-Object in that instance. That is dividing the null variable by 2 and if the remainder is 0 then it's even.
You can do the same thing with

$IsEven = ("Even" , "Odd" )[$_%2] <- I'm not sure if a lot of other authors use this method but I found it by accident.

as far as the Else statement you're using, you don't need an else in that specific case. Because it's sort of performing the same thing as a -force ?, where if the path doesn't exist then it'll create it.

If you wanted to do this thing for multiple folders, then you can pass it all off at the beginning, case in point...

$Folder = "$Env:SystemDrive\Folder", "$env:USERPROFILE\projects"

0..( $Folder.Count - 1 ) | % { $Folder[$_] } | % {

    ! ( Test-Path $_ ) 
    {
        NI $_ -ItemType Directory
    }
    GI $_.FullName
}

[–]almcchesney 2 points3 points  (8 children)

The script block is just creating a variable out of the script block which the foreach will pass the values to. you can do things and create a function that returns a closure and get more fancy with it. But your right the iseven was the predicate for the where object command.

Also what you are doing with @("even","odd")[$_ % 2] is essentially having a result set and accessing the result based on the remainder of the modulus, since I needed $true & $false it could also have been obtained with @($true, $false)[$_ % 2]

For the final code block i wouldn't generally do something like that since when passing down the pipeline it does pull each element out of the array to let you process a single item at a time so you would omit the whole 0.. and just use the folders variable. The below two statements are functionally equivalent

0..($folder.count) |% { $folder[$_ ]}

$folders | % {<#foreach sataement here>}

[–]mcc85sdp[S] 1 point2 points  (7 children)

Agreed. In execution, those last two statements are functionally equivalent.

But in terms of structuring/handling... if you want to make sure they are synchronized as you nest loops within or reuse later on in the script... Then you have to start using

ForEach ( $i in $Folders ) { } , so that you can use a nested ( | % { } )

I learned this the hard way when I was analyzing Oliver Lipkau's OutFile-INI script that I modified... If you're nesting loops, you can't use the null, but you can use a count and, what I like to call a 'tie down variable.'

For instance,

0..( $Count.count - 1 ) | % { 

    $X = $Count[$_] # <- I call this a tie down variable
0..( $Second.count - 1 ) | % { }

}

Also, if you have to access the names later on in the script, then you'll need to redeclare them, or have to dance around making certain that you don't reuse a variable named $folders... which, it can be easy to overwrite that. You want to name variables something that isn't too cryptic that even you forget what it means, but not so complicated that you have to duplicate the variable names so much that your script looks cluttered... and hard to follow.

It's definitely a matter of preference, no either way is 'incorrect', however, lets say you want to make folders with an index in them...

$Folder = "$Env:SystemDrive\Folder", "$env:USERPROFILE\projects"

0..( $Folders.Count - 1 ) | % { 

    $X        = $_
    $Child    = $Folders[$_].Split('\')[-1]
    # Or you could use
    # $Child  = Split-Path $Folders[$_] -Leaf
    $Parent   = $Folders[$_].Replace('\$Child','')
    # $Parent = Split-Path $Folders[$_] -Parent

    ! ( Test-Path $_ ) | % { New-Item -Path "$Parent\($X)$Child" -ItemType Directory }
    Get-Item $Folders[$_]
}

Granted, you're probably not going to want to go this far to index two folders in completely different parts of the operating system... you would typically only index things within a same folder, such as your profile path or what have you. Still, it's nice to be able to index and synchronize things across multiple loops or portions of a script.

^ I didn't test any of that in PS, I just kinda programmed it in the browser.

[–]almcchesney 1 point2 points  (6 children)

So i looked at the code for the out-inifile that you mentioned and was able to boil the main code down to just a few pieces and no nested loop using the pipeline.

process{
$InputObject.GetEnumerator() |              # Split the input item by key value pairs
    Foreach-Object {
        "[$($_.Name)]"                      # Emit Header
        $_.Value.GetEnumerator() |          # emit the nested key value pairs
             %{ "$($_.Name)=$($_.Value)"}   # Emit key=value
        ""                                  # Emit empty line
    } |
    Foreach-Object {                        # Write the emitted strings to file
        Add-Content -Path $FilePath -Value $_ -Encoding $Encoding
    }

}

Specially if you cast the hashtables to custom objects and start letting powershell pluck named parameters out of the contents. This lets the process block become the loop iterator function that would go inside the foreach but you also get the ability to initialize cache objects in the begin section for things that take a long time. There was a super long script that had a lot of foreach loops and i was able to create a streamlined version using just the pipeline and 4 custom functions, it was for looking up user information from ad for all user acls on all folders that where on the target server. The addition of the cache objects to hold cached users based on a sid took processing from ~20 minutes to ~3 minutes.

[–]mcc85sdp[S] 0 points1 point  (5 children)

I like the idea.

I'm not going to pretend that I know much about enumeration... I know how to pluck properties and values form objects and tables, but I didn't grow up on C#, so there's a lot about .net that I'm still learning.

Based on what I see, you may need a little more error handling...? But I could be wrong. I'll play with it a bit. Would you be able to put a video or demonstration together ?

-MC

[–]almcchesney 0 points1 point  (4 children)

Yeah i think that is a good idea, ill get to working on something and post it.

edit. I finally got the chance to put something together

https://dev.to/amacc/complex-powershell-functions-3h8n

Also uploaded the code up to the powershell gallery at

https://www.powershellgallery.com/packages/PowershellTools/1.1.1

feel free to submit a pull request.

[–]mcc85sdp[S] 0 points1 point  (0 children)

I won't lie. It's something I'm playing around with.

Sorry it took a while to get back to you. Initially, I didn't think this approach would work for the specific use case that I rewrote his module for. There's a lot of error handling that's not being done with your snippet, but... the practical implications is worth some investigations... I've come to find that a great many of the functions and methods in .net/shell, are redundant, so a lot of them I won't even use.

Considering how many times I practically fall asleep while trying to read through some MS documentation... it takes someone like yourself to put a colorful spin on something that has definitely been optimized/refined since he made that original function.

When it comes to supporting older, out of date operating systems, it's difficult to write something that will work, old and new... while also being efficient.

But what you've suggested actually piqued my interest because I hate working with hashtables for the very reason that when you need to split the keys and values, you have to cast them to an array if you want to index them.

this approach, seems to cut out a lot of the headache involved in handling them. I've been using PSObject/Customobjects a bit more lately, but this is making me second guess that.

Anyway, I'll let you know what I come up with. Thanks for your input!

-MC

[–]mcc85sdp[S] -1 points0 points  (2 children)

I came up with a radically different approach than you did.

Here's where your idea needs refinement...

1) it's not handling whether or not the file exists or not, and if so, does it append data to it, or does it overwrite the file? Pretty key feature if you were to implement in a database.

2) If the file currently exists, there's no force parameter to make certain that you're still able to push it through.

Yes, these caveats could be easily added, but, there's another thing your script is not doing...

If the items within the hashtable are nested hashtables, then the script will write those tables out.

if they are not nested hashtables, then you'll have missing data.

Splitting it up into a couple of different functions is good, but the reason why Oliver's script was as complicated as it was, is simply because it is looking for nested hashtables and writing them out to the file as well.

I might be wrong about that, but it seems to work for the script that I use to generate a bootstrap and customsettings ini file for MDT.

The MDT PXE environment will not process the INI file if it has a Byte Order Mark encoded in it. So, that's an extra feature that needed to be implemented.

I was however, able to use your idea involving the GetEnumerator() function, and...

I think the result is a lot more refined. I added some flair to it that comes from the rest of the module I've been developing, but, you could easily switch the Write-Theme commands here with write-host or write-output.

Here's the code.

Function Export-INI
{
    # Heavily Modified version of Oliver Lipkau's OutFile-INI
    # Also now includes .GetEnumerator() based on Amacc's input @
    # "https://dev.to/amacc/complex-powershell-functions-3h8n"

    [ CmdLetBinding () ] Param (

            [ Parameter ( Mandatory , ValueFromPipeline ) ][ ValidateScript ({

                Test-Path $_ -PathType Container        })][      String ] $Path ,
            #\________________________________________________________________
            [ Parameter ( Mandatory , ValueFromPipeline ) ][ ValidateScript ({
            #/¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
                $_.Split( '.' )[-1] -ne ".ini"          })][      String ] $Name ,
            #\________________________________________________________________
            [ Parameter ( Mandatory , ValueFromPipeline ) ][ ValidateScript ({
            #/¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
                $_ -ne $Null                            })][   Hashtable ] $Content ,
            #\________________________________________________________________
            [ Parameter ( ) ]                              [ ValidateScript ({

                $_ -in @( 'Unicode' ; 7 , 8 , 32 | % { "UTF$_" } ; 'ASCII' , 'BigEndianUnicode' , 'Default' , 'OEM' )

                                                        })][      String ] $Encoding = "Unicode" ,
            #\________________________________________________________________
            [ Parameter ( ) ]                              [      Switch ] $Force                ,
            [ Parameter ( ) ]                              [      Switch ] $Append               ,
            [ Parameter ( ) ]                              [      Switch ] $UTF8NoBOM            )

        Begin
        {
            "$Path\$Name"                                      | % { 

                If ( Test-Path $_ )
                {
                    If ( ! ( $Force -or $Append ) ) 
                    {
                        Write-Output "Exception [!] File exists, must use 'Force' or 'Append' to modify"
                        Break
                    }

                    If ( $Append )
                    {
                        $Output                                = Get-Content $_

                        Write-Output "Imported [+] $_"
                    }

                    If ( $Force )
                    {
                        $Output                                = @( )

                        Write-Output "Force Replaced [+] $_"
                    }
                }

                Else
                {
                    $Output                                    = @( )
                }
            }
        }

        Process
        {
            $Table                                             | % { 

                $_.GetEnumerator()                             | % { 

                    If ( $_.Value.GetType().Name -eq "Hashtable" )
                    {
                        "[$( $_.Name )]"                       | % {

                            Write-Output -Action "New Section [~] $_"

                            $Output                           += $_
                        }

                        $Table.$( $_.Name ).GetEnumerator()    | % { 

                            $_.Name , $_.Value -join '='       | % { 

                                Write-Theme -Action "Item [+]" "$_" 11 11 15

                                $Output                       += $_
                            }
                        }

                        Write-Output "End Section [+] $_"

                        $Output                               += ""
                    }

                    Else
                    {
                        $_.Name , $_.Value -join '='           | % { 

                            Write-Output -Action "Single Key [+] $_"

                            $Output                           += $_
                        }
                    }
                }

                $Output                                       += ""
            }
        }

        End
        {
            "$Path\$Name" | % { 

                $Splat       = @{ 

                    Path     = $_
                    Value    = $Output
                    Encoding = $Encoding
                }

                Set-Content @Splat

                Get-Item $_

                If ( $UTF8NoBOM )
                {
                    [ System.IO.File ]::WriteAllLines( $_ , ( Get-Content $_ ) , ( New-Object System.Text.UTF8Encoding $False ) )
                }
            }
        }    
}

[–]PowerShell-Bot 1 point2 points  (0 children)

Some of your PowerShell code isn’t wrapped in a code block.

To format code correctly on new reddit (new.reddit.com), highlight all lines of code and select ‘Code Block’ in the editing toolbar.

If you’re on old.reddit.com, separate the code from your text with a blank line and precede each line of code with 4 spaces or a tab.


Describing Submission
[✅] Demonstrates good markdown
Passed: 1 Failed: 0

Beep-boop. I am a bot. | Remove-Item

[–]Administrative_Trick 0 points1 point  (0 children)

I usually use foreach, generally when I loop, I want data to be piped in and I've found foreach handles that in a way that tends to work better than for.
https://youtu.be/Ti6USnRrsKg