Users Online

· Guests Online: 26

· Members Online: 0

· Total Members: 188
· Newest Member: meenachowdary055

Forum Threads

Newest Threads
No Threads created
Hottest Threads
No Threads created

Latest Articles

Introduction to scripting in PowerShell

Introduction to scripting in PowerShell

 

 

This module introduces you to scripting with PowerShell. It introduces various concepts to help you create script files and make them as robust as possible.

Learning objectives

  • Understand how to write and run scripts.
  • Use variables and parameters to make your scripts flexible.
  • Apply flow-control logic to make intelligent decisions.
  • Add robustness to your scripts by adding error management.

 

 

Prerequisites

  • Basic familiarity with using a command-line shell like Command Prompt or Git Bash
  • Visual Studio Code
  • Ability to install Visual Studio Code extensions
  • Ability to install software on your computer, if you're not using a Windows operating system
  • Familiarity with running commands in PowerShell

Introduction

 

In this module, you'll learn the basics of PowerShell scripting. Scripting is about automation. It's about storing steps that you do often in a file. By running files that contain your scripts, you can save time and reuse existing solutions. You can then spend that time on something more valuable.

In this module, you'll learn some helpful constructs of the PowerShell language and how you can use them to create and run scripts.

Learning objectives

After completing this module, you'll be able to:

  • Write and run scripts.
  • Use variables and parameters to make your scripts flexible
  • Apply flow-control logic to make intelligent decisions
  • Add robustness to your scripts by adding error management

Introduction to scripting

 

PowerShell scripting is the process of writing a set of statements in the PowerShell language and storing those statements in a text file. Why would you do that? After you use PowerShell for a while, you find yourself repeating certain tasks, like producing log reports or managing users. When you've repeated something frequently, it's probably a good idea to automate it: to store it in such a way that makes it easy to reuse.

The steps to automate your task usually include calls to cmdlets, functions, variables, and more. To store these steps, you'll create a file that ends in .ps1 and save it. You'll then have a script you can run.

Before you start learning to script, let's get an overview of the features of the PowerShell scripting language:

  • Variables. You can use variables to store values. You can also use variables as arguments to commands.

  • Functions. A function is a named list of statements. Functions produce an output that display in the console. You can also use functions as input for other commands.

      Note

    Many of the tasks you'd use PowerShell for are about side effects or modifications to system state (local or otherwise). Often the output is a secondary concern (reporting data, for example).

  • Flow control. Flow control is how you control various execution paths by using constructs like IfElseIf, and Else.

  • Loops. Loops are constructs that let you operate on arrays, inspect each item, and do some kind of operation on each item. But loops are about more than array iteration. You can also conditionally continue to run a loop by using Do-While loops. For more information, see About Do.

  • Error handling. It's important to write scripts that are robust and can handle various types of errors. You'll need to know the difference between terminating and non-terminating errors. You'll use constructs like Try and Catch. We'll cover this topic in the last conceptual unit of this module.

  • Expressions. You'll frequently use expressions in PowerShell scripts. For example, to create custom columns or custom sort expressions. Expressions are representations of values in PowerShell syntax.

  • .NET and .NET Core integration. PowerShell provides powerful integration with .NET and .NET Core. This integration is beyond the scope of this module.

Run a script

You need to be aware that some scripts aren't safe. If you find a script on the internet, you probably shouldn't run it on your computer unless you understand exactly what it does. Even with scripts you consider safe, there might be a risk. For example, imagine a script that cleans things up in a test environment. That script might be harmful in a production environment. You need to understand what a script does, whether it was written by you or by a colleague or if you got it from the internet.

PowerShell attempts to protect you from doing things unintentionally in two main ways:

  • Requirement to run scripts by using a full path or relative path. When you run a script, you always need to provide the script's path. Providing the path helps you to know exactly what you're running. For example, there could be commands and aliases on your computer you don't intend to run, but that have the same name as your script. Including the path provides an extra check to ensure you run exactly what you want to run.
  • Execution policy. An execution policy is a safety feature. Like requiring the path of a script, a policy can stop you from doing unintentional things. You can set the policy on various levels, like the local computer, current user, or particular session. You can also use a Group Policy setting to set execution policies for computers and users.

These two mechanisms don't stop you from opening a file, copying its contents, placing the contents in a text file, and running the file. They also don't stop you from running the code via the console. These mechanisms help to stop you from doing something unintentional. They aren't a security system.

To create and run a script:

  1. Create some PowerShell statements like the following and save them in a file that ends with .ps1:

    PowerShell
    # PI.ps1
    $PI = 3.14
    Write-Host "The value of `$PI is $PI"
    
  2. Run the script by invoking it by its name and path:

      Note

    Before you run the script, ensure the current shell is PowerShell. Alternatively, on Linux or macOS, you can put a shebang at the top of the script file to define PowerShell as the script interpreter.

    Bash 
    ./PI.ps1
    

    We recommend you include the file extension in the invocation, but it's not required.

Execution policy

