Fun With PowerShell Providers

Introduction

Providers are an interesting concept in PowerShell. A provider allows us to use a standard method to communicate with different areas of your computer using the same set of commands. For example, the file system is accessed using a provider. When you use Get-ChildItem it goes through the file system provider to return a list of the files on your computer.

We’ll take a deeper look in a moment, but first let me mention that for all of the examples we’ll display the code, then under it the result of our code. In this article I’ll be using PowerShell Core, 7.1.5, and VSCode. The examples should work in PowerShell 5.1 in the PowerShell IDE, although they’ve not been tested there.

To run a snippet of code highlight the lines you want to execute, then in VSCode press F8 or in the IDE F5. You can display the contents of any variable by highlighting it and using F8/F5.

Listing the Providers

To start with, let’s get a list of the available providers. To do that, PowerShell has the Get-PSProvider cmdlet.

Get-PSProvider

Result:

Name         Capabilities                        Drives
----         ------------                        ------
Registry     ShouldProcess                       {HKLM, HKCU}
Alias        ShouldProcess                       {Alias}
Environment  ShouldProcess                       {Env}
FileSystem   Filter, ShouldProcess, Credentials  {C, D, E, H…}
Function     ShouldProcess                       {Function}
Variable     ShouldProcess                       {Variable}
Certificate  ShouldProcess                       {Cert}

As you can see, there are a variety of providers available out of the box. The default provider is the FileSystem. As stated in the introduction, when you use Get-ChildItem it goes through the FileSystem provider to get a list of files, or more specifically file system objects.

In the right most column of the output, you can see a list of drives. Using the drives we can navigate the provider. To see a detailed list of drives, you can use the Get-PSDrive cmdlet.

Get-PSDrive

Result:

Name           Used (GB)     Free (GB) Provider      Root                 CurrentLocation
----           ---------     --------- --------      ----                 ---------------
Alias                                  Alias
C                 898.79         53.83 FileSystem    C:\                  \Demo\PSCore-QuickStart
Cert                                   Certificate   \
D                 953.72          0.02 FileSystem    D:\
E                 875.38         56.00 FileSystem    E:\
Env                                    Environment
Function                               Function
HKCU                                   Registry      HKEY_CURRENT_USER
HKLM                                   Registry      HKEY_LOCAL_MACHINE
Variable                               Variable
WSMan                                  WSMan

The Name column shows the name we will use when we want to change our location. First look at the column Provider, then at the rows with FileSystem. It shows three drives, C, D, and E. These are the three hard drives on my computer.

The CurrentLocation shows where we are in the provider tree. In the FileSystem provider, these locations are folders. For the C drive, I happen to be in the \Demo\PSCore-QuickStart folder.

Let’s look at how to use some of the other providers.

The Environment Provider

To change to a different provider, you simply use Set-Location followed by the name to move to, just as you would to change to a different hard drive.

Set-Location env
Get-ChildItem

Result:

Name                           Value
----                           -----
__COMPAT_LAYER                 DetectorsAppHealth
ALLUSERSPROFILE                C:\ProgramData
APPDATA                        C:\Users\arcan\AppData\Roaming
CHROME_CRASHPAD_PIPE_NAME      \\.\pipe\crashpad_7700_VQTNLEXXNDEMJHTN
COLORTERM                      truecolor
CommonProgramFiles             C:\Program Files\Common Files
CommonProgramFiles(x86)        C:\Program Files (x86)\Common Files
CommonProgramW6432             C:\Program Files\Common Files
...list truncated for brevity...

This time Get-ChildItem recognized it was operating in the context of the Environment provider, and returned a list of environment variables and their values. (Note I’ve shortened the output for brevity.)

We can access these like any other collection.

$envVars = Get-ChildItem
$envVars[1].Name
$envVars[1].Value

Result:

ALLUSERSPROFILE
C:\ProgramData

Let’s look at another provider.

The Variable Provider

