Powershell: Make Pretty Scripts..With Scripts

I released a new module for standardizing and beautifying your PowerShell code. Aside from code indentation it also can reduce line length, replace here-strings, and a whole lot more.

Introduction

Some people sit down after a long day’s work, turn on the TV, and proceed to do suduku or crossword puzzles to wind down. I sit down and marathon watch old star trek series with the wife and hack through coding algorithms or puzzles that I cannot get out of my head. This module is the result of ‘just one more’ puzzle after another on the quest to automatically reformat some of my old PowerShell projects in an automated manner. I’ve taken it far enough along that I think it may be worth publicizing a little bit to see if I’m on the right path or if I need to simply stop obsessing over AST, tokens, and code formatting functions.

So what I’ve done is create a PowerShell module and it is creatively called….

Format PowerShell Code Module

This is a set of functions to re-factor your script code in different ways with the aim of beautifying and standardizing your code.

  1. This module has multiple goals. Here are a few things one might use it for:
  2. Cleanse and format code copied from the web (fix characters)
  3. Refactor your old code to adhere to best practices in line length, alias usage, type definition usage, indentation and so on.
  4. Use as a pre-build tool to maintain consistency across your code base.
  5. Turn someone else’s insane semi-colon riddled one liner into a script that doesn’t hurt your eyes quite as much.

My selfish reasons for this project were primarily to fix up my old code though. I’ve got tens of thousands of lines of code I want to add features too and improve upon but everytime I open it up one of these old scripts I find myself tediously editing the code for style and other waste of time changes which should be automatic.

Limitations

What this module is not going to do is fix broken PowerShell! Much of exported cmdlets use AST which can only parse functioning code (with some interesting exceptions).

Stupid Cmdlet Names

Well I think they are kind of silly at least. To keep the cmdlets in this code distinct I’ve gone with the following rather non-standard naming standard:

Format-ScriptWhatTheFormattingDoes

It feels a bit wonky but we can always change it later I suppose….

Warnings

I really don’t think this should need to be stated but here it is anyway…

Do NOT just read in your source code and blindly pipe it to the cmdlets included in this module and then write the results out to the same file! I’ve tried to account for a large number of caveats and scenarios but I’m positive I’ve not thought of them all. Additionally I’ve written this code primarily for myself (hey, we are all selfish creatures). What seems to work fine for me may not work at all for your code.

Even though every function defaults to validating the script text after processing I’d go as far as to say you should unit test your code before and after any reformatting done by this module to ensure you get consistent results.

Consider yourself warned.

Logic

Each formatting function has its own special logic. Generally though we tend to perform the actual string manipulations (script formatting) working from the bottom up. Working in reverse lets us not have to refactor token/string locations after every change made. This is especially true of token driven updates like tabifying your script with Format-ScriptFormatCodeIndentation.

There are many interesting exceptions I’ve run into which required some elegant and not so elegant methods to work around. In these cases I try to note in comments where I think more elegant code or algorithms could have been used (which I simply was unable to figure out). A good example is NamedBlockAST or StatementBlockAST code expansion. As there can be embedded blocks beneath each block you find you cannot simply make a change without all the extent start and end locations for every AST element below it changing. So I recreate the AST search results on every iteration for every change made. It feels… awkward but I’ve no better solution yet.

NOTE: None of the functions in this module touch comments! I’ve no way to tell what you are intending with your comments so we do our very best to simply leave them alone. This doesn’t mean that I’ve tested every variant of comments existing in oddball places in your code so I’ll repeat that you should proceed with caution!

Usage

Each function included with this module can be used individually but many of these functions were built around one another for specific purposes. Simply piping all your code through all the cmdlets exported in this module is likely to make your code even more grotesque looking than it was beforehand. Here are a few example usages which you may find handy.

NOTE: Most functions which affect newlines in any manner (expanding code blocks, removing semicolons, et cetera) do nothing for your indentation. This was done on purpose to keep each function as basic as possible. This means you will almost always run your code through Format-ScriptFormatCodeIndentation at the very end of any transformations you are performing!

Example 1 – Condense and Remove ‘Here Strings’

Here-strings are pretty useful variable assignments which are essentially multi-lined strings. I’ve used them for embedding quick templates into my code among other things. They are also totally unwieldy when it comes to making your code look nice. This is because they have strict requirements as to where the terminating here string characters must be (the start of the next line in column 0). Here is an example function with a here string assignment embedded within:

The here-strings are embedded in a function and are thusly unable to be indented without breaking the script entirely. Here is what we would like to happen to fix this:

  1. Convert here strings into simple multiple part string assignments
  2. As these string assignments will likely be very long we would also like to automatically reduce the line length of the script by automatically inserting line breaks in appropriate positions.
  3. Automatically indent the resulting code.

To achieve these tasks with this module you would simply do the following:

The resulting code would look a bit less unsightly (though not by much as it was a fast and dumb example to begin with):

Example 2 – De-obfuscation

A truly obfuscated bit of PowerShell code will require more than this module to de-obfuscate but this module may help a little bit in making it more readable. You may ‘de-obfuscate’ a crazy looking one-liner you came up with to just get a job done in the heat of the moment. Here is a one-liner I purposefully made look like crap. It is a function that gets the lines of a script that token kinds are found between:

