Use PowerShell to make QR Codes

Since Elon Musk has decided to have a complete mantrum about posting links to Mastodon or any other social media, I thought I’d talk about a fun workaround: using QR Codes to post links.

As it turns out, doing so with PowerShell is really trivial:

  1. In a PowerShell window (on any platform you can run PowerShell on, this is not Windows-only at all), install QRCodeGenerator: Install-Module -Name QRCodeGenerator
  2. Once that’s installed, you can import the module for your session, although installing it in a root PowerShell session makes it available to everyone. To import: Import-Module -Name QRCodeGenerator
  3. There’s a few commands available, but really, the basic New-QRCodeText will work well for this: New-QRCodeText -Text “<URL you want to encode>” -OutPath <pathtofile.png> There’s an optional -Show parameter if you want to see the QR code before sending it.

That’s it. You can integrate that into any script you want that can call pwsh in the shell, so bash, AppleScript, whichever you prefer. So in five minutes, you can piss off an overly hemotional manbaby billionaire, and really, isn’t that what automation is for?

Advertisement

Adding Help to PowerShell Scripts

The Scripting Version of “Be Kind, Rewind”

There’s two hard parts to writing a script:

  1. Getting the silly thing to work correctly
  2. Showing people who aren’t you how to use it correctly

The first one is, I maintain, the easier. By far. Getting someone who isn’t you to see what you mean is significantly harder. This is one area where most scripting languages fall down, in that they don’t have a built-in help system available. So you have to add some home-built thing, which you then have to maintain. Man pages are okay, but they’re a separate set of files from the script, requiring additional work, and we all know how much coders love writing documentation.

No one can do much about the tediousness of writing docs, but PowerShell has an awesome built-in help system that not only applies to PowerShell binaries, but that you can build into literally any PowerShell script. The basic documentation is available as always from Microsoft:

There’s a lot of really good info, but you can build a simple help system for a script without having to try real hard. Basically, it’s all comments, but specific comments. Here’s one for my Get-MacInfo script:

<#
.SYNOPSIS
This is a powershell script for macOS that replicates, or tries to, the "Get-ComputerInfo" command for Windows Powershell

.DESCRIPTION
It's not a 1:1 replication, some of it wouldn't make any sense on a Mac. Also, it does check to make sure it's running
on a mac. This pulls information from a variet of sources, including uname, sysctl, AppleScript, sw_ver, system_profiler,
and some built-in powershell functions. It shoves it all into an ordered hashtable so there's some coherency in the output.
If you run the script without any parameters, you get all the items in the hashtable. If you provide one key as a parameter, 
you get the information for that key. You can provide a comma-separated list of keys and you'll get that as a result.

Note: the keys labled "Intel Only" don't exist for Apple Silicon.

Current keys are:
macOSBuildLabEx
macOSCurrentVersion
macOSCurrentBuildNumber
macOSProductName
macOSDarwinVersion
SystemFirmwareVersion
OSLoaderVersion
HardwareSerialNumber
HardwareUUID
ProvisioningUDID
HardwareModelName
HardwareModelID
ActivationLockStatus
CPUArchitecture
CPUName
CPUSpeed (Intel Only)
CPUCount (Intel Only)
CPUCoreCount
CPUL2CacheSize (Intel Only)
CPUBrandString
L3CacheSize (Intel Only)
HyperThreadingEnabled (Intel Only)
RAMAmount
AppMemoryUsedGB
VMPageFile
VMSwapInUseGB
BootDevice
FileVaultStatus
EFICurrentLanguage
DSTStatus
TimeZone
UTCOffset
DNSHostName
LocalHostName
NetworkServiceList
CurrentUserName
CurrentUserUID
CurrentDateTime
LastBootDateTime
Uptime

.EXAMPLE
Get-MacInfo by itself gives you all the parameters it can output

.EXAMPLE
Get-MacInfo TimeZone gives you the current timezone for the computer

.EXAMPLE
Get-MacInfo TimeZone,FileVault status gives you the current timezone and the filevault status for the computer

.NOTES
This can be used as a Powershell module or as a standalone script. 

.LINK
https://github.com/johncwelch/Get-MacInfo
#>

As you can see, it’s one really long block comment, with specific headers (.SYNOPSIS, .EXAMPLE, etc) that work when someone enters Get-Help <module or script name>. So if you have my Get-MacInfo script or module, and you enter Get-Help Get-MacInfo (tab completion works here, because PowerShell’s tab completion is TOTAL R0XX0RZ), you see:

Get-Help Get-MacInfo                                

NAME
    Get-MacInfo
    
SYNOPSIS
    This is a powershell script for macOS that replicates, or tries to, the "Get-ComputerInfo" command for Windows 
    Powershell
    
    
SYNTAX
    Get-MacInfo [[-keys] <Object>] [<CommonParameters>]
    
    
