Subscribe to access all episodes. View plans →

#7: Command Line Applications with Escript

Published February 27, 2017

Elixir 1.3.4

Escript

Source code on GitHub


In this episode, we’ll build a game of Rock-Paper-Scissors also known as Roshambo, that we can play from the command line.

We’ll do this by building an ‘escript’. According to Elixir’s docs, “an escript is an executable that can be invoked from the command line”. Let’s start by creating a new project called ‘roshambo’.

$ mix new roshambo

Then let’s go into the project and open it.

$ cd roshambo

Now to create the ‘escript’, we need to do some simple setup. Let’s open our ‘mixfile’, and in our ‘project’ function, let’s add ‘escript’ and then a new private function to configure our ‘escript’ we’ll call ‘escript_config’.

Now we need to set the module that’s invoked when our ‘escript’ is run. We’ll do that in our ‘escript_config’. We’ll set ‘main_module’ with the module we want to invoke, in this case our ‘Roshambo’ module.

mix.exs

  def project do
    […
    escript: escript_config(),
    …]
  end

  defp escript_config do
    [main_module: Roshambo]
  end

Inside our ‘Roshambo’ module, we need to have a function called ‘main’ defined. This will be called when our program is run.

We don’t care about the arguments right now, but let’s print a message so we now everything is working.

lib/roshambo.ex

defmodule Roshambo do

  def main(_) do
    IO.puts("Hello!")
  end

end

Going back to the command line, let’s build our ‘escript’.

$ mix escript.build

We see it created an executable called ‘roshambo’. Let’s run it:

$ ./roshambo

Great it looks like everything is setup correctly and our message was printed.

Because our game depends on the user to provide a move, let’s print out how our arguments are passed to our ‘main’ function.

lib/roshambo.ex

defmodule Roshambo do

  def main(args) do
    IO.inspect(args)
  end

end

From the command line, let’s build our escript again.

$ mix escript.build
$ ./roshambo hello

Ok, great it looks like we can access the arguments as a list.

Back in our ‘Roshambo’ module, let’s grab the players move from the list. And then let’s print it out to ensure it’s correct. We’ll also create another ‘main’ function that pattern matches on an empty list so we can let the player know they need to make a move.

lib/roshambo.ex

defmodule Roshambo do

  def main([]) do
    IO.puts("Please provide a value of 'rock', 'paper', or 'scissors'")
  end

  def main(args) do
    player_move = List.first(args)
    IO.puts("You played #{player_move}")
  end
end

Let’s build again.

$ mix escript.build

And if we try to call our program without any move we see our message printed.

$ ./roshambo

So let’s try running it again with a move:

$ ./roshambo rock

Perfect our move is printed below. Now let’s get our game working.

Back in our ‘Roshambo’ module we’ll create a private function called ‘play’ that will take the players move, and then get a random move for the computer.

Now we need a way to compare the two moves to determine who the winner is. Let’s do this comparison in another private function called ‘determine_winner’ that will take the computers’s move and players’s move and return a message with the results.

Let’s start off by defining all the ways the computer will win.

Now we need to determine if there was a tie.

Let’s use a guard clause to check if the computers move is equal to the player’s.

With that done all other move combinations will result in the player winning.

We can handle this in one more ‘determine_winner’ function. And since we won’t use the players move, we can ignore it.

Now let’s print the response of the determine_winner function.

Looking at our ‘play’ function right now we can clean it up a bit to make it more readable. Let’s start by pulling our computer move into it’s own function.

Then we can take its result and pipe it into the determine_winner function

Then we can print the result of determine_winner with IO.puts/2

lib/roshambo.ex

defmodule Roshambo do

  def main([]) do
    IO.puts("Please provide a value of 'rock', 'paper', or 'scissors'")
  end

  def main(args) do
    player_move = List.first(args)
    play(player_move)
  end
  
  defp play(player_move) do
    comp_move
    |> determine_winner(player_move)
    |> IO.puts
  end

  defp comp_move do
    Enum.random(["rock", "paper", "scissors"])
  end

  defp determine_winner("paper", "rock") do
    "You lost, computer played paper"
  end
  defp determine_winner("rock", "scissors") do
    "You lost, computer played rock"
  end
  defp determine_winner("scissors", "paper") do
    "You lost, computer played scissors"
  end
  defp determine_winner(comp_move, player_move) when comp_move == player_move do
    "It was a tie - you both played #{comp_move}"
  end
  defp determine_winner(comp_move, _) do
    "You win! Computer played #{comp_move}"
  end

