cancel
Showing results for 
Search instead for 
Did you mean: 
option8
Novitiate III

Before we dive into writing and debugging our own scripts with PowerShell and VSCode, I thought it might be helpful to examine a few of the ways in which PowerShell and bash differ, and where your previous experience with one might help in embracing the other.

Note: When I say “bash” I’m usually referring to Unix shell scripting in general. I use it as a generic term for bash, zsh, ksh, and their posix shell (sh) siblings. When there is a distinction to be made, I will be sure to acknowledge as such: e.g. “this is true for zsh, but not for bash version 5.”

Irreconcilable Philosophical Differences

One of the primary points of divergence between Windows and any Unix-derived operating system like macOS, is the foundational design choice of how to store and access data. Every object in Unix – whether it’s a file, a running process, or a hardware interface – is treated as if it were a file. That includes everything from access permissions and path names, to the tools users utilize to access and interact with the disparate pieces of the operating system. At a basic level, sending output to a piece of hardware is the same process as saving data in a document.

Whereas Unix treats everything as a file, Windows considers every interaction, at a fundamental level, as an API call. Rather than presenting some arbitrary collection of data in the same way it would a file read from a hard drive, Windows will structure the data as if it came from an API returning the results of a query. Keep this in mind as you explore PowerShell, and the different approaches to scripting on PowerShell versus bash will begin to make more sense.

For the most part, however, when you’re interacting with your Mac, whether it’s via PowerShell or zsh, it’s still a Mac. File paths will still start with a slash, not a letter (C:), and be delimited by more slashes, not backslashes. Processes and users will still have the same permissions as before, and all the usual security restrictions (SIP, PPPC) still apply.

Le Big Mac

For the following examples, I opened two Terminal windows to the same working directory. In one, I invoked PowerShell with “pwsh”. The other is running the default zsh. 

A frequent starting point for scripts is listing the files in the current directory. With bash I can retrieve a simple list of file names thus:

zsh % ls                                                
ReadMe.md tcctool.sh test.csv

Or expand on that to see size, modification date, and so on:

zsh % ls -l 
total 112
-rw-r--r--@ 1 option8  staff    809 Feb  1 21:30 ReadMe.md
-rwxr-xr-x@ 1 option8  staff  11357 Feb  1 21:31 tcctool.sh
-rw-r--r--  1 option8  staff  40335 Nov 22 12:15 test.csv

By tweaking the parameters we pass to “ls”, specific columns can be turned on or off, the list reordered, and so on. We can enable the display of invisible files, or customize the output to match however you prefer to display or parse that list.

The standard DOS/Windows file listing command “dir” works in PowerShell as you would expect, even on macOS, with customary nods to the Unix permissions model:

 

PS > dir         
     
    Directory: 

UnixMode   User             Group                 LastWriteTime           Size Name
--------   ----             -----                 -------------           ---- ----
-rw-r--r-- option8          staff                2/1/2023 21:30            809 ReadMe.md
-rwxr-xr-x option8          staff                2/1/2023 21:31          11357 tcctool.sh
-rw-r--r-- option8          staff              11/22/2022 12:15          40335 test.csv

 

This displays the expanded file information with column headings and justified text. The default layout is nice for presentation, but makes for some complicated parsing, as-is. There must be a way to display only the file names, by modifying the “dir” command (though PowerShell insists on referring to them as “cmdlets”). Let’s find out.

Bash has the built-in manual command “man” (e.g. “man ls” displays helpful information and examples for the “ls” command). The PowerShell equivalent is “help”. Invoking “help dir” and digging a little further, you will see that “dir” is really an alias for “Get-ChildItem”. According to the help text, without a path parameter pointing to a parent object, “Get-ChildItem” defaults to displaying the children (i.e. files) of the current working directory. To limit the display to only the “name” property of each child item (file name), we can add the “-name” parameter.

 

PS > dir -name
ReadMe.md
tcctool.sh
test.csv

 

Of course, since PowerShell is running on macOS, it also has access to all the command line tools that can be invoked from any other shell:

 

PS > ls
ReadMe.md	tcctool.sh	test.csv

 

