Users Online
· Members Online: 0
· Total Members: 188
· Newest Member: meenachowdary055
Forum Threads
Latest Articles
Articles Hierarchy
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
If
,ElseIf
, andElse
. -
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
andCatch
. 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:
-
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"
-
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 isUnrestricted
. 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 useDefault
as the value. That value sets the policy toRestricted
on Windows clients andRemoteSigned
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:
$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:
PowerShellWrite-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:
PowerShellWrite-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:PowerShellWrite-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:
PowerShellYou can see that the variable
$test
is visible in both the local scope and its child scope, in this case, the script scope.
-
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:
Description | Path |
---|---|
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:
-
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. -
Select a profile type and create a text file at its location by using a command like this one:
New-Item -Path $Profile.CurrentUserCurrentHost
. -
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.
-
Enter
pwsh
in a terminal window to start a PowerShell session:Bashpwsh
-
Run this command:
PowerShell$Profile | Select-Object *
The output will display something similar to this text:
OutputCurrentUserAllHosts CurrentUserCurrentHost ------------------- ---------------------- /home/<user>/.config/PowerShell/profile.ps1 /home/<user>/.config/PowerShell/Microsoft.…
-
Create a profile for the current user and the current host by running the command
New-Item
:PowerShellNew-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. -
Run
pwsh
to create a new shell. You should now see the following (in green):OutputHello <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.
-
Ensure you have an existing PowerShell session running. In the console window, enter this code:
PowerShell
-
Create a file named PI.ps1 in the current directory and open it in your code editor:
PowerShellNew-Item -Path . -Name "PI.ps1" -ItemType "file" code PI.ps1
-
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"
-
Run the script by specifying the path to it:
Bash./PI.ps1
Your output displays the following text:
OutputThe 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. -
Enter
$PI
in the console window:Output3.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:
Param()
Inside the parentheses, you define your parameters, separating them with commas. A typical parameter declaration might look like this:
# 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:
./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
. TheIf/Else
construct allows you to check the value of a parameter and then decide what to do. Here's an example:PowerShellParam( $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 theParameter[]
decorator:PowerShellParam( [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:Outputcmdlet 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:PowerShellcmdlet 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:
PowerShellParam( [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.
-
In your Cloud Shell terminal, run these bash commands:
Bashmkdir app cd app touch index.html app.js cd ..
You should now have a directory named app. You're ready to work with PowerShell.
-
In the same terminal, start a PowerShell shell (if it's not already started) by running
pwsh
:Bashpwsh
-
Create a script file named Backup.ps1 in the current directory and open it in your code editor.
Bashtouch Backup.ps1 code Backup.ps1
-
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.
-
Run the script:
PowerShell./Backup.ps1
You should see this output:
OutputCreated 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.
-
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.PowerShellParam( [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. -
Change the code in the file to use the parameters, then save the file. Backup.ps1 should now look like this:
PowerShellParam( [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')"
-
Rename your app directory to webapp by running this command:
Bashmv 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. -
Remove your backup file, replacing
<current date as YYYY-MM-DD>
with the current date:Bashrm 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. -
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:
OutputLine | 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. -
Test your script by running it:
PowerShell./Backup.ps1 -Path './webapp'
You'll see a message similar to the one you got earlier:
OutputCreated 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 If
, ElseIf
, 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:
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 isTrue
.$False
indicates that an expression isFalse
.
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:
$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:
$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 If
, ElseIf
can take an expression, so it helps to think of ElseIf
as a secondary If.
Here's an example that uses ElseIf
:
# _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:
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:
touch Backup.ps1
code Backup.ps1
Add this code to the file:
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.
-
Use an existing PowerShell shell if you have one running. Otherwise, start one by typing
pwsh
in a terminal:Bashpwsh
-
Add a check for the
$Path
parameter by adding this code right after theParam
section, then save the file:PowerShellIf (-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. -
Ensure the script works as intended by running it:
PowerShell./Backup.ps1 -Path './app'
You should see this output:
OutputThrow "The source directory $Path does not exist, please specify … | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | The source directory ./app does not exist, please specify an | existing directory
-
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:
OutputCreated 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.
-
Replace the code in the file with this code, then save the file:
PowerShellParam( [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:PowerShellIf (-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" }
-
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:
OutputWrite-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
orTrap
. 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 aTry
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. ATry
must have at least oneCatch
orFinally
block. Here's how it looks:PowerShellTry { # 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. ACatch
follows immediately after aTry
. You can include more than oneCatch
— one for each type of error — if you want. Here's an example:PowerShellTry { # 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 lastCatch
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:PowerShellTry { # 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:
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 withTry/Catch
, like so:PowerShellTry { Get-Content './file.txt' -ErrorAction Stop } Catch { Write-Error "File can't be found" }
By using the
-ErrorAction
parameter and the valueStop
, you can cause an error thatTry/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:PowerShellTry { 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, aThrow
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.
-
If you haven't completed the previous exercises in this module, run the following bash commands in a terminal:
Bashmkdir webapp cd webapp touch index.html app.js cd ..
These commands will create a directory that contains files typically associated with web development.
-
You also need a file named Backup.ps1. Run these commands:
Bashtouch 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:
PowerShellParam( [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.
-
Use an existing PowerShell shell, if you have one running. Otherwise, start one by typing
pwsh
in a terminal:Bashpwsh
-
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.
-
Under the
Param
section, add this code, then save the file:PowerShellIf ($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 isTrue
orFalse
. 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.
-
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:
OutputSource files looks good, continuing Created backup at ./backup-2021-12-30.zip
-
Using your terminal, create a directory named python-app. In the new directory, create a file called script.py:
Bashmkdir 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
-
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:
OutputNo 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
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.