Real-World PowerShell Tips from a 2011 Scripting Games Winner

Real-World PowerShell Tips from a 2011 Scripting Games Winner

  • Comments 3
  • Likes

Summary: Advanced PowerShell 2011 Scripting Games winner, Bartek Bielawski, shares his real-world thoughts about scripting.

Microsoft Scripting Guy, Ed Wilson, here. Our guest today is the winner in the Advanced category of the 2011 Scripting Games, Bartek Bielawski.

Photo of Bartek Bielawski

Bartek has been working in IT for more than 10 years for one company, PAREXEL, a global organization with headquarters in the U.S. Before he got a real job, he was big Linux fan and amateur Linux admin, with some bash scripting and all kinds of awking, seding, cutting, and greping. That's why one of his first tasks at work (other than putting out fires all around) was to improve backup scripts (done in cmd back then). That forced him to get to know cmd.exe better. The experience was bad when compared to bashing, but it paid the bills...

A few years later, he tried to automate some tasks, which required something that cmd could not provide. His options were VBScript or Windows PowerShell. Even though he knows VBScript a bit, he always preferred the interactive Windows PowerShell. In his opinion, Windows PowerShell is the best product from Microsoft.

The Scripting Games 2011 are over. Sure, Ed still has a lot of work to do, but this is probably the nice part of the games—being Santa Claus is always cool. We, the contestants on the other hand, feel a little empty space inside. Our next “event” won’t be for PoshCode—it will be for the company that we work for. It may be some modification of a great script we wrote for the Scripting Games, but we won’t get a grade and comments, other than, “Oh, it works! Great! Here is the next task.”

The prize will be a salary at the end of the month, and maybe more time for family and friends, because we managed to automate something and saved ourselves from wasting time on clicking. And because nobody will ever look at it, we will probably do it quick and dirty, and regret it the next time we read it. Unless…

Write code so that you can read it and use comments to understand it.

The Scripting Games are, first of all, a learning experience—for the people who attend and for the people who just watch. Everyone can read comments about individual scripts and blog posts by the judges that cover the things they noticed, such as typical errors in scripts. One post that I found very useful was the one written by Andy Schneider: Extra Points for Style when Writing PowerShell Code.

I also received a comment on a similar topic from Jeffery Hicks. Both comments are about the readability of the script. When you write script, it’s usually a matter of seconds to add an empty space or a comment with a link to resources you’ve used to come up with the solution. Empty spaces make your script more elegant and easier to read, as do paragraphs or sentences in a book. Comments with useful information require a few seconds spent now when this knowledge is at your fingertips waiting for you to grab it and put it inside a comment block, and the time may grow to several hours if you leave them out.

It’s also part of the automation process—you do not want to repeat click-click-click—and because of that, you use Windows PowerShell. Why repeat the whole google-not here-google-not here-msdn-not here process, if you can save yourself and others who might read your script later a lot of time. Probably sometimes more than avoiding clicks.

Speaking of clicks…

Sometimes you would like to give others the option to perform automation tasks without actually digging into the code. And that’s the next thing I learned from the Scripting Games.

What lives in GUI, should be born in GUI and die in GUI.

My Advanced 8 script, which I’m very proud of, had one very serious issue: it relied on the user having pictures inside the My Pictures folder. This is something I strongly discouraged users from doing in Windows XP, because MY Pictures was subfolder of My Documents, and all those family pictures were copied during the backup process.

We were not able to tell which pictures were important, work-related pictures, and this is a real issue now that everybody has a digital camera. Very late, I noticed that my script did not work when the folder with pictures is empty, and I decided to go with an inline workaround. I could provide script with the Path parameter and change the startup folder to the one with pictures. But it was a bad idea.

I would expect a GUI control to show up when I run a GUI script. So, the first thing I plan to do is to rewrite this part, and simply add a friendly control that will allow the user to select a folder when the default folder is free of pictures. It’s a really bad practice to give a person the information that he needs WPK/ PSImageTools/ WIA.ImageFile COMObject, and in the end (when he has it all), show him a red error. Quite frustrating behavior for a GUI script, to say the least. 900 lines to produce a few red lines is kind of overkill…

Speaking of overkills, we can move to the next topic.

Keep it simple.

Another good tip from Andy Schneider was about keeping the script simple. For one of events, I wanted to have automatically formatted output, and yet keep objects in the pipe. This is something I always try to do in my scripts and functions (it was one of additional tasks we were assigned by Ed).

It was obvious to me that we should use some kind of .ps1xml trick to make it happen. But to get the formatting file to work, you need to have a known object type. Generally, PSObject should remain untouched; otherwise, many objects will try to follow our formatting instructions and probably will look bad.

