Saturday, March 3, 2012

PowerShell pretty printer / code cleaner V1

PowerShell Beautifier


Now on GitHub: https://GitHub.com/DTW-DanWard/PowerShell-Beautifier

Formatting Matters

Tabs or spaces; spaces or tabs? If spaces, how many? We sure do take whitespace seriously. But when writing ‘commit-worthy’ PowerShell code, there’s more than just whitespace to think about. Shouldn’t you use cmdlet names instead of aliases? And shouldn’t you have correct casing for cmdlets, methods and types?

PowerShell Beautifier is a PowerShell command-line utility for cleaning and reformatting PowerShell script files, written in PowerShell. Sure, it will change all indentation to tabs or spaces for you - but it will do more than just that. A picture is worth 1KB words; here’s a before/after showing all types of changes including spaces & tabs:

Here's a simpler pic focusing on the alias-replacement and casing changes:



The PowerShell Beautifier makes these changes:
  • properly indents code inside {}, [], () and $() groups
  • replaces aliases with the command names: dir → Get-ChildItem
  • fixes command name casing: get-childitem → Get-ChildItem
  • fixes parameter name casing: Test-Path -path → Test-Path -Path
  • fixes [type] casing
    • changes all PowerShell shortcuts to lower: [STRING] → [string]
    • changes other types (if in memory): [system.exception] → [System.Exception]
  • cleans/rearranges all whitespace within a line



