A command-line utility powered by Cuprum that provides tools and utilities for defining command-line tools.
Generators are used by the New File command to define the output files generated for a given input path and options.
class MarkdownGenerator < Cuprum::Cli::Files::Generator
match_file(/\.md\z/)
option :template
output '%<file_path>s', template: 'templates/docs_template.md.erb'
end
For a full list of available methods, see the Reference documentation.
To define a generator, we declare a new class that inherits from Cuprum::Cli::Files::Generator:
class MarkdownGenerator < Cuprum::Cli::Files::Generator
match_file(/\.md\z/)
output '%<file_path>s', template: 'templates/docs_template.md.erb'
end
Each generator class requires two parts - at least one match statement, and at least one output statement. Match statements are used when deciding which generator to use for a given input file (and options), while output statements determine what files are output by the generator.
In our above example, we declare that the generator will match input files ending with the .md extension, and that when called, it will output a file at the specified path using the template at 'templates/docs_template.md.erb'.
A generator’s match statements are used to determine which generator is invoked by the New File command, or more generally whether the generator can generate output files of the requested type.
Each generator must have at least one match statement. If a generator has more than one match, then any file path and options that matches any of the match statements will match the generator.
class YamlGenerator < Cuprum::Cli::Files::Generator
match_file(/\.yaml\z/)
match_file(/\.yml\z/)
end
The above generator will match file paths that end in either .yaml or .yml. Once a generator has defined match statements, they can be checked using the generator_class.matches?(file_path, **options) class method, which returns true if any match statements match the given file name and options, or false if none of the match statements match.
To declare a match statement, use the .match_file(pattern) method, which must be provided a pattern to match, either a String or a Regexp. For more complex matching, you can instead pass a block to .match_file { |file_path, **options| } (see Block Matchers, below).
A generator with a String matcher will match any file path that ends with the given String. For example, the match_file('.txt') matcher will match against any file that has a .txt extension, and the match_file('_spec.rb') matcher will match against RSpec files that end with _spec.rb.
A generator with a Regexp matcher will match any file path that matches the given pattern. For example, the match_file(/\.txt\z/) matcher will match any file that has a .txt extension, and the match_file(/\Aspec/) matcher will match any file in the spec directory.
You can exercise fine-grained control over a generator’s matches by passing a block to .match_file { |file_path, **options| }. The block must take one positional argument (the input file path) and any number of keywords, the options passed into the generator. If the block returns true (or any truthy value), the generator matches the file path and options; if the block returns false (or nil), the generator will not match.
Block matchers are the only match statements that allow matching against the options passed to the generator as well as the filename. For example, a generator for model files might match against both files in the the lib/models directory as well as when the --type=model option is set.
A generator’s output statements are used to determine which files will be created when the generator is called. Each output has up to three parts: the file path for the generated file, an optional key, and an optional template.
class DocsGenerator < Cuprum::Cli::Files::Generator
match_file '.md'
option :template, required: true
output '%<file_path>s'
output File.join('%<dir_name>s', '%<short_name>.yml'),
key: :data,
template: 'templates/docs/data.yml.erb'
end
The above generator will generate two files:
--template option..yml extension. The YAML file will be generating using the 'templates/docs/data.yml.erb' template, and has the unique :data key.For example, if we call this generator with a file path of docs/errors/unknown_error.md and option --template=templates/docs/doc.md.yml, it will generate two files:
docs/errors/unknown_error.md, using the template at 'templates/docs/doc.md.yml'.docs/errors/unknown_error.yml, using the template at 'templates/docs/data.yml.erb'.Each output must specify an output path, which is a String which can contain format directives (the same format as used in Kernel#sprintf; see the Ruby documentation for full details). These format directives will be resolved when the generator is called.
In addition to the values from the generator options, each generator parses the input file path for a number of parameters that are useful for defining new files relative to the input file path. The following examples use an input file path of "lib/path/to/file.rb":
:base_name"file.rb".:dir_name"lib/path/to".:ext_name".rb".:file_path"lib/path/to/file.rb".:relative_path"path/to".:root_path"lib"`.:short_name"file".Each of these parameters can be used in the file path and when evaluating the file template, as can each of the generator option values.
Each output can also define a template, which can be one of three types of value:
String, which is interpreted as a file path and converted to a FileTemplate.String, which is interpreted as a raw template literal and converted to a StringTemplate.The template (along with the generator options and the parameters parsed from the input path) is used to generate the contents of the output file.
The template can be omitted from the output, in which case the generator must define a corresponding template option and the end user pass the desired template file path when calling the generator. See custom templates for more information.
When a generator defines multiple outputs, it uses the output :key to identify specific a specific output. This is used when filtering outputs or using a custom template, but it can also be a useful signal for the developer when reading a generator class. If the key is omitted, the output is defined with a key of :default.
If you try and define an output on the same generator class with an existing key, Cuprum::Cli will raise an OutputAlreadyExistsError. However, you can freely redefine outputs on a subclass of a generator class - this allows you to customize the behavior of the generator for a particular context.
Generators define the same Options DSL as Commands, and can define new options using the .option(option_name, **opts) class method. Any option values passed to the generator can be used when generating file paths and when rendering the contents of an output.
Additionally, Cuprum::Cli also allows filtering outputs and customizing templates based on the options passed to the generator.
Cuprum::Cli uses Template objects internally to determine the contents of generated files. A template may represent a file on the file system or may wrap a raw template value. In addition, each template defines an optional engine, which is used to process the raw template and the generator parameters to build the final contents of the output file.
You can also define custom template classes by defining a subclass of Cuprum::Cli::Files::Template. The subclass must define a #call method that either returns a String (the raw template) or a failing Cuprum::Result with a Cuprum::Error. For example, you could define a template that retrieves the contents from a web url:
UrlTemplate = Cuprum::Cli::Files::Template.define(:url) do
def call
conn = Faraday.new(url:) do |faraday|
faraday.response :raise_error # raise Faraday::Error on status code 4xx or 5xx
end
response = conn.get(url)
response.body
rescue Faraday::Error => exception
error = Cuprum::Error.new(message: exception.message)
failure(error)
end
end
A FileTemplate represents a template definition stored on the local file system.
file_path = 'templates/docs.md.erb'
template = Cuprum::Cli::Files::Templates::FileTemplate.build(file_path)
template.file_path
#=> 'templates/docs.md.erb'
template.engine
#=> 'cuprum.cli.files.engines.erb'
If you pass a file path to FileTemplate.build, it will automatically detect ERB files that end with a .erb suffix. You can also manually generate a template using FileTemplate.new(engine:, file_path:).
A StringTemplate represents a template definition stored as a String literal.
raw_template = <<~MARKDOWN
# Greetings, Starfighter
You have been recruited by the Star League to defend the frontier
against Xur and the Ko-Dan armada!
MARKDOWN
template = Cuprum::Cli::Files::Templates::StringTemplate.build(raw_template)
template.engine
#=> nil
template.raw_template
#=> "# Greetings, Starfighter\n\n..."
You can also manually generate a template using StringTemplate.new(engine:, raw_template:).
Each template defines an optional #engine property. When generating the file contents, the template engine is matched against the definitions in Cuprum::Cli::Files::Engines. If a matching definition is found, that engine is used to generate the file contents using the raw template and the generator parameters.
engine = Cuprum::Cli::Files::Engines.fetch(Cuprum::Cli::Files::Engines::ERB)
engine
#=> Cuprum::Cli::Files::Engines::RenderErb
engine.call(raw_template, **parameters)
#=> The generated contents of the file.
To use a custom engine, define a subclass of Cuprum::Command with a #process method that takes a raw_template String argument and any keywords. The #process method must return either the generated String contents or a failing Cuprum::Result with a Cuprum::Error explaining the failure.
class SprintfEngine < Cuprum::Command
private
def process(raw_template, **parameters)
sprintf(raw_template, parameters)
rescue KeyError => exception
error = Cuprum::Error.new(message: exception.message)
failure(error)
end
end
Once the engine is defined, register the engine in Cuprum::Cli::Files::Engines:
Cuprum::Cli::Files::Engines.register('sprintf', SprintfEngine)
Any subsequent generators that receive a template with engine: 'sprintf' will generate the file contents using the defined SprintfEngine command.
Cuprum::Cli has one default engine which generates ERB content using the Herb toolchain.
The recommended way of using generators is via the New File command.
generators = [
MarkdownGenerator,
YamlGenerator
]
command = Cuprum::Cli::Files::NewCommand.new(generators:)
command.call('docs/generators.md')
The Files::NewCommand automatically takes care of finding the matching generator class from its list of configured generators, initializing the generator, and calling it with any options.
However, generators can also be invoked directly using the #call method.
generator = DocsGenerator.new(dry_run: true)
generator.call('docs/generators.md')
Either way, once a generator is called, it performs the following steps:
If any of these steps fails, the generator will return a failing Result.
In addition to any custom options defined for the generator class, each generator has several standard options:
:directoriestrue, generates intermediate directories, similar to the -p flag for the mkdir utility. Defaults to true:dry_runtrue, does not generate the actual output files, but outputs to the terminal as normal. Defaults to false.:quiet:verbose--dry-run to preview the file contents.The contents of each generated file depends on three things: the raw template and the template engine configured for the output, and the generator parameters. When the generator is called, the raw template and the parameters are passed to the engine, and the resulting text will be used as the contents of the generated file.
Cuprum::Cli has one default engine which generates ERB content using the Herb toolchain. All other templates are treated as plain text, and the exact contents of the template will be used as the contents of the generated file.
When generating a file, both the generated file name (via the defined output) and the file contents (via the template engine) can accept parameterized values. By default, these values are filled from the following sources:
To override this behavior, define a generator subclass and override the #parameters method. For example, to make the current timestamp available when generating the file, you could use the following:
class GeneratorWithTimestamp < Cuprum::Cli::Files::Generator
# Define outputs here.
def parameters
super.merge(timestamp: Time.now.utc.iso8601)
end
end
The template used for a given output can be customized by defining a matching option. If the output does not have a :key, the corresponding option should be named :template, while the option for a keyed output should be the key followed by _template. For example, the option for the :ruby output would be defined using option :ruby_template.
Once the option is defined, you can then pass a custom template path to the generator, either in the generator options (as template: 'path/to/template.txt.erb') or on the command line (as --template=path/to/template.txt.erb). This template path will be used when generating the corresponding output file instead of whatever template was originally defined for that output.
If the output does not define a template, the generator will need to be provided a template option for that output. In such cases, use required: true for that option.
In addition to customizing templates, you can use options to determine which outputs are actually generated when the generator is called. To do so, define an option with type: :boolean whose name matches the :key of the output. For example, the option to disable the :ruby output would be defined as option :ruby, type: :boolean, default: true. You can instead pass default: false, indicating that the output should be skipped unless specifically requested by the user.
Once the option is defined, you can then pass a true or false value to the generator, either in the generator options (as ruby: false) or on the command line (as --ruby to enable the output, or --skip-ruby to disable it). If an output is disabled, the generator will not evaluate the output name, generate the file contents, or write that output to the file system.
Cuprum::Cli defines several built-in generators for defining Ruby and RSpec source files.
The RubyGenerator creates a Ruby source file at the given file path, with contents that define a new class or module whose name matches the file path. Additionally, it creates a spec file in the spec directory that describes the newly created class. For the input path lib/space/rocket.rb and option --parent-class=Vehicle, the generator creates the following files.
In lib/space/rocket.rb:
# frozen_string_literal: true
require 'space'
module Space
class Rocket < Vehicle
end
end
In spec/space/rocket_spec.rb:
# frozen_string_literal: true
require 'space/rocket'
RSpec.describe Space::Rocket do
pending
end
RubyGenerator defines the following options:
:parent_classModule, the generated Ruby file defines a Class that inherits from the given parent class.:rspecrspec: false to disable generating the RSpec file (on the command line, --skip-rspec).:rspec_template:rubyruby: false to disable generating the Ruby file (on the command line, --skip-ruby).:ruby_templateThe RSpecGenerator creates an RSpec spec file at the given file path, with contents that describe a class or module whose name matches the file path. For the input path spec/space/rocket_spec.rb, the generator creates the following files:
In spec/space/rocket_spec.rb:
# frozen_string_literal: true
require 'space/rocket'
RSpec.describe Space::Rocket do
pending
end