Another useful provider is the Variable provider. It gives access to a list of all variables and their values currently in memory. This includes both user defined variables and built in PowerShell variables. In this example I’ll create a new variable, $aVariable. I’ll then move to the variable provider and list them.

$aVariable = 'Some Value'
Set-Location variable
Get-ChildItem

Result:

Name                           Value
----                           -----
?                              True
^                              Set-Location
$                              Get-ChildItem
args                           {}
aVariable                      Some Value
ConfirmPreference              High
DebugPreference                SilentlyContinue
...list truncated for brevity...

This returns both the names and values for all of the variables. This can be an excellent debugging tool, for example logging all values when an error occurs.

Other Providers

In addition to the built in list of providers you have seen, many modules will add their own providers that you can navigate. On of my favorites is the SQL Server module.

I’ll go over it more in a future post, but know that through it you can use Get-ChildItem to navigate your list of servers, instances, databases, tables, and columns all as if they were your file system.

You will find many modules supply providers you’ll find useful.

Conclusion

In this post we took a brief look at providers, and how to navigate them. In addition, you are now aware there is a big world of providers outside the few default ones you saw in this post.

The demos in this series of blog posts came from my Pluralsight course PowerShell 7 Quick Start for Developers on Linux, macOS and Windows, one of many PowerShell courses I have on Pluralsight. All of my courses are linked on my About Me page.

If you don’t have a Pluralsight subscription, just go to my list of courses on Pluralsight . At the top is a Try For Free button you can use to get a free 10 day subscription to Pluralsight, with which you can watch my courses, or any other course on the site.

Fun With VSCode Snippets for Markdown and PowerShell

Introduction

I have a confession. I love Markdown. There I said it, my confession is out in the open for all to see.

Seriously though, I do find this documentation language very useful, and easy to use. With just a few commands I can produce a nicely formatted document that can be displayed in my code editor, as well as on platforms like GitHub. I’ve even begun authoring these blog posts in Markdown.

A big reason for me is the ability to integrate it into my projects. VSCode, as well as the full blow Visual Studio, support Markdown (with of course the proper extensions installed). When I create a new PowerShell project in VSCode, I can store the projects documentation in Markdown format right alongside the PowerShell code.

Speaking of VSCode…

VSCode Snippets

A great thing about VSCode is the ability to create your own code snippets. A snippet is a text replacement system where I can enter a few characters of text and VSCode will then replace it with the full snippet. Snippets are activated using the Intellisense feature of VSCode.

One example, I have a standard header I put at the top of my PowerShell scripts. When I defined this header as a snippet, I named it psheader. Now I can enter pshead into my PowerShell script. VSCode’s intellisense feature will prompt me with the list of snippets I have that being with pshead. I can then pick the one I want (psheader), press enter and the snippet of psheader will be replaced with the full text of my file header.

By default, pretty much every language in VSCode has the ability to handle snippets.

Except Markdown.

Markdown Snippets in VSCode

Shocking right? How could such an awesome language like Markdown not have snippets? (And yes, Markdown may not be a language in the strictest sense, but it’s a close enough word for now.)

Well it’s possible to enable Markdown snippets in PowerShell. Sometime back I created a GitHub project that shows you how to enable and use snippets for Markdown. In addition, I included my snippets for both PowerShell and Markdown.

Rather than reiterating everything here, I’ll just point you to that repository.

https://github.com/arcanecode/VSCode_User_Snippets

The main file in the repository, ReadMe.md gives a brief overview and explanation on how to use snippets.

The file Enable_Intellisense_in_Markdown.md does just what is says, shows you how to enable intellisense for Markdown in VSCode.

In VSCode, you can use certain replacements in your snippets. For example, you can embed the $CURRENT_YEAR snippet variable in your snippet (no matter what language) and when the snippet is generated into your code, it will replace the $CURRENT_YEAR with the actual current year.

I included a file, Snippet_Reference.md that lists the various snippet variables and gives a bit more explanation on how to use them.

