The case for PowerShell On macOS and Linux
Often dismissed as an eccentric and verbose scripting language from Microsoft, PowerShell frequently finds itself relegated to the background when it comes to shell languages on non-Windows platforms. Mocked as a lackluster corporate attempt at mimicking a genuine shell scripting language, it rarely receives the attention it deserves. However, after leveraging PowerShell across various operating systems I’ve come to recognize its value. While its usefulness on Windows is beyond question, there exists a compelling argument for its adoption on other platforms. Let’s delve into its strengths as well as the criticisms.
But the Linux commands are shorter!
To better illustrate PowerShell's capabilities in comparison to traditional Linux commands, let's examine some common operations and their corresponding PowerShell equivalents.
List files and directories
Linux: ls
PowerShell:
Get-ChildItem
orls
,dir or gci
Print working directory
Linux: pwd
PowerShell:
Get-Location
orgl
Concatenate two files
Linux:
cat file1.txt file2.txt > combined.txt
PowerShell:
Get-Content file1.txt, file2.txt | Set-Content combined.txt
Or how cat is actually used
Linux:
cat file1.txt
PowerShell:
gc file1.txt
Download files from the web
Linux:
curl -L -o raylib-master.zip https://github.com/raysan5/raylib/archive/refs/heads/master.zip
PowerShell:
Invoke-WebRequest -Uri "https://github.com/raysan5/raylib/archive/refs/heads/master.zip" -OutFile "raylib-master.zip"
using Aliases
:iwr -Uri "https://github.com/raysan5/raylib/archive/refs/heads/master.zip" -OutFile "raylib-master.zip"
As you can see, most PowerShell Aliases are comparable to the core utils in length. Additionally, you can employ the Get-Alias Cmdlet to explore all aliases associated with each Cmdlet on your system, and memorize the most common ones. However, looking at the core utils and PowerShell as a either or situation is a false dichotomy. In reality, you can seamlessly integrate core util commands within PowerShell. The decision to use either PowerShell cmdlets or core utils depends on your familiarity and the task's requirements. Personally, I continue to use "cd" and "ls" instead of "gci" and "sl" due to my initial learning experiences. In this way you can think of the new PowerShell cmdlets as additional command line programs you have access to, as oppossed to an entire replacement for you previous shell knowledge.
PowerShell's Absence as a Native tool
When PowerShell isn't integrated natively into your system, it's important to recognize that this isn't an exclusive issue limited to PowerShell. Many individuals opt to download alternative shells to supplant their system's default options. Whether you favor…
bash
dash
zsh
fish
tcsh
oil
The fact that their are so many shows that not everyone has the same preference. In fact there is a tool called chsh
(change shell) who sole purpose is to replace the default shell.
Upon receiving a new work laptop, I, like many, have a list of preferred programs I install. I suspect you do as well. It's true that some of us might lack the privilege of installing additional tools on our work computers, and if you fall into that category, I feel for you. However, if you are productive with a tool why restrict yourself to the lowest common denominator? If that were the case we’d all still being using Thompson shell (The predecessor to bash). Choose what amplifies your productivity.
Just use Python
I understand and share the sentiment that advocating for alternative shells inevitably brings Python into the equation as well. Personally, I've used this rationale to justify my reluctance to delve into bash scripting. If I ever had to write some scripting code that was longer than could fit on a command line, I would reach for Python. But more and more I’ve been getting frustrated with Python as a language. For example, I recently needed to perform some Optical Character Recognition (OCR) on some files. I was given a script, along with some instructions on what dependencies I needed to install.
It seemed simple enough, so I downloaded the data and tried to install the libraries. I immediately ran into some dependency issues that I ended up having to debug, instead of solving the task I originally set out to do. These issues happen all the time, largely due to Python's multifaceted role, requiring it to create more than just command line programs.. This makes the interconnected web of libraries and code that need to play nice together large.
While PowerShell is not immune to dependency conflicts, its relatively confined scope makes such issues less prevalent. Unlike Python, PowerShell is not tailored for building websites or any other large visual program, thus circumventing certain classes of dependencies. Moreover, PowerShell's innate ability to streamline output piping between programs offers a unique advantage, a feat less straightforward in languages outside the shell paradigm. A key advantage of this approach lies in the ability to create succinct, self-contained programs linked within a pipeline. Consequently, individual components can excel at specific tasks with minimal dependencies. Less dependencies means less issues with dependency hell. The Unix philosophy taught us this 50 years ago.
In the time I spent fiddling with the Python issues, I could have just used PowerShell. Utilizing the command line variant of Tesseract, which was already a prerequisite for running the Python script, and crafting a PowerShell wrapper that leveraged Tesseract for processing the target files, would have been simpler. Adding to the elegance, I could have delegated the PowerShell script to the background using the Process-Job
Cmdlet, and monitored the task's progress with Get-Jobs
. PowerShells Cmdlets, along with core utils programs, offers purpose-built tools that often do what I inteded to do in Python anyway. I’ve come to the conclusion that the effort invested to master these tools not only enriches your skill set, but also adds additional flexibility to your existing command line programs.
Harnessing the Power of Objects
PowerShell's commitment to user convenience shines through its approach to managing command output. Virtually every command generates an object with a variety of fields, enabling seamless querying and simplifying common tasks.
The Get-Member
Cmdlet shows you the properties of the objects produced by a Cmdlet. If we run Get-ChildItem
(the PS equivalent of ls
), and pipe that into Get-Member we get some helpful information
We see that the objects created by Get-ChildItem
are of the type System.IO.FileInfo
, and there are a number of helpful methods and properties attached to the object.
Lets look at a practical example using one of the fields on a FileInfo object called WorkingSet. The WorkingSet is the total amount of memory being used by the program. If I wanted to get all the processes on my computer that had memory usages > 100Mb, and sort them in descending order, I would do this
Get-Process | Where WorkingSet -gt 100MB | Sort-Object -Property WorkingSet -Descending
This fits in one line on a terminal splitting half of a 1080p screen with the default font, so it’s not terribly long. If it didn’t, I could just hit the enter key after one of the pipes, and then continue the command on the next line like this
Get-process | Where WorkingSet -gt 100MB |
Sort-Object -Property WorkingSet -Descending
If I just wanted the names of the programs, I could wrap the whole thing in parenthesis and use the dot operator to get the name field
(Get-process | Where WorkingSet -gt 100MB |
Sort-Object -Property WorkingSet -Descending).name
And if my life depended on compressing my PowerShell queries down to the absolute smallest number of keystrokes I could write it with aliases like this…
gps | ? WS -gt 100MB | Sort-Object -d WS
Note that on Windows Sort-Object is aliased to sort so you can make it even shorter! This alias would interfere with the sort program on macOS and Linux and so it is not set
PowerShell works hard to give you properties and method on its objects that are useful, as opposed to only being able to handle strings (though it can do that as well). It even gives you helpful aliases for common storage sizes like KB, MB, GB, TB, and PB to make the conversions easy. It also gives you the option to write as tersely, or as verbosely as you want. If you do end up writing a terse script that turns out to be very useful, there are tools in programs like Visual Studio that will expand the aliases. That means that any script you give to your co-workers can be written in convenient shorthand, and expanded to their full clear length afterwards.
Harnessing the Power of Strings
Despite its focus on objects, PowerShell can also do string manipulation. Consider a scenario where a file named "data.txt" contains the following information…
Name Age Occupation
John 25 Engineer
Jane 30 Doctor
Mike 28 Architect
Lisa 35 Lawyer
Tom 32 Teacher
I want to get the data in the Name column. How would you do this using a shell script? Probably like this…
awk '{print $1}' data.txt
How would you do this in PowerShell?
awk '{print $1}' data.txt
Again, PowerShell doesn’t take the core utils away from you (Unless you are on Windows). If you want to use awk
go ahead! You can even mix and match core utils with PowerShell cmdlets
cat ./data.txt | ForEach-Object {($_ -split '\s+')[0]}
However, in scenarios where Windows is one of your primary operating systems, here are two exclusively PowerShell solutions. The first is more idiomatic, while the second caters to die hard string fans
1. Import-Csv ./data.txt -Delimiter ' ' | ForEach-Object {$_.Name}
2. Get-Content ./data.txt | ForEach-Object {($_ -split '\s+')[0]}
Don’t buy into the argument that PowerShell can only work with objects because that is simply not true. If there is a process or file that you need to work with that would best be solved with string manipulation in PowerShell go right ahead.
Writing Command line programs
Scaling up from a repetitive one-liner to a script is one of PowerShells strengths. The most basic approach involves saving the one-liner as a script and executing it through the command line like this…
./myscript.ps1
But what if you wanted to really round it out? Let’s take a look at a hypothetical program. It will split a file and save it as a smaller set of files by a specified number of lines. We will call it split_file.ps1
.
You might want to start with some parameters to your file. Our script will take 3. ‘file
’, ‘lines
’, and ‘outputName
’. Here is how you would specify that at the top of your script.
param (
$file,
$lines,
$outputName
)
PowerShell allows you to decorate parameters with a lot of useful information that will be helpful to any future contributors, as well as PowerShell itself. Lets annotate them now
param (
[Parameter(Mandatory=$true, HelpMessage="Path to the file to be split.")]
[string]$file,
[Parameter(Mandatory=$true, HelpMessage="Number of lines per split file.")]
[int]$lines,
[Parameter(Mandatory=$false, HelpMessage="Base name for the split output files. Defaults to the original filename if not specified.")]
[string]$outputName
)
As you can see we can annotate the expected types we want the parameters to be. We can also write a helpful docstring to be shown when Get-Help
is run on our script. We can describe whether we want our parameters to be mandatory or or optional. This will influence the way each command is described in the syntax of the help output for our file. It also has the side effect that if you run the script without specifying the required parameters, PowerShell will just ask you for them instead of immediately failing.
Moving on to the body of our script, let’s write an actual function to split files
function Split-File {
param (
[string]$filePath,
[int]$linesPerFile,
[string]$outputBaseName
)
if (-not (Test-Path $filePath)) {
throw "Error: $filePath does not exist."
}
$fileContent = Get-Content -Path $filePath
$totalLines = $fileContent.Count
# If no output base name is provided, default to the original filename
if (-not $outputBaseName) {
$outputBaseName = [System.IO.Path]::GetFileNameWithoutExtension($filePath)
}
$extension = [System.IO.Path]::GetExtension($filePath)
for ($i = 0; $i -lt $totalLines; $i += $linesPerFile) {
$end = $i + $linesPerFile
$chunk = $fileContent[$i..($end - 1)]
$chunk | Out-File -Path $("split_${outputBaseName}_$(($i / $linesPerFile) + 1)$extension")
}
Write-Host "File $filePath has been split into $(($totalLines + $linesPerFile - 1) / $linesPerFile) files."
}
Most of this code is just standard PowerShell commands, but there are a few things I’d like to highlight. PowerShell doesn’t use the standard == or != for comparisons and equality. Instead it uses things like -eq
, -ne
, and -gt
(equals, not equals, and greater than). This might seem strange considering that PowerShell tries to stick to the C-style of syntax with brackets to encapsulate scope. Jeffrey Snover the creator of PowerShell talks about this 2 hours and 25 minutes into his PowerShell: Zero to Hero Course with Jason Helmick.
The gist is… It’s hard to make a language that can behave both interactively and also like a scripting language. Using the =
operator in that context like is traditionally done in other languages creates ambiguity in the parser and they weren’t able to resolve it.
The other thing I’d like to point out is this line right here
$outputBaseName = [System.IO.Path]::GetFileNameWithoutExtension($filePath)
It doesn’t look like PowerShell and that is because it’s not. Here’s what is happening
[System.IO.Path]: This is a reference to the
Path
class found in the C# .NETSystem.IO
namespace. PowerShell can access and use .NET classes directly::GetFileNameWithoutExtension($filePath): This is a static method of the
Path
class. The double colon::
is used to call static methods in PowerShell. The method, as its name suggests, retrieves the filename without its extension from a given path
PowerShell has 273 Cmdlets on macOS and Linux, and many more on Windows. But not every Cmdlet covers all of the use cases you would need. In this case we could have done a string split to get the filename without the extension, but there are other cases where our only option is to rely on the C# library instead. But the fact that PowerShell gives you the option of dropping down to C# when needed means that you get all the benefits of the dotnet platform it is built on. This is a unique advantage of PowerShell compared to other shells.
Moving to the final line, we call our our function with the arguments we passed to it, completing our script.
Split-File -filePath $file -linesPerFile $lines -outputBaseName $outputName
Getting Help
PowerShell has a cmdlet called Get-Help
which allows us to view user created, and auto generated documentation associated with a script or cmdlet. Calling Get-Help on our script shows this output.
split_file.ps1 [-file] <string> [-lines] <int> [[-outputName] <string>] [<CommonParameters>]
Lets break down each component.
[-file] <string>
When a parameter to a script does not contain both the parameter and argument in brackets then it is a mandatory parameter. Since -lines
and -file
are both decorated as mandatory in the code, they are written the same way.
[[-outputName] <string>]
The outputName
Parameter was not declared as mandatory at the top of our script, so it along with its argument are wrapped in brackets. This mean that it is an optional parameter.
All PowerShell scripts and files have a common set of parameters that can be run on them. Our script doesn’t need them so we won’t worry about them, but PowerShell still indicates that they are available with the syntax [<CommonParameters>]
While the auto generated syntax string is nice, as a program gets more complicated it can become dense and difficult to decipher. Let’s make our program more user friendly by adding some in-code documentation at the top of our program.
<#
.SYNOPSIS
This script splits a file into multiple smaller files based on a specified number of lines.
.DESCRIPTION
The script reads a file and divides its content into smaller chunks, each containing a certain number of lines.
The resulting files are named with a prefix (either the original file's name or a user-specified name), followed by a sequential number.
.PARAMETER file
The path to the source file that needs to be split.
.PARAMETER lines
Specifies how many lines each of the split files should contain.
.PARAMETER outputName
Optional parameter. Specifies the base name for the split output files. If not provided, the original filename is used.
.EXAMPLE
.\split_file.ps1 -file "path_to_file.txt" -lines 100
This will split "path_to_file.txt" into multiple files, each containing 100 lines.
.EXAMPLE
.\split_file.ps1 -file "path_to_file.txt" -lines 100 -outputName "MyFile"
This will split "path_to_file.txt" into multiple files, each containing 100 lines, and the output files will have names starting with "MyFile".
.NOTES
File Name : split_file.ps1
Author : Diego Crespo
Prerequisite : PowerShell V2
Copyright 2023 : Deus In Machina
#>
Now lets run our Get-Help
on our newly documented script file
Get-Help split_file.ps1 -detailed
You can see that every heading that we made in the script that started with a ‘.’ is represented in the Get-Help
output in a nicely formatted string. But let’s be real… We are programmers and we don’t read the friendly manual, we just want some examples. PowerShell makes this easy with the -examples
flag, provided you’ve actually made some examples.
Now we get an output of our examples and our synopsis, great! As you can see PowerShell is not only geared towards working interactively on a command line, but also can be used in a full on scripting context. You can also probably tell that is was invented by a large corporation who needed the power of a shell (haha) but also needed something that had robust tooling to make sharing scripts in large enterprise contexts possible. Being able to scale up a script to many lines of code, while still maintaining readability, documentability, and user friendlyness is a strength of PowerShell. And all of this works as expected in a cross platform manner.
A few more positive misc reasons to use PowerShell
You can write Cmdlets entirely in C#
Many services have PowerShell bindings (Azure, AWS etc)
A central repository for packages and a Cmdlet to install them
Great Books! I recommend PowerShell in a month of Lunches
Large userbase and communities like the Reddit and Discord
It’s not the command prompt or batch scripting 🤮
It’s got great tooling in the various popular IDEs and text editors
100 “approved” verbs means easier time guessing names of Cmdlets
Companies trust Microsoft so less friction integrating their technologies. I.E “No one ever got fired for choosing Microsoft”1
Easier to learn compared to a fully fledged programming language
Okay but what’s the catch?
I think I’ve highlighted plenty of good reasons to integrate PowerShell into your non Windows workflow, but it would be disingenuous if I didn’t mention some cons.
Performance Concerns
Starting a PowerShell process is slower than other shells. It’s not unbearably slow, but definitely noticeable. Also executing Cmdlets are generally slower than their core util counterparts. This shouldn’t come as a surprise as PowerShell is doing a lot under the hood creating objects and properties, but that does create overhead, especially if you don’t need them. The amount of slowness depends on the Cmdlet but one recent example I noticed was while comparing download speeds of a file using wget/curl
vs Invoke-WebRequest
. The wget/cur
l workflow was noticeably faster. Another area I’ve noticed a slowdown is when right click pasting text to the terminal. This is something I only noticed while running PS on my Raspberry Pi 4. There is a reason for this, but it still sucks.
Microsoft-Centric Content
PowerShell was made cross platform 7 years ago. In that time, Linux and macOS have gained 273 cmdlets, and that number continues to grow. But every once in awhile you will come across Windows specific cmdlets or features not present in the macOS and Linux version. PowerShell in a month of Lunches, one of my favorite books for learning PowerShell, tries very hard to stick to cross platform Cmdlets. But even it has a couple of chapters that only make sense for Windows users. This is getting better overtime, but there are some things in PowerShell that you will never be able to do on non Windows Machines.
Quirks and Inconsistencies
Some things that come to mind…
Case insensitivity behavior that is different on Windows vs Non Windows platforms
These gotchas are present in all languages, but they especially diminish a shell languages usefulness. Shells have to be especially careful of this as there are other scripting languages like Javascript, Python, Perl, Lua, Tcl, R and others that are all waiting in the wings to eat its lunch the moment it stops being useful. Since PowerShell tries to bridge the gap between a traditional shell language and a scripting language, it inevitably will be compared to the scripting languages.
Installation Complexity
Installing PowerShell on certain *Unixes can be tricky. Sometimes I can use a .deb, other times it requires a special install.sh script, and sometimes it’s easier to download when you just install the whole dotnet sdk and then run
dotnet tool install --global PowerShell
Finally, some *Unixes don’t like setting PowerShell as their default shell. On Manjaro for instance, Even though I have PowerShell in my /etc/shells, and I’ve prun chsh -s /usr/bin/pwsh
it refuses to take.
While these drawbacks might be deal-breakers for some individuals, they're important facets to acknowledge. Armed with this information, you can make an educated decision on whether the advantages of integrating PowerShell into your workflow outweigh the associated drawbacks. A comprehensive assessment ensures that your choice aligns with your unique requirements and preferences as opposed to gut feelings.
Call To Action 📣
If you made it this far thanks for reading! If you are new welcome! I like to talk about technology, niche programming languages, AI, and low-level coding. I’ve recently started a Twitter and would love for you to check it out. I also have a Mastodon if that is more your jam. If you liked the article, consider liking and subscribing. And if you haven’t why not check out another article of mine! Thank you for your valuable time.
I wish this wasn’t the case but it’s just the reality. I’ve seen companies that were AWS shops get acquired and immediately be forced to use Azure and teams.