DESCRIPTION
    It's not a 1:1 replication, some of it wouldn't make any sense on a Mac. Also, it does check to make sure it's 
    running
    on a mac. This pulls information from a variet of sources, including uname, sysctl, AppleScript, sw_ver, 
    system_profiler,
    and some built-in powershell functions. It shoves it all into an ordered hashtable so there's some coherency in 
    the output.
    If you run the script without any parameters, you get all the items in the hashtable. If you provide one key as a 
    parameter, 
    you get the information for that key. You can provide a comma-separated list of keys and you'll get that as a 
    result.
    
    20221001 added code for Apple Silicon
    
    Note: the keys labled "Intel Only" don't exist for Apple Silicon.
    
    Current keys are:
    macOSBuildLabEx
    macOSCurrentVersion
    macOSCurrentBuildNumber
    macOSProductName
    macOSDarwinVersion
    SystemFirmwareVersion
    OSLoaderVersion
    HardwareSerialNumber
    HardwareUUID
    ProvisioningUDID
    HardwareModelName
    HardwareModelID
    ActivationLockStatus
    CPUArchitecture
    CPUName
    CPUSpeed (Intel Only)
    CPUCount (Intel Only)
    CPUCoreCount
    CPUL2CacheSize (Intel Only)
    CPUBrandString
    L3CacheSize (Intel Only)
    HyperThreadingEnabled (Intel Only)
    RAMAmount
    AppMemoryUsedGB
    VMPageFile
    VMSwapInUseGB
    BootDevice
    FileVaultStatus
    EFICurrentLanguage
    DSTStatus
    TimeZone
    UTCOffset
    DNSHostName
    LocalHostName
    NetworkServiceList
    CurrentUserName
    CurrentUserUID
    CurrentDateTime
    LastBootDateTime
    Uptime
    

RELATED LINKS
    https://github.com/johncwelch/Get-MacInfo

REMARKS
    To see the examples, type: "Get-Help Get-MacInfo -Examples"
    For more information, type: "Get-Help Get-MacInfo -Detailed"
    For technical information, type: "Get-Help Get-MacInfo -Full"
    For online help, type: "Get-Help Get-MacInfo -Online"

As you go through the docs, you’ll see where you can do a lot more, but that’s how amazingly simple it is to write useful, accurate, updateable documentation for a PowerShell script or module that lives where it should live: in the script or module, and best of all, that’s the standard PowerShell way to add help.

It doesn’t take much to write a decent help system for a PowerShell script or module, and if you do, you save yourself a lot of tech support time, which is way more annoying than writing documentation. So really, there’s not excuse not to.

Get-Macinfo Update

tl;dr, updated for Apple Silicon

During my talk at JNUC, a few folks pointed out that my Get-Macinfo script didn’t work well on Apple Silicon. I wasn’t surprised, but as I don’t have an Apple Silicon Mac, I can’t exactly test for that. However, some of y’all really came through with details on command results, and with the help of folks, in particular Kelly Dickson and Dr. Michael Richmond, I was able to get the info I needed.

For Apple Silicon, in the system profiler hardware report, the following values:

  • CPU Speed
  • CPU Count
  • L2 Cache
  • L3 Cache
  • Hyperthreading

don’t exist. Not a shock, but as that query is dumped into an array, missing 5 items meant my array references were all wrong.

I’ve got the first update for Apple Silicon up at ye olde github site, so anyone with an Apple Silicon Mac who wants to look at it and feels like installing/running PowerShell on their Mac (if they don’t already have it) can beat on it. It still seems to work correctly on Intel.

Again, thanks to everyone who helped out, it’s really appreciated, and if anyone has anything they’d like to see added to the list of things Get-Macinfo reports on, I’m happy to add where I can.

Thanks!

Azure Management tip for PowerShell on MacOS

The other day I was messaging back and forth with a good friend and former minion who was talking about a roadblock he’d hit with trying to use PowerShell on a Mac to manage Azure servers. We talked a bit, and then I went hunting and stumbled on a way to do this, so I thought I should share it with you.

In this specific case, he was trying to manage his Exchange instance, and when he’d run Connect-ExchangeOnline, he’d get the web auth dialog, authenticated, and then get the following error:

Exception: This parameter set requires WSMan, and no supported WSMan client library was found. WSMan is either not installed, or unavailable for this system.

I tried it myself and got the same error, so I started poking. A bit of searching on the PowerShell Gallery led me to PSWSMan, and the docs for that helped me then run Install-WSMan (part of this for the Mac uses MacPorts, fyi.) You have to do the install as root, but once you’ve installed the modules and enabled, them, then you should be able to do many Azure things via PowerShell from your Mac.

If you’re ever trying to find a PowerShell module, I cannot recommend PowerShell Gallery enough as a starting point, it’s an amazing resource.

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!

Porting an Application from AppleScriptObjectiveC to Xamarin/C#

This ended up taking less time than I thought it would, as it turns out, about 90% of the work I did on the Swift version was usable with little modifications in the Xamarin version. Like mostly differences between variable declarations in C# vs Swift, object inits, etc.