If you aren’t familiar with Markdown, or don’t use it very often, you’ll find the file Markdown_Cheatsheet.md useful. It has a list of the most often used Markdown formatting commands.

Finally I included two .json files. These are the snippets I use for PowerShell and Markdown on my system. You can use these as a guide in creating your own snippets, or copy the ones you find useful onto your VSCode installation.

If you use the VSCode sync settings feature, they you will be happy to know snippets are included as part of the sync process. You can modify your snippet files on one computer and they will be copied to all the other computers you sync VSCode on.

Conclusion

This was a brief post that primarily served to bring awareness to snippets, as well as the ability to use them with Markdown files. Go take a look at the repository and with just a little effort I believe you’ll find yourself becoming much more productive with the addition of snippets to your toolbox.

Fun With PowerShell Write-Debug

Introduction

In my previous post, Fun With PowerShell Write-Verbose, I introduced the use of the built in -Verbose switch. In this post we’ll dive into its counterpart, the -Debug switch and its companion Write-Debug cmdlet.

In covering Write-Verbose, I mentioned verbose messages are typically targeted at the average user. The -Debug messages are meant to target the developer. These messages are meant to assist the PowerShell developer in trouble shooting and debugging their code.

We’ll take a deeper look at Debug momentarily, but first let me mention that for all of the examples we’ll display the code, then under it the result of our code. In this article I’ll be using PowerShell Core, 7.1.4, and VSCode. The examples should work in PowerShell 5.1 in the PowerShell IDE, although they’ve not been tested there.

To run a snippet of code highlight the lines you want to execute, then in VSCode press F8 or in the IDE F5. You can display the contents of any variable by highlighting it and using F8/F5.

$DebugPreference – An Introduction

Before we start discussing the Write-Debug cmdlet, we need to cover a PowerShell built in variable, $DebugPreference. If you want to see the value currently there, just type $DebugPreference into your terminal window and hit ENTER.

Depending on how your system is configured, this will usually be set to either Continue or SilentlyContinue. (If it is set to something else then likely you previously ran a script which changed it).

Usually you want it set to Continue. This will display the message provided to the Write-Debug cmdlet, after doing so it will continue to the next line of code.

If it is set to SilentlyContinue, the message is suppressed. Even if Write-Debug is executed, no text is displayed.

For these demos to work, you need to ensure your $DebugPreference is set to Continue, at least to start with. To set it, just assign it.

$DebugPreference = 'Continue'

Later we’ll see some more options when it comes to $DebugPreference, but for now let’s dig into the Write-Debug cmdlet.

Write-Debug

Just like the Verbose switch, the Debug switch is built in. You don’t have to explicitly declare it. Within your code simply use the Write-Debug cmdlet. If PowerShell detects the -Debug switch is passed in, it will display any messages from the Write-Debug cmdlet.

For this demo, we’ll reuse the Show-FileInfo from the previous blog post, Fun With PowerShell Write-Verbose.

function Show-FileInfo ()
{
  [CmdletBinding()]
  param ( [Parameter (ValueFromPipeline)]
          $file
        )

  begin
  {
    $fn = "$($PSCmdlet.MyInvocation.MyCommand.Name)"
    $st = Get-Date
    Write-Verbose @"
  `r`n  Function: $fn
  Starting at $($st.ToString('yyyy-MM-dd hh:mm:ss tt'))
"@
  }

  process
  {
    $dbgMsg = @"
`r`n  Function.......: $fn
  File Name......: $($file.FullName)
  File Length....: $("{0:N0} bytes." -f $file.Length)
  DebugPreference: $DebugPreference
  PS Version.....: $($PSVersionTable.PSVersion.ToString())
"@

    Write-Debug -Message $dbgMsg

    $retVal = "$($file.Name) is {0:N0} bytes long." -f $file.Length
    $retVal
  }

  end
  {
    $et = Get-Date
    $rt = $et - $st  # Run Time

    # Format the output time
    if ($rt.TotalSeconds -lt 1)
      { $elapsed = "$($rt.TotalMilliseconds.ToString('#,0.0000')) Milliseconds" }
    elseif ($rt.TotalSeconds -gt 60)
      { $elapsed = "$($rt.TotalMinutes.ToString('#,0.0000')) Minutes" }
    else
      { $elapsed = "$($rt.TotalSeconds.ToString('#,0.0000')) Seconds" }

    Write-Verbose @"
  `r`n  Function: $fn
  Finished at $($et.ToString('yyyy-MM-dd hh:mm:ss tt'))
  Elapsed Time $elapsed
"@
  }
}

