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.
Rainbow
37 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.
Swift Package Manager https://swift.org/package-manager/↩︎
Swift Argument Parser https://github.com/apple/swift-argument-parser↩︎
Swift: Properties https://docs.swift.org/swift-book/LanguageGuide/Properties.html↩︎
SwiftR Book Examples https://github.com/hrbrmstr/swiftr-book-examples↩︎