topics #fedora #git #linux #ruby

Learn Ruby: 4. Bundler, Gems, Pry, RegExp, and More

Bundler

Bundler is Ruby's dependency manager — it lets you install, update, and manage gems in your project.

bundle init

Let's write a small program that calls the GitHub API and prints the response. We'll use the http and pry libraries.

Create a lecture4 folder and run:

bundle init

This generates a Gemfile:

# frozen_string_literal: true

source "https://rubygems.org"

# gem "rails"

Any gem you want to use must be declared in this file.

bundle install

Add the two gems to your Gemfile:

gem 'pry'
gem 'http'

Then run:

bundle install

This installs the gems and generates a Gemfile.lock with the exact resolved versions.

Now create repo_info.rb:

require 'http'
require 'json'
require 'pry'

class RepoInfo
  attr_reader :repo_name

  GITHUB_API_URL = 'https://api.github.com'

  def initialize(repo_name)
    @repo_name = repo_name
  end

  def call
    data = parse_response(fetch_repo)
    puts fetch_repo_info(data)
  end

  private

  def fetch_repo
    HTTP.get("{GITHUB_API_URL}/repos/{repo_name}").to_s
  end

  def parse_response(resp)
    JSON.parse(resp)
  rescue => e
    puts "Something was wrong: {e}"
  end

  def fetch_repo_info(data)
    return 'No data' unless data && data['id']

    <<~HERE
      Name: {data['name']}, Description: {data['description']}"
      Homepage: {data['homepage']}
    HERE
  end
end

RepoInfo.new('rails/rails').call
puts '---'
RepoInfo.new('rails/rail').call

Running it:

$ ruby repo_info.rb
=>
Name: rails, Description: Ruby on Rails"
Homepage: https://rubyonrails.org
---
No data

bundle update

There are a few nuances depending on how you specify gem versions:

gem 'pry'          # always updates to the latest version
gem 'pry', '0.14.0'   # locks to exactly 0.14.0
gem 'pry', '~> 0.14.0' # allows 0.14.x but not 0.15

Read more: Semantic Versioning

bundle exec

Runs a command using the gem versions specified in your Gemfile, not whatever is installed globally on the system:

bundle exec rails s
bundle exec rake tmp:my_task

More commands: bundler.io/docs.html

Gems

A gem is a Ruby library. You can manage gems directly with the gem command:

gem install pry       # install
gem search ^pry       # search
gem list              # list installed gems
gem uninstall pry     # uninstall

That said, it's better to manage gems through Bundler in most projects.

Pry Debugger

Pry lets you pause Ruby execution at any point by inserting binding.pry:

def call
  data = parse_response(fetch_repo)
  binding.pry
  puts fetch_repo_info(data)
end

When execution hits that line, you get an interactive console where you can inspect variables or run any Ruby code:

From: repo_info.rb:16 RepoInfo#call:

    14: def call
    15:   data = parse_response(fetch_repo)
 => 16:   binding.pry
    17:   puts fetch_repo_info(data)
    18: end

Type help to see all available commands. Some useful ones:

ls          # show vars and methods in current scope
cd          # move into a new context
whereami    # show surrounding code
wtf?        # show backtrace of the last exception

View documentation inline:

ri String#downcase

View source of any library:

show-source -a Pry

Coding Style — RuboCop

RuboCop is a Ruby code analyzer (linter) and formatter. Add it to your Gemfile:

gem 'rubocop', require: false

Run it:

$ rubocop repo_info.rb

Offenses:

repo_info.rb:1:1: C: Style/FrozenStringLiteralComment: Missing frozen string literal comment.
repo_info.rb:5:1: C: Style/Documentation: Missing top-level class documentation comment.
repo_info.rb:8:20: C: Style/MutableConstant: Freeze mutable objects assigned to constants.
repo_info.rb:30:3: C: Style/RescueStandardError: Avoid rescuing without specifying an error class.

1 file inspected, 7 offenses detected, 6 offenses auto-correctable

Auto-fix correctable offenses:

rubocop -a repo_info.rb

You can also create a .rubocop.yml config file to customize rules:

Layout/LineLength:
  Max: 120

Regular Expressions

Regular expressions let you match and extract text by pattern. In Ruby:

/[a-zA-Z]/
%r{[a-zA-Z]}

Use match or =~ for matching:

"zvlex@vaba.co".match(/[a-zA-z]+@vaba.co/)
=> #

"zvlex@example.com".match(/[a-zA-z]+@vaba.co/)
=> nil

The MatchData object can be accessed with result[0], result[1], etc.

Extracting named values from a string:

info = "Name: Alex
Age: 28"
data = /Name: (w+)
Age: (d+)/.match(info)
name, age = data[1], data[2]

puts name  # => Alex
puts age   # => 28

Great tool for practicing regexps: rubular.com

Set

A Set is a hybrid Array+Hash that stores only unique elements:

require 'set'

s1 = Set[1, 2, 3, 4, 5]
s2 = Set[3, 5, 7, 9]
s3 = Set[3, 5, 7, 9]

s2 == s3       # => true

s1.add(6)      # => #
s1.add(5)      # => # (no duplicate)

s1.merge(s2)   # => #
s2.subset?(s3) # => true

Date, Time, and DateTime

Date

require 'date'

d = Date.new(2021, 9, 12)
puts d.year, d.month, d.day
# => 2021, 9, 12

d = Date.parse("2021-09-12")

(Date.parse("2021-09-25") - Date.parse("2021-09-11")).to_i
# => 14

Time

t = Time.now
puts t.hour, t.min, t.sec

timestamp = Time.now.to_i
# => 1631458963

Time.at(timestamp)
# => 2021-09-12 19:02:43 +0400

DateTime

Note: DateTime does not account for leap seconds or DST.

require 'date'

d = DateTime.parse("2021-09-12T19:06:33")
puts d.year, d.month, d.day, d.hour, d.min, d.sec
# => 2021, 9, 12, 19, 6, 33

Use strftime to format date/time output:

Time.now.strftime("%Y-%m-%d %H:%M:%S")