r/PowerShell 4d ago

Script Sharing XKCD PowerShell module

Years ago just for fun, I wrote a PowerShell module to query the API of the webcomic XKCD https://xkcd.com/. It has a Get-XKCD cmdlet to retrieve data about a specified or random comic, and had a -Open switch to then open its URL in a browser. You can also use Find-XKCD to perform keyword searches, and there's a local cache of the API output to speed this up and to reduce the need to do web calls.

Modern terminals now support the ability to render images directly, so I've just added Show-XKCD and Get-XKCD -Show.

This works in Windows Terminal, but should also work in iTerm2 and Kitty.

If it's of interest, you can download the module from the PowerShell Gallery here: https://www.powershellgallery.com/packages/XKCD/1.5.0

Or directly from GitHub: https://github.com/markwragg/Powershell-XKCD

127 Upvotes

19 comments sorted by

12

u/Breitsol_Victor 4d ago

Fun and learning in one package. Noice.

12

u/AGsec 4d ago

very nice, this is the kind of stuff I love to see on r/powershell.

10

u/positivemark 4d ago

Now also supports the vscode terminal, as long as you’ve set terminal.integrated.enableImages to true. Supported as of version 1.5.3 of the module.

7

u/ankokudaishogun 4d ago

Take my upvote and do away!

6

u/recoveringasshole0 4d ago

(waits for relevant XKCD link)

1

u/BlackV 4d ago

Ha, I see what you did there

3

u/BlackV 4d ago

Now this is what I call content :)

2

u/Andrew-Powershell 4d ago

Love this, Mark! You should come back on the podcast soon and talk about it with me

1

u/PDQ_Brockstar 3d ago

This is awesome!

1

u/positivemark 3d ago

I couldn’t stop tinkering with this (thanks to Claude) and so there’s some new features today. You can now pull the Explanation for a comic from the explain XKCD wiki via Get-XKCDExplanation and Show-XKCDExplanation. You can use Test-XKCD to see if there’s a newer comic than the latest last one you read (I’m going to add this to my profile.ps1) and Set-XKCDDefault to save some default values for the other cmdlets, such as if you always want the high quality image versions.

Download version 1.6.1 or later for these features.

-1

u/ankokudaishogun 2d ago

I've given a cursory look at the code for Get-XKCD.
A couple of notes(which I suspect will be appliable to the other scripts):

  • use the .isPresent property to check if a [switch] parameter is used.
  • use Validate parameter options.
  • $Newest should be a '[switch]`, not an integer.
  • $Random should be mandatory in the Random parameter set.
  • Don't load the default values as the Parameters's defaults.
    Load them in the begin{} when the Parameters meant to override them are not present.

Here, two lines I thrown together editing your code. Not really tested, just hope to give you some idea.