end

Let’s go to the command line and rebuild our ‘escript’.

$ mix escript.build

Now let’s see if we can play it.

$ ./roshambo rock
$ ./roshambo paper
$ ./roshambo paper
$ ./roshambo paper

Great it looks like it’s working!

Now let’s create a flag for our program that, if passed, it will tell the computer to play the move we declare like this:

$ ./roshamba --rock

Back in our roshambo module we’ll create another function to parse the command line options.

In it we can use Elixir’s ‘OptionParser’ module to parse the command line options. Because a flag determines what move we want the computer to play, we can define ‘switches’ to parse them into a format we want.

We’ll create a keyword list of the switches we are expecting and the value of the switch. In this case, let’s use a boolean for all ours.

Then let’s add it to OptionParser.parse/2.

Now we’ll use a case statement pattern match our parsed data.

OptionParser.parse/2 returns a three-element tuple.

The first element is keyword list of our parsed switches, for example if we told the computer to play ‘rock’ this would return rock and ‘true’. The second is a list of that will include our move. And the third element would be any invalid options.

Let’s start by matching cases where a switch is present.

And we’ll pattern match to get the switch.

Then we’ll get the ‘players_move’ from the second element in the tuple.

We don’t care about invalid options in this demo so we can ignore it. Then we’ll return a two element tuple, with the first element being our switch converted to a string for our computer’s move, and the second is the player’s move.

For our next case we only care about the player’s move, so let’s pattern match to get it. Then let’s return with another two element tuple, and we’ll get the computers move here.

Now we can go back to our ‘main’ function and we’ll update it to pipe the arguments from the command line into ‘parse_args’. We’ll pass its return into our ‘determine_winner’ function.

Finally we’ll print the message from ‘determine_winner’ with IO.puts/2.

Because we want to pass the result of ‘parse_args’ into ‘determine_winner’, we need to update all our ‘deterine_winner’ functions to pattern match on a two element tuple.

And with this update, we no longer need our ‘play’ function so let’s remove it.

lib/roshambo.ex

defmodule Roshambo do

  def main([]) do
    IO.puts("Please provide a value of 'rock', 'paper', or 'scissors'")
  end

  def main(argv) do
    argv
    |> parse_args
    |> determine_winner
    |> IO.puts
  end

  defp get_comp_move do
    Enum.random(["rock", "paper", "scissors"])
  end

  defp determine_winner({"paper", "rock"}) do
    "You lost, computer played paper"
  end
  defp determine_winner({"rock", "scissors"}) do
    "You lost, computer played rock"
  end
  defp determine_winner({"scissors", "paper"}) do
    "You lost, computer played scissors"
  end
  defp determine_winner({comp_move, player_move}) when comp_move == player_move do
    "It was a tie - you both played #{comp_move}"
  end
  defp determine_winner({comp_move, _}) do
    "You win! Computer played #{comp_move}"
  end

  defp parse_args(argv) do
    switches = [rock: :boolean, paper: :boolean, scissors: :boolean]
    parse = OptionParser.parse(argv, switches: switches)
    case parse do
      {[ {switch, true } ], [player_move], _ } ->
        {to_string(switch), player_move}
      {_, [player_move], _} ->
        {get_comp_move, player_move}
    end
  end
end

And back in the command line, let’s build our ‘escript’ one last time.

$ mix escript.build

Running it with no move still gives us our expected message.

$ ./roshambo

So let’s play paper and tell the computer to play rock.

$ ./roshambo paper --rock

And great, our command line game of rock paper scissors is working and what’s best is we never have to lose because we can tell the computer what move to play.

Thanks for watching and happy coding.

© 2024 HEXMONSTER LLC