What can you do to get your custom objects to behave like known objects? Two things:

  • Create a new type inline by using Add-Type and a definition of your new class.
  • Use the method of the custom object that allows you to add any name to it on-the-fly.

The first option seems clean, but it’s a bit of overkill to create a class only to have proper formatting. I guess that’s the main reason why we can name our custom objects, and the reason why the formatting subsystem is also paying attention to the object’s nickname. This can keep us away from Visual Studio and writing C# code in Windows PowerShell script.

Of course it won’t prevent us from knowing .NET—there are still gaps in Windows PowerShell that we have to cover with direct .NET calls. And there are some Windows PowerShell elements where the Help you will find is more for cmdlet developers, such as ShouldProcess support—which is another thing I finally managed to work inside my Windows PowerShell script.

Take control.

There are occasions where you want to control the behavior of elements that you use inside your script. That includes error handling—all ErrorAction and ErrorVariable directives. It should also, in my opinion, support ShouldProcess.

Last year when I used it, I had no luck finding a way to avoid multiple prompts for the user’s approval. I first got a prompt from my script, and then from all the cmdlets that support ShouldProcess. It was a really bad experience, especially for the not-so-rare occasions when I wanted the script to move on. The YesToAll and NoToAll options where blocked by the fact that my script was calling cmdlets in each process block. Because it was always a “new” call, I got a prompt no matter how hard I tried not to.

This year I wanted to do it the proper way. And because I could not find decent documentation about $psCmdlet methods, I had to read and understand documentation for the .NET class that this variable maps to behind the scenes. This is probably something I would like to describe in more detail. Let’s start with a function that only pretends to support ShouldProcess:

function Foo {

[CmdletBinding(SupportsShouldProcess = $true)]

param (

    [Parameter(ValueFromPipeline = $true)]

    [int]$Count

)

process {

    $path = [io.path]::GetTempFileName() + '.txt'

    Write-Host "Creating file # $Count"

    New-Item -ItemType File -Path $path

}

}

 

1..10 | Foo –Confirm

Yes, you get a prompt. But see how YesToAll and NoToAll are no different from Yes and No? That’s what I did not like, and I had a hard time fixing it. I was told that I should use $psCmdlet.ShouldProcess/ ShouldContinue to support ShouldProcess. And so I did that as follows:

function Foo2 {

[CmdletBinding(SupportsShouldProcess = $true)]

param (

    [Parameter(ValueFromPipeline = $true)]

    [int]$Count

)

process {

    $path = [io.path]::GetTempFileName() + '.txt'

    if ($psCmdlet.ShouldContinue("Create file # $Count`: $path`?",

                'Confirm creating a file')) {

        New-Item -ItemType File -Path $path

    }

}

}

 

1..10 | Foo2 –Confirm

It’s a little bit better. Now I give you my prompt. But see what happens if you choose to perform my action? New-Item kicks in with its prompt, and again gives the YesToAll and NoToAll options. This option won’t be passed to next instance of the process block, so you will be asked again each time you decide to select Yes. Annoying to say the least.

To make it actually work, you have to do the following:

  • Prevent New-Item from supporting ShouldProcess—you will do it yourself.
  • Add Yes and No to all options for your own prompt.

The last part took me awhile, and it was probably the most educational experience for me during this year’s Scripting Games. Take a look at the final code of function that actually supports ShouldProcess:

function Bar {

[CmdletBinding(SupportsShouldProcess = $true)]

param (

    [Parameter(ValueFromPipeline = $true)]

    [int]$Count

)

 

begin {

    if ($ConfirmPreference -eq 'Low') {

       

        <#

            User:

            * selected -Confirm

            * has $Global:ConfirmPreference set to Low.

        #>

        $YesToAll = $false

   

    } else {

   

        # No -Confirm, so we won't prompt the user...

        $YesToAll = $true

       

    }

   

    # NoToAll is always $false - we want to give it a try...

    $NoToAll = $false

   

}

 

process {

    $path = [io.path]::GetTempFileName() + '.txt'

    if ($psCmdlet.ShouldContinue("Create file # $Count`: $path`?",

                'Confirm creating a file',

                [ref]$YesToAll,

                [ref]$NoToAll)) {

        New-Item -ItemType File -Path $path -Confirm:$false

    }

}

}

 

1..10 | Bar –Confirm

The first thing I had to do was keep New-Item silent. Because Confirm will change $ConfirmPreference to “Low” for the life of your function, any cmdlet that supports ShouldProcess will prompt you. That’s how they should behave.

But you can always tell them to ignore $ConfirmPreference. It’s easy to force them to prompt you—just add the Confirm parameter. Making it silent requires that you pass a $false value for the switch parameter, and the only way to do that is to use a colon: Confirm:$false. With that we get a single prompt for each function cycle, but the option to skip prompts at one point is not available to us. What should we do next?