One caveat, though: while external tools like “ls” can be called from within PowerShell, many of bash’s builtin commands are not – like “eval”, “do…while”, “printf”. To further complicate matters, cmdlets with the same names as builtin commands exist in PowerShell, but their syntax and output is often wildly different.

Viewing and sorting the output from a command is pretty basic stuff, though. Once you get used to the little differences, PowerShell should become as familiar as your current preferred shell. Provided you have a modicum of coding experience, switching from one command line interface to another and being able to translate tasks between the two, ought to come easily enough.

Until you get tripped up by something as basic as variables.

Royale With Cheese

In bash, a variable name can be pretty much any string, bounded by a handful of rules:

  • Variables can’t be named the same as an existing command or a few other reserved words
  • They must contain alphanumeric characters and underscores only
  • Names can’t start with a number. 

The variable’s value is set with the simple expression “variable=value” and the variable content is substituted later in your script by use of the dollar sign ($) “substitution” command. Note that there are no spaces before or after the equals sign.

zsh % intro="Fourscore and seven years ago"
zsh % echo $intro
Fourscore and seven years ago

Skimming through a few PowerShell examples, you will likely notice that variables are also referenced by $name and perhaps be misled into thinking PowerShell variables work in much the same way as in bash. For example:

 

PS > $intro = "Fourscore and seven years ago"
PS > echo $intro
Fourscore and seven years ago

 

In this example, the dollar sign indicates that what follows is a variable, rather than signifying variable substitution. Every time you use a variable in PowerShell, even when declaring it or setting its value for the first time, it needs the $. Oh, and “echo” is a bash builtin command, so, like “dir” it’s really an alias to “Write-Output”. To further confuse matters only slightly, a $variable or string by itself at the PS prompt implies “Write-Output” so:

 

PS > $intro
Fourscore and seven years ago

 

Also, note that in declaring $intro, spaces are allowed around the =.  In PowerShell, whitespace is much less significant than in bash, which balks at attempting to interpret “intro[space]” as a call to a function or command called “intro”. You can also worry less about case sensitivity causing headaches in PowerShell. 

 

PS > $INTRO; $Intro; $iNtRo
Fourscore and seven years ago
Fourscore and seven years ago
Fourscore and seven years ago

 

Even so, best practice is to not rely on the terminal or IDE to correctly interpret differently capitalized variations, so try to be consistent.

As for naming variables, PowerShell has its own set of reserved and pre-existing names. And while it is possible to name variables with special characters and even spaces in them, it’s best to stick to alphanumeric characters and underscores for the sake of readability and portability. A perfectly cromulent, but questionable example:

 

PS > ${Gettysburg address, first line} = "Fourscore and seven years ago"

 

So, you can see that, while the syntax is different, variables work pretty much the same in both environments. You declare the variable by name, set its value, and retrieve that value later. Easy peasy.

Only, remember how Windows treats everything as structured API data? This becomes more evident when we look again at “ls” and “dir”.

In bash, to get that list of files into a variable, the $ substitution invokes a subshell, and the value is set to the output of the command. Like so:

zsh % list=$(ls -l)

zsh % echo $list   
total 192
-rw-r--r--@ 1 option8  staff    809 Feb  1 21:30 ReadMe.md
-rwxr-xr-x@ 1 option8  staff  11357 Feb  1 21:31 tcctool.sh
-rw-r--r--  1 option8  staff  40335 Nov 22 12:15 test.csv

Similarly in PowerShell, command output can be redirected into a variable, though no subshell is necessary:

 

PS > $list = dir                      
PS > $list

    Directory: 

UnixMode   User             Group                 LastWriteTime           Size Name
--------   ----             -----                 -------------           ---- ----
-rw-r--r-- option8          staff                2/1/2023 21:30            809 ReadMe.md
-rwxr-xr-x option8          staff                2/1/2023 21:31          11357 tcctool.sh
-rw-r--r-- option8          staff              11/22/2022 12:15          40335 test.csv

 