The only modifications made are to the process block. We created a variable, $dbgMsg to hold the debugging message we wish to display. Since this is a multiline string, I’m going to do the same thing I did with the Verbose messages and begin with a carriage return / line feed escape sequence ( `r`n ). This will let the word DEBUG appear on a line by itself. Also note I indented the remaining lines two spaces, and used periods so the colons could be aligned. It’s a simple bit of formatting, but it makes the provided information much easier to read.

On the first line I have the name of the function. You’d be surprised how useful this can be. When developing a module or a large complex script with many functions, there may be times when you think your script is calling one function when it is actually calling another.

In this example I then show the file name, including the full path to the file. This can be a useful bit of information to the developer, as it lets us know exactly which directory we are in. Especially useful as the main function only displays the file name, not the full path to the file. The line after shows the file length.

I then display some environmental information. First, I show the value in the previously mentioned $DebugPreference variable. This, perhaps, could help me understand why some debug messages are not getting displayed.

On the next line I show the version of PowerShell this code is running in. Again, a potentially valuable piece of information. There’s always the possibility the developer is running a different version of PowerShell than the user having issues, and this may be a clue to an issue.

Finally, we call the Write-Debug cmdlet. The remaining code is the same as the original function from my previous blog post.

After you highlight the function and run it to put it in memory, let’s call it. For this demo I created a folder C:\Demo and copied a few random files into it.

First, let’s call it without the -Debug switch.

Get-ChildItem -Path 'C:\Demo' | Show-FileInfo

Result:

05 - Arrays and Hashtables.ps1 is 8,486 bytes long.
06 - Logic Branching and Looping.ps1 is 4,315 bytes long.
07 - Functions.ps1 is 11,200 bytes long.
08 - Classes.ps1 is 6,210 bytes long.
09 - Examples.ps1 is 3,125 bytes long.
SQLServer2019-x64-ENU-Dev.iso is 1,433,974,784 bytes long.
ubuntu-20.10-desktop-amd64.iso is 2,942,003,200 bytes long.
WideWorldImporters-2021-03-29.zip is 731,246,592 bytes long.
WideWorldImportersDW-2021-03-29.zip is 237,830,144 bytes long.

OK, so far so good. Since we didn’t use the -Debug switch, the Write-Debug statement is ignored. Now let’s call it again, only this time we’ll use -Debug when calling Show-FileInfo.

Get-ChildItem -Path 'C:\Demo' | Show-FileInfo -Debug

Result:

DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\05 - Arrays and Hashtables.ps1
  File Length....: 8,486 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
05 - Arrays and Hashtables.ps1 is 8,486 bytes long.
DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\06 - Logic Branching and Looping.ps1
  File Length....: 4,315 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
06 - Logic Branching and Looping.ps1 is 4,315 bytes long.
DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\07 - Functions.ps1
  File Length....: 11,200 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
07 - Functions.ps1 is 11,200 bytes long.
DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\08 - Classes.ps1
  File Length....: 6,210 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
08 - Classes.ps1 is 6,210 bytes long.
DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\09 - Examples.ps1
  File Length....: 3,125 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
09 - Examples.ps1 is 3,125 bytes long.
DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\SQLServer2019-x64-ENU-Dev.iso
  File Length....: 1,433,974,784 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