Carefully read ShouldContinue overload definitions. That’s not easy to do from Windows PowerShell (and probably not very informative). But MSDN has good documentation about that method (for more information, see Cmdlet.ShouldContinue Method). Reading the second overload in the list, you will probably notice that it allows you to support the YesToAll and NoToAll options. It references Boolean values for that. So all we need to do is the following:

  • Set YesToAll to the correct value, depending on the user’s selection as shown here:

    if ($ConfirmPreference -eq 'Low') {

       

        <#

            User:

            * selected -Confirm

            * has $Global:ConfirmPreference set to Low.

        #>

        $YesToAll = $false

   

    } else {

   

        # No -Confirm, so we won't prompt the user...

        $YesToAll = $true

       

    }

  • Pass references to YesToAll and NoToAll in the ShouldContinue method call, as shown here:

    if ($psCmdlet.ShouldContinue("Create file # $Count`: $path`?",

                'Confirm creating a file',

                [ref]$YesToAll,

                [ref]$NoToAll)) {

        New-Item -ItemType File -Path $path -Confirm:$false

    }

The output of this function in Windows PowerShell.exe will look similar to the one shown here:

Image of command output

As you can see, there were no prompts other than the one we created. Objects were processed in the pipeline, and we still got proper support for the NoToAll option. The same would happen for YesToAll. So now we can be sure that our function really supports ShouldProcess. This is something I would probably never try to fix if I wasn’t forced by Scripting Games event number 10.

And imagine…all that would not have happened if I stopped after I got 1* for my script. And believe me—I spent quite a bit of time that I hardly have to forge a script that was, in my opinion, very good—and I was sure it worked. 1* is for a script that does not work. That leads me to the last, more general lesson that I learned during this year’s Scripting Games.

Don’t ever give up just because you don’t get rewarded instantly.

I’m ambitious, and I’m not good at losing. And when I saw the note for script, I felt really bad. Several hours of my life was spent on something that someone marked as “not working.” But luckily, I was not alone with the harsh grade. And other’s reactions were calmer. What helped me back was a single sentence from Boe Prox on Twitter, which I will quote again, because when you extend it to anything you do in your life, it should help you do your best, regardless of harsh opinions from others:

“Remember that as long as you did your best on your script and are happy with what you did, then grading should be secondary.”

I could be bitter and frustrated and make future scripts some kind of revenge for the 1* grade on my script, but would I learn new things then? Would I be able to learn new techniques and improve others? Would I have a chance to go to TechEd in Atlanta? Would I be able to meet Ed Wilson and other Windows PowerShell superstars in person?

It was really hard to agree with that grade. It was even worse because it was not accompanied by any comment describing the reason for such a low grade. But it probably forced me to try harder with my next scripts. I already knew that I wouldn’t receive a 5* grade for each, and that I would have to do my best to repeat the third place that I had last year.

And here I am. I won the 2011 Scripting Games. Thanks, Boe! I owe you this one, really. This is probably the most important thing I have learned, because it concerns life as a whole, not only one of the skills I have.

Bartek, thank you for writing a great blog. I love your real-world style.

The 2011 Scripting Games Wrap-up Week will continue tomorrow when our guest blogger will be Steven Murawski.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Dear Bartek,

    well spoken!!! Nice to read through your article!

    And you are absolutely right: It is hard to accept the 1* or 2* grades, when you've done a lot to make your scripts look good, work and maybe added some extras!

    I didn't know, how complicated a proper use of the -Confirm switch would be!

    And, you know what? I will learn from your experience not to implement this switch in my scripts ... unless I have to ... or PS V3 might make it easier :-)))

    Thanks Bartek,

    I hope you are having a wonderful time in Atlanta@teched2011 ( have a beer for me with Ed)

    Klaus.

  • OK, I worked too hard with this -Confirm... :/

    Of course you can do all this heavy-lifting yourself, but there is easier way to do that... ;)

    All you need to do is use ShouldProcess instead of ShouldContinue:

    function Get-ItEasy {

    [cmdletbinding(SupportsShouldProcess = $true)]

    param (

       [Parameter(ValueFromPipeline = $true)]

       [int]$Count,

       [switch]$Force

    )

    process {

       if ($Force -or $pscmdlet.ShouldProcess($Count,"Create Temp File")) {

           $tmp = [io.path]::GetTempFileName() + "_$Count.txt"

           New-Item -ItemType File -Path $tmp -Confirm:$false

       }

    }

    }

    You still need to prevent cmdlet that support ShouldProcess from prompting users, but as you can see code is more clear, and works much better.

    This is typical for me: I have strong tendency for making simple things complicated. Sorry... ;)

  • thank you