A command-line utility powered by Cuprum that provides tools and utilities for defining command-line tools.
To make developing and testing commands easier, Cuprum::Cli defines Command Dependencies, modules which wrap external behavior. Each built-in dependency wraps an external interface, such as the file system or standard IO streams.
Internally, Command Dependencies use the Plumbum library for dependency management.
Before using a dependency in a command class, we need to declare that dependency for the command using the .dependency(name) class method.
class GreetCommand < Cuprum::Cli::Command
dependency :standard_io
argument :name, default: 'programs'
private
def process
standard_io.write_output("Greetings, #{name}!")
end
end
The above code declares that the command uses the built-in StandardIo dependency. Declaring the dependency then automatically defines the #standard_io helper method, which returns the value of that dependency in the current command instance. We can then use the methods or properties defined on the dependency to implement our business logic - in the above example, we use the #write_output method from StandardIo to display a message in the STDOUT stream.
Why use dependencies instead of calling the functionality directly?
Using a dependency allows us to reuse code for handling errors and edge cases instead of reimplementing the same functionality over and over. Because the dependency adds a layer of indirection, we can also inject a mock dependency to make testing our command much easier.
In addition to the built-in dependencies (FileSystem, StandardIo, and SystemCommand), we can also define our own custom dependencies.
The approach above only works when your class inherits from Cuprum::Cli::Command. To use a dependency in other contexts (such as a base Cuprum::Command that provides reusable functionality), we need to make a couple of additional steps.
class DisplayGreeting < Cuprum::Cli::Command
include Plumbum::Consumer
prepend Plumbum::Parameters
provider Cuprum::Cli::Dependencies.provider
dependency :standard_io
end
Including Plumbum::Consumer defines the DSL for declaring providers and consumers, while prepending Plumbum::Parameters allows us to pass in a custom dependency value to the command (useful when testing commands with dependencies). We also need to use the .provider class method to allow our class to resolve the built-in dependencies to their default values.
For more information on Plumbum dependencies, see the Plumbum documentation.
Cuprum::Cli defines three built-in dependencies - FileSystem, StandardIo, and SystemCommand. Each built-in dependency wraps an external interface.
The FileSystem dependency wraps the native file system and provides methods for reading from and writing to files and directories. It defines the following methods:
#create_directory(path, recursive: true)recursive is true, creates intermediate directories, like the -p option for mkdir.#directory?(path)#each_file(pattern, &block)Enumerable over the matching file names or yields each file name to the given block.#file?(path)#read_file(file_or_path)#with_tempfilewrite_file(file_or_path, data)For more information, see the FileSystem reference.
A FileSystem::Mock creates a simulated directory for testing commands. It defines the above methods as well as a #files accessor to view the current contents of the simulated directory. Directories are represented as nested Hashes, while files are represented as StringIO objects.
The StandardIo dependency wraps the standard input, output, and error streams. It defines the following methods:
#color(text, color)#read_input#write_error(message = nil, newline: true)#write_output(message = nil, newline: true)For more information, see the StandardIo reference.
A StandardIo::Mock simulates the input, output, and error streams as StringIO objects and exposes them as #input_stream, #output_stream, and #error_stream. In addition, it defines a #combined_stream that captures any data written to either the output or error streams.
The SystemCommand dependency is used to execute shell commands and capture the status and output. It defines the following methods:
#capture(command,.arguments: [], environment: {}, options: {})#spawn(command, arguments: [], enviroment:, options: {})Each above command returns a Cuprum result, with a status of :success if the shell command exited with a passing status, or a result with a status of :failure if the shell command exited with a failing status.
For more information, see the SystemCommand reference.
A SystemCommand::Mock simulates shell commands by intercepting and recording outgoing commands and returning preconfigured outputs and statuses.
Cuprum::Cli enables testing commands by injecting dependency mocks into the command. This allows you to write tests without having to worry about potentially dangerous side effects, such as file system operations or shell commands. Using mock dependencies also allows you to write assertions against the mocked dependency rather than its outputs.
Let’s take a look at writing an RSpec spec for our GreetCommand using a mock dependency in place of StandardIo.
require 'cuprum/cli/dependencies/standard_io/mock'
RSpec.describe GreetCommand do
subject(:command) { described_class.new(standard_io:) }
let(:standard_io) { Cuprum::Cli::Dependencies::StandardIo::Mock.new }
describe '#call' do
it 'prints a greeting to STDOUT' do
command.call
expect(standard_io.output_stream.string).to eq("Greetings, programs!\n")
end
it 'does not print to STDERR' do
command.call
expect(standard_io.error_stream.string).to eq('')
end
end
end
As you can see, we inject a mock in place of the StandardIo dependency by passing the mock to the command constructor. We can then write assertions against the error and output streams.
Using a mock also ensures that the spec won’t pollute the test output with extra text; likewise, a mock for FileSystem prevents the test from making any changes to the actual file system, while a moc for SystemCommand ensures that the test doesn’t make any actual shell commands.