March 15th, 2019

Exploring PowerShell Scripting




Command Line Scripting

Over the summer I wrote an article about Batch scripting and just a few weeks ago wrote a follow up on Bash scripting. Today I'm exploring PowerShell scripting on Windows. I've used PowerShell at work recently for automating the deployment of .NET applications. The rest of this article looks at basic features of PowerShell and how it compares to Bash and Batch.

Powershell is a command line shell and scripting language first released for Windows in 20061. It was created as a better way to perform command line scripting on Windows. Before Powershell, Batch files were used for command line scripting in the cmd.exe CLI. While Batch was suitable for basic tasks, the scripting language was rough around the edges and lacked modern functionality. PowerShell aimed to improve upon these shortcomings.

PowerShell scripts are files with the .ps1 extension. The PowerShell language is dynamically typed and contains basic programming constructs familiar to software developers2. Powershell types are objects, and type definitions can be explicitly or implicitly declared on variables3. This is much different than Bash, which doesn't have types. In Bash everything is plain text which is interpreted differently depending on the context.

Let's look at a basic example. PowerShell has very clean syntax for handling script arguments (similar to JavaScript command line programs). In the script parameter definition, I explicitly declare the type of the single parameter as a string.

# basics.ps1 # Handle script arguments param ( [string]$author = "Andrew Jarombek" )

This code declares a single parameter for the basics.ps1 script. It creates a new variable $author of type string ([string]). If no argument is supplied to basics.ps1, the default value "Andrew Jarombek" is used.

PowerShell scripts are easily invoked in the PowerShell CLI by writing the path of the script followed by any arguments.

# No arguments basics.ps1 # Single argument basics.ps1 "Andy J"

Variables are written to stdout with the Write-Host command, which is admittedly a bit more verbose than its echo bash counterpart. The verbosity of commands is a common PowerShell gripe from developers accustomed to using Bash. The following command prints out the $author parameter.

Write-Host $author

One complicated aspect of PowerShell is its execution policy. Execution policies configure requirements that must be met for a PowerShell script to execute4. By default, the execution policy for PowerShell on Windows machines is Restricted, meaning no scripts are allowed to execute. When I first started using PowerShell this was quite confusing since all my scripts failed before executing.

While you can create complex execution policy hierarchies at the process, user, and computer levels (among others), the most basic way to change the execution policy is with the Set-ExecutionPolicy <policy> command. For example, the following command changes the execution policy to RemoteSigned, which provides some protection against running untrustworthy downloaded scripts.

Set-ExecutionPolicy RemoteSigned

You can also check the current execution policy with Get-ExecutionPolicy. All the different execution policies are explained on Microsoft's website.

As I previously mentioned, all types in PowerShell are objects. Because of this, variables have methods and properties that we can invoke and access. For example, the string type has methods such as Substring() and Replace().

[string]$author = "Andrew Jarombek" # Select just the lastname portion of the author string $lastname = $author.Substring(7) Write-Host $lastname # the -match operator checks if author contains "Jarombek" $lastname2 = $author -match "Jarombek" Write-Host $lastname2 # Replace Jarombek with Jarbek $altauthor = $author.Replace("Jarombek", "Jarbek") Write-Host $altauthor
Jarombek True Andrew Jarbek

I also utilized a PowerShell operator -match to match a regular expression to the $author string.

If you are coming from Batch or Bash, the fact that PowerShell has objects similar to an Object-oriented programming language is surprising. PowerShell objects are very helpful in creating concise scripts and integrate well with IDEs.

Because PowerShell has object types, performing arithmetic is simpler than Bash (and much simpler than Batch). PowerShell doesn't need an integer context to perform operations like simple addition.

$age = 23 $ageInTenYears = $age + 10 Write-Host $ageInTenYears

$age is implicitly typed in this example. It can be explicitly typed as [int]$age = 23.

PowerShells object based type system really shines when dealing with more complex types. For example, the following script handles the DateTime type.

# Get the current date set on the computer [DateTime]$CurrentDate = Get-Date -DisplayHint Date # Demonstrate that there is a lot of functionality availiable on a DateTime object Write-Host "It is currently Daylight Savings Time: $($CurrentDate.IsDaylightSavingTime())" Write-Host "Current Month: $($CurrentDate.Month)" Write-Host "Next Month: $($CurrentDate.AddMonths(1).Month)" # Prove that $CurrentDate didn't mutate from the previous method invocations Write-Host $CurrentDate # Build another DateTime object [DateTime]$EndOfMonth = (Get-Date -Year 2019 -Month 03 -Day 31) # Its extremely simple to compare dates if ($CurrentDate -GT $EndOfMonth) { Write-Host "It isn't March Anymore: $CurrentDate" } else { Write-Host "It's still March: $CurrentDate" }
It is currently Daylight Savings Time: True Current Month: 3 Next Month: 4 3/13/2019 11:21:00 AM It's still March: 03/13/2019 11:21:00

