My Solution: August 2015 PowerShell Scripting Games Puzzle

A couple of months ago, PowerShell.org announced that the PowerShell Scripting Games had been re-imagined as a monthly puzzle. In August, the second puzzle was published.

The instructions stated that a one-liner could be used if you were using a newer version of PowerShell. A public JSON endpoint can be found at https://www.telize.com/geoip and your goal is to write some PowerShell code to display output similar to the following:

1longitude latitude continent_code timezone
2--------- -------- -------------- --------
3-115.1685 36.2212  NA             America/Los_Angeles

Try to accomplish this with a one-liner but use full cmdlet and parameter names. Write an advanced function that's a wrapper for this endpoint.

Here's my one-liner solution. It requires PowerShell version 3 or higher:

1Invoke-RestMethod -Uri 'www.telize.com/geoip' |
2Select-Object -Property longitude, latitude, continent_code, timezone

I decided to write a reusable tool and create a script module out of it named "MrGeo" that I could add additional Geolocation related functions to in the future:

 1#Requires -Version 3.0
 2function Get-MrGeoInformation {
 3
 4<#
 5.SYNOPSIS
 6    Queries www.telize.com for Geolocation information based on IP Address.
 7
 8.DESCRIPTION
 9    Get-MrGeoInformation is a PowerShell function that is designed to query
10    www.telize.com for Geolocation information for one or more IPv4 or IPv6 IP
11    Addresses. If an IP Address is not specified, your public IP Address is used.
12
13.PARAMETER IPAddress
14    The IPAddress(es) to return the Geolocation information for.
15
16.EXAMPLE
17     Get-MrGeoInformation
18
19.EXAMPLE
20     Get-MrGeoInformation -IPAddress '46.19.37.108', '2a02:2770::21a:4aff:feb3:2ee'
21
22.EXAMPLE
23     '46.19.37.108', '2a02:2770::21a:4aff:feb3:2ee' | Get-MrGeoInformation
24
25.INPUTS
26    IPAddress
27
28.OUTPUTS
29    GeoInfo
30
31.NOTES
32    Author:  Mike F Robbins
33    Website: http://mikefrobbins.com
34    Twitter: @mikefrobbins
35#>
36
37    [CmdletBinding()]
38    param (
39        [Parameter(ValueFromPipeline)]
40        [ipaddress[]]$IPAddress
41    )
42
43    PROCESS {
44
45        if (-not($PSBoundParameters.IPAddress)) {
46            Write-Verbose -Message 'Attempting to retrieve Geolocation information for your public IP Address'
47            $Results = Invoke-RestMethod -Uri 'http://www.telize.com/geoip' -TimeoutSec 30
48        }
49        else {
50            $Results = foreach ($IP in $IPAddress) {
51                Write-Verbose -Message "Attempting to retrieving Geolocation information for IP Address: '$IP'"
52                Invoke-RestMethod -Uri "http://www.telize.com/geoip/$IP" -TimeoutSec 30
53            }
54        }
55
56        foreach ($Result in $Results) {
57            $Result.PSTypeNames.Insert(0,'Mr.GeoInfo')
58            Write-Output $Result
59        }
60
61    }
62
63}

Notice that in the previous code I added my initials (Mr) as a prefix for the noun to help prevent name collisions with other people's functions that are named the same thing. I also added additional functionality so that in addition to retrieving the information for your current public IP address, that one or more public IPv4 or IPv6 addresses could be specified. Comment based help has been included along with verbose output. Pipeline input is accepted for the IPAddress parameter and the [ipaddress] type accelerator is used to perform parameter validation for both IPv4 and IPv6 addresses for that parameter.

I created custom formating for the module to display the required output plus the IP address by default for both table and list output but additional data can retrieved by simply piping to Select-Object, Format-Table, or Format-List and specifying -Property * or specific properties without having to modify the function itself.

 1<?xml version="1.0" encoding="utf-8" ?>
 2<Configuration>
 3    <ViewDefinitions>
 4        <View>
 5            <Name>Mr.GeoInfo</Name>
 6            <ViewSelectedBy>
 7                <TypeName>Mr.GeoInfo</TypeName>
 8            </ViewSelectedBy>
 9            <TableControl>
10                <TableHeaders>
11                     <TableColumnHeader>
12                        <Width>30</Width>
13                    </TableColumnHeader>
14                    <TableColumnHeader>
15                        <Width>11</Width>
16                    </TableColumnHeader>
17                    <TableColumnHeader>
18                        <Width>10</Width>
19                    </TableColumnHeader>
20                    <TableColumnHeader>
21                        <Width>16</Width>
22                    </TableColumnHeader>
23                    <TableColumnHeader>
24                        <Width>20</Width>
25                    </TableColumnHeader>
26                </TableHeaders>
27                <TableRowEntries>
28                    <TableRowEntry>
29                        <TableColumnItems>
30                            <TableColumnItem>
31                                <PropertyName>ip</PropertyName>
32                            </TableColumnItem>
33                            <TableColumnItem>
34                                <PropertyName>longitude</PropertyName>
35                            </TableColumnItem>
36                            <TableColumnItem>
37                                <PropertyName>latitude</PropertyName>
38                            </TableColumnItem>
39                            <TableColumnItem>
40                                <PropertyName>continent_code</PropertyName>
41                            </TableColumnItem>
42                            <TableColumnItem>
43                                <PropertyName>timezone</PropertyName>
44                            </TableColumnItem>
45                        </TableColumnItems>
46                    </TableRowEntry>
47                 </TableRowEntries>
48            </TableControl>
49        </View>
50        <View>
51            <Name>Mr.GeoInfo</Name>
52            <ViewSelectedBy>
53                <TypeName>Mr.GeoInfo</TypeName>
54            </ViewSelectedBy>
55            <ListControl>
56                <ListEntries>
57                    <ListEntry>
58                        <ListItems>
59                            <ListItem>
60                                <PropertyName>ip</PropertyName>
61                            </ListItem>
62                            <ListItem>
63                                <PropertyName>longitude</PropertyName>
64                            </ListItem>
65                            <ListItem>
66                                <PropertyName>latitude</PropertyName>
67                            </ListItem>
68                            <ListItem>
69                                <PropertyName>continent_code</PropertyName>
70                            </ListItem>
71                            <ListItem>
72                                <PropertyName>timezone</PropertyName>
73                            </ListItem>
74                        </ListItems>
75                    </ListEntry>
76                </ListEntries>
77            </ListControl>
78        </View>
79    </ViewDefinitions>
80</Configuration>

