Portable PowerShell In an Application Bundle

So obviously, I have a fondness for PowerShell on macOS, but one of the downsides with really integrating it into things is being able to rely on it as a resource. It’d be nice if you could just include it with a script or bundle, so that it was available for that script or application to use as needed.

Well, turns out, you can, thanks to the PowerShell team making some really good decisions in how they distribute PowerShell on macOS. If you download the installer bundle from the PowerShell github site, https://github.com/PowerShell/PowerShell/releases/tag/v7.2.1 (the current stable version as of this post), and you open the bundle up in something like Pacifist, you realize that all the important powershell bits outside of the man page and an app launcher are all in the same directory:

Everything is in that directory

When I saw that, I decided to do a really quick and dirty test of my “Portable PowerShell” thought. So using Script Debugger, I created a script bundle, which is an AppleScript, but in a bundle format, which means it has the Contents/Resources/ tree. I then copied the usr/local/bin/microsoft/powershell/<version> bundle from the install package into the resources folder. In the script bundle itself, I set up the path to that bundle:

set thePwshAlias to ((path to me as text) & "Contents:Resources:powershell:usr:local:microsoft:powershell:7-preview:pwsh") as alias
set thePwshPath to quoted form of (POSIX path of thePwshAlias)

Then the statement that does the test of this:

do shell script thePwshPath & " -c \"Get-PSDrive\""

And the expected result:

Name           Used (GB)     Free (GB) Provider      Root                                 
----           ---------     --------- --------      ----  
/                 782.49        149.06 FileSystem    / 
Alias                                  Alias 
Env                                    Environment 
Function                               Function
Temp              782.49        149.06 FileSystem   /var/folders/0_/h5qr4rfj2     
Variable                               Variable                    

Which was the expected result. Now, some caveats:

  1. This is literally the extent of my testing. Making this work with custom modules, etc, is going to involve more work on the setup, if that’s even possible.
  2. I HAVE NO IDEA IF THE POWERSHELL LICENSE ALLOWS THIS. If the lawyers get flustered and stern, telling them “Well John said you could do it” will result in me laughing and saying “Yes, technically this is possible, I made no claims as to legality. It may be fine, it may be verboten, I have not checked either way.

My thought on this is that if you were creating a cross platform app and you wanted to implement PowerShell for that app, this is a way to ensure it works on macOS without making the user do a bunch of separate installs. It’s a proof of concept that happens to work, so coolness. As I get time, I’ll beat on it more, there’s some real potential here.

Also, many thanks to the PowerShell team for doing things in a way that made this trivial to test and play with.

Call PowerShell Commands from AppleScript/JXA

This will probably work within ASOC too, since do shell script works there. On macOS, the powershell “executable” is pwsh, usually in usr/local/bin/pwsh for the non-beta versions. If you run pwsh -h, you see that the pwsh utility is quite versatile, and by using the -c parameter, you can treat it like osascript, in that you can pass it a single command as a string, or an entire script block. You can also pass it a PowerShell script with the -f command. So to use this from within AppleScript/JXA, (AppleScript syntax shown), it looks like:

do shell script "/usr/local/bin/pwsh -c \"Get-PSDrive\" "

You can also use this with custom modules:

do shell script "/usr/local/bin/pwsh -c \"Get-MacInfo HardwareModelID\" "

So if you want to use a slightly more coherent shell environment like PowerShell from within AppleScript, you can.

More Complex Powershell AppleScript interactions

What If You Need More Than One Line?

So in the process of creating my series of ways to have PowerShell connect to AppleScript, thereby giving PowerShell far more access to the richness of what macOS has to offer, I realized that what I’d been doing had been limited to one-line commands. Choose File, Display Dialog, etc. Those are valuable, but sometimes, you need more than one line.

Now, usually, you’ll see a lot of really weird calls to osascript -e with complex quoting and escaping, things that tend to not work terribly well within the PowerShell world on macOS. But, there is another way to use the AppleScript runtime from the shell environment. You create a shell script, i.e. “osatest.sh”, but for the shebang, you use #!/usr/bin/osascript instead of the usual #!/bin/bash.

The advantage of this method is that you can then write fairly complex scripts with multiple lines, user input, probably even application control, and then provide a return to the powershell environment in a number of ways, i.e. return values or a return file path.

As a short example, here’s one I used in testing:

#!/usr/bin/osascript

display dialog "Look ma, multiple steps with osascript and no funky quotes!"
display alert "THIS IS SO COOL"
beep
set theList to {firstword:"This",secondword:"Is",thirdword:"a",fourthword:"list"}
return theList