SQLServer2019-x64-ENU-Dev.iso is 1,433,974,784 bytes long.
DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\ubuntu-20.10-desktop-amd64.iso
  File Length....: 2,942,003,200 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
ubuntu-20.10-desktop-amd64.iso is 2,942,003,200 bytes long.
DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\WideWorldImporters-2021-03-29.zip
  File Length....: 731,246,592 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
WideWorldImporters-2021-03-29.zip is 731,246,592 bytes long.
DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\WideWorldImportersDW-2021-03-29.zip
  File Length....: 237,830,144 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
WideWorldImportersDW-2021-03-29.zip is 237,830,144 bytes long.

As you can see, our debugging messages are now displayed. For each file it shows the debug message, then shows the normal output of the function, the file name and size.

While a simple example, seeing not just the file name but the full file name including its path could supply valuable information to the developer, not to mention the environmental information.

What about the verbose messages? Well there’s no problem using both switches to see all the messages.

Get-ChildItem -Path 'C:\Demo' | Show-FileInfo -Verbose -Debug

Result (note a few files were omitted for brevity):

VERBOSE:
  Function: Show-FileInfo
  Starting at 2021-08-17 05:48:44 PM
DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\05 - Arrays and Hashtables.ps1
  File Length....: 8,486 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
05 - Arrays and Hashtables.ps1 is 8,486 bytes long.

*** Some rows removed here for brevity ***

DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\WideWorldImportersDW-2021-03-29.zip
  File Length....: 237,830,144 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
WideWorldImportersDW-2021-03-29.zip is 237,830,144 bytes long.
VERBOSE:
  Function: Show-FileInfo
  Finished at 2021-08-17 05:48:44 PM
  Elapsed Time 24.4972 Milliseconds

As you can see, we get the starting and ending messages from the use of -Verbose, as well as the debugging messages.

$DebugPreference – Digging Deeper

Inquire

In an earlier section of this post we introduced the built-in variable, $DebugPreference, and discussed two of its possible settings, Continue and SilentlyContinue.

What if, though, we wanted PowerShell to stop every time it encounters a Write-Debug?

To do that, we can set the $DebugPreference value to Inquire, as we’ll do in our process block.

  process
  {
    $DebugPreference = 'Inquire'

    $dbgMsg = @"
`r`n  Function.......: $fn
  File Name......: $($file.FullName)
  File Length....: $("{0:N0} bytes." -f $file.Length)
  DebugPreference: $DebugPreference
  PS Version.....: $($PSVersionTable.PSVersion.ToString())
"@

    Write-Debug -Message $dbgMsg

    $retVal = "$($file.Name) is {0:N0} bytes long." -f $file.Length
    $retVal
  }

Run the function to get the new version into memory, then let’s call Show-FileInfo again.

Get-ChildItem -Path 'C:\Demo' | Show-FileInfo -Debug

This time, it displays our first debug message, but then displays a prompt in the terminal window.

Result:

DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\05 - Arrays and Hashtables.ps1
  File Length....: 8,486 bytes.
  DebugPreference: Inquire
  PS Version.....: 7.1.4
Confirm
Continue with this operation?
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help (default is "Yes"):

You now have the option to hit Y to continue (or just press ENTER), or stop by pressing H to stop execution. In this case, using S to suspend has the same affect as halting.

What about A, for Yes to All? You might think it will just run the rest of the script without stopping. For this demo though, that’s not the case. The A option is meant for the rare situations where you are piping data through the Write-Debug cmdlet.

In our demo, as I suspect in most situations you’ll encounter, pressing A has the same effect as pressing Y.

Stop

Using Inquire we can prompt the developer (or the person running the script) as to what they want to do once they hit the Write-Debug. Let’s imagine a situation though where want our script to stop running. In a moment we’ll look at a situation where this can be useful, but for now let’s just see stop in action.