As a best practice, always create a module manifest when creating a PowerShell module:

  1#
  2# Module manifest for module 'MrGeo'
  3#
  4# Generated by: Mike F Robbins
  5#
  6# Generated on: 8/8/2015
  7#
  8
  9@{
 10
 11# Script module or binary module file associated with this manifest.
 12RootModule = 'MrGeo'
 13
 14# Version number of this module.
 15ModuleVersion = '1.0'
 16
 17# ID used to uniquely identify this module
 18GUID = '942fa453-a5c2-4bbd-9e6e-a2783bdb48e8'
 19
 20# Author of this module
 21Author = 'Mike F Robbins'
 22
 23# Company or vendor of this module
 24CompanyName = 'mikefrobbins.com'
 25
 26# Copyright statement for this module
 27Copyright = '(c) 2015 Mike F Robbins. All rights reserved.'
 28
 29# Description of the functionality provided by this module
 30Description = 'Mike F Robbins Geo PowerShell Module'
 31
 32# Minimum version of the Windows PowerShell engine required by this module
 33PowerShellVersion = '3.0'
 34
 35# Name of the Windows PowerShell host required by this module
 36# PowerShellHostName = ''
 37
 38# Minimum version of the Windows PowerShell host required by this module
 39# PowerShellHostVersion = ''
 40
 41# Minimum version of Microsoft .NET Framework required by this module
 42# DotNetFrameworkVersion = ''
 43
 44# Minimum version of the common language runtime (CLR) required by this module
 45# CLRVersion = ''
 46
 47# Processor architecture (None, X86, Amd64) required by this module
 48# ProcessorArchitecture = ''
 49
 50# Modules that must be imported into the global environment prior to importing this module
 51# RequiredModules = @()
 52
 53# Assemblies that must be loaded prior to importing this module
 54# RequiredAssemblies = @()
 55
 56# Script files (.ps1) that are run in the caller's environment prior to importing this module.
 57# ScriptsToProcess = @()
 58
 59# Type files (.ps1xml) to be loaded when importing this module
 60# TypesToProcess = @()
 61
 62# Format files (.ps1xml) to be loaded when importing this module
 63FormatsToProcess = 'MrGeo.ps1xml'
 64
 65# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
 66# NestedModules = @()
 67
 68# Functions to export from this module
 69FunctionsToExport = '*'
 70
 71# Cmdlets to export from this module
 72CmdletsToExport = '*'
 73
 74# Variables to export from this module
 75VariablesToExport = '*'
 76
 77# Aliases to export from this module
 78AliasesToExport = '*'
 79
 80# DSC resources to export from this module
 81# DscResourcesToExport = @()
 82
 83# List of all modules packaged with this module
 84# ModuleList = @()
 85
 86# List of all files packaged with this module
 87# FileList = @()
 88
 89# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
 90PrivateData = @{
 91
 92    PSData = @{
 93
 94        # Tags applied to this module. These help with module discovery in online galleries.
 95        # Tags = @()
 96
 97        # A URL to the license for this module.
 98        # LicenseUri = ''
 99
100        # A URL to the main website for this project.
101        # ProjectUri = ''
102
103        # A URL to an icon representing this module.
104        # IconUri = ''
105
106        # ReleaseNotes of this module
107        # ReleaseNotes = ''
108
109    } # End of PSData hashtable
110
111} # End of PrivateData hashtable
112
113# HelpInfo URI of this module
114# HelpInfoURI = ''
115
116# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
117# DefaultCommandPrefix = ''
118
119}

Notice that PowerShell version 3 is specified in the manifest as the minimum version required by this module. The custom format ps1xml file is also specified in the manifest. If you plan to publish your module in a NuGet repository with PowerShellGet that ships in PowerShell version 5, you'll need to specify an author and description in the manifest.

Give it a try with both IPv4 and IPv6 addresses and both pipeline and parameter input:

1'46.19.37.108', '2a02:2770::21a:4aff:feb3:2ee' | Get-MrGeoInformation
2Get-MrGeoInformation -IPAddress '46.19.37.108', '2a02:2770::21a:4aff:feb3:2ee'

mr-geo1a.jpg

One of the reasons I prefer to place functions like this in a PowerShell script module is that with PowerShell version 3 and higher, you can simply call the function and the module will auto-load as long as it exists in the $env:PSModulePath. No need to remember or figure out where you saved that ps1 file that contains the function and no need to dot source it.

The MrGeo PowerShell script module shown in this blog article can be downloaded from my Scripting Games repository on GitHub.


Update February 20th, 2019:

The public API this functions uses has been shutdown. I've updated the function to use a different API and moved it to my PowerShell repository on GitHub.

µ