So this does a few simple things. Displays a dialog, displays an alert, builds a record, assigns it to a variable, and returns the variable to the calling process. But it’s literally bog-standard AppleScript. No special escaping or quoting needed, multiple lines are fine. you could copy everything but the shebang line into the macOS script editor, and it would work fine. (I literally did that, it did in fact work correctly)

If you call it from PowerShell, it works as expected, nothing funky:

/Users/homedir/osatest.sh

firstword:This, secondword:Is, thirdword:a, fourthword:list

So you can return basic data types, numbers, strings, records, lists to PowerShell from AppleScript and then use that within PowerShell. Which creates some complex potential for more than just one-off commands.

You can even have some back and forth, wherein you use the one-off commands to get information from the user to build a proper .sh script that is run against the AppleScript runtime, do things in other apps, like the Finder, Word, Photoshop, <other scriptable thing>, and then return usable results to PowerShell.

I’m a bit busy for the next month or so, but I’ll try to get some examples going to show just how neat this can be. I’m also going to start using the tag “powershell-applescript-bridge” to help folks find these posts easier.

Using AppleScript’s “Display Dialog” with PowerShell

Okay, I didn’t have a clever title. Sue me

One of my biggest complaints about PowerShell is the lack of UI primitives. Coming from AppleScript, I’m used to having a variety of very basic UI options for the using, without needing to do anything to set them up. Need to get a simple response from the user? Display Dialog. Want them to choose from a list? Choose From List. Choose Folder, Choose File, etc. They’re all a part of core AppleScript.

You can eventually do the same with PowerShell, but even on Windows, there’s a lot of .NET setup and silliness you have to deal with. Which is dumb, and something the PowerShell team should address at some point. But that’s way off in the future, if ever. So I set about solving this for myself, and came up with method that seems to work, albeit in an awkward way. The sample script and readme are up on my GitHub site, and it’s not all that complicated. I spend more time shoving the results of the command into a hashtable than I do running the command.

I used “Display Dialog” because it’s a simple command that is fairly representative of most AppleScript UI primitives. It’s a single-line command with various parameters, and it returns a record (hashtable in PowerShell parlance) as a comma- and colon- delimited string, at least as far as using it this way is concerned.

This uses the osascript command to run the AppleScript command(s). One thing that failed miserably was trying to use Invoke-Expression -Command for this. Primarily, this seems due to the complex use of single and double quotes within osascript that causes PowerShell to vomit all over itself. However, if you pipe the command to osascript, then it works alright, as seen below:

$results = 'display dialog "this is a test" default answer "default answer" with icon caution'|/usr/bin/osascript

this runs display dialog as expected, and gives you a single, comma-delimited string for the record returned. Each item in the record is split by a comma, so the initial return looks like:

button returned:OK, text returned:default answer

Okay, that works, it’s consistent. The next thing I do is split the string into an array with the split command:

$results = $results.Split(",")

So if all you want is each record in its own element, you can stop there. For my case, I wanted the return in PowerShell to be a hashtable, aka AppleScript record, so there’s a couple more steps. First, we iterate through $results to look at each item separately. Since the second item in the array is going to have a leading space, we run each item through Trim() to remove leading/trailing whitespace characters.

$result = $result.Trim()

Then I take each item and split it on the colon which creates a new array entry.

$temp = $result.Split(":")

From there, I take the elements of the array, and insert them into a hashtable:

$dialogReply.Add($temp[0],$temp[1])

If you’re dealing with truly large returns/strings, this would be a suboptimal method. But, AppleScript display primitives don’t really return massive amounts of data, so it’s not a big deal. Here’s the entire code block for the whole script below. The first line creates the dialogReply hashtable:

$dialogReply = [ordered]@{}
$results = 'display dialog "this is a test" default answer "default answer" with icon caution'|/usr/bin/osascript

$results = $results.Split(",")

foreach ($result in $results) {
     $result = $result.Trim()

     $temp = $result.Split(":")
     $dialogReply.Add($temp[0],$temp[1])
}

Then to display the hashtable, just call $dialogReply and you get:

$dialogReply

Name                Value
----                -----
button returned     OK
text returned       default answer

This should work with a large number of, if not all the AppleScript primitives. I’ll be looking at playing with different ones as I have time.

From My Heart and From My Hands

It’s ALIVE!!!

So the script I talked about here is now an actual importable Poweshell Module. It wasn’t hard, but it was a pain in the ass, more than I think it should have been. Most of this is because, unsurprisingly, almost all the documentation on modules is highly windows-centric. So hopefully, this post will help that.

This is a very basic module. It exports no cmdlets, just a single function. Which means this post is not going to be a huge help for some gigantic thing, but it hopefully is a start.