Here we’ll just change our $DebugPreference to Stop.

  process
  {
    $DebugPreference = 'Stop'

    $dbgMsg = @"
`r`n  Function.......: $fn
  File Name......: $($file.FullName)
  File Length....: $("{0:N0} bytes." -f $file.Length)
  DebugPreference: $DebugPreference
  PS Version.....: $($PSVersionTable.PSVersion.ToString())
"@

    Write-Debug -Message $dbgMsg

    $retVal = "$($file.Name) is {0:N0} bytes long." -f $file.Length
    $retVal
  }

As usual, re-run the function to refresh the version in memory, then call Show-FileInfo again.

Get-ChildItem -Path 'C:\Demo' | Show-FileInfo -Debug

Result:

DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\05 - Arrays and Hashtables.ps1
  File Length....: 8,486 bytes.
  DebugPreference: Stop
  PS Version.....: 7.1.4
Write-Debug:
Line |
  30 |      Write-Debug -Message $dbgMsg
     |      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | The running command stopped because the preference variable "DebugPreference" or common parameter is set to Stop:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\05 - Arrays and Hashtables.ps1
  File Length....: 8,486 bytes.
  DebugPreference: Stop
  PS Version.....: 7.1.4

You can see our debug message was displayed, then we got a message to let us know the debug preference was set to stop, which is why code execution stopped.

Conditional Stopping

The stop command becomes more useful when we make the stop conditional. We want our code to keep running until we find a certain condition. Once we find it, we’ll display debugging information then stop our script.

While Write-Debug doesn’t have a built in method for this, we can construct the code ourselves using a simple if statement. For this demo we’ll display our debug message until the point we hit a specified condition.

  process
  {
    if ($file.Length -gt 100000)
      { $DebugPreference = 'Stop' }
    else
      { $DebugPreference = 'Continue' }

    $dbgMsg = @"
`r`n  Function.......: $fn
  File Name......: $($file.FullName)
  File Length....: $("{0:N0} bytes." -f $file.Length)
  DebugPreference: $DebugPreference
  PS Version.....: $($PSVersionTable.PSVersion.ToString())
"@

    Write-Debug -Message $dbgMsg

    $retVal = "$($file.Name) is {0:N0} bytes long." -f $file.Length
    $retVal
  }

In this example, pretend we know something goes wrong when our file length exceeds 100 KB (100,000 bytes). We use an if statement to check the length. If it is greater than 100,000 bytes, it will set the $DebugPreference to Stop, otherwise it will set it to Continue.

Note we could have also chosen not to call Write-Debug at all if the length is less then 100,000 bytes, or any other number of things. I chose to go ahead and display the debug info in this case, as there’s always the possibility it may provide useful information. Let’s update our function in memory by running it, then we can execute it.

Get-ChildItem -Path 'C:\Demo' | Show-FileInfo -Debug

Result:

DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\05 - Arrays and Hashtables.ps1
  File Length....: 8,486 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
05 - Arrays and Hashtables.ps1 is 8,486 bytes long.
DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\06 - Logic Branching and Looping.ps1
  File Length....: 4,315 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
06 - Logic Branching and Looping.ps1 is 4,315 bytes long.
DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\07 - Functions.ps1
  File Length....: 11,200 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
07 - Functions.ps1 is 11,200 bytes long.
DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\08 - Classes.ps1
  File Length....: 6,210 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
08 - Classes.ps1 is 6,210 bytes long.
DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\09 - Examples.ps1
  File Length....: 3,125 bytes.
  DebugPreference: Continue
  PS Version.....: 7.1.4
09 - Examples.ps1 is 3,125 bytes long.
DEBUG:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\SQLServer2019-x64-ENU-Dev.iso
  File Length....: 1,433,974,784 bytes.
  DebugPreference: Stop
  PS Version.....: 7.1.4