This code snippet also shows off some modern PowerShell features such as string interpolation.

Powershell arrays are also objects that are easy to work with.

# Basic array syntax @() [string[]]$names = @("Andy", "Tom", "Joe") # Arrays are objects with methods and properties Write-Host $names.GetUpperBound(0) # Implicitly typed array of towns in Connecticut $towns = @("Greenwich","Darien","New Canaan","Wilton","Ridgefield") Write-Host $towns[0] # Shortened syntax to create an array of a number range $numberArray = @(0..10) $subArray = $numberArray[5..9] # Collect all the towns north of I-95 $northernTownsArray = $towns[2..4] $northernTownsString = "" foreach ($element in $northernTownsArray) { $northernTownsString += $element + " " } Write-Host $northernTownsString # An alternative way to iterate over an array is to use a pipe and a foreach loop # Collect all the towns along the coast $coastalTownsArray = $towns[0..1] $coastalTownsString = "" $coastalTownsArray | foreach { $coastalTownsString += $_ + " " } Write-Host $coastalTownsString # PowerShell supports multidimensional arrays $multiDimensional = @( @("Andrew", "Jarombek"), @("Tom", "Caul"), @("Joseph", "Smoth") ) $tom = $multiDimensional[1][0] Write-Host $tom
2 Greenwich New Canaan Wilton Ridgefield Greenwich Darien Tom

PowerShell has pretty clean syntax for writing functions. By default variables defined in functions are scoped locally to the function and don't leak into the global scope. This is the opposite of Bash which leaks variables globally by default.

$age = 23 function Test-Scoping() { # $age is scoped locally to the function $age = 24 Write-Host $age } Test-Scoping Write-Host $age
24 23

To create a global variable from a function, the global: variable prefix is used. PowerShell also provides a script: variable prefix which persists the variable across function invocations.

function Test-Scoping-Variables() { # By default, variables created in functions are local $countLocal = 0 # By using the $script: prefix, variables are persisted across function calls $script:countScript = 0 # By using the $global: prefix, variables become global $global:countGlobal = 0 Write-Host "Local count: $countLocal, Script count: $script:countScript, Global count: $global:countGlobal" } Test-Scoping-Variables Write-Host "Local count: $countLocal, Script count: $countScript, Global count: $countGlobal"
Local count: 0, Script count: 0, Global count: 0 Local count: , Script count: 0, Global count: 0

Function parameters and return values are a bit funky in PowerShell. Parameters are defined similar to script parameters with a param() block. Return values are captured at the end of a function or when a return keyword is encountered. Therefore a return keyword isn't needed for functions to return a value! They simply declare a function exit point5.

I think the easiest way to demonstrate this behavior is with an example. The following three functions multiply a string a number of times and return a new string. They all behave the same way despite the return keyword used differently (or missing entirely) in each.

# Output of functions are captured and returned. # There is a return keyword, but it just specifies an exit point. function Mult() { param([string]$str, [int]$count) $str * $count } # Therefore, Mult2 is equivalent to Mult... function Mult2() { param([string]$str, [int]$count) return $str * $count } # ...and Mult3 is equivalent to Mult2 and Mult function Mult3() { param([string]$str, [int]$count) $str * $count return } # The result is the same when invoking the three functions $meows = Mult -str "meow" -count 3 Write-Host $meows $meows2 = Mult2 -str "meow" -count 3 Write-Host $meows2 $meows3 = Mult3 -str "meow" -count 3 Write-Host $meows3
meowmeowmeow meowmeowmeow meowmeowmeow

Besides for return values, PowerShell functions behave as you likely expect.

One thing that fascinates me is the different design decisions across programming languages. I really enjoyed comparing the biggest three scripting languages - Bash, PowerShell, and Batch. I plan to use all three throughout the rest of my career, and will write more discovery posts about them as I learn more. All the code from this article is available on GitHub.

[1] "PowerShell",

[2] "PowerShell: Scripting",

[3] "Working with Object Types in PowerShell",

[4] "About Execution Policies",

[5] "Function return value in PowerShell",