One of the more satisfying sorts of applications to write is one that just does a simple job from the command line, with no fuss. Elixir has some decent tooling for things like getting data from the command line and executing scripts in the context of your application, and some subscribers had requested I do something regarding Command Line Scripts, so here we are.

Project

For our project, let's write a module to fetch current weather data for a given area and return it as a string.

Let's start a new project to play with this.

mix new current_weather
cd current_weather

I looked through some weatcher APIs and just picked one that looked handy - didn't have rate limiting or developer keys to get in the way of a quick hack. I came up with the Yahoo! Weather RSS Service. Let's make a new module to fetch the weather. Open up lib/current_weather/yahoo_fetcher.ex and start off a module.

defmodule CurrentWeather.YahooFetcher do
  def fetch(woeid) do
    body = get(woeid)
    temp = extract_temperature(body)
    temp
  end

  defp extract_temperature(body) do
    # Put code here to extract the temperature from the xml
    IO.puts body
  end

  defp get(woeid) do
    {:ok, 200, _headers, client} = :hackney.get(url_for(woeid))
    {:ok, body, _client} = :hackney.body(client)
    body
  end

  defp url_for(woeid) do
    base_url <> woeid
  end

  defp base_url do
    "http://weather.yahooapis.com/forecastrss?w="
  end
end

So this module is pretty basic. Basically, we're expecting to use hackney to fetch this API endpoint, then we're going to have a function that we use to extract the temperature out of the XML that gets returned. Let's go ahead and add hackney as a dependency and make it start up with our app. Open up mix.exs:

defmodule CurrentWeather.Mixfile do
  use Mix.Project

  def project do
    [ app: :current_weather,
      version: "0.0.1",
      elixir: "~> 0.11.3-dev",
      deps: deps ]
  end

  # Configuration for the OTP application
  def application do
    [
      applications: [
        :inets,
        :hackney
      ],
      mod: { CurrentWeather, [] }
    ]
  end

  # Returns the list of dependencies in the format:
  # { :foobar, git: "https://github.com/elixir-lang/foobar.git", tag: "0.1" }
  #
  # To specify particular versions, regardless of the tag, do:
  # { :barbat, "~> 0.1", github: "elixir-lang/barbat" }
  defp deps do
    [
      { :hackney, github: 'benoitc/hackney' }
    ]
  end
end

Go ahead and fetch the dependency with mix deps.get.

Now I'm leaving for Charlottesville, Virginia later today. I just looked up their WOEID and it's 2378489. Let's figure out what the weather's going to be like there.

Go ahead and open up an iex session with iex -S mix and let's try to fetch the weather data to make sure this is working as we expect it to:

$ iex -S mix
Erlang R16B02 (erts-5.10.3) [source-b44b726] [64-bit] [smp:8:8]
[async-threads:10] [kernel-poll:false]