Write-Debug:
Line |
  33 |      Write-Debug -Message $dbgMsg
     |      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | The running command stopped because the preference variable "DebugPreference" or common parameter is set to Stop:
  Function.......: Show-FileInfo
  File Name......: C:\Demo\SQLServer2019-x64-ENU-Dev.iso
  File Length....: 1,433,974,784 bytes.
  DebugPreference: Stop
  PS Version.....: 7.1.4

As you can see, the first file we encounter with a length of 100,000 bytes or greater causes execution to stop.

Bear in mind, using Stop has a dramatic effect on your code. For my normal day to day development I may use a Stop, but when I send this script to production I’d either remove the stop logic all together, or at least replace it with the previously discussed Inquire setting.

Also note that if you use Stop, the script STOPS. The end block will never execute. This could potentially leave your script, as well as the scripting environment in an unstable state. So as cool as the Stop setting is, I recommend you use it sparingly and as a general rule do not include it in production code.

Resetting $DebugPreference

Once your script is done, it’s a good idea to restore the $DebugPreference setting so it is in a default state for next time. Of course, in the end block you could simply add $DebugPreference = 'SilentlyContinue' (or Continue), but what if that wasn’t the state when your script began?

It’s a better idea to capture the state of $DebugPreference when your script begins. So in the ‘begin’ block, we’ll add a line at the very top, $currentDebugPreference = $DebugPreference.

Then in the end block we’ll add a line to reset the debug preference variable to its value from the start of the script, $DebugPreference = $currentDebugPreference.

Below is the completed script with one other alteration, which you’ll find in the process block. I changed the Stop to Inquire and the Continue to SilentlyContinue. The first change will give us the option to stop or continue. The second will suppress the debug message for the files we (in our fictional scenario) know are OK, those below 100,000 bytes. This was done just for demo purposes, so you could see the effect of the various settings for $DebugPreference.

function Show-FileInfo ()
{
  [CmdletBinding()]
  param ( [Parameter (ValueFromPipeline)]
          $file
        )

  begin
  {
    $currentDebugPreference = $DebugPreference
    $fn = "$($PSCmdlet.MyInvocation.MyCommand.Name)"
    $st = Get-Date
    Write-Verbose @"
  `r`n  Function: $fn
  Starting at $($st.ToString('yyyy-MM-dd hh:mm:ss tt'))
"@
  }

  process
  {
    if ($file.Length -gt 100000)
      { $DebugPreference = 'Inquire' }
    else 
      { $DebugPreference = 'SilentlyContinue' }

    $dbgMsg = @"
`r`n  Function.......: $fn
  File Name......: $($file.FullName)
  File Length....: $("{0:N0} bytes." -f $file.Length)
  DebugPreference: $DebugPreference
  PS Version.....: $($PSVersionTable.PSVersion.ToString())
"@

    Write-Debug -Message $dbgMsg

    $retVal = "$($file.Name) is {0:N0} bytes long." -f $file.Length
    $retVal
  }

  end
  {
    $DebugPreference = $currentDebugPreference
    $et = Get-Date
    $rt = $et - $st  # Run Time

    # Format the output time
    if ($rt.TotalSeconds -lt 1)
      { $elapsed = "$($rt.TotalMilliseconds.ToString('#,0.0000')) Milliseconds" }
    elseif ($rt.TotalSeconds -gt 60)
      { $elapsed = "$($rt.TotalMinutes.ToString('#,0.0000')) Minutes" }
    else
      { $elapsed = "$($rt.TotalSeconds.ToString('#,0.0000')) Seconds" }

    Write-Verbose @"
  `r`n  Function: $fn
  Finished at $($et.ToString('yyyy-MM-dd hh:mm:ss tt'))
  Elapsed Time $elapsed
"@
  }
}

Now let’s run it, after highlighting the function and executing it so it is refreshed in memory. First I’ll set the $DebugPreference to Continue, so it will be different from either the Inquire or SilentlyContinue. Then I’ll call it, using both the Verbose and Debug switches. Finally I’ll display the $DebugPreference again, to prove it was reset at the end of the function. Note too when the Inquire kicks in I will just answer Y so the script can complete normally.