You can manage execution policy using these cmdlets:

  • Get-ExecutionPolicy. This cmdlet returns the current execution policy. On Linux and macOS, the value returned is Unrestricted. For these operating systems, you can't change the value. That limitation doesn't make Linux or Mac any less safe. Remember, an execution policy is a safety feature, not a security mechanism.

  • Set-ExecutionPolicy. If you're using a Windows computer, you can use this cmdlet to change the value of an execution policy. It takes an -ExecutionPolicy parameter. There are a few possible values. It's a good idea to use Default as the value. That value sets the policy to Restricted on Windows clients and RemoteSigned on Windows Server. Restricted means you can't run scripts. You can run only commands, which makes sense on a client. RemoteSigned means that scripts written on the local computer can run. Scripts downloaded from the internet need to be signed by a digital signature from a trusted publisher.

      Note

    There are other values you can use. To learn more, see About execution policies.

Variables

Variables aren't just for scripts. You can also define them on the console. You can store values in variables so you can use them later. To define a variable, precede it with the $ character. Here's an example:

PowerShell
$PI = 3.14

Working with variables: Quotation marks and interpolation

When you output text via Write-Host or Write-Output, you can use single or double quotation marks. Your choice depends on whether you want to interpolate the values. There are three mechanisms you should know about:

  • Single quotation marks. Single quotation marks specify literals; what you write is what you get. Here's an example:

    PowerShell
    Write-Host 'Here is $PI' # Prints Here is $PI
    

    If you want to interpolate — to get the value of $PI interpreted and printed — you need to use double quotation marks.

  • Double quotation marks. When you use double quotation marks, variables in strings are interpolated:

    PowerShell
    Write-Host "Here is `$PI and its value is $PI" # Prints Here is $PI and its value is 3.14
    

    There are two things going on here. The back tick (`) lets you escape what would be an interpolation of the first instance of $PI. In the second instance, the value is interpolated and is written out.

  • $(). You can also write an expression within double quotation marks. To do that, use the $() construct. One way to use this construct is to interpolate properties of objects. Here's an example:

    PowerShell
    Write-Host "An expression $($PI + 1)" # Prints An expression 4.14
    

Scope

Scope is how PowerShell defines where constructs like variables, aliases, and functions can be read and changed. When you're learning to write scripts, you need to know what you have access to, what you can change, and where you can change it. If you don't understand how scope works, your code might not work as you expect it to.

Types of scope

Let's talk about the various scopes:

  • Global scope. When you create constructs like variables in this scope, they continue to exist after your session ends. Anything that's present when you start a new PowerShell session can be said to be in this scope.

  • Script scope. When you run a script file, a script scope is created. For example, a variable or a function defined in the file is in the script scope. It will no longer exist after the file is finished running. For example, you can create a variable in the script file and target the global scope. But you need to explicitly define that scope by prepending the variable with the global keyword.

  • Local scope. The local scope is the current scope, and can be the global scope or any other scope.

Scope rules

Scope rules help you understand what values are visible at a given point. They also help you understand how to change a value.

  • Scopes can nest. A scope can have a parent scope. A parent scope is an outer scope, outside of the scope you're in. For example, a local scope can have the global scope as a parent scope. Conversely, a scope can have a nested scope, also known as a child scope.

  • Items are visible in the current and child scopes. An item, like a variable or a function, is visible in the scope in which it's created. By default, it's also visible in any child scopes. You can change that behavior by making the item private within the scope. Here's an example that uses a variable defined in the console:

    PowerShell
    $test = 'hi'
    

    If you have a Script.ps1 file that contains the following content, it will print "hi" when the script runs:

    PowerShell

    You can see that the variable $test is visible in both the local scope and its child scope, in this case, the script scope.

Write-Host $test # Prints hi
  • Items can be changed only in the created scope. By default, you can change an item only in the scope in which it was created. You can change this behavior by explicitly specifying a different scope.

Profiles

A profile is a script that runs when PowerShell starts. You can use a profile to customize your environment to, for example, change background colors and errors and do other types of customizations. PowerShell will apply these changes to each new session you start.

Profile types

PowerShell supports several profile files. You can apply them at various levels, as you see here:

DescriptionPath
All users, all hosts $PSHOME\Profile.ps1
All users, current host $PSHOME\Microsoft.PowerShell_profile.ps1
Current user, all hosts $Home[My ]Documents\PowerShell\Profile.ps1
Current user, current host $Home[My ]Documents\PowerShell\Microsoft.PowerShell_profile.ps1

 

There are two variables here: $PSHOME and $Home$PSHOME points to the installation directory for PowerShell. $Home is the current user's home directory.

 

Other programs also support profiles, like Visual Studio Code.

Create a profile

When you first install PowerShell, there are no profiles, but there is a $Profile variable. It's an object that points to the path where each profile to apply should be placed. To create a profile:

  1. Decide the level on which you want to create the profile. You can run $Profile | Select-Object * to see the profile types and the paths associated with them.

  2. Select a profile type and create a text file at its location by using a command like this one: New-Item -Path $Profile.CurrentUserCurrentHost.

  3. Add your customizations to the text file and save it. The next time you start a session, your changes will be applied.

Exercise - Scripting

 

Set up a profile

A profile is a script that runs when you start a new session. Having a customized environment can make you more productive.

  1. Enter pwsh in a terminal window to start a PowerShell session:

    Bash
    pwsh
    
  2. Run this command:

    PowerShell
    $Profile | Select-Object *
    

    The output will display something similar to this text:

    Output
    CurrentUserAllHosts                        CurrentUserCurrentHost
    -------------------                        ----------------------
    /home/<user>/.config/PowerShell/profile.ps1 /home/<user>/.config/PowerShell/Microsoft.…
    
  3. Create a profile for the current user and the current host by running the command New-Item:

    PowerShell
    New-Item `
      -ItemType "file" `
      -Value 'Write-Host "Hello <replace with your name>, welcome back" -foregroundcolor Green ' `
      -Path $Profile.CurrentUserCurrentHost -Force
    

    The -Force switch will overwrite existing content, so be careful if you run this locally and have an existing profile.

  4. Run pwsh to create a new shell. You should now see the following (in green):

    Output
    Hello <your name>, welcome back
    

Create and run a script

Now that you have a profile set up, it's time to create and run a script.

  1. Ensure you have an existing PowerShell session running. In the console window, enter this code:

    PowerShell
$PI = 3.14
  1. Create a file named PI.ps1 in the current directory and open it in your code editor:

    PowerShell
    New-Item -Path . -Name "PI.ps1" -ItemType "file"
    code PI.ps1
    
  2. Add the following content to the file and save it. You can use CTRL+S on Windows and Linux or CMD+S on Mac to save your file.

    PowerShell
    $PI = 3
    Write-Host "The value of `$PI is now $PI, inside the script"
    
  3. Run the script by specifying the path to it:

    Bash
    ./PI.ps1
    

    Your output displays the following text:

    Output
    The value of $PI is now 3, inside the script
    

    Your script does two things. First, it creates a script-local variable $PI that shadows the $PI variable defined in the local scope. Next, the second row in the script interpolates the $PI variable because you used double quotation marks. It escapes interpolation the first time because you used a back tick.

  4. Enter $PI in the console window:

    Output
    3.14
    

    The value is still 3.14. The script didn't change the value.

 

 

 

Parameters

 

 

After you've created a few scripts, you might notice your scripts aren't flexible. Going into your scripts to change them isn't efficient. There's a better way to handle changes: use parameters.

Using parameters makes your scripts flexible, because it allows users to select options or send input to the scripts. You won't need to change your scripts as frequently, because in some cases you'll just need to change a parameter value.

Cmdlets, functions, and scripts all accept parameters.

Declare and use a parameter

To declare a parameter, you need to use the keyword Param with an open and close parenthesis:

PowerShell
Param()

Inside the parentheses, you define your parameters, separating them with commas. A typical parameter declaration might look like this:

PowerShell
# CreateFile.ps1
Param (
  $Path
)
New-Item $Path # Creates a new file at $Path.
Write-Host "File $Path was created"

The script has a $Path parameter that's later used in the script to create a file. The script is now more flexible.

Use the parameter

To call a script with a parameter, you need to provide a name and a value. Assume the above script is called CreateFile.ps1. You could call it like this:

PowerShell
./CreateFile.ps1 -Path './newfile.txt' # File ./newfile.txt was created.
./CreateFile.ps1 -Path './anotherfile.txt' # File ./anotherfile.txt was created.

Because you used a parameter, you don't need to change the script file when you want to call the file something else.

  Note

This particular script might not benefit much from using a parameter, because it only calls New-Item. As soon as your script is a few lines long, using the parameter will pay off.

Improve your parameters

When you first create a script that uses parameters, you might remember exactly what the parameters are for and what values are reasonable for them. As time passes, you might forget those details. You might also want to give a script to a colleague. The solution in these cases is to be explicit, which makes your scripts easy to use. You want a script to fail early if it passes unreasonable parameter values. Here are some things to consider when you define parameters:

  • Is it mandatory? Is the parameter optional or required?
  • What values are allowed? What values are reasonable?
  • Does it accept more than one type of value? Does the parameter accept any type of value, like string, Boolean, integer, and object?
  • Can the parameter rely on a default? Can you omit the value altogether and rely on a default value instead?
  • Can you further improve the user experience? Can you be even clearer to your user by providing a Help message?

Select an approach

All parameters are optional by default. That default might work in some cases, but sometimes you need your user to provide parameter values, and the values need to be reasonable ones. If the user doesn't provide a value to a parameter, the script should quit or tell the user how to fix the problem. The worst scenario is for the script to continue and do things you don't want it to do.

There are a couple of approaches you can use to make your script safer. You can write custom code to inspect the parameter value. Or, you can use decorators that do roughly the same thing. Let's look at both approaches.

  • Use If/Else. The If/Else construct allows you to check the value of a parameter and then decide what to do. Here's an example:

    PowerShell
    Param(
       $Path
    )
    If (-Not $Path -eq '') {
       New-Item $Path
       Write-Host "File created at path $Path"
    } Else {
       Write-Error "Path cannot be empty"
    } 
    

    The script will run Write-Error if you don't provide a value for $Path.

  • Use the Parameter[] decorator. A better way, which requires less typing, is to use the Parameter[] decorator:

    PowerShell
    Param(
       [Parameter(Mandatory)]
       $Path
    )
    New-Item $Path
    Write-Host "File created at path $Path"
    

    If you run this script and omit a value for $Path, you end up in a dialog that prompts for the value:

    Output
    cmdlet CreateFile.ps1 at command pipeline position 1
    Supply values for the following parameters:
    Path:
    

    You can improve this decorator by providing a Help message users will see when they run the script:

    PowerShell
    [Parameter(Mandatory, HelpMessage = "Please provide a valid path")]
    

    When you run the script, you get a message that tells you to type !? for more information:

    PowerShell
    cmdlet CreateFile.ps1 at command pipeline position 1
    Supply values for the following parameters:
    (Type !? for Help.)
    Path: !?  # You type !?
    Please provide a valid path  # Your Help message.
    
  • Assign a type. If you assign a type to a parameter, you can say, for example, that the parameter accepts only strings, not Booleans. That way, the user knows what to expect. You can assign a type to a parameter by preceding it with the type enclosed in brackets:

    PowerShell
    Param(
       [string]$Path
    )
    

These three approaches aren't mutually exclusive. You can combine them to make your script safer.

 

 

 

 

 

Exercise - Parameters

Create a backup script

A common task is to create a backup. A backup is usually a compressed file that stores all the files belonging to, for example, an app. When you installed PowerShell, you got the cmdlet Compress-Archive, which can help you complete this task.

  1. In your Cloud Shell terminal, run these bash commands:

    Bash
    mkdir app
    cd app
    touch index.html app.js
    cd ..
    

    You should now have a directory named app. You're ready to work with PowerShell.

  2. In the same terminal, start a PowerShell shell (if it's not already started) by running pwsh:

    Bash
    pwsh
    
  3. Create a script file named Backup.ps1 in the current directory and open it in your code editor.

    Bash
    touch Backup.ps1
    code Backup.ps1
    
  4. Add this content to the file and save the file. You can use CTRL-S on Windows and Linux or CMD+S on Mac to save.

    PowerShell
    $date = Get-Date -format "yyyy-MM-dd"
    Compress-Archive -Path './app' -CompressionLevel 'Fastest' -DestinationPath "./backup-$date"
    Write-Host "Created backup at $('./backup-' + $date + '.zip')"
    

    The script invokes Compress-Archive and uses three parameters:

    • -Path is the directory of the files that you want to compress.
    • -CompressionLevel specifies how much to compress the files.
    • -DestinationPath is the path to the resulting compressed file.
  5. Run the script:

    PowerShell
    ./Backup.ps1 
    

    You should see this output:

    Output
    Created backup at ./backup-<current date as YYYY-MM-DD>.zip
    

Add parameters to your script

If you add parameters to your script, users can provide values when it runs. You'll add parameters to your backup script to enable configuration of the locations of the source files and the resulting zip file.

  1. Add the following code to the top of the Backup.ps1 file.

      Note

    Use the code Backup.ps1 command to open the file if the editor isn't open.

    PowerShell
    Param(
      [string]$Path = './app',
      [string]$DestinationPath = './'
    )
    

    You've added two parameters to your script: $Path and $DestinationPath. You've also provided default values so users don't need to provide the values. Users can override the default values if they need to. You need to adjust the script to use these parameters. You'll do so next.

  2. Change the code in the file to use the parameters, then save the file. Backup.ps1 should now look like this:

    PowerShell
    Param(
      [string]$Path = './app',
      [string]$DestinationPath = './'
    )
    $date = Get-Date -format "yyyy-MM-dd"
    Compress-Archive -Path $Path -CompressionLevel 'Fastest' -DestinationPath "$($DestinationPath + 'backup-' + $date)"
    Write-Host "Created backup at $($DestinationPath + 'backup-' + $date + '.zip')"
    
  3. Rename your app directory to webapp by running this command:

    Bash
    mv app webapp
    

    Renaming the app directory simulates the fact that not all directories you'll need to back up will be called app.

    You can no longer rely on the default value for $Path. You'll need to provide a value via the console when you run the script.

  4. Remove your backup file, replacing <current date as YYYY-MM-DD> with the current date:

    Bash
    rm backup-<current date as YYYY-MM-DD>.zip
    

    You're removing this file to make sure you get a message stating that your $Path value doesn't exist. Otherwise, you'd get a message about the zip file already existing, and the problem we're trying to fix would be hidden.

  5. Run your script without providing parameters. (The script will use default values for the parameters.)

    Bash
    ./Backup.ps1
    

    You'll see an error message similar to this one:

    Output
    Line |
       8 |  Compress-Archive -Path $Path -CompressionLevel 'Fastest' -Destination …
         |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         | The path './app' either does not exist or is not a valid file system path.
    Created backup at ./backup-<current date as YYYY-MM-DD>.zip
    

    The script notifies you that it can't find the directory ./app. Now it's time to provide a value to the $Path parameter and see the benefit of adding parameters to your script.

  6. Test your script by running it:

    PowerShell
    ./Backup.ps1 -Path './webapp'
    

    You'll see a message similar to the one you got earlier:

    Output
    Created backup at ./backup-<current date as YYYY-MM-DD>.zip
    

    You can now use parameters if the directory you want to back up isn't called ./app or if you want to put the compressed file somewhere other than the current directory.

Congratulations. You created a backup script that you can use whenever you want to create a backup for an app directory or any other important directory. You then identified parts of your script that might need to change often and replaced static values with parameter values. That way, you most likely won't need to change the script itself when your requirements change (for example, if the name of the app changes or you need to name the destination file something else).

 

 

 

Flow control

 

 

Flow control refers to how your code runs in your console or script. It describes the flow the code follows and how you control that flow. There are various constructs available to help you control the flow. The code can run all the statements, or only some of them. It can also repeat certain statements until it meets a certain condition.

Let's examine these flow-control constructs to see what they can do:

  • Sanitize input. If you use parameters in a script, you need to ensure your parameters hold reasonable values so your script works as intended. Writing code to manage this process is called sanitizing input.

  • Control execution flow. The previous technique ensures you get reasonable and correct input data. This technique is more about deciding how to run code. The values set can determine which group of statements runs.

  • Iterate over data. Sometimes your data takes the form of an array, which is a data structure that contains many items. For such data, you might need to examine each item and perform an operation for each one. Many constructs in PowerShell can help you with that process.

      Note

    Iterating over arrays is outside the scope of this module. There are many constructs to handle flow control in PowerShell. We can't name them all, but we'll talk about some important ones that you're likely to encounter in scripts that you read or write.

Manage input and execution flow by using IfElseIf, and Else

You can use an If construct to determine if an expression is True or False. Depending on that determination, you might run the statement defined by the If construct. The syntax for If looks like this:

PowerShell
If (<expression that evaluates to True or False>) 
{
  # Statement that runs only if the preceding expression is $True.
}

Operators

PowerShell has two built-in parameters to determine if an expression is True or False:

  • $True indicates that an expression is True.
  • $False indicates that an expression is False.

You can use operators to determine if an expression is True or False. There are a few operators. The basic idea is usually to determine if something on the left side of the operator matches something on the right side, given the operator's condition. An operator can express conditions like whether something is equal to something else, larger than something else, or matches a regular expression.

Here's an example of using an operator. The -le operator determines if the value on the left side of the operator is less than or equal to the value on the right side:

PowerShell
$Value = 3
If ($Value -le 0) 
{
  Write-Host "Is negative"
}

This code won't display anything because the expression evaluates to False. The value 3 is clearly positive.

Else

The If construct runs statements only if they evaluate to True. What if you want to handle cases where they evaluate to False? That's when you use the Else construct. If expresses "if this specific case is true, run this statement." Else doesn't take an expression. It captures all cases where the If clause evaluates to False. When If and Else are combined, the code runs the statements in one of the two constructs. Let's modify the previous code to include an Else construct:

PowerShell
$Value = 3
If ($Value -le 0) 
{
  Write-Host "Is negative"
} Else {
  Write-Host "Is Positive"
}

Because we put the Else next to the ending brace for the If, we created a joined construct that works as one. If you run this code in the console, you'll see that Is Positive prints. That's because If evaluates to False, but Else evaluates to True. So Else prints its statement.

 Note

You can use Else only if there's an If construct defined immediately above it.

ElseIf

If and Else work great to cover all the paths code can take. ElseIf is another construct that can be helpful. ElseIf is meant to be used with If. It says "the expression in this construct will be evaluated if the preceding If statement evaluates to False." Like IfElseIf can take an expression, so it helps to think of ElseIf as a secondary If.

Here's an example that uses ElseIf:

PowerShell
# _FullyTax.ps1_
# Possible values: 'Minor', 'Adult', 'Senior Citizen'
$Status = 'Minor'
If ($Status -eq 'Minor') 
{
  Write-Host $False
} ElseIf ($Status -eq 'Adult') {
  Write-Host $True
} Else {
  Write-Host $False
}

It's possible to write this code in a more compact way, but this way does show the use of ElseIf. It shows how If is evaluated first, then ElseIf, and then Else.

 Note

As with Else, you can't use ElseIf if you don't define an If above it.

 

 

 

 

Exercise - Flow control

 

If you haven't completed the previous exercises in this module, run the following bash commands in a terminal:

Bash
mkdir webapp
cd webapp
touch index.html app.js
cd ..

 

Add checks to your script parameters

You've been working with a backup script so far, and you've been adding parameters to it. You can make your script even safer to use by adding checks that ensure the script only continues if it's provided reasonable parameter inputs.

Let's look at the current script. If you completed the previous exercise, you should have a file called Backup.ps1. If not, create the file and open it in your code editor:

Bash
touch Backup.ps1
code Backup.ps1

Add this code to the file:

PowerShell
Param(
  [string]$Path = './app',
  [string]$DestinationPath = './'
)
$date = Get-Date -format "yyyy-MM-dd"
Compress-Archive -Path $Path -CompressionLevel 'Fastest' -DestinationPath "$($DestinationPath + 'backup-' + $date)"
Write-Host "Created backup at $($DestinationPath + 'backup-' + $date + '.zip')"

As you know, the script will stop responding if $Path points to a directory that doesn't exist.

  1. Use an existing PowerShell shell if you have one running. Otherwise, start one by typing pwsh in a terminal:

    Bash
    pwsh
    
  2. Add a check for the $Path parameter by adding this code right after the Param section, then save the file:

    PowerShell
    If (-Not (Test-Path $Path)) 
    {
      Throw "The source directory $Path does not exist, please specify an existing directory"
    }
    

    You've added a test that checks if $Path exists. If it doesn't, you stop the script. You also explain to users what went wrong so they can fix the problem.

  3. Ensure the script works as intended by running it:

    PowerShell
    ./Backup.ps1 -Path './app'
    

    You should see this output:

    Output
    Throw "The source directory $Path does not exist, please specify  …
      |      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      | The source directory ./app does not exist, please specify an
      | existing directory
    
  4. Test that the script still works as intended. (Be sure to remove any backup files from the previous exercise before you continue.)

    PowerShell
    ./Backup.ps1 -Path './webapp'
    

    You should see a message that looks similar to this one:

    Output
    Created backup at ./backup-2021-01-19.zip 
    

    If you run the script again, it will stop responding. It will notify you that the zip file already exists. Let's fix that problem. We'll add code to ensure the backup is created only if no other backup zip file from the current day exists.

  5. Replace the code in the file with this code, then save the file:

    PowerShell
    Param(
      [string]$Path = './app',
      [string]$DestinationPath = './'
    )
    If (-Not (Test-Path $Path)) 
    {
      Throw "The source directory $Path does not exist, please specify an existing directory"
    }
    $date = Get-Date -format "yyyy-MM-dd"
    $DestinationFile = "$($DestinationPath + 'backup-' + $date + '.zip')"
    If (-Not (Test-Path $DestinationFile)) 
    {
      Compress-Archive -Path $Path -CompressionLevel 'Fastest' -DestinationPath "$($DestinationPath + 'backup-' + $date)"
      Write-Host "Created backup at $($DestinationPath + 'backup-' + $date + '.zip')"
    } Else {
      Write-Error "Today's backup already exists"
    }
    

    You did two things here. First, you created a new variable, $DestinationFile. This variable makes it easy to check if the path already exists. Secondly, you added logic that says "create the zip file only if the file doesn't already exist." This code implements that logic:

    PowerShell
    If (-Not (Test-Path $DestinationFile)) 
    {
      Compress-Archive -Path $Path -CompressionLevel 'Fastest' -DestinationPath "$($DestinationPath + 'backup-' + $date)"
      Write-Host "Created backup at $($DestinationPath + 'backup-' + $date + '.zip')"
    } Else {
      Write-Error "Today's backup already exists"
    }
    
  6. Run the code to make sure the script doesn't stop responding and that your logic is applied:

    Bash
    ./Backup.ps1 -Path './webapp' 
    

    You should see this output:

    Output
    Write-Error: Today's backup already exists
    

Congratulations. You've made your script a little safer. (Note that it's still possible to provide problematic input to $DestinationPath, for example.) The point of this exercise is to show how to add checks. Depending on the environment in which the script will run, you might want fewer or more checks. You might even want written tests; it all depends on the context.

 

 

 

 

Error handling

 

So far, you've seen how adding parameters and flow-control constructs can make your scripts flexible and safer to use. But sometimes, you'll get errors in your scripts. You need a way to handle those errors.

Here are some factors to consider:

  • How to handle the error. Sometimes you get errors you can recover from, and sometimes it's better to stop the script. It's important to think about the kinds of errors that can happen and how to best manage them.

  • How severe the error is. There are various kinds of error messages. Some are more like warnings to the user that something isn't OK. Some are more severe, and the user really needs to pay attention. Your error-handling approach depends on the type of error. The approach could be anything from presenting a message to raising the severity level and potentially stopping the script.

Errors

A cmdlet or function, for example, might generate many types of errors. We recommend that you write code to manage each type of error that might occur, and that you manage them appropriately given the type. For example, say you're trying to write to a file. You might get various types of errors depending on what's wrong. If you're not allowed to write to the file, you might get one type of error. If the file doesn't exist, you might get another type of error, and so on.

There are two types of errors you can get when you run PowerShell:

  • Terminating error. An error of this type will stop execution on the row where the error occurred. You can handle this kind of error using either Try-Catch or Trap. If the error isn't handled, the script will quit at that point and no statements will run.

      Note

    The Trap construct is outside the scope of this module. If you're interested, see About Trap.

  • Non-terminating error. This type of error will notify the user that something is wrong, but the script will continue. You can upgrade this type of error to a terminating error.

Managing errors by using Try/Catch/Finally

You can think of a terminating error as an unexpected error. These errors are severe. When you deal with one, you should consider what type of error it is and what to do about it.

There are three related constructs that can help you manage this type of error:

  • Try. You'll use a Try block to encapsulate one or more statements. You'll place the code that you want to run — for example, code that writes to a data source — inside braces. A Try must have at least one Catch or Finally block. Here's how it looks:

    PowerShell
    Try {
       # Statement. For example, call a command.
       # Another statement. For example, assign a variable.
    }
    
  • Catch. You'll use this keyword to catch or manage an error when it occurs. You'll then inspect the exception object to understand what type of error occurred, where it occurred, and whether the script can recover. A Catch follows immediately after a Try. You can include more than one Catch — one for each type of error — if you want. Here's an example:

    PowerShell
    Try {
       # Do something with a file.
    } Catch [System.IO.IOException] {
       Write-Host "Something went wrong"
    }  Catch {
       # Catch all. It's not an IOException but something else.
    }
    

    The script tries to run a command that does some I/O work. The first Catch catches a specific type of error: [System.IO.IOException]. The last Catch catches anything that's not a [System.IO.IOException].

  • Finally. The statements in this block will run regardless of whether anything goes wrong. You probably won't use this block much, but it can be useful for cleaning up resources, for example. To use it, add it as the last block:

    PowerShell
    Try {
       # Do something with a file.
    } Catch [System.IO.IOException] {
       Write-Host "Something went wrong"
    }  Catch {
       # Catch all. It's not an IOException but something else.
    } Finally {
       # Clean up resources.
    }
    

Inspecting errors

We've talked about exception objects in the context of catching errors. You can use these objects to inspect what went wrong and take appropriate measures. An exception object contains:

  • A message. The message tells you in a few words what went wrong.

  • The stacktrace. The stacktrace tells you which statements ran before the error. Imagine you have a call to function A, followed by B, followed by C. The script stops responding at C. The stacktrace will show that chain of calls.

  • The offending row. The exception object also tells you which row the script was running when the error occurred. This information can help you debug your code.

So how do you inspect an exception object? There's a built-in variable, $_, that has an exception property. To get the error message, for example, you would use $_.exception.message. In code, it might look like this:

PowerShell
Try {
     # Do something with a file.
   } Catch [System.IO.IOException] {
     Write-Host "Something IO went wrong: $($_.exception.message)"
   }  Catch {
     Write-Host "Something else went wrong: $($_.exception.message)"
   }

Raising errors

In some situations, you might want to cause an error:

  • Non-terminating errors. For this type of error, PowerShell just notifies you that something went wrong, by using the Write-Error cmdlet, for example. The script continues to run. That might not be the behavior you want. To raise the severity of the error, you can use a parameter like -ErrorAction to cause an error that can be caught with Try/Catch, like so:

    PowerShell
    Try {
       Get-Content './file.txt' -ErrorAction Stop
    } Catch {
       Write-Error "File can't be found"
    }
    

    By using the -ErrorAction parameter and the value Stop, you can cause an error that Try/Catch can catch.

  • Business rules. You might have a situation where the code doesn't actually stop responding, but you want it to for business reasons. Imagine you're sanitizing input and you check whether a parameter is a path. A business requirement might specify only certain paths are allowed, or the path needs to look a certain way. If the checks fail, it makes sense to throw an error. In a situation like this, you can use a Throw block:

    PowerShell
    Try {
       If ($Path -eq './forbidden') 
       {
         Throw "Path not allowed"
       }
       # Carry on.
    
    } Catch {
       Write-Error "$($_.exception.message)" # Path not allowed.
    }
    
    

     Note

    In general, don't use Throw for parameter validation. Use validation attributes instead. If you can't make your code work with these attributes, a Throw might be OK.

 

 

Exercise - Error handling

 

 

 

In this unit, you'll use Azure Cloud Shell on the right side of your screen as your Linux terminal. Azure Cloud Shell is a shell you can access through the Azure portal or at https://shell.azure.com. You don't have to install anything on your computer to use it.

In this exercise, you'll use a Try/Catch block to ensure the script stops responding early if a certain condition isn't met. You'll again work with your backup script.

Say you've noticed that you sometimes specify an erroneous path, which causes backup of files that shouldn't be backed up. You decide to add some error management.

  Note

Run the following commands only if you haven't completed any of the previous exercises in this module. We're assuming you've completed the previous exercises. If you haven't done so, you need a few files.

  1. If you haven't completed the previous exercises in this module, run the following bash commands in a terminal:

    Bash
    mkdir webapp
    cd webapp
    touch index.html app.js
    cd ..
    

    These commands will create a directory that contains files typically associated with web development.

  2. You also need a file named Backup.ps1. Run these commands:

    Bash
    touch Backup.ps1
    code Backup.ps1
    

    Now that you have an editor running, add the required code. Paste this code into the editor and save the file:

    PowerShell
    Param(
         [string]$Path = './app',
         [string]$DestinationPath = './'
       )
    
    If(-Not (Test-Path $Path)) 
    {
       Throw "The source directory $Path does not exist, please specify an existing directory"
    }
    
    $date = Get-Date -format "yyyy-MM-dd"
    
    $DestinationFile = "$($DestinationPath + 'backup-' + $date + '.zip')"
    If (-Not (Test-Path $DestinationFile)) 
    {
      Compress-Archive -Path $Path -CompressionLevel 'Fastest' -DestinationPath "$($DestinationPath + 'backup-' + $date)"
      Write-Host "Created backup at $($DestinationPath + 'backup-' + $date + '.zip')"
    } Else {
      Write-Error "Today's backup already exists"
    }
    

Implement a business requirement by using Try/Catch

Assume your company mostly builds web apps. These apps consist of HTML, CSS, and JavaScript files. You decide to optimize the script to recognize web apps.

  1. Use an existing PowerShell shell, if you have one running. Otherwise, start one by typing pwsh in a terminal:

    Bash
    pwsh
    
  2. Open Backup.ps1. In the Param section, add a comma after the last parameter, and then add the following parameter:

    PowerShell
    [switch]$PathIsWebApp
    

    You've added a switch parameter. If this parameter is present when the script is invoked, you perform the check on the content. After that, you can determine if a backup file should be created.

  3. Under the Param section, add this code, then save the file:

    PowerShell
    If ($PathIsWebApp -eq $True) {
       Try 
       {
         $ContainsApplicationFiles = "$((Get-ChildItem $Path).Extension | Sort-Object -Unique)" -match  '\.js|\.html|\.css'
    
         If ( -Not $ContainsApplicationFiles) {
           Throw "Not a web app"
         } Else {
           Write-Host "Source files look good, continuing"
         }
       } Catch {
        Throw "No backup created due to: $($_.Exception.Message)"
       }
    }
    

    The preceding code first checks if the parameter $PathIsWebApp is provided at runtime. If it is, the code continues to get a list of file extensions from the directory specified by $Path. In our case, if you run that part of the code on the webapp directory, the following code will print a list of items:

    PowerShell
    (Get-ChildItem $Path).Extension | Sort-Object -Unique
    

    Here's the output:

    Output
    .html
    .js
    

    In the full statement, we're using the -match operator. The -match operator expects a regular expression pattern. In this case, the expression states "do any of the file extensions match .html.js, or .css?" The result of the statement is saved to the variable $ContainsApplicationFiles.

    Then the If block checks whether the $ContainsApplicationFiles variable is True or False. At this point, the code can take two paths:

    • If the source directory is for a web app, the script writes out "Source files look good, continuing."
    • If the source directory isn't for a web app, the script throws an error that states "Not a web app." The error is caught in a Catch block. The script stops, and you rethrow the error with an improved error message.
  4. Test the script by providing the switch $PathIsWebApp:

     Note

    Before you run the script, make sure there are no .zip files present. They might have been created when you completed previous exercises in this module. Use Remove-Item *zip to remove them.

    PowerShell
    ./Backup.ps1 -PathIsWebApp -Path './webapp'
    

    The script should print output that looks similar to this text:

    Output
    Source files looks good, continuing
    Created backup at ./backup-2021-12-30.zip
    
  5. Using your terminal, create a directory named python-app. In the new directory, create a file called script.py:

    Bash
    mkdir python-app
    cd python-app
    touch script.py
    cd ..
    

    Your directory should now look like this:

    Output
    -| webapp/
    ---| app.js
    ---| index.html
    -| python-app/
    ---| script.py
    -| Backup.ps1
    
  6. In the PowerShell shell, run the script again, but this time change the -Path value to point to ./python-app:

    PowerShell
    ./Backup.ps1 -PathIsWebApp -Path './python-app'
    

    Your script should now print this text:

    Output
    No backup created due to: Not a web app
    

    The output indicates that the check failed. It should have, because there are no files in the directory that have an .html, .js, or .css extension. Your code raised an exception that was caught by your Catch block, and the script stopped early.

    Congratulations! You've implemented a business requirement.


 

 

 

 

 

 

 

 

 

 

Check your knowledge

1. 

Which file extension is correct for a script?

2. 

What's the correct way to declare a required parameter?

3. 

How can you cause a terminating error?

Summary

 

 

 

In this module, you learned how you can use PowerShell to automate tasks by writing and running scripts.

You went on to improve your scripts by using variables and parameters to make the scripts more flexible.

You then learned about flow control, and how you can use it to control how a script is run. You implemented some checks to sanitize input to ensure the script will exit early if certain conditions aren't met. You also added checks to ensure the script carries out its task (backing up files) only if there's no pre-existing backup file.

Finally, you were introduced to error handling. You learned how to differentiate between non-terminating and terminating errors and how to manage both.

You should now have a good understanding of how to write and run scripts. You should also be able to use various PowerShell constructs to improve a script's flexibility and robustness.

 

Comments

No Comments have been Posted.

Post Comment

Please Login to Post a Comment.

Ratings

Rating is available to Members only.

Please login or register to vote.

No Ratings have been Posted.
Render time: 0.80 seconds
10,799,105 unique visits