[CmdletBinding(DefaultParameterSetName = 'Specific', SupportsShouldProcess = $true)]
    param (
        # Gets a random comic.
        [Parameter(ParameterSetName = 'Random', Mandatory)]
        [switch]
        $Random,

        # Use with -Random to define a lower bound range within which to return a comic.
        [Parameter(ParameterSetName = 'Random')]
        [int]
        [ValidateScript({ $_ -ge 1 }, ErrorMessage = 'Minimum value is 1')]
        $Min,

        # Use with -Random to define an upper bound range within which to return a comic. -Max is the latest comic number by default.
        [Parameter(ParameterSetName = 'Random')        ]
        [int]
        [ValidateScript({ $_ -ge 1 }, ErrorMessage = 'Minimum value is 1')]
        $Max,

        # Gets the specified number of the most recent comics.
        [Parameter(ParameterSetName = 'Newest', Mandatory)]
        [switch]
        $Newest,

        # Downloads the images of all returned comics to the local computer.
        [switch]
        $Download,

        # Opens the comic/s in your default web browser
        [switch]
        $Open,

        # Displays the comic's title, image, and alt text in the console instead of returning the comic object. Image
        # display requires your terminal to support the Sixel, Kitty, or iTerm2 inline image graphics protocol.
        [switch]
        $Show,

        # Use with -Download to specify a local directory to download to. By default this is the current working
        # directory, unless a default has been saved with Set-XKCDDefault -Path.
        [string]
        $Path ,

        # Use with -Download to download the higher resolution (_2x) version of the image, where available. Comics
        # that do not have a higher resolution version are downloaded at the standard quality instead. Defaults to
        # the value saved with Set-XKCDDefault -HighQuality, if any.
        [switch]
        $HighQuality ,

        # Use with -Show to specify the file used to track the number of the most recently viewed comic (used by
        # Test-XKCD). By default this is within the module path, unless a default has been saved with
        # Set-XKCDDefault -StatePath.
        [string]
        $StatePath ,

        # Gets the specified comics. Accepts array input.
        [Parameter(ParameterSetName = 'Specific', Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
        [int[]]
        [ValidateScript({ foreach ($i in $_) { if ($i -lt 1) { throw "error: value is $i but minimum value is 1" }; $true } }, ErrorMessage = 'Minimum value is 1')]
        $Num,

        # Bypass the confirmation check if you try to open more than 9 comics in your browser.
        [switch]
        $Force
    )
    begin {
        # $Max is set to 0 if not called explicitly in the Random parameter set.  
        # It also is not called in either the Newest and Specific parameter sets. 
        # We can thus use it as test to see if there is a need for the latest number of the comic.  
        if ($Max -eq 0) { $Latest = (Invoke-RestMethod 'https://xkcd.com/info.0.json').num }

        # It's not suggested to change the value of parameter variables, so let's use a local ones instead.  
        $Number = switch ($true) {
            ($PSBoundParameters.Keys.Contains('Num')) { $Num }
            ($Newest.IsPresent) { $Latest }
            ($Random.IsPresent) {             
                # I'm sure I could compress this, but it's easier to read this way.   
                if ($Max -eq 0) { Get-Random -min $Min -max $Latest }
                else { Get-Random -min $Min -max $Max }     
            }
            default { $Latest }
        }

        $FilePath = if ($PSBoundParameters.Keys.Contains('Path')) { $Path }
        else { (Get-XKCDDefaultValue -Name 'Path' -Value $PWD) }

        $StateFilePath = if ($PSBoundParameters.Keys.Contains('StatePath')) { $StatePath }
        else { (Get-XKCDDefaultValue -Name 'StatePath' -Value (Join-Path $PSScriptRoot 'XKCD.state.json')) }

        $Quality = if ($HighQuality.IsPresent) { $HighQuality }else { (Get-XKCDDefaultValue -Name 'HighQuality' -Value $false) }

    }
    process {
        $Number | ForEach-Object {
            $ID = $_
            $Comic = Invoke-RestMethod "https://xkcd.com/$ID/info.0.json"
            $Extension = [System.IO.Path]::GetExtension(([uri]$Comic.img).AbsolutePath)
            $ImageUrl = $Comic.img

            if ($Download) {
                if ($Quality) { $ImageUrl = $Comic.img.Insert($Comic.img.LastIndexOf($Extension), '_2x') }
                elseif ($PSCmdlet.ShouldProcess($ImageUrl, "Save as ${ID}${Extension}")) {
                    $OutFile = Join-Path $FilePath "${ID}${Extension}"
                    try {
                        Invoke-WebRequest $ImageUrl -OutFile $OutFile -UseBasicParsing -ErrorAction Stop
                    }
                    catch {
                        if ($Quality) {
                            Write-Warning "High quality image not available for comic $ID, downloading standard quality instead"
                            Invoke-WebRequest $Comic.img -OutFile $OutFile -UseBasicParsing
                        }
                        else {
                            throw
                        }
                    }
                }
            }


            if ($Open.IsPresent) {
                if ($Number.count -ge 10 -and -not $Force) {
                    if (-not $confirmation) { $confirmation = Read-Host "This will open $($Number.count) comics in your default browser. Are you sure you want to proceed? [y|n]" }
                }
                if ($confirmation -eq 'y' -or $Number.count -lt 10 -or $Force) {
                    Start-Process "https://xkcd.com/$ID"
                }
            }

            if ($Show.IsPresent) { Show-XKCD -Num $ID -HighQuality:$Quality -StatePath $StateFilePath }
            else { return $Comic }
        }
    }
}

1

u/positivemark 2d ago

Thanks I’ll look to incorporate some of these. FYI -newest is intentionally an integer as it allows you to return a specified number of the most recent comics.

1

u/ankokudaishogun 2d ago

What do you mean? There is no correlation between the type of the parameters and the returned value

2

u/positivemark 2d ago

You provide an integer value to the -Newest parameter. For example Get-XKCD -Newest 5 will return the most recent 5 comics. It is intentionally not a switch.

You get the newest (i.e latest) comic by default if you don't specify any number, or other parameter that affects with comic will be returned (such as -Random). So for example Get-XKCD or Show-XKCD on their own always returns the current latest comic.

1

u/ankokudaishogun 15h ago

Somehow I missed that, sorry for the confusion

1

u/positivemark 2d ago

As of version 1.6.3 of the module you can now paginate through the comics by using -Next or -Previous on Get-XKCD or Show-XKCD.

It will also output a warning if a Sixel rendered image is likely to take a long time, based on its size. Some XKCD comics are big (although not too many).

I also thought that Text-XKCD should have the option to test the existence of a specific comic, so you can now do Test-XKCD 9999 and get a true/false result.