The first thing you want to do is make sure you have a working script, aka a .ps1 file. Doesn’t have to be complicated, Get-MacInfo really isn’t. It just grabs a bunch of info, shoves it into a hashtable and shows you what it found. Once your basic code and logic is working, you’ll want to copy the .ps1 file to a .psm1 file. That’s important in the PowerShell world, as that’s the traditional extension for a PowerShell Module.

Next, wrap all your code that isn’t comment-based help in a function. In my case, it was just: function Get-MacInfo {<code>} Since it’s a single-function module, the only function is well, Get-MacInfo. If you’re going to have multiple functions, including setter functions, you really want to read the pertinent MS docs, starting here. Understanding those will help. Once you’ve wrapped it in a function, you want to export that function so it’s available to Powershell. To do that, add an Export-ModuleMember line to the bottom of your .psm1 file like so:

Export-ModuleMember -Function 'Get-MacInfo'

This is the only function I have, so I only need a simple line. If you’re exporting cmdlets or both functions and cmdlets…you’re probably well beyond what I know or can help with.

Next, we want to create the module manifest. This is fairly straightforward, but there’s some gotchas that can make you a bit bonkers. The basic creation is simple, you use the New-ModuleManifest. There’s a lot of parameters you can use, the only one that’s required is, I believe, the -Path variable. Mine looked like this:

New-ModuleManifest -Path 'Path for where you want the .psd1 file to be created' -ModuleVersion '1.0'-Author 'John C. Welch' -Company 'Bynkii.com' -Description 'A macOS version of the Get-ComputerInfo module' -ProjectUri 'https://github.com/johncwelch/Get-MacInfo' -ReleaseNotes 'First module release' -HelpInfoURI'https://github.com/johncwelch/Get-MacInfo/wiki' 

That will create a basic .psd1 file in the location that you specify in the -Path parameter, and you’re almost good to go.

Almost

So there’s a couple things you have to do manually. First, you have to actually tell the manifest what it’s referring to. If you open the manifest file (it’s basic XML), look for the #RootModule = ” line. MAKE SURE YOU UNCOMMENT IT. I didn’t and lost my fool mind for a few hours. Put the name of the .psm1 file in between the single quotes. Mine looks like:

RootModule = 'Get-MacInfo.psm1'

Next, look for the FunctionsToExport line, make sure IT is uncommented (it should be) and put in the name of the function(s) you’re exporting from the .psm1 file. Since I only have the one, mine looks like this:

FunctionsToExport = @('Get-MacInfo')

Everything else should be okay as is. Once you have those two files set up, you want to put them in the right place. You can put them anywhere, but putting them in the paths that PowerShell knows about makes your life much easier. On my machine, since I’m running the 7.1 preview, my paths (and the command to show them) look like this:

 $env:PSModulePath -split ‘:’                                                                                                                                                 ~/.local/share/powershell/Modules                                                                                                                                                  /usr/local/share/powershell/Modules
/usr/local/microsoft/powershell/7-preview/Modules                

If you only want to have the modules available to a single user, put them in the home directory Modules folder. For everyone on the machine, put them in one of the others. I have specific install instructions on the GitHub wiki for the project, so I won’t belabor those here.

If you put the module in the right paths, running Get-MacInfo will automatically import the module, and it will just work. If you put it somewhere else, you have to deal with manually managing Import-Module, and that’s on you.

Please note this is a really simple project. Modules can be really, really complicated and include binary files. This guide will be of no help at all for those. But if you’re just getting started, this may be of use. In any event, if you want to use Get-MacInfo, it’s on GitHub, with the install instructions,

PowerShell Fun

As some of you may know, I’ve been dabbling in Powershell for some time now, and have started trying to make it more useful on the Mac. There’s a lot that you can do on Windows that should work on the Mac and as of yet does not. One of the sillier things is “Get-ComputerInfo” which on Windows shows you all kinds of neat things about your computer, and doesn’t exist on the macOS version of PowerShell. I thought this was a shame so I set about trying to replicate this functionality, so that you get at least some of it. (There’s a lot of windows things that don’t make sense on the mac, so I didn’t worry if I didn’t get those.)

The name of the script is “Get-MacInfo” and it’s available from my Github site. It’s a pretty simple script, that should work under current versions of PowerShell. I’ve built and tested it under the 7.1 preview versions, but there’s nothing in there that shouldn’t work under 7.0. Prior to 7.0, no idea.

The script itself pulls in data from a number of sources, including uname, sw_ver, system_profiler, osascript, sysctl, and some built-in PowerShell functions. It dumps them all into a hashtable so they can be displayed/retrieved easier. If you run the script without any parameters, you get the full table dump, as seen here:

