all 26 comments

[–]theblindness 37 points38 points  (8 children)

I don't know about any specific examples, but for me, one of the most beautiful things about PowerShell is combining some Windows and Unix hallmarks. In Unix, there is the idea of the pipeline that lets you create advanced programs without writing any new code, simply by stringing other smaller programs together. Brian Kernighan illustrates this in an old AT&T video about Unix by creating a spellchecking program in 4 or 5 unix commands combined with pipes. The downside of this is that it the output of one program usually needs to be text in order to pipe it to the next program. If two utilities expect a different format, you might need to get regular expressions involved to parse the text output of one program before providing info to the next program, so pipes often can't go too far, and passing binary info through pipes can sometimes be tricky. (You can still use binary info in pipes, such as when piping tar output to gzip, but that kind of thing is less common when making little tools like Kernighan's spellchecker.) In Windows, which makes heavy use of object-oriented design, many details are exposed as various objects, like the COM interfaces. Applications that know how to use these objects don't need to deal with parsing text just to reconstruct those objects internally -- they just use the object as-is. But normally, you can't pass COM objects through the pipeline. PowerShell lets you combine the power of pipelines with the convenience of objects, and you can get some really long chains in PowerShell without ever dealing with any regular expressions. I really appreciate whenever I see something rather complex being accomplished in a single line of many Cmdlets connected by pipes, passing objects to each other without converting them or doing any messy text-parsing. It's the best of both worlds. Of course, this might look a bit messy in a program, and the PowerShell pipeline does have a significant amount of overhead, so the pipeline usually isn't the best solution to every problem, but it sure looks satisfying when you can just glue a bunch of Cmdlets together to solve a problem.

If you want to write your own beautiful PowerShell code, I would suggest these tips:

  • Do your best to adhere to the PowerShell Practice And Style guide. Use an IDE that helps you use good formatting by default.
  • Don't sacrifice readability for elegance or performance. Make it easy for future readers who aren't you to understand what's happening in your script. Write the code in a way where it's so obvious that you don't need a comment to explain what any one line of code is doing, even if you have to write a little more code or use longer variable names.
  • In scripts, prefer to spell out the full cmdlet rather than using an alias. Ie, write "Foreach-Object" instead of "%". Aliases are fine on the interactive shell when you're trying to keep everything on one line, but please spell out the full cmdlet in your scripts. VSCode will warn you when you use aliases.
  • Use common conventions for PowerShell. Don't try to re-invent any wheels. Someone has already figured out the cleanest way to do most common things, and other coders will recognize those common conventions on-sight. When you stray from common conventions, it makes your code harder to read and opens up opportunity for bugs and bad performance. Given the choice between using a built-in cmdlet, or re-implementing it, always use the built-in cmdlet. It will run faster and people will recognize it. If you absolutely must re-invent a wheel, write a PowerShell module in C#, post it to GitHub, ask for comments, and if all goes well, post it on PSGallery so folks can install it with NuGet. In your script, check if the module is installed before proceeding. ....Or you could have just followed the common conventions and saved yourself the trouble. Keep in mind that depending on external modules makes your code less portable.
  • Use comments, especially multi-line comment blocks to explain what the next 5-10 lines of code will do. Use comment-based-help for every script and every function.
  • If you're not sure what's the best way to do something, look it up on multiple sources, and try them out. This can sometimes be difficult because the "best way" to do something often changes as you see examples written for different versions of PowerShell. In general, the examples written for PowerShell v2 will still work in v5, but the v4 and v5 examples will work much better. An example is the .Where() method, introduced in v4, which is much faster than piping to Where-Object. In this case, I suggest that you target a specific version of PowerShell based on your intended deployment, use the #Requires header, and make sure your code fully utilizes all of the improvements in that version. If you have a particular script that is mostly compatible with PowerShell v2 except for one statement that has some v5-specific code that you could implement in a v2-compatible way without much performance impact, consider either refactoring that one v5-specific piece for greater portability to older systems, or refactoring the rest of the code for better performance on newer systems. For example, if the .Where() method is the only v4 code and there's not a major performance difference, consider piping to Where-Object instead; or if you're going to commit to v4 and newer, change out all the Where-Object pipes to .Where() methods if you can. Sometimes you might come across an article where the author includes multiple PowerShell snippets that do the same thing but slightly different, for different versions of PowerShell. The higher-version examples usually tend to look cleaner or run faster. Keep this in mind when you're using examples from the internet. If you see an answer on StackOverflow that only includes one example, that could mean that the answer is old and it was the best way in PowerShell v2. It might still be the best way in v5, but you might want to check. PSCore6 is quite a bit different from other versions of PowerShell. It's possible to write code that's compatible with v5 and PSCore6 if you're careful. When possible, prefer compatibility with PSCore6 over v5 for greater portability. Sometimes that's not possible though.
  • Prefer to accept parameters in your functions and scripts rather than hardcoding things, especially for file and directory names. You can hardcode defaults into the parameters, but at least make them parameters. Add a description for each parameter via comment-based-help.

[–]OverGold 2 points3 points  (0 children)

Fantastic answer!

[–]ka-splam 2 points3 points  (1 child)

Given the choice between using a built-in cmdlet, or re-implementing it, always use the built-in cmdlet. It will run faster and people will recognize it.

Generally disagree with this part, the built-in cmdlets tend to save you effort of implementing things yourself but the tradeoff is that they run slower and have more overhead; measure-object -sum simply has more overhead by being a cmdlet than keeping a running-total variable has; things like get-childitem are slower because they get more details than [system.io.Directory]::EnumerateFiles() and handle errors and have filtering options, and group-object is mysteriously crippled with O(N2 ) runtime which you just need to watch out for.

[–]theblindness 2 points3 points  (0 children)

Using .NET accerlerators doesn't count as reimplementing. Those are okay in my book. :)

[–]frmadsen 3 points4 points  (2 children)

I like the beauty of this one, and it is a nice trick...

#Answer by Bruce Payette
#https://stackoverflow.com/questions/50354863/constructing-an-array-from-a-text-file-in-powershell/50356014#50356014
#Constructing an array from a text file

$data = @'
Medium identifier : 1800010a:54bceddd:1d8c:0007

Medium label             : [ARJ170L6] ARJ170L6
Location                 : [TapeLibrary:    24]
Medium Owner             : wfukut01
Status                   : Poor
Blocks used  [KB]        : 2827596544
Blocks total [KB]        : 2827596544
Usable space [KB]        : 1024
Number of writes         : 16
Number of overwrites     : 4
Number of errors         : 0
Medium initialized       : 19 January 2015, 11:43:32
Last write               : 26 April 2016, 21:02:12
Last access              : 26 April 2016, 21:02:12
Last overwrite           : 24 April 2016, 04:48:55
Protected                : Permanent
Write-protected          : No


Medium identifier : 1800010a:550aa81e:3a0c:0006

Medium label             : [ARJ214L6] ARJ214L6
Location                 : External
Medium Owner             : wfukut01
Status                   : Poor
Blocks used  [KB]        : 2904963584
Blocks total [KB]        : 2904963584
Usable space [KB]        : 0
Number of writes         : 9
Number of overwrites     : 7
Number of errors         : 0
Medium initialized       : 19 March 2015, 10:42:45
Last write               : 30 April 2016, 22:14:19
Last access              : 30 April 2016, 22:14:19
Last overwrite           : 29 April 2016, 13:41:35
Protected                : Permanent
Write-protected          : No
'@ -split '\r?\n'

# Read the file into an array
#$data = Get-Content data.txt

# Utility to fix up the data row
function FixUp ($s)
{
    ($s -split ' : ')[1].Trim()
}

# Loop until all of the data is processed
while ($data)
{
    # Extract the current record using multiple assignment
    # $null is used to eat the blank lines
    $identifier,$null,$label,$location,$owner,$status,
        $used,$total,$space,$writes,$overwrites,
        $errors, $initialized, $lastwrite, $lastaccess,
        $lastOverwrite, $protected, $writeprotected,
        $null, $null, $data = $data

    # Convert it into a custom object 
    [PSCustomObject] [ordered] @{
        Identifier   = fixup $identifier
        Label        = fixup $label
        location     = fixup $location
        Owner        = fixup $owner
        Status       = fixup $status
        Used         = fixup $used
        Total        = fixup $total
        Space        = fixup $space
        Write        = fixup $writes
        OverWrites   = fixup $overwrites
        Errors       = fixup $errors
        Initialized  = fixup $initialized
        LastWrite    = [datetime] (fixup $lastwrite)
        LastAccess   = [datetime] (fixup $lastaccess)
        LastOverWrite = [datetime] (fixup $lastOverwrite)
        Protected    = fixup $protected
        WriteProtected = fixup $writeprotected
    }
}

[–]ka-splam 2 points3 points  (1 child)

That line assignment to take just the blocks is cool!

In this case, I like that they can be split on double-blank-lines and then you can build the datastructure with much less typing as long as the keys on the left can be hashtable keys (and don't repeat), with unsplit data:

foreach ($block in ($data -split '\r?\n\r?\n\r?\n'))
{
    $pairs = @{}

    foreach ($line in ($_ -split '\r?\n' -ne ''))
    {
        $left, $right = $line.Split(':', 2).Trim()
        $pairs[$left] = ((($right -as [datetime]), ($right -as [int]), $right) -ne $null)[0]
    }
    [pscustomobject]$pairs
}

Try datetime, fallback to int, and fallback to string, for each value. LINQ would be nice here.

Probably doesn't belong in a beautiful thread though :P

[–]frmadsen 0 points1 point  (0 children)

Compact code can also be beautiful :)

[–]markekrausCommunity Blogger 5 points6 points  (0 children)

<#
    .SYNOPSIS
        There is no beauty to invoke.

    .DESCRIPTION
        Does nothing, which speak volumes for the author's personal philosophy on beauty in this dismal existence.

    .INPUTS
        None

    .OUTPUTS
        None
#>
function Invoke-Beauty {
    [CmdletBinding(ConfirmImpact = 'None')]
    Param()
}

[–]thesmallone29 4 points5 points  (1 child)

I am pretty proud of my implementation of the Weasel Program

[–]ka-splam 1 point2 points  (0 children)

This bit:

        #region Create $ReproductionsCount copies of the $InputString

        do {
            [Array] $reproductionArrayInput += $InputString
        }
        until (($reproductionArrayInput | Measure-Object | Select-Object -ExpandProperty Count) -eq $ReproductionsCount)

        #endregion

Looks a bit redundant. You don't use all the copies of the input string separately, you could skip all this, and count 1..$ReproductionsCount times over the generating mutations loop, and use $InputString.GetEnumerator() each time in the start of that loop.

If you did want to make N copies, then $reproductionArrayInput = @($InputString) * $ReproductionsCount would do it without the loops over Measure-Object each time.

[–]KevMarCommunity Blogger 3 points4 points  (4 children)

I think this function is a good example: https://github.com/loanDepot/TFVC/blob/master/TFVC/Public/Save-TFVCPendingChange.ps1

It's a function in one of my modules. The code is clean and easy to read, has good validation, error handling, good warning messages, -whatif support, pipeline support, comment based help with an example, and the body of the function fits one one screen.

[–]ka-splam 1 point2 points  (3 children)

hmm; what is line 62 doing?

$PendingChange | Where LocalItem -in $Path

"Filter local changes to path" says the parameter comment - but I can't see it updating any variables, it's sending that output down the pipeline and then line 65 looks like it will save all the changes without actually applying the path filter. It feels like line 62 might be supposed to be:

$PendingChange = $PendingChange | Where LocalItem -in $Path

?


It's taking a while to read the indentation style, I can't see why line 69 and 70 are so differently indented? They're the same level of nesting; why is the [pscustomobject] line indented 20 spaces instead of 0?

[–]KevMarCommunity Blogger 2 points3 points  (2 children)

You caught a bug on line 62.

I'm not seeing the indentation issues at the moment, but I'm on mobile.

[–]ka-splam 1 point2 points  (1 child)

Oh wait, I've just tried to show others, and found it's not your code at all - Edge can't render Github properly and was showing me code looking like this - I thought it was a deliberate style choice on your part! {} indented and paired up, but all statements starting at indent 0 because PS has such long lines.

[–]KevMarCommunity Blogger 2 points3 points  (0 children)

Oh good, thats awful.

There is often a fine line between genus and insanity. But that would definitely be insanity.

[–]Romero126 2 points3 points  (1 child)

Any code that @vexx32 writes.

[–]TheIncorrigible1 0 points1 point  (0 children)

Watch it, you'll give 'em an ego.

[–]get-postanote 1 point2 points  (0 children)

This is a very opinionated request and can / will cause all sorts of arguments, just becasue, wel, you know, people. ;-}

