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.

Things AppleScript does better

Seeing as I’ve (deservedly) been slightly hard on the Apple automation team(s), I’d like to point out that for all its faults, there are things AppleScript does far better than PowerShell, and things that Microsoft could learn from to improve.

  1. Basic UI functions: I’m talking about really basic things that you might want to put in a script, like a UI for choosing a file, a folder, etc. Like this isn’t even close, PowerShell at best is almost as easy as AppleScript for some things, but at worst…ugh. Like to choose a new file, that’s pretty simple:

    $FileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{ InitialDirectory = [Environment]::GetFolderPath('Desktop') }

    $null = $FileBrowser.ShowDialog()


    That’s not significantly worse that AppleScript’s “Choose File” dialog, which forgoes only the ShowDialog() method. The issue is that depending on how you’re running the script, the file browser may show up behind other windows, and there’s no easy way to force it to the front.

    Where PowerShell falls over is if you want the user to enter text in a dialog box. For AppleScript, it’s literally a single line:

    set theReply to text returned of display dialog "I am a dialog" default answer "default text"

    That’s it. That’s all you need. You can also get the button returned or if the gave up flag hit, it’s all in a single record if you want all three.

    In PowerShell, this is like around 30 lines of code where you’re building the OK and Cancel buttons by hand, setting their sizes, etc. That’s ridiculous for something that simple. The same thing for building a list of text items to choose from. In AppleScript:

    set theChoice to choose from list {"one", "two", "three"} with title "Choose from the list" with prompt "Pick something" default items {"one"} with multiple selections allowed without empty selection allowed

    The result is a list of choices.

    PowerShell, again, you have to build the entire dialog by hand. Like, if i’m doing .NET dev in a text editor, sure. But for what should be a higher-level scripting implementation, that’s ridiculous.

    Ironically, in PowerShell in macOS, you can integrate PowerShell with AppleScript UI primitives like Display Dialog easier than you can do the same thing in Windows. Y’all, come on. This is just stupid.
  2. Interacting with applications. The syntax for AppleScript gets a lot of guff, but launching an application in AppleScript and similar is far more intuitive and easier to learn than in PowerShell.

    tell application “Microsoft Excel” to launch

    or if you want it to launch and become active:

    tell application “Microsoft Excel” to activate

    With PowerShell, it can be simple if the application is in a known location, but if not, you have to know the path, etc. Which can be more than a little painful, but that’s an OS issue.

    Being able to just tell an application to do stuff, “tell application “Microsoft Excel”… is also nice when actually interacting with the running application. As opposed to working with the running copy of Excel:

    $theExcelApp=[Runtime.InteropServices.Marshal]::GetActiveObject("Excel.Application")

    This is the part of a higher level language that shouldn’t require this level of work. The level of detail needed to do simple stuff in PowerShell is way higher than it should be.

There’s other items, but some of them depend on syntax preferences. I find the english-ish nature of AppleScript to be helpful, other people hate it, I hate dot languages, I find them unnecessarily obtuse. The two items I listed here cover rather a lot, so it’s not just “oh two things”.

Honestly, this is part of the reason I find Apple’s hostility towards user-created automation via things like AppleScript so infuriating. The OS has a good base, only needing a better system-level automation framework so you can have more coherency across the board for automation, but instead of building on that foundation, it really seems like Apple is moving to gut the entire thing and force you into Shortcuts, Swift or nothing. That’s really quite dumb.

Manually Install PowerShell Modules in macOS

Okay, this is really just so it maybe gets picked up by Google. If you have a custom/homegrown PowerShell Module you want to install in macOS, and have them “live” there as it were, you want to copy the module folder with at least the .psd1 and .psm1 files in it to: ~/.local/share/powershell/Modules. Make sure the module folder has the same name as the module. So in the case of my Get-MacInfo module, (yes, I know, the EFIVersion is a bit weird at the moment), you would create a folder named “Get-MacInfo” in ~/.local/share/powershell/Modules, and copy the Get-MacInfo.psd1 and Get-MacInfo.psm1 files into it, then restart any running powershell sessions. At that point, Get-MacInfo would just work:

% pwsh -c "Get-MacInfo HardwareModelID"
HostName: not set
Name                          Value 
----                          ----- 
HardwareModelID               MacBookPro16,1   

No need to deal with Import-Module et al.