My First CLI Gem
As part of the Learn.co cirriculum we are required to take assesments that consist of putting together what we have learned in the lessons to build a project of our choice that meets the requirements specified. My first project was to build a CLI Ruby Gem with the following specifications:
- Package as a gem
- Provide a CLI on gem installation.
- CLI must provide data from an external source, whether scraped or via a public API.
- Data provided must go at least a level deep, generally by showing the user a list of available data and then being able to drill into a specific item.
I decided for my project to build a NBA-Drilldown application to list the teams and players so you could see the players of a team. The hardest part of this project was finding a good external source to scrape the information from. I spent a few weeks trying to use NBA.com however I eventually decided to use ESPN.com
To start creating a gem I simply needed to use bundler: bundle gem nba_drilldown
. This created the simple file structure needed to create my very first gem. Now it was time to get coding. I first decided how I wanted my program to function and I decided that I wanted the program to welcome the user and to list the teams. It was then time to set up the enviornment for the program.
I knew that I wanted the ability for a user to install the gem and then simply run nba-drilldown
from the terminal and that would fire off the program. The bundler created that file in the bin directory for me and we will get to how to create an executable later in this post. In the bin/nba-drilldown file I coded the following:
#!/usr/bin/env ruby
require "bundler/setup"
require "nba_drilldown"
NbaDrilldown::CLI.new.call
The first line sets the enviornment to ruby so that the computer knows how to read the code. I then require bundler/setup (which pulls code from the bundler gem) and nba_drilldown which is the main module of the program. The last line is creating a new CLI object and executing the call method. The call
method is standard procedure in Object-Oriented Ruby. I then prepared my nba_drilldown
module.
require "pry"
require "nokogiri"
require "open-uri"
require_relative './nba_drilldown/cli'
require_relative './nba_drilldown/version'
require_relative './nba_drilldown/team'
require_relative './nba_drilldown/player'
module NbaDrilldown
# Your code goes here...
end
In this file I required the gem dependencies needed for development and the two needed for scraping. It was also neccessary to require all of the relative files which would soon contain the different classes needed to create my program. Lets take a look at the cli class:
class NbaDrilldown::CLI
def call
NbaDrilldown::Team.create_teams if NbaDrilldown::Team.all.empty?
puts "Welcome to NBA Drilldown!"
puts "Please enter the number for a team to see its players or type 31 to exit"
NbaDrilldown::Team.list_teams
puts "31. Exit!!!"
input = gets.chomp.to_i
if input == 31
puts "Goodbye!"
elsif (1..30).include?(input)
team = NbaDrilldown::Team.find_team(input)
team.create_players_from_team
puts "The players of the #{team.name}"
team.list_players
continue?
else
puts "I am sorry please enter a valid team number"
end
end
def continue?
puts "Would you like to look at another team? Yes to return or No to exit."
input = gets.chomp
if input.downcase.capitalize == "Yes"
call
else
puts "Goodbye!"
end
end
end
In this class we have two instance methods. call
and continue?
call is what starts the program and welcomes the user it then scrapes the teams from ESPN and creates a new team object for each one. The teams are then listed out and the program waits for input from the user. When the user types in a number it will search the teams for that specific team and then list the players of that team. After listing the players the continue?
method is called and prompts the user if they would like to look at another team. If so it will execute the call
method again but this time it will not create the teams again since they already exist. Or the program will exit. Lets take a look at the Team and Player classes now.
class NbaDrilldown::Team
attr_accessor :name, :conference, :players, :url
@@all = []
def initialize
@players = []
end
def self.create_teams
doc = Nokogiri::HTML(open("http://espn.go.com/nba/teams"))
doc.search("a.bi").each do |team|
new_team = NbaDrilldown::Team.new
new_team.name = team.text
new_team.url = team["href"]
@@all << new_team
end
end
def list_players
puts "Name|Number|Position"
self.players.each do |player|
puts "#{player.name}|#{player.number}|#{player.position}"
end
end
def self.find_team(input)
team = @@all[input-1]
team
end
def self.list_teams
@@all.each_with_index do |team, index|
puts "#{index + 1}. #{team.name}"
end
end
def self.all
@@all
end
def create_players_from_team
self.url = self.format_team_url
doc = Nokogiri::HTML(open(self.url))
doc.search("td.sortcell a").each do |player|
new_player = NbaDrilldown::Player.create_from_data(player)
new_player.add_player_info
new_player.team = self
self.players << new_player
end
end
def format_team_url
split_array = self.url.split("/")
new_array = []
split_array.each do |text|
if text == "_"
text = "roster"
new_array << text
else
new_array << text
end
end
new_array.pop
new_array.insert(6,"_")
new_array.join("/")
end
end
class NbaDrilldown::Player
attr_accessor :name, :team, :conference, :position, :number, :url
@@all = []
def initialize
end
def self.create_from_data(player)
#doc.search("td.sortcell a").each do |player|
new_player = NbaDrilldown::Player.new
new_player.name = player.text
new_player.url = player['href']
@@all << new_player
new_player
end
def add_player_info
doc = Nokogiri::HTML(open(self.url))
self.number = doc.search("ul.general-info li").first.text.match(/\d+/)
self.position = doc.search("ul.general-info li").first.text.match(/[A-Z]+/)
end
def self.list_players
@@all.each do |player|
puts player.name
end
end
end
Both the Player and Team class have the responsibility to create Team and Player objects by scraping ESPN. Once I was able to get all of the code working it was time to finish my gem and publish it to RubyGems.org To do this I first needed to make some changes to my gemspec
file.
Gem::Specification.new do |spec|
spec.name = "nba_drilldown"
spec.version = NbaDrilldown::VERSION
spec.authors = ["tuckerbohman5"]
spec.email = ["tucker.bohman@gmail.com"]
spec.summary = "A gem to learn about NBA teams and players"
spec.description = "A gem to learn about NBA teams and players more functionaility to come."
spec.homepage = "http://github.com/tuckerbohman5"
spec.license = "MIT"
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
# delete this section to allow pushing this gem to any host.
#if spec.respond_to?(:metadata)
# spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
#else
#raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
#end
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
spec.bindir = "bin"
#spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.executables = ["nba-drilldown"]
spec.require_paths = ["lib"]
spec.add_development_dependency "bundler", "~> 1.10"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "pry"
spec.add_dependency "nokogiri"
end
You can change the name, author, summar, description, etc. to match your information. I changed the bindir to bin where my executable file was and also changed the executables to equal nba-drilldown
so now my program can be ran simply by typing nba-drilldown
in the command line.
I then commited my final changes and pushed them to github. Then to package my program as a gem I ran rake build
and then to push it to RubyGems.org you simply run gem push nba_drilldown-0.1.1.gem
in the package directory. NOTE: You will need to create an account with Ruby Gems before you can publish a gem there.
You can install my gem from the terminal with gem install nba_drilldown
and run it with nba-drilldown
. I love that I was able to build this and will definitely add more functionality and features in the future.