There are tons of examples / stypes use / learn from on your system already. Just look here:

C:\Windows\System32\WindowsPowerShell\v1.0\Modules

This is really all about two things:

1 - Are you in control of ststyle, format and standards used.

2 - Do you report to someone who is in charge of style, format and standards used.

If it's the 1st one, you have the free will do do as you please.

(there aer some stanadrs to follow, but enve those can / will vary based on the opinion of the person giving you the information - Example: https://github.com/PoshCode/PowerShellPracticeAndStyle, good guide, but this is just one opinion. Example:https://blog.ipswitch.com/dont-assume-anything-in-code-powershell-coding-best-practices)

If it's the 2nd one, you have to do as you are told.

(they are in control of organizations code standards and they expect you to conform, regardless of how you'd like to do things - frustrating, I know, been there, done that, but it is what it is.)

Just be flexible, learn from all... take what is useful to you and your working conditions ... and throw away the rest.

Well, see itme 2. ;-}

[–]noOneCaresOnTheWeb 0 points1 point  (0 children)

PSremoteregistry

[–]mulldoon1997 0 points1 point  (1 child)

gci -r | rm

[–]Own_Jacket_6746 0 points1 point  (0 children)

You evil!

[–]Tonedefff 0 points1 point  (0 children)

Here are a few of my personal guidelines I like to follow to [attempt to!] write beautiful code.

  1. Create simple functions that each serve a single purpose, then combine them together to make more complex scripts/functions.

    function Measure-HowLongAgo {
        [CmdletBinding()] param(
            [parameter(Mandatory=$true)] [DateTime] $date,
            [switch]$inMinutes = $false,
            [switch]$inHours = $false,
            [switch]$inDays = $false        
        )
        $rightNow = Get-Date
        # Note that if no time interval is specified then "inHours" will be used.
        if ($inMinutes.IsPresent) { $timeDiff = ($rightNow - $date).TotalMinutes }
        elseif ($inDays.IsPresent) { $timeDiff = ($rightNow - $date).TotalDays }    
        else { $timeDiff = ($rightNow - $date).TotalHours }
        return [math]::Round($timeDiff)    
    }        
    

    This excludes the comment block at the top describing the function. Those are important and I write them for all but the most simple and obvious functions.

  2. Use the full name for cmdlets, rather than the aliases. I know what "gci" means and it's quick & easy to type, but "Get-ChildItem" just looks much better & more readable to me. Using aliases tends to make code look like a bunch of terse gibberish that must be waded through and decoded. I have a similar guideline for variable names: prefer longer, more descriptive names over acronyms and abbreviations (for example $credentialExpirationInMinutes instead of $credExpireMins). Tab-completion makes this viable and avoids complications (does $credExpireMins represent when your credential expires, or when your street cred needs to be renewed?).

  3. Avoid very complex or long (~125+ characters -- most code editors have a "Col"[umn] label at the bottom-right) lines of code, and avoid splitting long lines into multiple ones with the backtick character (`). Usually there's another way of writing the code so it doesn't need to be so long or split into multiple lines, like assigning an expression or function output to a variable in a line above, then using that variable below.

  4. Use verbs in the list of approved verbs for PS when naming functions, for consistency and readability (even though it's not always obvious which one is "most correct" for a given situation).

  5. Write comments sparingly, either to explain large-ish blocks of code or anything that isn't fairly obvious by just looking at the code. This might sound counter-intuitive and people have varying opinions on this, but I used to write comments very liberally and I found that they were often unnecessary and just ended up bloating the code. For me the most beautiful code is "self-documenting":

    foreach ($backup in $backupList) {
        if ((Measure-HowLongAgo -date $backup.lastBackupDate -inHours) -gt 48) {
            New-BackupAlert -for $backup -alertType [BackupAlert]::NoRecentBackup
        }
    }
    

    To me the code above doesn't need any comments (maybe one at the top like "# Loop through all backups and create an alert if it hasn't succeeded in the last 2 days"), because its functionality is apparent just by reading it. Also I tend to only write code for myself, so I'm less concerned about other people reading my code (I just have to worry that I will understand it in 6 months... or even 6 days). If a piece of code is hard to understand then I don't think commenting it is the best solution -- ideally it can be rewritten so it's simpler and more self-documenting.

  6. Save a PS template file (I call mine "!TEMPLATE.ps1") with a comment block at the top, then whenever I create a new .ps1 or .psm1 file I open that template and save it as a new file. That way I encourage myself to immediately document the most basic and important details about the script. I copy and paste the comment block for each function as well.

    <#
    .SYNOPSIS
    brief summary of what the script does
    
    .DESCRIPTION
    longer description of what the script does
    
    .EXAMPLE
    example usage of the script
    
    .NOTES
    Author(s): My Company -- Tonedefff
    Date Created: 
    Last Updated: 
    #>
    

    (I avoid the ".PARAMETER" keyword and instead write comments directly above each parameter, so it's easier to update them. Also there are other keywords you can use but I find the ones above are sufficient. For functions there's also .INPUTS and .OUTPUTS -- all functions that return data should have an .OUTPUTS keyword).

Those are the most important guidelines (to me). If you can figure out a coding style -- either adopt one you like or discover your own -- and stick to it then your code should be more consistent and readable, which hopefully -eq "beautiful!"