Links:

ASOC Original
Swift Version
Xamarin/C# Version

So in terms of just coding, MS and the Xamarin team have done a really good job. Like, there’s very few things that actually tripped me up going from Cocoa to Xamarin. Usually, it was odd things, like having to use Environment.Username to get the current user name because NSUsername() isn’t an option, or similar. Like little things that are annoying more than OH GOD NO. The syntax MS uses for the .NET Core Mac bits is really similar to the Swift implementation in Cocoa, which is a huge benefit to anyone looking to use VS.

I think a lot of that is just C# and .NET being things that have existed for a while outside of the Mac, and so you’re going to have odd little differences where they’re not going to implement NSUsername() when they have something that already works.

As an aside, Git is just awful, can someone please invent something that doesn’t have a UI/UX that only Linus Torvalds could love? Also, VS’s Git interface is…it is a thing, it exists but it is not very good even for simple things. Also, if you have any idea you may ever want to use Github et al for your project ALWAYS SAY YOU WANT TO USE GIT WHEN YOU CREATE THE PROJECT. Ye gods did dealing with that suck.

The major thing is dealing with the UI. It’s very reminiscent of when Interface Builder was not a part of Xcode. To build a UI in Visual Studio, you basically shell out to Xcode, build your storyboards there, then dump back in to VS. No SwiftUI yet, but there’s no reason you couldn’t do it. (It is still amusing to me that VS will have native ARM support on the Mac before it does on Windows.) You also need Xcode to publish to the MAS if that is your wish. I’m honestly not sure there’s a way to not need Xcode for the UI bits that wouldn’t be more trouble than it’s worth, but I could be wrong, and I’d be happy to be wrong.

Could you use Visual Studio: Mac for “real” development or cross-platform development? I think you could and if they fix a few relatively minor issues, it would be not awful at all.

I mean, for the purposes of my experiment, I was rather happy with VS/Xamarin/C#/.NET, and I am glad I’ve done the work I have in PowerShell, that was quite a help. Now, if you could use PowerShell in VS:Mac, that would be awesome, since you know, PowerShell is available for the Mac and all.

Just sayin’…

Sometimes, High-Level Languages don’t suck

So recently, as an exercise, I wanted to see about moving one of my simpler ASOC (AppleScript ObjectiveC) applications to a different language. Then I thought, why not two? So the first one I’ve been working with is C# via Visual Studio on the Mac (the 2022 preview). It’s um…interesting. First, you still need Xcode to do all the UI setup. I’m not sure why, and it’s a bit janky, but it’s not bad per se.

I think the Xamarin docs need a lot more sample code, it’s really intimidating if you’re new to the platform. But anyway, part of this app, ScutilUtil (https://github.com/johncwelch/ScutilUtil) uses the scutil command to pull info about the computer’s host name. (There may be a “proper” way to do it, but that’s not worth the search when scutil is right there.) To do so, you run scutil with various switches. i.e. /usr/sbin/scutil --get ComputerName to get the computer name.

In ASOC, this is trivial, literally one line thanks to AppleScript’s ‘do shell script’ command:

set my theCurrentComputerName to do shell script "/usr/sbin/scutil --get ComputerName"

Easy-peasy. In C#/Xamarin…sigh. The best way I found was:

Process scutilTest = new Process();
scutilTest.StartInfo.UseShellExecute = false;
scutilTest.StartInfo.RedirectStandardOutput = true;
scutilTest.StartInfo.FileName = "/usr/sbin/scutil";
scutilTest.StartInfo.Arguments = " --get LocalHostName";
scutilTest.Start();
string localHostName = scutilTest.StandardOutput.ReadToEnd();
scutilTest.WaitForExit();      

Like, it works right? But really? I mean, yes, this is a very cross-platform way to do it, but all that to run a single one-line command…it seems unelegant, and it seems unelegant on any of the platforms C#/Xamarin run on. Like, macOS/Linux/Windows all have a command line environment. They all have command line utilities that ship with the OS you might want to use because they’re there, and pretty fast.

Why make calling them so hard? Why not have something that gets you closer to do shell script. I mean, out of all of them, the only command/script environments you will never see on all three is cmd.exe, that’s windows-only and AppleScript is macOS – only, (but not something you’d run often in cases like this.) But shell? All three can have it. PowerShell? All three.

So wouldn’t it make more sense to have a way to test for the presence of a command environment, and a way to just use a command environment? Like say:

string localHostName = RunCommand -Environment zsh -Command "/usr/sbin/scutil" -Arguments "--get localHostName" -NoNewWindow -Wait

I’m using PowerShell-ish syntax here, because VS and .NET et al, but you get the idea. You could even have a way to check for a specific command environment if you wanted to be fancy, etc. Again, all the non-phone/non-tablet .NET platforms have a command-line environment of some form. Why not just allow for that to be used easily?

It’s times like these I wish Visual Studio Mac supported PowerShell as a language. Sigh.

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.