$DebugPreference = 'Continue'
Get-ChildItem -Path 'C:\Demo' | Show-FileInfo -Verbose -Debug
$DebugPreference

Result:

VERBOSE:   
  Function: Show-FileInfo
  Starting at 2021-08-17 06:25:46 PM
05 - Arrays and Hashtables.ps1 is 8,486 bytes long.
06 - Logic Branching and Looping.ps1 is 4,315 bytes long.
07 - Functions.ps1 is 11,200 bytes long.
08 - Classes.ps1 is 6,210 bytes long.
09 - Examples.ps1 is 3,125 bytes long.
DEBUG: 
  Function.......: Show-FileInfo
  File Name......: C:\Demo\SQLServer2019-x64-ENU-Dev.iso
  File Length....: 1,433,974,784 bytes.
  DebugPreference: Inquire
  PS Version.....: 7.1.4
Confirm
Continue with this operation?
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help (default is "Yes"):
SQLServer2019-x64-ENU-Dev.iso is 1,433,974,784 bytes long.
DEBUG: 
  Function.......: Show-FileInfo
  File Name......: C:\Demo\ubuntu-20.10-desktop-amd64.iso
  File Length....: 2,942,003,200 bytes.
  DebugPreference: Inquire
  PS Version.....: 7.1.4
Confirm
Continue with this operation?
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help (default is "Yes"):
ubuntu-20.10-desktop-amd64.iso is 2,942,003,200 bytes long.
DEBUG: 
  Function.......: Show-FileInfo
  File Name......: C:\Demo\WideWorldImporters-2021-03-29.zip
  File Length....: 731,246,592 bytes.
  DebugPreference: Inquire
  PS Version.....: 7.1.4
Confirm
Continue with this operation?
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help (default is "Yes"):
WideWorldImporters-2021-03-29.zip is 731,246,592 bytes long.
DEBUG: 
  Function.......: Show-FileInfo
  File Name......: C:\Demo\WideWorldImportersDW-2021-03-29.zip
  File Length....: 237,830,144 bytes.
  DebugPreference: Inquire
  PS Version.....: 7.1.4
Confirm
Continue with this operation?
[Y] Yes [A] Yes to All [H] Halt Command [S] Suspend [?] Help (default is "Yes"):
WideWorldImportersDW-2021-03-29.zip is 237,830,144 bytes long.
VERBOSE:   
  Function: Show-FileInfo
  Finished at 2021-08-17 06:25:57 PM
  Elapsed Time 11.7336 Seconds
Continue

As you would probably expect by this point in the post, we show our verbose information. We then see a list of files from the normal code in the script. The debug messages for files under 100,000 bytes were suppressed because we’d set the $DebugPreference to SilentlyContinue.

We then see the debug messages, along with inquiries as to our wish to continue. Since we said yes to all of them, the code in the end block was able to execute and display the closing verbose message.

The very last line of the result is Continue, which came from the last line we used to run the script, $DebugPreference. This confirms the value was indeed reset to the value that was present when the script started.

Conclusion

Well this was a monster sized post, so thanks for sticking with it to the end. In it, we saw how the built in -Debug switch works along with the companion Write-Debug. We also covered the built-in PowerShell variable $DebugPreference, and saw how it can affect the behavior of the Write-Debug cmdlet.

As you can see, the use of Debug can provide valuable feedback to you as a PowerShell developer, supplying critical information about the internal state of your script as well as the environment it is running in.

If you want to learn more about PowerShell, check out my Pluralsight course PowerShell 7 Quick Start for Developers on Linux, macOS and Windows, one of many PowerShell courses I have on Pluralsight. All of my courses are linked on my About Me page.

If you don’t have a Pluralsight subscription, just go to my list of courses on Pluralsight . At the top is a Try For Free button you can use to get a free 10 day subscription to Pluralsight, with which you can watch my courses or any other course on the site.