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.

Leave a comment