Fun with Get-Command

So when doing shell things in macOS, (or Linux for that matter), you use things like “which” and “locate” a lot. They’re handy. Well, PowerShell, unsurprisingly has its own version, Get-Command, (Full syntax and examples at the Get-Command documentation page.)

There’s a lot there, but I wanted to talk about just two cases, getting a single command, and getting a list of similar commands. So if you just run Get-Command on a single command, i.e. Get-Command pwsh, you get:

single command results

So this looks nice, but it’s functional. Let’s redo that command, but assign it to a variable: $thePwshCommand = Get-Command pwsh

If we just run the variable name, $thePwshCommand, you see the same thing as you would for Get-Command pwsh. So like any generic string, but with some nicer display formatting right? Well, no. As it turns out, the results of Get-Command are not just a string as shown by $thePwshCommand.GetType():

results of GetType()

So as we see, it’s not a string, but rather something called CommandInfo. What does this mean? Well, let’s say you don’t care about the type of executable a command is, or the version, you just want the path. No string parsing needed, just use $thePwshCommand.Source:

Et voila, no parsing of any kind needed

Since CommandInfo is a kind of object, you have some fun options for using what it returns without having to dink around with sed, grep, or what have you. The language and runtime take care of it for you.

But wait, there’s more. Now, as we can see, I’m running a preview version of PowerShell. Which means there’s a chance that there’s other versions of pwsh on my system. (By default, Get-Command returns the first hit it finds.) So how do we get all the versions of pwsh? Wildcards Get-Command pws*:

all the pws* things

so as we can see, there’s a few things. But what happens if we use a var for that? Well, it’s kind of neat…first the command, $allThingsPwsh = Get-Command pws*. If we just display the var, we get about what we expect for $allThingsPwsh:

No surprises here

So that’s another CommandInfo object right? Nope, as $allThingsPwsh.GetType() shows us:

Well that’s different

So when you get multiple items returned, it’s now an array, of CommandInfo objects. So if we want to see the second item, we get just that entry for $allThingsPwsh[1]:

Second item in the array

But as it’s an array of objects, we gets more flexibility, like if we just want to see the path for that entry, we use $allThingsPwsh[2].Source:
/usr/local/bin/pwsh-preview

If we just want the paths for all the items, we use $allThingsPwsh.Source, and we get:
/usr/local/microsoft/powershell/7-preview/pwsh
/usr/local/bin/pwsh
/usr/local/bin/pwsh-preview

If we just want the name and the path separated by a tab, easily done with
$allThingsPwsh[2].Name + "`t" +  $allThingsPwsh[2].Source:
pwsh-preview /usr/local/bin/pwsh-preview

So yeah, because PowerShell understands objects and arrays better than things like shell, you get a lot of flexibility in the command, saving you from a lot of the endless string/output parsing legerdemain you have to do with shell.

For more info on arrays:
https://docs.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-arrays?view=powershell-7.2 and https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_arrays?view=powershell-7.2

Enjoy!

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.

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,