Name                           Value
----                           -----
macOSBuildLabEx                17.7.0:
macOSCurrentVersion            10.13.6
macOSCurrentBuildNumber        17G12034
macOSProductName               Mac OS X
macOSDarwinVersion             17.7.0
EFIVersion                     87.0.0.0.0
SMCVersion                     1.70f6
HardwareSerialNumber           ***************
HardwareUUID                   ********-****-****-****-************
HardwareModelName              MacBook Pro
HardwareModelID                MacBookPro8,3
CPUArchitecture                x86_64
CPUName                        Intel Core i7
CPUSpeed                       2.2 GHz
CPUCount                       1
CPUCoreCount                   4
CPUL2CacheSize                 256 KB
CPUBrandString                 Intel(R) Core(TM) i7-2720QM CPU @ 2.20GHz
L3CacheSize                    6 MB
RAMAmount                      16 GB
AppMemoryUsedGB                9.9869
VMPageFile                     /private/var/vm/swapfile
VMSwapInUseGB                  1.5210
BootDevice                     /dev/disk1s1
FileVaultStatus                Off
EFICurrentLanguage             English (United States)
DSTStatus                      True
TimeZone                       America/New_York
UTCOffset                      -05:00:00
DNSHostName                    ********.local
LocalHostName                  *********
NetworkServiceList             Ethernet, iPhone USB, Wi-Fi, iPad USB, FireWire, Bluetooth PAN, Thunderbolt Bridge
CurrentUserName                ******
CurrentUserUID                 ******
CurrentDateTime                5/20/2020 8:42:19 PM
LastBootDateTime               May 7 17:26
Uptime                         13.03:16:50

If you just want one or more of the parameters, then you’d supply those as a comma-delimited list, i.e.:

./Get-MacInfo.ps1 RAMAmount,NetworkServiceList,macOSCurrentBuildNumber                
Name                          Value                                                                                               
----                          -----                                                                                               
RAMAmount                     16 GB                                                                                               
NetworkServiceList            Ethernet, iPhone USB, Wi-Fi, iPad USB, FireWire, Bluetooth PAN, Thunderbolt Bridge                  
macOSCurrentBuildNumber       17G12034                     

The script itself takes about a second or two to run, as it collects all the info first, then displays what you want. Yes, that’s wasteful to some, but a) takes less than two seconds to run on ten-year-old gear and b) I’m lazy. You’re welcome to improve on it.

Actually, that last part is serious. I didn’t get every possible parameter, i can’t think of them all. The script itself is extensively commented, so even a novice should be able to figure out what’s going on and add to it. I’m also not that clever with output formatting as you can see from the code.

Anyway, now that I have the parameter input done, my next step is to turn it into a proper PowerShell module so you can incorporate it into your system. Hopefully, that will be sooner than later, and I’ll try to put a post up about it when I can.

In terms of using PowerShell, as a language, it’s really very nice. I like it better than most, it’s really easy to get up and running quickly and there is a LOT of documentation and support from both Microsoft and third-parties. (HEY APPLE! YOU SHOULD THINK ABOUT STEALING HOW MICROSOFT DOCUMENTS THEIR AUTOMATION STUFF. IT’S REALLY USEFUL. HINT!!!!)

Visual Studio Code (VSC) is a really nice dev environment for the Mac, and you really only have to install like two extensions to get it working well with PowerShell and GitHub, specifically “PowerShell” and “GitHub Pull Requests and Issues”. Both are easy to use and in conjunction with VSC give you most of the tools you need to get things done.

My biggest complaint about PowerShell is the complete lack of UI primitives, like simple dialog boxes and lists. Building those without external modules is just ridiculously tedious and kind of inexcusable. Come on Microsoft, AppleScript has had “Display Dialog” and “Choose from List” for how many decades? This is just silly that a modern language doesn’t allow you to create simple UI elements without a gob of code setup.

My second biggest is that its OS integration on macOS is pants. I mean, look at the reason for this post. What would be neat, and probably doable, instead of trying to replicate Apple’s existing scripting implementations in PowerShell would be for the PowerShell team to just build an event broker that could take Commands from PowerShell, spit out Apple Events to the OS/other applications and return the results to the script. I’m not saying that’s easy but over time, it’s probably less work to build a daemon that does that instead of trying to replicate many decades of work in terms of OSA languages.

Either way, Powershell on macOS is quite usable, and y’all should give it a try. The instructions for doing so are here, and you can do it via either Homebrew, or just downloading the installer from MS.