In order to make this look more like a version which doesn’t instantly give you a migraine you’d need to perform several transformations. Here is the general logic of what we will do:

  1. Turn statement separators (semicolons) into newlines
  2. Expand function blocks (function{})
  3. Expand named blocks (begin/process/end)
  4. Expand parameter blocks (param())
  5. Expand statement blocks (if/then/else)
  6. Move starting curly braces to the end of the prior line (a personal preference)
  7. Auto-indent all blocks with 4 spaces

With this module you would accomplish this with the following:

Then you can go ahead and paste the output into your favorite editor to get something more palatable:

NOTE: I’ve included a vanity function you can tack on the end of any transform to move the beginning curly brace to the end of the prior line called  Format-ScriptCondenseEnclosures. I prefer my code with less wasted lines but its just a personal preference so the default for all expansion transforms is to place the start of blocks ({) on their own line.

 

Included Functions

Thus far I’ve completed and done testing with the following exported module members:

FormatPowerShellCode Functions

FunctionDescriptionExample
Format-ScriptCondenseEnclosuresMove left curly braces to be at the end of the prior line instead of on their own lineWhile ($true)
{ Write-Output 'hello'}

becomes…
While ($true) { Write-Output 'hello'}
Format-ScriptFormatOperatorSpacingPlaces a space before and after every operator
Format-ScriptFormatTypeNamesConverts typenames to be case-formated[bool] becomes [Bool] and [system.string] becomes [System.String]
Format-ScriptPadOperatorsPad assignment operators with single spaces$a = 0 # $a = 0
becomes,
$a = 0 # $a = 0
Format-ScriptExpandTypeAcceleratorsExpand shortened type accelerators to full name format.
Has a flag to expand all types if you want to make your code look way more complex than it actually is 🙂
[String] becomes [System.String]
Format-ScriptFormatCodeIndentationIndent all code appropriately. Default is 4 spaces.
Format-ScriptFormatCommandNamesProperly formats command elements to be PascalCased.
Also has a flag to expand command aliases
write-output 'test'
becomes..
Write-Output 'test'
Format-ScriptPadExpressionsPad binary expressions with single spaces$b = $a+ 1 / 2*(2-10)+(50/20) +1
becomes,
$b = $a + 1 / 2 * (2 - 10) + (50 / 20) + 1
Format-ScriptRemoveStatementSeparatorsRemoves superfluous semicolons at the end of individual lines of code and splits them into their own lines of code.
Ignores for loops.
Format-ScriptRemoveSuperfluousSpacesRemoves superfluous spaces at the end of any lines of code.
Herestrings and comments are ignored.
Format-ScriptReplaceHereStringsFinds herestrings and replaces them with equivalent code to eliminate the herestring,
this is best followed by Format-ScriptReduceLineLength for obvious reasons.
Format-ScriptReduceLineLengthAny lines past 130 characters (or however many characters you like)
are broken into newlines at the pipeline characters if possible
Format-ScriptTestCodeBlockConfirm code block is valid
Format-ScriptExpandFunctionBlocksExpand any function code blocks from inline to a more readable format. Has a flag to skip single line blocks if desired.function testfunction { Write-Output $_ }

becomes this:

function testfunction
{
Write-Output $_
}
Format-ScriptExpandNamedBlocksExpand any named code blocks (begin/process/end) found in curly braces
from inline to a more readable format. Has a flag to skip single line blocks if desired.
begin { Write-Output $_ }

becomes this:

begin
{
Write-Output $_
}
Format-ScriptExpandParameterBlocksExpand any parameter code blocks (like if/then/else) found in curly braces from
inline to a more readable format. Has a flag to split parameter types into their own lines
if that is your thing.
Format-ScriptExpandStatementBlocksExpand any statement code blocks (like if/then/else) found in curly braces from
inline to a more readable format. Has a flag to skip single line blocks if desired.
if ($a) { Write-Output $true }

becomes this:

if ($a)
{
Write-Output $true
}
Format-ScriptReplaceInvalidCharactersFind and replace goofy characters you may have copied from the web.
Ignores here-strings, strings, and comments.
“ becomes "
” becomes "
’ becomes '
‘ becomes ' (This is NOT the same as the line continuation character, the backtick, even if it looks the same in many editors)

Conclusion

Well this was a long post but I’m releasing a fairly large module to to community so it was probably warranted. If you find some time to take this module and kick its tires I’d love input and community involvement. You can install the module like so:

Otherwise with PowerShell 5 you can simply install from the gallery with the following:

Or manually download/clone/fork the project at Github: https://github.com/zloeber/FormatPowershellCode

Comments (2)

  1. 1:46 PM, 10/15/2015Mike Shepard  / Reply

    Looks like a lot of fun. Going to have to dig in to the code!

    Thanks for sharing!

    • 3:37 PM, 10/15/2015Zachary Loeber  / Reply

      As it is pure PowerShell and all functions are their own files I’m hoping that code digging you will be doing will be fairly easy to get started with 🙂 Thanks for checking it out!

Leave a Reply

Pingbacks (0)

› No pingbacks yet.

Follow

Get every new post delivered to your Inbox

Join other followers