36 comments:

  1. Nice Script - thanks!

    I get a lot of requests for such a script so it's nice to have one to recommend.

    Looking forward to V2.

    ReplyDelete
  2. Please add the ability to insert a new line for every line of the script. Will make them more readable

    ReplyDelete
  3. Try running it on InvokeSqlQuery.psm1 from here: http://powershell4sql.codeplex.com/downloads/get/125089

    The output at the start of Invoke-SqlQuery function gets all screwed up.

    Cheers,
    Andrew

    ReplyDelete
    Replies
    1. WOW! Holy cow, what is going on there?

      Thank you for the heads up, Andrew. I tested this on a number of scripts I found on the internet but clearly not this one. I'm not sure what's going on; clearly sets of characters are being removed from the stream somehow. I will take a look into it soon...

      Delete
    2. Thank you, I'm checking here for the updates from time to time =)
      Andrew.

      Delete
  4. Ran it on two of my files, both with very similar formatting. The first file ran fine, came out perfect. Ran it on the second, smaller file, and suddenly pieces of words were missing, variables go cut in half, strings were left open, it looked like someone went in there and tried to break it in every way possible.

    I can't upload the files to show, as they are work-related, but I was surprised at the difference between two such similar files.

    ReplyDelete
  5. Unfortunately I've been crazy busy the past few months (working mostly 6 and 7 days a week) and haven't had the chance to look into this. I was able to reproduce the issue with the Invoke-SqlQuery file, which was surprising as I tested this script on many (hundreds) of script files before releasing it. I haven't looked closely yet but I wonder if it's choking on weird character, a Unicode space or some Unicode character that uses three bytes or more. I think all the Unicode files I tested only contained double-byte characters.

    Try saving your script file as an ASCII file then run it through the pretty printer.

    ReplyDelete
    Replies
    1. Darn, that didn't work.... had the same output.

      thanks for the quick response though.

      Delete
    2. Oh actually, I think I found the issue. For some reason my text editor only added a LF character whenever I hit return int he second file, instead of giving me a full CR/LF. That would most likely cause it!

      (weird text editor?)

      Delete
    3. AH now that sounds like that could be it. I haven't looked at the code in awhile but I know reads from the source as a big byte array and writes to the output. The index it uses to walk through the source array uses the actual length of the items on the line BUT I don't think it reads the end of line characters individually and checks the length - it probably assumes the length is 2. If so, it adds 2 to the source index, even though it should be adding 1, which means it's skipping ahead of legitimate content, which is why the content is lost when it writes the updated file.

      Try taking the source file and re-saving it in a editor that forces the CR/LF to be added. Just to be sure, diff the before/after files to make sure the LF has been added. Once you are sure they are present, run it through the cleaner. Fingers crossed.

      If this is it, I'll change the code to explicitly check the length of the newline but at least we'll have this workaround in the meantime.

      Delete
    4. Well actually my previous comment was that I actually had already fixed it, and I was pointing out my observation on what caused it.

      I did end up using the "re-save" method to make it add the CR token in there.

      That aside, thanks for the responses!

      Delete
    5. Awesome, glad it's working for you. I'll update the code at some point; maybe this weekend...

      Delete
  6. Replies
    1. Load the module then check the help for a bunch of examples:

      Get-Help Edit-DTWCleanScript -Full

      Delete
    2. Import-Module .\DTW.PS.PrettyPrinterV1.psd1
      Get-Help Edit-DTWCleanScript -Full

      Delete
  7. Here is another sample to test on:
    http://powershelldevtools.wordpress.com/2010/07/20/how-to-deal-with-a-combobox-control/

    Before, it works
    After, it kinda works but not quite and you get 4 errors.

    --------
    I was really looking forward to seeing this work.
    I also hoped I could add it to PowerGUI as an add-on and then right-click on a TAB and select PrettyPrinter but no.

    For V2 I hope you can add ways to select how much spacing it adds. I can't stand Double-spaced code because I need to scroll around too much to see a code block. I like tightly formatted code like this:

    If ( ) {
    [code]
    } else {
    [code]
    }

    yours does this (very AIR-y):

    If ( )
    {
    [code]
    }
    else
    {
    [code]
    }

    ReplyDelete
  8. The bug (which I haven't fixed; life has been busy) was handling files that used unix-style end of line characters (i.e. LF) instead of Windows-style EOL (CR+LF). Test files that failed had either all LF or a mix of each and this utility only currently handles only EOL CR+LF, so you might want to re-save your file explicitly with the Windows style and re-run the utility. I've seen this most often with script people cut-and-paste from browsers.

    Yeah, I totally agree with you on the spacing issue. One of the configurations I planned on having in V2 was the ability to tighten the code based on the overall expression length. So, if the expression length was below some value (you choose) and the tighten was enabled, it might even shorten a short code block like this:

    if ($num -lt 5)
    {
    $num + 3
    }
    else
    {
    $num + 1
    }

    to this:

    if ($num -lt 5)
    { $num + 3 }
    else
    { $num + 1 }

    or even this:
    if ($num -lt 5) { $num + 3 } else { $num + 1 }

    I normally like things spaced out but for concise code, especially if it's repeated often, all in one line is nice.

    Also, I started down the road awhile back to PowerGUI-ify it but again, ran out of time, alas.

    ReplyDelete
  9. ok...forgive me for being a total Noob or not exactly knowing what to do with this..recently I was flamed by someone for not formatting my scripts properly..being new to PS and not having any formal script training I have a hard time with that..and no one seems to want to teach me..so..I found your tool..exactly what do I do with the files once I have downloaded them?
    I created a new folder in the modules directory..copied the files into it..open PS..type IPMO edit-DTWCleanscript and get a message stating no valid module is found..
    for us noobs can you provide a "readme" file in the zip file?

    ReplyDelete
  10. No problem, Fed Up. This isn't a built-in PowerShell module, it's a custom one, so the easiest way to load it is store it some custom folder and then load it into your shell using the full path. So let's say you downloaded and unzipped the files to c:\temp\PS, you would load my module into your shell using this:

    Import-Module "C:\temp\PS\DTW.PS.PrettyPrinterV1.psd1"

    You should now be able to see it in memory by typing Get-Module; it'll list at least mine (if not more) with the Name DTW.PS.PrettyPrinterV1. You will also see two ExportedCommands, one of which is Edit-DTWCleanScript. That's the main function that you call to clean a script.

    To see a few examples of how to use Edit-DTWCleanScript plus the rest of it's help, call the function with Get-Help:

    Get-Help Edit-DTWCleanScript -Full

    Please read all the help text - the SINGLE most important detail in there is MAKE SURE YOU BACK UP YOUR SCRIPT before running this utility on it. My function, by default, is designed to rewrite your files in place, overwriting the original copies. You need to check in your file, copy it to a temp location or test on a temp copy first - and then maybe diff after - to ensure that it doesn't screw up your script.

    Now, all that said, this is V1 of the PrettyPrinter (and I don't know if I'll ever get to V2). This version does not restructure your code across lines - it does not move script block openings, closings, etc. (that was planned for V2), it cleans up whitespace, expands types names, resolves aliases and a bunch of other things.

    If you are looking for something to completely rewrite your code and make it "perfect", this may not be the exact tool, but it's a good start. If you want a good example to learn from - to see how to write detailed, clean code with proper help, types on parameters, etc. - take a look at other people's code. Start with mine - download PowerGUI (a PowerShell editor) and take a look through the file DTW.PS.PrettyPrinterV1.psm1. That's the best way to learn, but mind you, everyone formats their code a little differently depending their background.

    Have fun learning PowerShell!

    ReplyDelete
  11. When do you plan to release the prospected V2 :)

    ReplyDelete
  12. Oh, no idea. I started a new job last year and I've been crazy busy since. I have no idea when I'll get back to this... :-(

    ReplyDelete
  13. Well done. This is the only thing out there of its kind that I see. Version 2? Hell, if you charged a little bit...

    ReplyDelete
  14. Thanks! I haven't done any serious PowerShell scripting in some time so I'm thinking about start v2 soon. Please comment if you have any functionality requests.

    ReplyDelete
  15. Would be cool to configure spacing for () and {}.

    I like to use tabs to indent.

    If you put the code up on github and then I would be happy to help contribute.

    ReplyDelete
  16. The script is removing brackets

    For example [System.Windows.Forms.MessageBox] would end up with brackets removed, thus not working. Everything else seems to be intact.

    ReplyDelete
  17. Very useful. Thank you.

    ReplyDelete
  18. Hello,

    I know this is a old post, but i was getting the below error when i run your script....

    PS U:\> Edit-DTWCleanScript -SourcePath C:\users\user\Downloads\test.ps1
    Reading source: C:\users\user\Downloads\test.ps1
    Tokenizing script content
    Tokenize-SourceScriptContent : Tokenize-SourceScriptContent:: error occurred tokenizing source content
    At C:\Users\user\Downloads\PS\DTW.PS.PrettyPrinterV1.psm1:1513 char:5
    + Tokenize-SourceScriptContent -EV Err
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Tokenize-SourceScriptContent

    Property 'Name' cannot be found on this object. Make sure that it exists.
    At C:\Users\user\Downloads\PS\DTW.PS.PrettyPrinterV1.psm1:766 char:33
    + Write-Error -Message "$($MyInvocation.MyCommand.Name):: $($_.Message) Co ...
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], PropertyNotFoundException
    + FullyQualifiedErrorId : PropertyNotFoundStrict

    :: Unrecognized token in source text. Content: @, line: 259, column: 79
    At C:\Users\user\Downloads\PS\DTW.PS.PrettyPrinterV1.psm1:765 char:29
    + $Err | ForEach-Object {
    + ~
    + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException

    Property 'Name' cannot be found on this object. Make sure that it exists.
    At C:\Users\user\Downloads\PS\DTW.PS.PrettyPrinterV1.psm1:766 char:33
    + Write-Error -Message "$($MyInvocation.MyCommand.Name):: $($_.Message) Co ...
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], PropertyNotFoundException
    + FullyQualifiedErrorId : PropertyNotFoundStrict

    ReplyDelete
  19. This comment has been removed by the author.

    ReplyDelete
  20. Nice tool dude! I always struggle with making my scripts look neat.

    ReplyDelete
  21. Your script doesn't support full UNC paths. I.e., this doesn't work:

    PS C:\> Edit-DTWCleanScript \\my_share_server\share_drive\start-Procmon.ps1

    Exception calling "ReadAllText" with "1" argument(s): "The given path's format is not supported."
    At C:\Users\username\Documents\WindowsPowerShell\Modules\DTW.PS.PrettyPrinter\DTW.PS.PrettyPrinterV1.psm1:735 char:5
    + $script:SourceScriptString = [System.IO.File]::ReadAllText($SourcePath)
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : NotSupportedException

    Tokenizing script content
    Edit-DTWCleanScript : Edit-DTWCleanScript :: error occurred during processing
    At line:1 char:1
    + Edit-DTWCleanScript -SourcePath \\my_share_server\share_drive\start-Procmon.ps1
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Edit-DTWCleanScript

    Edit-DTWCleanScript : Exception calling ".ctor" with "3" argument(s): "The given path's format is not supported."
    At line:1 char:1
    + Edit-DTWCleanScript -SourcePath \\my_share_server\share_drive\start-Procmon.ps1
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Edit-DTWCleanScript

    Write destination file: Microsoft.PowerShell.Core\FileSystem::\\my_share_server\share_drive\start-Procmon.ps1
    Finished in 0.285 seconds.



    Although, that looks like it's a bug with System.IO.File rather than in your code. Just figured I'd point it out. Simple work around is to just copy the file to a place with a drive letter, and then copy it back.

    ReplyDelete
  22. Just what the doctor ordered - thank you so much!

    ReplyDelete
  23. Dan, is there any chance you might have interest in being involved in making a beautifier for Atom? https://github.com/Glavin001/atom-beautify/issues/333

    I believe you're existing module could do the trick, and there seems to be a number of interested people on github.

    ReplyDelete
  24. Hey Eric -
    I've been planning on putting the putting the PowerShell beautifier / pretty printer up on GitHub in the next few weeks. As part of that I'd love to work with the atom-beautify folks to get PowerShell support integrated.
    -Dan

    ReplyDelete
  25. Imagine my surprise when opening this page! Keep up the good stuff and Hello from Luxembourg!

    ReplyDelete
  26. You are my hero! Thanks for sharing it on github.

    ReplyDelete