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