Chapter 4 Building a Real Command Line Tool - Part 2

All that’s left to make our command line tool complete is to take in input from the user. At a minimum we need them to input their latitude and longitude. We will also need to enable them to pass in a date unless they want the sunrise and sunset times for today.

We’ll accomplish all this fairly quickly via a relatively recent addition to the Swift universe.

4.1 Introducing Swift Packages and ArgumentParser

Much like R, Swift has a concept of packages where targeted functionality is bundled up into a standard directory and file structure.

Unlike R (sorry R Core/CRAN) Swift’s package manager34 has a robust system for automating the process of downloading, compiling, and linking these dependencies which we will demonstrate now.

The package we’ll be using is Swift Argument Parser35. To make it available to our project, head over to File->Swift Packages->Add Package Dependency…:

Enter https://github.com/apple/swift-argument-parser in the text box on the next dialog:

And click through to the end:

There’s quite a bit going on in the “package options” dialog, but the gist of it is that you have control over how/when the local package is updated (Xcode will check and download updates automatically based on the schema specified).

You’ll see a new section in the Project Navigator for Swift Package Dependencies and it will have an entry for swift-argument-parser. Explore this tree since it has extensive documentation on the features of the ArgumentParser module.

4.2 Adding Support for Command Line Arguments

We’ll make changes in two files to bring command line argument capabilities to ephemerids. First, we’ll move the do…catch into a function to keep as much cruft out of main.swift as possible. Add the following to the end of sunriseset.swift:

func doSunRiseSet(date: Date, lng: Double, lat: Double) {

  do {
    if try require("daybreak") {
      let res = sunRiseSet(date: date, lng: lng, lat: lat)
      print("Sunrise 🌅: \(res.rise.decimalTimeToHM) GMT\n Sunset 🌇: \(res.set.decimalTimeToHM) GMT")
    } else {
      print("The {daybreak} package is not available. Please remotes::install_github('hrbrmstr/daybreak')")
    }
  } catch {
    print("Error: \(error)")
  }

}

ArgumentParser uses a plethora of property wrappers36 to reduce the amount of typing it takes to create the arguments configuration.

This configuration can be super complex, but for ephemerids it will be fairly simple:

  • provide an overview of what the tool does when a user asks for help
  • use a default date of today, but let users specify one via --date
  • take positional longitude and latitude arguments, but default to one set of them (since it’s my program :-)

So, if a user just enters ./ephemerids at a command line prompt it will show the sunrise and sunset times for today near where I live (yes, those coordinates are my town).

If they enter ./ephemerids -h they’ll see a help page (which I’ll show after we define the parser configuration).

Thanks to the property wrappers and sane parameter names, the follwoing is pretty self-documenting:

Replace the contents of main.swift with this:

import Foundation
import ArgumentParser

struct Sunriset: ParsableCommand {
  
  static var configuration = CommandConfiguration(
    abstract: "Outputs sunrise/sunset times for a given longitude/latitude for today or a speficied date. ",
    discussion: "Use -- before positional parameters if any are negative.\ne.g. ephemerids -- -70.8636 43.2683"
  )
  
  @Option(help: "A ISO3601 date. — e.g. 2021-01-01") var date: String = Date().ISODate // default --date= to today

  @Argument(help: "Longitude, decimal — e.g. -70.8636. ")
  var lng: Double = -70.8636 // provide a default

  @Argument(help: "Latitude, decimal — e.g. 43.2683. Use -- before positional parameters if any are negative.")
  var lat: Double = 43.2683 // provide a default

  mutating func run() throws {
    doSunRiseSet(date: Date(fromISODate: date), lng: lng, lat: lat)
  }

}

initEmbeddedR()

Sunriset.main()

R_RunExitFinalizers()
R_CleanTempDir()
Rf_endEmbeddedR(0)

Here’s what the help looks like:

$ ./ephemerids -h
OVERVIEW: Outputs sunrise/sunset times for a given longitude/latitude for today or a speficied date.

Use -- before positional parameters if any are negative.
e.g. ephemerids -- -70.8636 43.2683

USAGE: sunriset [--date <date>] [<lng>] [<lat>]

ARGUMENTS:
  <lng>                   Longitude, decimal — e.g. -70.8636.  (default: -70.8636)
  <lat>                   Latitude, decimal — e.g. 43.2683. Use -- before positional parameters if any are negative.
                          (default: 43.2683)

OPTIONS:
  --date <date>           A ISO3601 date. — e.g. 2021-01-01 (default: 2021-01-03)
  -h, --help              Show help information.

Apple did alot of work for you.

You can test it yourself by firing up a terminal then running cd ~/PLACEWHEREYOUPUTTHIS/ephemerids/DerivedData/ephemerids/Build/Products/Debug/ followed by ./ephemerids (or with options).

4.3 Up Next

We’re going to hold off on rounding out all the corners — add an Info.plist and make a distributable version of the executable — of this application and look at how to avoid most of C-interface code and embed R scripts right into a macOS command line app.

Before diving in, the current product lacks input error checking (i.e. bounds checking on latitude, longitude and validity checking on date). Bad things can happen if users enter invalid things, so take some time to identify where input validation needs to happen and add some sanity to the codebase.

Human-readable output is great, but what if we want to use the data programmatically? Add a --json argument and the code necessary to output something like { "sunrise": "12:15", "sunset": "21:20", "tz": "GMT" }.

Unless you live in GMT, most humans can’t do the math in their heads to translate GMT to the current time zone. Look in the Xcode developer documentation for how to do this with Date (and/or date formatters), but you can also brute force it with the output of something like TimeZone.current.secondsFromGMT()/60/60.

Finally, macOS folk really seem to like colorized command line output. Rainbow37 is a Swift package that makes it fairly straightforward to work with color on the command line. Practice adding this package and add some color to our output.

Remember, code examples can be found on GitHub38.