Project: package the CLI as a gem
The library
The todo CLI from lesson 46 becomes a gem in three files: the library, the command, and the gemspec that ties them together. The library keeps the version constant and the JSON file handling:
require "json"
module Todo
VERSION = "0.1.0"
FILE = "todos.json"
def self.read
return [] unless File.exist?(FILE)
JSON.parse(File.read(FILE))
end
def self.write(items)
File.write(FILE, JSON.pretty_generate(items))
end
end
The command
The command lives in exe/, requires the library by its gem name, and parses the three flags from the capstone lesson:
#!/usr/bin/env ruby
require "optparse"
require "todo"
options = {}
OptionParser.new do |parser|
parser.on("-a", "--add TITLE", "Add a todo") { |value| options[:add] = value }
parser.on("-l", "--list", "List todos") { options[:list] = true }
parser.on("-d", "--done N", Integer, "Mark a todo done") { |value| options[:done] = value }
end.parse!
items = Todo.read
if options[:add]
items << { "title" => options[:add], "done" => false }
Todo.write(items)
end
if options[:done]
items[options[:done] - 1]["done"] = true
Todo.write(items)
end
if options[:list] || options.empty?
items.each_with_index { |item, index| puts "#{index + 1}. #{item["title"]}#{" (done)" if item["done"]}" }
end
The gemspec
The gemspec reads the version from the library and names the executable, so a new release is one constant and one build:
require_relative "lib/todo"
Gem::Specification.new do |spec|
spec.name = "todo"
spec.version = Todo::VERSION
spec.summary = "A tiny todo list for the terminal"
spec.authors = ["Arthur Isaev"]
spec.license = "MIT"
spec.files = ["lib/todo.rb", "exe/todo"]
spec.bindir = "exe"
spec.executables = ["todo"]
spec.require_paths = ["lib"]
spec.required_ruby_version = ">= 3.0"
end
Build and install
gem build todo.gemspec
Successfully built RubyGem
Name: todo
Version: 0.1.0
File: todo-0.1.0.gem
gem install ./todo-0.1.0.gem
Successfully installed todo-0.1.0
Parsing documentation for todo-0.1.0
Installing ri documentation for todo-0.1.0
Done installing documentation for todo after 0 seconds
1 gem installed
Use it
The command now works from any folder, and it writes its JSON file in the folder it runs in:
todo --add "Buy milk"
todo --add "Write tests"
todo --list
1. Buy milk
2. Write tests
todo --done 1
todo --list
1. Buy milk (done)
2. Write tests
cat todos.json
[ { "title": "Buy milk", "done": true }, { "title": "Write tests", "done": false } ]
What to watch for
Write the JSON file next to the working folder, not next to the gem. A
gem installed system-wide lives under the Ruby
installation, and a data
file there is invisible to the user and lost on the next update.
Test the installed command, not only the source: build, install, run
todo --list. A file missing from
spec.files works from the project folder and
fails from the gem.
Next: Project: a pipeline for your own project
Article author: Arthur Isaev