However, whereas in bash, the “ls” command’s output is a long string of text, with the necessary line breaks and invisible characters to make it readable, PowerShell commands like “dir” (which is really “Get-ChildItem”) generate objects, complete with properties and methods. The output may display as if it were a simple string, but when examined more closely, a variable like $list proves to be a much more complex entity. It’s almost as if PowerShell is really an object-oriented programming language, masquerading as a command shell.

To examine the full list of properties for a PowerShell object, use “Get-Member”. Don’t worry too much about these terms yet: “members”, “objects”, and “child items” will become clearer in a future installment. We’ll be getting plenty of mileage out of “Get-Member”

 

PS > Get-Member -inputObject $list

 

Will return about 40 lines of definitions for properties and methods that $list will respond to, beginning with 

 

TypeName: System.Object[]

 

So, $list isn’t merely a variable that contains a string, it’s a structured object in memory, with three child objects – one for each of the files in the listing. To address a specific child object within $list, use fairly standard array[index] notation. All the properties of the first item are displayed with $list[0]

 

PS > Get-Member -inputObject $list[0]

   TypeName: System.IO.FileInfo
…
PSChildName               NoteProperty   string PSChildName=ReadMe.md
…

 

If we want just the name of that first file, combine brackets denoting array indices with dot notation to indicate the property of an object (again fairly common in object-oriented programming languages). It looks like the property we’re after is “PSChildName”

 

PS > $list[0].PSChildName
ReadMe.md

 

But that’s not very impressive, is it? It’s nothing you couldn’t get from just parsing the text of the listing itself. How about something that doesn’t already appear in the simple output from “ls”? Like the complete path to that file:

 

PS > $list[0].FullName   
/Users/option8/Documents/PowerShell/ReadMe.md

 

Still not impressed? How about a rundown of everything the operating system knows about that file from the time “dir” was run:

 

PS > $list[0].UnixStat

Inode            : 90174536
Mode             : 33188
UserId           : 501
GroupId          : 20
HardlinkCount    : 1
Size             : 809
AccessTime       : 2/10/2023 2:43:48 PM
ModifiedTime     : 2/1/2023 9:30:33 PM
StatusChangeTime : 2/10/2023 2:43:46 PM
BlockSize        : 4096
DeviceId         : 16777233
NumberOfBlocks   : 8
ItemType         : File
IsSetUid         : False
IsSetGid         : False
IsSticky         : False

 

To be clear, displaying the “UnixStat” properties of the object/file in question doesn’t require another call to the filesystem to grab that data. All that information was stored in the $list object when it was first declared. To show this, I can change the modification date of the ReadMe.md file, and the data in $list[0] won’t change. 

 

PS > touch -t 200101010000 ./ReadMe.md
PS > stat -f %Sm ./ReadMe.md
Jan  1 00:00:00 2001

 

The “UnixStat” property is itself a list, with its own methods and properties, one of which is “ModifiedTime” 

 

PS > $list[0].UnixStat.ModifiedTime
Wednesday, February 1, 2023 9:30:33 PM

 

Yes, it’s properties all the way down.

The Object of Your Affection

I understand that some of you reading this series are only expecting to pick up a few scripting tips for working in an unfamiliar shell, not a whole object-oriented programming language. Why can’t it be both?

More often than not, modern automation and admin tasks require parsing structured data – JSON, XML, PLIST. Finding, sorting, and making sense of information presented in complicated and opaque formats  is one place bash tends to fall short. Relying on third-party (jq) and specialized (xmllint) tools means learning and working around each one’s quirks. While it’s not always the right tool for the job, and definitely has drawbacks of its own, most of that functionality is already built into PowerShell.

As an interactive shell, there’s much to recommend about learning enough PowerShell to get around, especially for those times when you interact with a Windows server. Conversational PowerShell, if you will, doesn’t mean you need to become fluent. Even if your current responsibilities don’t involve bouncing back and forth between Windows and macOS, being bilingual will help leverage your knowledge into career opportunities in mixed platform roles.

In our next installment, we’ll roll up our sleeves and get some real work done. Stay tuned, true believers. 

3 Comments
You Might Like

New to the site? Take a look at these additional resources:

Community created scripts

Our new Radical Admin blog

Keep up with Product News

Read our community guidelines

Ready to join us? You can register here.