Compiled lib/current_weather/yahoo_fetcher.ex
Generated current_weather.app
Interactive Elixir (0.11.3-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> CurrentWeather.YahooFetcher.fetch("2378489")
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
                <rss version="2.0"
xmlns:yweather="http://xml.weather.yahoo.com/ns/rss/1.0"
xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#">
                        <channel>
                
<title>Yahoo! Weather - Charlottesville, VA</title>
<link>http://us.rd.yahoo.com/dailynews/rss/weather/Charlottesville__VA/*http://weather.yahoo.com/forecast/USVA0143_f.html</link>
<description>Yahoo! Weather for Charlottesville, VA</description>
<language>en-us</language>
<lastBuildDate>Tue, 03 Dec 2013 5:52 am EST</lastBuildDate>
<ttl>60</ttl>
<yweather:location city="Charlottesville" region="VA"   country="United
States"/>
<yweather:units temperature="F" distance="mi" pressure="in" speed="mph"/>
<yweather:wind chill="36"   direction="0"   speed="0" />
<yweather:atmosphere humidity="89"  visibility="8"  pressure="29.89"  rising="1"
/>
<yweather:astronomy sunrise="7:11 am"   sunset="4:52 pm"/>
<image>
<title>Yahoo! Weather</title>
<width>142</width>
<height>18</height>
<link>http://weather.yahoo.com</link>
<url>http://l.yimg.com/a/i/brand/purplelogo//uh/us/news-wea.gif</url>
</image>
<item>
<title>Conditions for Charlottesville, VA at 5:52 am EST</title>
<geo:lat>38.03</geo:lat>
<geo:long>-78.48</geo:long>
<link>http://us.rd.yahoo.com/dailynews/rss/weather/Charlottesville__VA/*http://weather.yahoo.com/forecast/USVA0143_f.html</link>
<pubDate>Tue, 03 Dec 2013 5:52 am EST</pubDate>
<yweather:condition  text="Fair"  code="33"  temp="36"  date="Tue, 03 Dec 2013
5:52 am EST" />
<description><![CDATA[
<img src="http://l.yimg.com/a/i/us/we/52/33.gif"/><br />
<b>Current Conditions:</b><br />
Fair, 36 F<BR />
<BR /><b>Forecast:</b><BR />
Tue - Partly Cloudy. High: 58 Low: 43<br />
Wed - Mostly Cloudy. High: 59 Low: 51<br />
Thu - Few Showers. High: 68 Low: 62<br />
Fri - Showers. High: 64 Low: 41<br />
Sat - Cloudy. High: 48 Low: 32<br />
<br />
<a
href="http://us.rd.yahoo.com/dailynews/rss/weather/Charlottesville__VA/*http://weather.yahoo.com/forecast/USVA0143_f.html">Full
Forecast at Yahoo! Weather</a><BR/><BR/>
(provided by <a href="http://www.weather.com" >The Weather Channel</a>)<br/>
]]></description>
<yweather:forecast day="Tue" date="3 Dec 2013" low="43" high="58" text="Partly
Cloudy" code="30" />
<yweather:forecast day="Wed" date="4 Dec 2013" low="51" high="59" text="Mostly
Cloudy" code="28" />
<yweather:forecast day="Thu" date="5 Dec 2013" low="62" high="68" text="Few
Showers" code="11" />
<yweather:forecast day="Fri" date="6 Dec 2013" low="41" high="64" text="Showers"
code="11" />
<yweather:forecast day="Sat" date="7 Dec 2013" low="32" high="48" text="Cloudy"
code="26" />
<guid isPermaLink="false">USVA0143_2013_12_07_7_00_EST</guid>
</item>
</channel>
</rss>

<!-- api10.weather.bf1.yahoo.com Tue Dec  3 11:52:19 PST 2013 -->

:ok

Alright, so we get a ton of XML back. Super. Let's go ahead and find the current temperature in this chunk of XML. I can see that it's in the <yweather:condition> element, so let's start out by extracting that. We covered XML Parsing in Episode 28, so I'll just recap it very briefly.

We're going to add the record definitions for xml to the top of this module:

defrecord :xmlElement, Record.extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl")
defrecord :xmlText, Record.extract(:xmlText, from_lib: "xmerl/include/xmerl.hrl")
defrecord :xmlAttribute, Record.extract(:xmlAttribute, from_lib: "xmerl/include/xmerl.hrl")

Then we're going to implement extract_temperature/1:

  defp extract_temperature(body) do
    { xml, _rest } = :xmerl_scan.string(bitstring_to_list(body))
    [ condition ] = :xmerl_xpath.string('/rss/channel/item/yweather:condition/@temp', xml)
    condition.value
  end

Now let's try it out from iex:

$ iex -S mix
Erlang R16B02 (erts-5.10.3) [source-b44b726] [64-bit] [smp:8:8] [async-threads:10] [kernel-poll:false]

Compiled lib/current_weather/yahoo_fetcher.ex
Generated current_weather.app
Interactive Elixir (0.11.3-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> CurrentWeather.YahooFetcher.fetch("2378489")
'36'

Alright, so we have a basic module that can fetch a temperature from the Yahoo! Weather RSS feed. Now let's write a CLI script that lets us pass in the woeid we care about from the command line, and tells us the weather in some nicely formatted manner.

CLI parts

We're going to make a new directory, called 'scripts', and put a script in there:

mkdir scripts
vim scripts/get_temperature.exs

We expect to pass this script an argument on the command line to specify the woeid we want to look up. Let's see what that looks like:

[woeid|_rest] = System.argv
temp = CurrentWeather.YahooFetcher.fetch(woeid)
IO.puts "The current weather for woeid #{woeid} is #{temp}"

Alright, so that part's pretty easy. I don't think it takes a ton of explaining

  • System.argv returns a list of the arguments passed on the command line.

Now, we can't just call this script directly - our app has to boot up, as do our dependencies. So how do we invoke it? Mix provides a facility to run a script after starting up our application: mix run.

We'll just run the script with mix run scripts/get_temperature.exs 2378489

$ mix run scripts/get_temperature.exs 2378489
The current weather for woeid 2378489 is 35

Summary

And that's it, that's all it takes to do some CLI scripting in Elixir. Obviously, this episode had a lot of fluff around it that wasn't directly related to the CLI scripting bits, but that's because in a real world situation you aren't likely to just be invoking some script - you're going to want to make use of libraries, OTP applications, and the like in your script, and if you're going to do that you have to use mix to run it.

There's a lot more to play with in the System module, but realistically this is 90% of what you'll end up doing. The other 10% would be extracting env vars with System.getenv/1.

See you soon!

Resources