NuGem
creates a scaffold project for a new gem in a new git repository.
After you add your special code to the gem scaffold,
the project is ready to be released to a public or private gem server.
This gem generates a new working Visual Studio Code project with the following features:
rbenv
.Gemfile
and .gemspec
files set up.rspec
.rubygems.org
.The following features are still in development, so they probably do not work yet:
minitest
and minitest-reporters
.$ gem install nugem
If you are using rbenv
to manage Ruby instances, type:
$ rbenv rehash
To update the program:
$ gem update nugem
NuGem
has 4 subcommands plain
, jekyll
, help
and rails
.
Currently, only plain
, jekyll
and help
have been properly tested.
help
SubcommandThe following lists the available subcommands:
$ nugem help
The following provides detailed help for the specified subcommand:
$ nugem help SUBCOMMAND
The plain
, jekyll
and rails
subcommands have common options.
The default option values assume that:
rubygems.org
Common options for the plain
, jekyll
and rails
subcommands are:
--executable
--host
bitbucket
,
github
and geminabox
.
--out_dir
generated/
.
--private
--quiet
--no-todos
TODO:
strings in generated code.The plain
, jekyll
and rails
subcommands have common behavior.
Gem scaffolds are created within the generated/
directory of the current directory by default.
If your user name is not already stored in your git global config, you will be asked for your GitHub or BitBucket user name. You will also be asked to enter your GitHub or BitBucket password when the remote repository is created for you.
After you create the gem, edit the gemspec
and change the summary and the description.
Then commit the changes to git and invoke rake release
,
and your gem will be published.
plain
Subcommand$ nugem plain NAME [COMMON_OPTIONS] [--test-framework=minitest|rspec]
NAME
is the name of the gem to be generated.
The default test framework for the plain
subcommand is rspec
,
but you can specify minitest
instead like this:
(Warning: this has not been properly tested)
$ nugem plain my_gem --test-framework=minitest
jekyll
SubcommandThe jekyll
subcommand extends the plain
subcommand and creates a new Jekyll plugin with the given NAME:
$ nugem jekyll NAME [OPTIONS]
NAME
is the name of the Jekyll plugin gem to be generated.
In addition to the common options, the jekyll
-specific OPTIONS
are:
--block
, --blockn
, --filter
, --hooks
, --tag
, and --tagn
.
(Warning: only ‑‑block
and ‑‑tag
been properly tested.)
Each of these options causes nugem
to prompt the user for additional input.
The test framework for jekyll
plugins is rspec
.
All of the above options can be specified more than once, except the ‑‑hooks
option.
For example:
$ nugem jekyll test_tags --tag my_tag1 --tag my_tag2
The above creates a Jekyll plugin called test_tags
,
which defines Jekyll tags called my_tag1
and my_tag2
.
You might use these tags in an HTML document like this:
<pre> my_tag1 usage: {% my_tag1 %} my_tag2 usage: {% my_tag2 %} </pre>
For more information, type:
$ nugem help jekyll
rails
SubcommandThe rails
subcommand extends the plain
subcommand and creates a new Rails plugin with the given NAME:
$ nugem rails NAME [OPTIONS]
NAME
is the name of the Ruby on Rails plugin gem to be generated.
In addition to the common options, rails
OPTIONS
are
--engine
and --mountable
.
You can specify if the plugin should be an engine (--engine
) or a mountable engine (--mountable
).
Each of these options causes nugem
to prompt the user for additional input.
The test framework for rails
gems is minitest
.
For more information, type:
$ nugem help rails
The following shows all files that were committed to the newly created git repository,
after nugem jekyll
finished making two tag blocks:
$ git ls-tree --name-only --full-tree -r HEAD .envrc .gitignore .rspec .rubocop.yml .simplecov .travis.yml .vscode/extensions.json .vscode/launch.json .vscode/settings.json CHANGELOG.md Gemfile LICENCE.txt README.md Rakefile bin/attach bin/console bin/rake bin/setup demo/Gemfile demo/_bin/debug demo/_config.yml demo/_drafts/2022/2022-05-01-test2.html demo/_includes/block_tag_template_wrapper demo/_layouts/default.html demo/_posts/2022/2022-01-02-redact-test.html demo/assets/css/style.css demo/assets/images/404-error.png demo/assets/images/404-error.webp demo/assets/images/favicon.png demo/assets/images/jekyll.png demo/assets/images/jekyll.webp demo/assets/js/clipboard.min.js demo/assets/js/jquery-3.4.1.min.js demo/blog/blogsByDate.html demo/blog/index.html demo/index.html jekyll_test.code-workspace jekyll_test.gemspec lib/jekyll_test.rb lib/jekyll_test/version.rb lib/my_block1.rb lib/my_block2.rb spec/jekyll_test_spec.rb spec/spec_helper.rb test/jekyll_test_test.rb test/test_helper.rb
If you have not installed the
Snippets extension,
Visual Studio Code will suggest that you do so the first time you open this project with Visual Studio Code.
You can also review the list of suggested extensions of with the CTRL-P
Extensions: Show Recommended Extensions
command.
The predefined snippets for nugem
are defined in
.vscode/nugem.json.code-snippets
.
These snippets are focused on maintaining nugem
itself.
.vscode/settings.json
defines file associations for various flavors of Thor templates in the
"files.associations"
section.
You can disable them by commenting some or all of those definitions.
Similarly, for each gem project generated by nugem
, Visual Studio Code will suggest
the user install missing extensions the first time those projects are opened.
The predefined snippets for gem projects generated by nugem
are defined in
their .vscode/gem.json.code-snippets
file.
These snippets are focused on writing Jekyll plugins.
After checking out the repository, run bin/setup
to install dependencies.
Then, run rake test
to run the tests.
You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run:
$ exec rake install
To release a new version, run:
$ exec rake release
The above will create a git tag for the version, push git commits and tags,
and push the .gem
file to rubygems.org
.
Bug reports and pull requests are welcome on GitHub at
github.com/
.
The Ruby language has many libraries for parsing command-line arguments.
This article discusses OptionParser
, which,
although only 3 years old when this was written,
has become one of the most popular libraries for parsing arguments.
This is its GitHub repository.
OptionParser
is part of the standard Ruby runtime library,
so once Ruby itself is installed, there are no additional steps for installation.
If you are building an application,
add the following line to your application’s Gemfile
:
gem 'optparse'
And then execute:
$ bundle
If you are building a gem,
add the following line to your gem’s .gemspec
:
spec.add_dependency 'optparse'
And then execute:
$ bundle
My stabilize_video
program is an example of how I like to use OptionParser
.
Below are portions of three Ruby source files:
option.rb
parses the options and generates the help text.stabilize_video.rb
parses the mandatory arguments.stablize.rb
receives the mandatory arguments and options.
First, let’s look at the parse_options
method,
which uses OptionParser
to parse the optional arguments.
def parse_options options = { shake: 5, loglevel: 'warning' } OptionParser.new do |parser| parser.program_name = File.basename __FILE__ @parser = parser parser.on('-f', '--overwrite', 'Overwrite output file if present') parser.on('-l', '--loglevel LOGLEVEL', Integer, "Logging level (#{VERBOSITY.join ', '})") parser.on('-s', '--shake SHAKE', Integer, 'Shakiness (1..10)') parser.on('-v', '--verbose VERBOSE', 'Zoom percentage') parser.on('-z', '--zoom ZOOM', Integer, 'Zoom percentage') parser.on_tail('-h', '--help', 'Show this message') do help end end.order!(into: options) help "Invalid verbosity value (#{options[:verbose]}), must be one of one of: #{VERBOSITY.join ', '}." if options[:verbose] && !options[:verbose] in VERBOSITY help "Invalid shake value (#{options[:shake]})." if options[:shake].negative? || options[:shake] > 10 options end
:shake
and :loglevel
keys.
parser.on
is passed the name of an option value in UPPER CASE,
it creates an entry in the options
hash with that name, in lower case.
The above code shows the following examples:
LOGLEVEL
provides a means for the user to specify a value to replace the default value of the
loglevel
entry in the options
hash, which was initialized with the string value 'warning'
.
SHAKE
provides a means for the user to specify a to replace the default value of the
shake
entry in the options
hash, which was initialized with the integer value 5
.
VERBOSE
provides a means for the user to specify a string value for a new entry in the
options
hash, with the key verbose
.
ZOOM
provides a means for the user to specify a string value for a new entry in the
options
hash, with the key zoom
.
OptionParser.order!
has the side effect that option keywords and key/value pairs
that match parser.on
statements are removed from ARGV
.
end.order!
statement with (into: options)
causes the parsed option key/value pairs to be added or updated in the hash called options
.
options
are returned.
Let’s see how stabilize_video.rb
parses the mandatory arguments:
require 'colorator'
require_relative 'stabilize_video/version'
require_relative 'options'
# Require all Ruby files in 'lib/', except this file
Dir[File.join(__dir__, '*.rb')].each do |file|
require file unless file.end_with?('/stabilize_video.rb')
end
def main
options = parse_options
help 'Video file name must be provided.' if ARGV.empty?
help "Too many parameters specified.\n#{ARGV}" if ARGV.length > 1
video_in = ARGV[0]
video_out = "#{File.dirname video_in}/stabilized_#{File.basename video_in}"
StablizeVideo.new(video_in, video_out, **options).stabilize
end
main
Here are some notes to help you understand the above code:
colorator
or
rainbow
to output colored strings helps readability, but is not required.
OptionParser
removes each argument from ARGV
that it recognizes,
when it finishes all that should be left on the command line are the mandatory arguments.
The main
method calls parse_options
,
which as we know calls OptionParser
,
and then ensures that a mandatory filename parameter is provided.
parse_options
returns a hash of name/value pairs,
which can optionally be passed when doubly dereferenced with two asterisks (**options
).
This is done in the highlighted code above when creating a new StablizeVideo
instance.
In the following code, the optional values returned by parse_options
are provided to the StablizeVideo.
method.
Once again, double asterisks are used.
def initialize(video_in, video_out, **options) @options = options @loglevel = "-loglevel #{options[:loglevel]}" @loglevel += ' -stats' unless options[:loglevel] == 'quiet' @shakiness = "shakiness=#{options[:shake]}" @video_in = MSUtil.expand_env video_in @video_out = MSUtil.expand_env video_out unless File.exist?(@video_in) printf "Error: file #{@video_in} does not exist.\n" exit 2 end unless File.readable? @video_in printf "Error: #{@video_in} cannot be read.\n" exit 2 end return unless File.exist?(@video_out) && !options.key?(:overwrite) printf "Error: #{@video_out} already exists.\n" exit 3 end
Notice in the above code that:
-l
/ --loglevel
option is obtained from options[:loglevel]
.
-s
/ --shake
option is obtained from options[:shake]
.
-f
(--overwrite
) option, that is detected by options.key?(:overwrite)
.
I find that using the automatically generated help text results in a more complex program for little gain,
because there are so many moving parts to keep track of.
Explicitly writing the help
method is a more maintainable way of showing the user what they need to know.
The help
method below generates the help text, which might be preceded with an error message.
def help(msg = nil) printf "Error: #{msg}\n\n".yellow unless msg.nil? msg = <<~END_HELP stabilize: Stabilizes a video using the FFmpeg vidstabdetect and vidstabtransform filters. Syntax: stabilize [Options] PATH_TO_VIDEO Options: -f Overwrite output file if present -h Show this help message -s Shakiness compensation 1..10 (default 5) -v Verbosity; one of: #{VERBOSITY.join ', '} -z Zoom percentage (computed if not specified) See: https://www.ffmpeg.org/ffmpeg-filters.html#vidstabdetect-1 https://www.ffmpeg.org/ffmpeg-filters.html#toc-vidstabtransform-1 END_HELP printf msg.cyan exit 1 end
A Ruby squiggly heredoc is used to store a multiline string as the help text.
Now let's run the above program and view the generated help text:
$ stabilize -h stabilize -h stabilize: Stabilizes a video using the FFmpeg vidstabdetect and vidstabtransform filters. Syntax: stabilize [Options] PATH_TO_VIDEO Options: -f Overwrite output file if present -h Show this help message -s Shakiness compensation 1..10 (default 5) -v Verbosity; one of: trace, debug, verbose, info, warning, error, fatal, panic, quiet -z Zoom percentage (computed if not specified) See: https://www.ffmpeg.org/ffmpeg-filters.html#vidstabdetect-1 https://www.ffmpeg.org/ffmpeg-filters.html#toc-vidstabtransform-1
BTW, highline
is a gem that is often found in CLIs that are built with OptionParser
.
The agree
method is particularly useful.
Both the agree
and ask
methods
have an undocumented feature:
Putting a space at the end of the question string suppresses the newline between the question and the answer.
The optional character
parameter causes the first character that the user types to be grabbed and processed without requiring Enter.
$ irb irb(main):001> require 'highline' => true irb(main):002* begin irb(main):003* printf "Work work work" irb(main):004> end while HighLine.agree "\nAll done! Do you want to do it again? ", character = true Work work work All done! Do you want to do it again? Please enter "yes" or "no". All done! Do you want to do it again? y Work work work All done! Do you want to do it again? y Work work work All done! Do you want to do it again? n => nil irb(main):005>
RuboCop is a Ruby code style checker (linter) and formatter based on the community-driven Ruby Style Guide. There is a Rubocop Visual Studio Code plugin that works well.
Copying the following lines into .vscode/settings.json
makes Rubocop work in Visual Studio Code.
"ruby.rubocop.useBundler": true, "ruby.rubocop.configFilePath": "./.rubocop.yml"
The above settings could also be located in a
.code-workspace
file:
{ "folders": [ { "path": "~/work/myProject" }, ], "settings": { "ruby.rubocop.useBundler": true, "ruby.rubocop.configFilePath": "./.rubocop.yml" } }
The following is the Rubocop configuration file (.rubocop.yml
) that I use for this Jekyll website.
It should be located within the top-level directory of a Ruby or Jekyll project.
require: - rubocop-jekyll - rubocop-md - rubocop-performance - rubocop-rake - rubocop-rspec inherit_gem: rubocop-jekyll: .rubocop.yml AllCops: Exclude: - _site/**/* - binstub/**/* - Gemfile* - exe/**/* - jekyll/**/* - vendor/**/* NewCops: enable TargetRubyVersion: 2.6 Gemspec/RequireMFA: Enabled: false Jekyll/NoPutsAllowed: Enabled: false # Some of my Ruby code is plugins, other Ruby code is not, so this rule is a PITA Naming/FileName: Exclude: - _bin/**/* Layout/HashAlignment: EnforcedColonStyle: table EnforcedHashRocketStyle: table Layout/LeadingCommentSpace: Exclude: - _bin/**/* Layout/LineLength: Max: 150 Layout/FirstHashElementIndentation: Enabled: false Layout/MultilineMethodCallIndentation: Enabled: false Metrics/AbcSize: Max: 40 Metrics/BlockLength: Max: 50 Metrics/CyclomaticComplexity: Max: 15 Metrics/MethodLength: Max: 30 Metrics/PerceivedComplexity: Max: 20 Style/Alias: Exclude: - _plugins/symlink_watcher.rb - blog/bin/avImport Style/Documentation: Enabled: false Style/FrozenStringLiteralComment: Enabled: false Style/HashSyntax: EnforcedStyle: ruby19 EnforcedShorthandSyntax: consistent Style/PercentLiteralDelimiters: Enabled: false Style/RegexpLiteral: Enabled: false Style/StringLiterals: Enabled: false Style/StringLiteralsInInterpolation: Enabled: false Style/TrailingCommaInArrayLiteral: Enabled: false Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: comma
I, Mike Slinn, have been working with Ruby for a long time now. Back in 2005, I was the product marketing manager at CodeGear (the company was formerly known as Borland) for their 3rd Rail IDE. 3rd Rail supported Ruby and Ruby on Rails at launch.
In 2006, I co-chaired the Silicon Valley Ruby Conference on behalf of the SD Forum in Silicon Valley. As you can see, I have the t-shirt. I was the sole chairman of the 2007 Silicon Valley Ruby Conference.
Several court cases have come my way over the years in my capacity as a software expert witness. The court cases featured questions about IP misappropriation for Ruby on Rails programs. You can read about my experience as a software expert if that interests you.
I currently enjoy writing Jekyll plugins in Ruby for this website and others, as well as Ruby utilities.
]]>Ever want to stop your Ruby program without generating a stack trace? It is simple enough to do if you control the execution environment, but if your program is loaded by another program, then the loading program can trap your attempts to exit.
Jekyll traps exceptions thrown by its plugins.
If you attempt to call abort
or
exit
,
those calls will work,
but there is nothing you can do to prevent a long stack trace from being issued.
This is not a pleasant experience for users.
You might try calling Process.kill
, like this:
Process.kill('KILL', Process.pid) # Like kill -9 in bash # or: Process.kill('SEGV', Process.pid) # Like kill -11 in bash
Unfortunately, not only does this generate a stack trace, but it also dumps lots of other information.
This is worse than just calling abort
.
The only way I found that worked was to terminate the process by loading another process
using Kernel:exec
.
The following method prints an error message in red
using colorator
,
then replaces the Jekyll process with the bash command, echo ''
.
require 'colorator' # If a Jekyll plugin needs to crash exit, and stop Jekyll, call this method. # It does not generate a stack trace. # This method does not return because the process is abruptly terminated. # # @param error StandardError or a subclass of StandardError is required # # Do not raise the error before calling this method, just create it via 'new', like this: # exit_without_stack_trace StandardError.new('This is my error message') # # If you want to call this method from a handler method, the default index for the backtrace array must be specified. # The default backtrace index is 1, which means the calling method. # To specify the calling method's caller, pass in 2, like this: # exit_without_stack_trace StandardError.new('This is my error message'), 2 def exit_without_stack_trace(error, caller_index = 1) raise error rescue StandardError => e file, line_number, caller = e.backtrace[caller_index].split(':') caller = caller.tr('`', "'") warn "#{self.class} died with a '#{error.message}' #{caller} on line #{line_number} of #{file}".red exec "echo ''" end
Recently, I wrote a command-line program (CLI) in Ruby. It was straightforward to write and worked well.
However, if I had used a special-purpose library for building CLIs, over the lifetime of my CLI program, the accumulated time for maintenance might have been much less. This could only be realized if the mythical library:
Unfortunately, open-source software rarely fits this description.
Next, I wanted to write another command-line program, this time a generator of various types of Ruby gems. Unlike my previous CLI project, this one had the following criteria:
I found an old Ruby gem generator, creategem
,
written by Igor Jancev.
Although the project had not been updated in seven years, it was well written.
I decided to update the project and extend it for my own use.
The project used the Thor gem.
I could have used dry-cli
or
mercenary
instead of thor
.
But I did not.
Maybe next time.
Nugem
is off to a promising start.
Check it out!
Written by the Ruby on Rails programmers, Thor is an open-source toolkit for building powerful command-line interfaces.
This software is a classic example of programmers scratching their own itch. They were not motivated at all to help others learn how to use the program. If you want to understand how to best use Thor, you should expect to spend time reading the source code.
Despite the huge number of GitHub projects that are based on Thor,
very few of them were written by humans.
Instead, Ruby on Rails has used thor
since Rails v3 for code generation.
For example, commands like rails new
, rails console
, rails server
,
and rails routes
are implemented with thor
.
The above is not the full story.
Thor::Actions
are helpers for your Thor tasks that make typical actions, like file system interaction or
command-line user dialogue, easier.
The Actions
whose sole purpose is to interact with a user are:
ask
, indent
,
mute
, mute?
, no
,
print_in_columns
, print_table
, print_wrapped
,
say
, say_error
, set_color
,
terminal_width
, and
yes?
.
Thor is:
option
statement was renamed some years ago to method_option
,
but the old options
variable was not renamed.
I have not found any mention of that in the documentation, and it makes looking at old code confusing.
Actions
and other portions of the code base.
This means the
RubyDoc for Thor
is useful if you have time and the ability to put it all together in your mind.
Maybe all that is needed is for someone to write a decent book about Thor.
A command called thor
was installed when the thor
gem installed.
$ which thor /home/mslinn/.rbenv/shims/thor
$ thor help Commands: thor help [COMMAND] # Describe available commands or one specific command thor install NAME # Install an optionally named Thor file into your system commands thor installed # List the installed Thor modules and commands thor list [SEARCH] # List the available thor commands (--substring means .*SEARCH) thor uninstall NAME # Uninstall a named Thor module thor update NAME # Update a Thor file from its original location thor version # Show Thor version
Unfortunately, the help message does not state what the thor
command is for.
Google brought me to the
thor
man page,
which was weird because no such man page got installed on my Ubuntu system when I installed the thor
gem,
and apt
does not have such a package available from the default Ubuntu 23.04 PPAs.
The man page says:
rake
, sake
and rubigen
.
Really? I never would have known that from any other source. If this were true, I would expect at least some documentation. I call bullshit. Please prove me wrong.
No one needs the thor
command-line utility for any purpose,
unless they want an example of how to use thor
.
If that is something you want, the command’s source is provided in
lib/thor/runner.rb
.
Just keep in mind that this program contains a lot of use-case-specific code that probably does not match
your needs, and their use case is not expressed anywhere.
I humbly suggest that nugem
is a much better example to learn from.
BTW, rubigen
is a defunct GitHub project that vanished without a trace.
Sake
was never anything to speak of.
Never mind, thor
is a useful and valuable program,
even if its creators are woefully misguided in the documentation department.
Other than the comments in the source code, accessible via RubyDoc, I have not found any user-accessible documentation on Thor templates.
However, I realized that templates use ERB syntax after I saw error messages from erb. Since Thor has no defined dependencies that pertain to scriptlet support and ERB is built-in to Ruby, ERB syntax was apparently implemented.
The comment
“This implementation is based in Templater actions”,
found in the Actions module source code,
suggests that this portion of the Thor source code was modified from the templater
source code
since Thor does not have the templater
gem as a dependency.
The templater
RubyDoc
seems to fit what I have noticed so far about Thor’s template capability.
This does not qualify as proper documentation for thor
,
but it should help guide future detective work.
Thor Actions
can:
The code I inherited from Igor Jancev used the
directory
and template
actions.
The run
command is
documented
as having options :verbose
, :capture
and :with
.
Examining the
source for run,
we see the following undocumented, yet very useful, additional options:
:abort_on_failure
, :capture
, and :pretend
.
Some commands, like git-commit
, do not return zero return codes on a successful invocation.
To make them work without prematurely ending the Thor program, write:
run "git commit -aqm 'Initial commit'", abort_on_failure: false
The following code shows a few handy techniques that you might find helpful:
require 'English' require 'thor' require_relative 'bogus_cli/version' module BogusCli class CLI < Thor include Thor::Actions package_name 'BogusCli' # Return a non-zero status code on error. See https://github.com/rails/thor/issues/244 def self.exit_on_failure? true end desc('build [FILE_NAME]', 'build the future') method_option :output, type: :string, required: false, desc: 'output file', aliases: '-o' def build(file_name) # Implementation does not matter for this example end # Display help with additional context def help(command = nil, subcommand: false) say <<~END_HELP This could be a detailed overview of the CLI... END_HELP super command, subcommand end end end
After using Thor to write a few CLIs, I find it awkward to use.
The scant documentation makes me not want to use it.
If find that just using OptionParser
makes it quite easy to write CLIs.
Thor
GitHub project
option
was renamed to method_option
.
The Ruby language allows modules and classes to be redefined and/or enhanced at runtime. This may seem strange to programmers who work with other computer languages.
Similar to the Python language, Ruby classes and modules are mutable.
Class and module methods are just attributes of the enclosing class or module,
and they can be changed.
New methods can be added to a pre-existing module
or class
.
Modifying Ruby libraries in this way is called monkey patching, and this powerful feature can be dangerous.
However, for your own code,
reopening your own module
s and class
es
to add new methods and variables can be a terrific way to write in a modular fashion,
and this code is generally quite maintainable.
My git_tree
Ruby gem
takes advantage of mutable modules
to provide a simple and
flexible mechanism for organizing code in a manageable manner.
Git_tree
mostly consists of one module
, GitTree
,
that contains common code for all the git-tree
subcommands
(git-tree-evars
, git-tree-exec
, and git-tree-replicate
).
module GitTree def self.directories_to_process(root) # implementation end end
In the above code, the self.
prefix for the method name marks this method as being
a module-level method.
Module methods can be invoked without having to include
or extend
the containing module.
When a subcommand is launched,
the GitTree
module
is re-opened,
and methods are added that are specific to that subcommand,
then the code is executed.
The above GitTree
module is re-opened in the following 3 files.
In each of them:
require_relative
statement reads the original definition of the
GitTree
module
, shown above.
module GitTree
statement re-opens the module
definition and adds new methods to it for the subcommand that the file implements.
Two kinds of methods are common to each subcommand:
command_xxx
methods contain the top-level logic for the xxx
subcommand.help_xxx
methods display the help message for the xxx
subcommand.
Notice that no Ruby class
es are used for modularity purposes.
require_relative 'git_tree' module GitTree def self.command_evars(root) # implementation end def self.help_evars(msg = nil) # implementation end # Other methods also end
require_relative 'git_tree' module GitTree def self.command_exec(root) # implementation end def self.help_exec(msg = nil) # implementation end # Other methods also end
require_relative 'git_tree' module GitTree def self.command_replicate(root) # implementation end def self.help_replicate(msg = nil) # implementation end # Other methods also end
Stubs are used for debugging command-line invocations because it is
awkward to repeatedly compile-edit-debug installed gems.
There is one debug stub for each of the three git-tree
subcommands.
The debug stubs are not included in the gem.
require_relative '../lib/git_tree_evars' GitTree.command_evars
require_relative '../lib/git_tree_exec' GitTree.command_exec
require_relative '../lib/git_tree_replicate' GitTree.command_replicate
The Visual Studio Code launch.json
entries for debugging are also simple.
Each of the above three stubs can be debugged separately.
{ "version": "0.2.0", "configurations": [ { "args": [ "$jekyll_img $jekyll_pre", "version" ], "name": "Debug git-tree-exec", "type": "Ruby", "request": "launch", "program": "${workspaceRoot}/test/git_tree_exec.rb" }, { "args": [ "$work" ], "name": "Debug git-tree-evars", "type": "Ruby", "request": "launch", "program": "${workspaceRoot}/test/git_tree_evars.rb" }, { "args": [ "$work" ], "name": "Debug git-tree-replicate", "type": "Ruby", "request": "launch", "program": "${workspaceRoot}/test/git_tree_replicate.rb" }, } }
Simple, elegant, flexible and easy to understand modularity.
I, Mike Slinn, have been working with Ruby for a long time now. Back in 2005, I was the product marketing manager at CodeGear (the company was formerly known as Borland) for their 3rd Rail IDE. 3rd Rail supported Ruby and Ruby on Rails at launch.
In 2006, I co-chaired the Silicon Valley Ruby Conference on behalf of the SD Forum in Silicon Valley. As you can see, I have the t-shirt. I was the sole chairman of the 2007 Silicon Valley Ruby Conference.
Several court cases have come my way over the years in my capacity as a software expert witness. The court cases featured questions about IP misappropriation for Ruby on Rails programs. You can read about my experience as a software expert if that interests you.
I currently enjoy writing Jekyll plugins in Ruby for this website and others, as well as Ruby utilities.
]]>I, Mike Slinn, have been working with Ruby for a long time now. Back in 2005, I was the product marketing manager at CodeGear (the company was formerly known as Borland) for their 3rd Rail IDE. 3rd Rail supported Ruby and Ruby on Rails at launch.
In 2006, I co-chaired the Silicon Valley Ruby Conference on behalf of the SD Forum in Silicon Valley. As you can see, I have the t-shirt. I was the sole chairman of the 2007 Silicon Valley Ruby Conference.
Several court cases have come my way over the years in my capacity as a software expert witness. The court cases featured questions about IP misappropriation for Ruby on Rails programs. You can read about my experience as a software expert if that interests you.
I currently enjoy writing Jekyll plugins in Ruby for this website and others, as well as Ruby utilities.
]]>This article continues my search for a database framework to use with Ruby Sinatra.
Although this article is not about Ruby on Rails, it is mentioned many times. I have used the Rails abbreviation for Ruby on Rails for convenience.
Rails and Ruby Sinatra share many architectural features and conventions.
This article is structured as follows:
rake
,
the command-line interface to the Ruby build system.
sinatra-activerecord
.
The code developed throughout this article is provided in a GitHub project called
min-sin
.
This article stops once min-sin
is fully explained.
The resulting webapp serves web pages, but it does not include CRUD operations.
You are welcome to use min-sin
as the template for your next Sinatra-ActiveRecord project.
Active record is an architectural pattern for object-relational database access.
The Rails team developed a database ORM named after the architectural pattern, and it integrates deeply with the Ruby build system. Active Record is presented in technical literature and documentation as being part of Ruby on Rails. However, Active Record is actually an independent project that merely shares the same philosophy, conventions and build system as Ruby on Rails.
The entire name of the ORM product is capitalized (Active Record), while the name of the architectural pattern is not capitalized (active record), subject to normal English capitalization rules.
The name rake
is a contraction of Ruby make
.
Make
is the original build system for UNIX systems.
Although make
can work with any computer language,
rake
is focused on building Ruby projects.
Rake has some novel features – primarily the command-line interface for build tasks.
Built-in rake
tasks
include file operations, publishing sites via FTP/SSH, and running tests.
Rake
looks for project-specific
task definitions
in files called Rakefile
or rakefile
.
Rails also allows rake
tasks to be defined in files called lib/tasks/whatever.rake
This is the help message for rake
.
$ rake -h rake [-f rakefile] {options} targets...
Options are ... --backtrace=[OUT] Enable full backtrace. OUT can be stderr (default) or stdout. --comments Show commented tasks only --job-stats [LEVEL] Display job statistics. LEVEL=history displays a complete job list --rules Trace the rules resolution. --suppress-backtrace PATTERN Suppress backtrace lines matching regexp PATTERN. Ignored if --trace is on. -A, --all Show all tasks, even uncommented ones (in combination with -T or -D) -B, --build-all Build all prerequisites, including those which are up-to-date. -C, --directory [DIRECTORY] Change to DIRECTORY before doing anything. -D, --describe [PATTERN] Describe the tasks (matching optional PATTERN), then exit. -e, --execute CODE Execute some Ruby code and exit. -E, --execute-continue CODE Execute some Ruby code, then continue with normal task processing. -f, --rakefile [FILENAME] Use FILENAME as the rakefile to search for. -G, --no-system, --nosystem Use standard project Rakefile search paths, ignore system wide rakefiles. -g, --system Using system wide (global) rakefiles (usually '~/.rake/*.rake'). -I, --libdir LIBDIR Include LIBDIR in the search path for required modules. -j, --jobs [NUMBER] Specifies the maximum number of tasks to execute in parallel. (default is number of CPU cores + 4) -m, --multitask Treat all tasks as multitasks. -n, --dry-run Do a dry run without executing actions. -N, --no-search, --nosearch Do not search parent directories for the Rakefile. -P, --prereqs Display the tasks and dependencies, then exit. -p, --execute-print CODE Execute some Ruby code, print the result, then exit. -q, --quiet Do not log messages to standard output. -r, --require MODULE Require MODULE before executing rakefile. -R, --rakelibdir RAKELIBDIR, Auto-import any .rake files in RAKELIBDIR. (default is 'rakelib') --rakelib -s, --silent Like --quiet, but also suppresses the 'in directory' announcement. -t, --trace=[OUT] Turn on invoke/execute tracing, enable full backtrace. OUT can be stderr (default) or stdout. -T, --tasks [PATTERN] Display the tasks (matching optional PATTERN) with descriptions, then exit. -AT combination displays all of tasks contained no description. -v, --verbose Log message to standard output. -V, --version Display the program version. -W, --where [PATTERN] Describe the tasks (matching optional PATTERN), then exit. -X, --no-deprecation-warnings Disable the deprecation warnings. -h, -H, --help Display this help message.
Active Record provides additional
rake
tasks.
These tasks allow you to continuously refine your application's database and associated code.
Active Record rake
task definitions include tasks that support data migrations.
This article will demonstrate and explain how to include Active Record rake tasks into a Ruby project.
Later, this article turns the Ruby project into a Sinatra webapp.
Before rake
tasks can be used, however,
additional dependencies must be installed and configured.
Read on, and I will reveal the simple magic behind the curtain!
Only two files are required to define a Ruby project that demonstrates this:
Gemfile
and Rakefile
.
Provided that the following 2 gems have been included in your project, like this:
source 'https://rubygems.org' gem 'sinatra-activerecord' gem 'rake', require: false
... then all you have to do is write a one-line Rakefile
to include Active Record tasks into your Ruby project:
require 'sinatra/activerecord/rake'
With just those two files in place,
a new Ruby project is defined that contains Active Record rake
tasks, through its
transitive dependency, Active Record.
Let’s install our minimal project’s dependencies so we can examine the Active Record rake
tasks provided by sinatra-activerecord
:
$ bundle Fetching gem metadata from https://rubygems.org/.......... Resolving dependencies... Using concurrent-ruby 1.2.2 Using minitest 5.18.0 Using i18n 1.12.0 Using bundler 2.4.6 Fetching rack 2.2.7 Using tzinfo 2.0.6 Using ruby2_keywords 0.0.5 Using tilt 2.1.0 Using activesupport 7.0.4.3 Using mustermann 3.0.0 Using activemodel 7.0.4.3 Using activerecord 7.0.4.3 Installing rack 2.2.7 Using rack-protection 3.0.6 Using sinatra 3.0.6 Using sinatra-activerecord 2.0.26 Bundle complete! 2 Gemfile dependencies, 15 gems now installed. Use `bundle info [gemname]` to see where a bundled gem is installed.
We will use the bundle exec
command a lot for the remainder of this article.
Here is the help information:
$ bundle exec -h BUNDLE-EXEC(1) BUNDLE-EXEC(1)
NAME bundle-exec - Execute a command in the context of the bundle
SYNOPSIS bundle exec [--keep-file-descriptors] command
DESCRIPTION This command executes the command, making all gems specified in the [Gemfile(5)][Gemfile(5)] available to require in Ruby programs.
Essentially, if you would normally have run something like rspec spec/my_spec.rb, and you want to use the gems specified in the [Gemfile(5)][Gemfile(5)] and installed via bundle in‐ stall(1) bundle-install.1.html, you should run bundle exec rspec spec/my_spec.rb.
Note that bundle exec does not require that an executable is available on your shell´s $PATH.
OPTIONS --keep-file-descriptors Exec in Ruby 2.0 began discarding non-standard file descriptors. When this flag is passed, exec will re‐ vert to the 1.9 behaviour of passing all file descrip‐ tors to the new process.
BUNDLE INSTALL --BINSTUBS If you use the --binstubs flag in bundle install(1) bun‐ dle-install.1.html, Bundler will automatically create a di‐ rectory (which defaults to app_root/bin) containing all of the executables available from gems in the bundle.
After using --binstubs, bin/rspec spec/my_spec.rb is identi‐ cal to bundle exec rspec spec/my_spec.rb.
ENVIRONMENT MODIFICATIONS bundle exec makes a number of changes to the shell environ‐ ment, then executes the command you specify in full.
• make sure that it´s still possible to shell out to bundle from inside a command invoked by bundle exec (using $BUN‐ DLE_BIN_PATH)
• put the directory containing executables (like rails, rspec, rackup) for your bundle on $PATH
• make sure that if bundler is invoked in the subshell, it uses the same Gemfile (by setting BUNDLE_GEMFILE)
• add -rbundler/setup to $RUBYOPT, which makes sure that Ruby programs invoked in the subshell can see the gems in the bundle
It also modifies Rubygems:
• disallow loading additional gems not in the bundle
• modify the gem method to be a no-op if a gem matching the requirements is in the bundle, and to raise a Gem::Load‐ Error if it´s not
• Define Gem.refresh to be a no-op, since the source index is always frozen when using bundler, and to prevent gems from the system leaking into the environment
• Override Gem.bin_path to use the gems in the bundle, mak‐ ing system executables work
• Add all gems in the bundle into Gem.loaded_specs
Finally, bundle exec also implicitly modifies Gemfile.lock if the lockfile and the Gemfile do not match. Bundler needs the Gemfile to determine things such as a gem´s groups, autore‐ quire, and platforms, etc., and that information isn´t stored in the lockfile. The Gemfile and lockfile must be synced in order to bundle exec successfully, so bundle exec updates the lockfile beforehand.
Loading By default, when attempting to bundle exec to a file with a ruby shebang, Bundler will Kernel.load that file instead of using Kernel.exec. For the vast majority of cases, this is a performance improvement. In a rare few cases, this could cause some subtle side-effects (such as dependence on the ex‐ act contents of $0 or __FILE__) and the optimization can be disabled by enabling the disable_exec_load setting.
Shelling out Any Ruby code that opens a subshell (like system, backticks, or %x{}) will automatically use the current Bundler environ‐ ment. If you need to shell out to a Ruby command that is not part of your current bundle, use the with_clean_env method with a block. Any subshells created inside the block will be given the environment present before Bundler was activated. For example, Homebrew commands run Ruby, but don´t work in‐ side a bundle:
Bundler.with_clean_env do `brew install wget` end
Using with_clean_env is also necessary if you are shelling out to a different bundle. Any Bundler commands run in a sub‐ shell will inherit the current Gemfile, so commands that need to run in the context of a different bundle also need to use with_clean_env.
Bundler.with_clean_env do Dir.chdir "/other/bundler/project" do `bundle exec ./script` end end
Bundler provides convenience helpers that wrap system and exec, and they can be used like this:
Bundler.clean_system(´brew install wget´) Bundler.clean_exec(´brew install wget´)
RUBYGEMS PLUGINS At present, the Rubygems plugin system requires all files named rubygems_plugin.rb on the load path of any installed gem when any Ruby code requires rubygems.rb. This includes executables installed into the system, like rails, rackup, and rspec.
Since Rubygems plugins can contain arbitrary Ruby code, they commonly end up activating themselves or their dependencies.
For instance, the gemcutter 0.5 gem depended on json_pure. If you had that version of gemcutter installed (even if you also had a newer version without this problem), Rubygems would ac‐ tivate gemcutter 0.5 and json_pure <latest>.
If your Gemfile(5) also contained json_pure (or a gem with a dependency on json_pure), the latest version on your system might conflict with the version in your Gemfile(5), or the snapshot version in your Gemfile.lock.
If this happens, bundler will say:
You have already activated json_pure 1.4.6 but your Gemfile requires json_pure 1.4.3. Consider using bundle exec.
In this situation, you almost certainly want to remove the underlying gem with the problematic gem plugin. In general, the authors of these plugins (in this case, the gemcutter gem) have released newer versions that are more careful in their plugins.
You can find a list of all the gems containing gem plugins by running
ruby -e "puts Gem.find_files(´rubygems_plugin.rb´)"
At the very least, you should remove all but the newest ver‐ sion of each gem plugin, and also remove all gem plugins that you aren´t using (gem uninstall gem_name).
October 2022 BUNDLE-EXEC(1)
The following rake
tasks are now available to our minimal Ruby Sinatra project.
These tasks require additional configuration before they can be used without error. I will walk you through that so you know how all the moving parts connect.
$ bundle exec rake -T rake db:create # Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases) rake db:create_migration # Create a migration (parameters: NAME, VERSION) rake db:drop # Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases) rake db:encryption:init # Generate a set of keys for configuring Active Record encryption in a given environment rake db:environment:set # Set the environment value for the database rake db:fixtures:load # Loads fixtures into the current environment's database rake db:migrate # Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog) rake db:migrate:down # Runs the "down" for a given migration VERSION rake db:migrate:redo # Rolls back the database one migration and re-migrates up (options: STEP=x, VERSION=x) rake db:migrate:status # Display status of migrations rake db:migrate:up # Runs the "up" for a given migration VERSION rake db:prepare # Runs setup if database does not exist, or runs migrations if it does rake db:reset # Drops and recreates all databases from their schema for the current environment and loads the seeds rake db:rollback # Rolls the schema back to the previous version (specify steps w/ STEP=n) rake db:schema:cache:clear # Clears a db/schema_cache.yml file rake db:schema:cache:dump # Creates a db/schema_cache.yml file rake db:schema:dump # Creates a database schema file (either db/schema.rb or db/structure.sql, depending on ENV['SCHEMA_FORMAT'] or config.active_...) rake db:schema:load # Loads a database schema file (either db/schema.rb or db/structure.sql, depending on ENV['SCHEMA_FORMAT'] or config.active_re...) rake db:seed # Loads the seed data from db/seeds.rb rake db:seed:replant # Truncates tables of each database for current environment and loads the seeds rake db:setup # Creates all databases, loads all schemas, and initializes with the seed data (use db:reset to also drop all databases first) rake db:version # Retrieves the current schema version number
All of the above tasks must be run by prefacing them with bundle exec
.
If this becomes tiresome, define an alias for rake
to bundle exec rake
as follows:
$ echo 'rake="bundle exec rake"' >> ~/.bash_aliases $ source ~/.bash_aliases
If you define an alias as shown above, then when you type a command like:
$ rake db:some_task_name
... the bash alias will expand your command line so the following is executed:
$ bundle exec rake db:some_task_name
Before the Active Record tasks can be used,
database parameters must be specified.
Typically this is done by creating
config/database.yml
.
The following specifies sqlite
for development and testing,
and PostgreSQL for production:
default: &default adapter: sqlite3 timeout: 5000 development: <<: *default database: db/development.sqlite3 test: <<: *default database: db/test.sqlite3 production: adapter: postgresql encoding: unicode pool: 5 host: <%= ENV['DATABASE_HOST'] || 'db' %> database: <%= ENV['DATABASE_NAME'] || 'sinatra' %> username: <%= ENV['DATABASE_USER'] || 'sinatra' %> password: <%= ENV['DATABASE_PASSWORD'] || 'sinatra' %>
The above database parameters needs two more gems in Gemfile
,
one for each type of database:
gem 'sqlite3' gem 'pg'
The pg
gem requires either libpq
to be installed in the OS,
or a PostgreSQL client package must be installed, for example one of these, depending on your OS:
$ sudo apt install libpq-dev $ sudo yum install postgresql-devel $ sudo zypper in postgresql-devel $ sudo pacman -S postgresql-libs
You need to install the newly added gems before you can do anything further with this project:
$ bundle Fetching gem metadata from https://rubygems.org/......... Resolving dependencies... Using rake 13.0.6 Using concurrent-ruby 1.2.2 Using minitest 5.18.0 Using i18n 1.12.0 Using tzinfo 2.0.6 Using pg 1.5.1 Using rack 2.2.7 Using tilt 2.1.0 Using sqlite3 1.6.2 (x86_64-linux) Using bundler 2.4.6 Using ruby2_keywords 0.0.5 Using activesupport 7.0.4.3 Using mustermann 3.0.0 Using activemodel 7.0.4.3 Using rack-protection 3.0.6 Using activerecord 7.0.4.3 Using sinatra 3.0.6 Using sinatra-activerecord 2.0.26 Bundle complete! 4 Gemfile dependencies, 18 gems now installed. Use `bundle info [gemname]` to see where a bundled gem is installed.
Additional configuration is possible for Active Record.
Add the following to Rakefile
:
require 'sinatra/activerecord'
If the above is not present in Rakefile
,
the following error will appear each time you attempt to run an Active Record rake
task:
ActiveRecord::AdapterNotSpecified: The `development` database is not
configured for the `development` environment.
At present, Rakefile
is the only file in this project that contains Ruby code.
Active Record has all the logic for creating CRUD models,
and the sinatra-activerecord
Gem integrates it with Sinatra.
DATABASE_URL
should not be defined
unless you specifically set it up for your project.
Undefine it like this in the shell that launches your Sinatra program:
$ unset DATABASE_URL
The environment variables
RAILS_ENV
, RACK_ENV
and APP_ENV
are all similar
and can be used interchangeably much of the time.
Their purpose is to define the mode in which to run the application.
Common choices are: development
, production
, and test
,
but you can define other values.
The default value is development
.
APP_ENV
RACK_ENV
rack
applications by defaultRAILS_ENV
I recommend that you adopt one of the following conventions:
$ unset RAILS_ENV RACK_ENV APP_ENV
$ unset RAILS_ENV RACK_ENV APP_ENV $ export APP_ENV=production
$ export APP_ENV=production $ export RACK_ENV=$APP_ENV $ export RAILS_ENV=$APP_ENV
Now that you know how Active Record is distinct from Rails,
you might find this article helpful:
Active Record Basics,
by Rails Guides.
This documentation does not emphasize that the Rails build system is actually rake
,
and that the rails
command simply forwards build-related command lines to rake
.
You can use the min-sin
Ruby Sinatra project that we just recreated
to learn about Active Record from the Rails Guide without using Rails
by substituting bundle exec rake
every time you see bin/rails
.
Thus, when the Active Record documentation says:
$ bin/rails db:migrate
You should write instead:
$ bundle exec rake db:migrate
There are so many rake
tasks!
It seems overwhelming.
Happily, you only need to know a few of them most of the time.
Provided that you followed along and installed all the dependencies, the following tasks should all be operational. This article merely discusses them, but does not use them to add CRUD functionality to the webapp. Please experiment!
Normally, the db:setup
task is the first Active Record rake
task to run.
This task creates the database, loads the schema, and initializes the schema with any available seed data.
$ bundle exec rake db:setup
The db:create_migration
task accepts two optional parameters
(NAME
and VERSION
),
and uses them to create a new migration.
Run the db:create_migration
task like this:
$ bundle exec rake db:create_migration NAME=create_users_table
Once you have created a migration,
the db:migrate
task creates the corresponding database table(s).
$ bundle exec rake db:migrate
The db:drop
task drops the database.
The db:reset
task drops the database and sets it up again.
Use this task as follows:
$ bundle exec rake db:reset
This is functionally equivalent to:
$ bundle exec rake db:drop db:setup
The above shows how two tasks can be specified at once; they are executed in sequence.
If you commit the change introduced by a migration and later discover a problem with the migration,
then you cannot just edit the migration and rerun it.
This is because rake
only runs migrations once,
so nothing happens when you attempt to run the db:migrate
task again.
You must first use db:rollback
task to roll back the most recent migration
before editing it and rerunning the db:migrate
task.
$ bundle exec rake db:rollback
bundle exec rake db:rollback
can be invoked more than once;
it deletes the most recent migration each time it is used.
The following example rolls back the two most recent migrations,
then attempts the most recent migration again.
$ bundle exec rake db:rollback db:rollback $ # Make changes to the project's problem migration $ bundle exec rake db:migrate
At present, the min-sin
project is not actually a Sinatra webapp.
Yes, you can create data migrations and data models,
but the min-sin
project cannot yet serve web pages.
This article was structured this way so you could realize the contribution that Active Record makes
to a project, separate from the contribution that Sinatra makes.
To turn the min-sin
project into a Sinatra webapp that can serve web pages,
we just need to make two small files: config.ru
and app.rb
.
To understand why the following instructions work,
you need to know that Sinatra (and Rails) comply with the
rack
specification.
Rack applications are normally launched with the
rackup
command.
By default, the rackup
command loads and runs a file called config.ru
in the top-level directory of a rack
-compliant webapp.
The config.ru
file has no relationship with the config/
directory we saw earlier.
The similar names can be confusing.
As we saw earlier,
the config/
directory defines this project’s databases for Active Record.
In contrast, config.ru
is for launching the Sinatra webapp;
this works because Sinatra is rack
-compliant.
To be precise, Rack middleware
is launched by running config.ru
.
This is the rackup
help information:
$ rackup -h Usage: rackup [ruby options] [rack options] [rackup config]
Ruby options: -e, --eval LINE evaluate a LINE of code -d, --debug set debugging flags (set $DEBUG to true) -w, --warn turn warnings on for your script -q, --quiet turn off logging -I, --include PATH specify $LOAD_PATH (may be used more than once) -r, --require LIBRARY require the library, before executing your script
Rack options: -b BUILDER_LINE, evaluate a BUILDER_LINE of code as a builder script --builder -s, --server SERVER serve using SERVER (thin/puma/webrick) -o, --host HOST listen on HOST (default: localhost) -p, --port PORT use PORT (default: 9292) -O NAME[=VALUE], pass VALUE to the server as option NAME. If no VALUE, sets it to true. Run '/home/mslinn/.rbenv/versions/3.1.0/bin/rackup -s SERVER -h' to get a list of options for SERVER --option -E, --env ENVIRONMENT use ENVIRONMENT for defaults (default: development) -D, --daemonize run daemonized in the background -P, --pid FILE file to store PID
Profiling options: --heap HEAPFILE Build the application, then dump the heap to HEAPFILE --profile PROFILE Dump CPU or Memory profile to PROFILE (defaults to a tempfile) --profile-mode MODE Profile mode (cpu|wall|object)
Common options: -h, -?, --help Show this message --version Show version
If you place config.ru
in the top-level directory of a Sinatra/rack
project,
when we run the rackup
command to launch the webapp,
config.ru
will be found without requiring any options.
require_relative 'app' run Sinatra::Application
As you can see above, config.ru
require
s app.rb
;
this is the entry point for the logic that we want to provide in the Sinatra webapp.
Next, config.ru
starts the Sinatra web server.
app.rb
is similarly simple;
it can either define a classic Sinatra webapp
or a modular Sinatra webapp.
I find modular Sinatra webapps easier to maintain,
so the following is the smallest possible modular Sinatra webapp.
require "sinatra/base" class MyApp < Sinatra::Base get '/' do "A very classy "hello" to you!!" end end
Now we can run our modular Sinatra webapp:
$ rackup 2023-04-27 09:20:35 -0400 Thin web server (v1.8.2 codename Ruby Razor) 2023-04-27 09:20:35 -0400 Maximum connections set to 1024 2023-04-27 09:20:35 -0400 Listening on localhost:9292, CTRL+C to stop
Point your web browser to localhost:9292
and you should see:
A very classy "hello" to you!
This article removed the mystery of setting up Active Record with Sinatra.
You can use the min-sin
Sinatra Active Record project as a starting point.
Now you should be able to stumble towards making a functional CRUD app without getting confused by the Rails documentation when you encounter it.
I, Mike Slinn, have been working with Ruby for a long time now. Back in 2005, I was the product marketing manager at CodeGear (the company was formerly known as Borland) for their 3rd Rail IDE. 3rd Rail supported Ruby and Ruby on Rails at launch.
In 2006, I co-chaired the Silicon Valley Ruby Conference on behalf of the SD Forum in Silicon Valley. As you can see, I have the t-shirt. I was the sole chairman of the 2007 Silicon Valley Ruby Conference.
Several court cases have come my way over the years in my capacity as a software expert witness. The court cases featured questions about IP misappropriation for Ruby on Rails programs. You can read about my experience as a software expert if that interests you.
I currently enjoy writing Jekyll plugins in Ruby for this website and others, as well as Ruby utilities.
]]>
Ruby gems
provide a handy way of packaging reusable Ruby code for use in Ruby programming projects.
Bundler
simplifies and automates the process of downloading, installing and maintaining
compatible versions of Ruby gem dependencies for a project.
Ruby gems are also easy to publish.
My jekyll_plugin_support
gem
contains classes called JekyllBlock
, JekyllTag
,
JekyllBlockNoArgParsing
, and JekyllTagNoArgParsing
.
These classes are meant to be subclassed.
New Jekyll plugins are created around these subclasses,
and these plugins are normally packaged as gems as well.
To clarify,
while gems cannot be subclassed,
when the jekyll_plugin_support
gem is added to a project as a dependency,
at least one of the four classes that I mentioned will need to be subclassed.
The subclasses are used to define a Jekyll plugin.
Recently, I wanted to add optional functionality into jekyll_plugin_support
that would be available to JekyllBlock
and JekyllTag
subclasses.
The new jekyll_plugin_support
functionality required it to recognize when it was being invoked by a gem
and to obtain the information stored in the invoking gem’s Gem::Specification
.
This required a deep dive into how Jekyll loads plugins, how Liquid dispatches tags, and the inner workings of Ruby gems. This article is not going to drag you through all the details; instead, just the most important details will be discussed.
Ruby gems are defined by a
Gem::Specification
,
normally saved in a
.gemspec
file.
If you have ever looked at a Ruby gem’s source code, this is familiar to you.
For example, here is the gem specification for jekyll_plugin_support
:
require_relative 'lib/jekyll_plugin_support/version' Gem::Specification.new do |spec| github = 'https://github.com/mslinn/jekyll_plugin_support' spec.bindir = 'exe' spec.authors = ['Mike Slinn'] spec.email = ['mslinn@mslinn.com'] spec.files = Dir['.rubocop.yml', 'LICENSE.*', 'Rakefile', '{lib,spec}/**/*', '*.gemspec', '*.md'] spec.homepage = 'https://www.mslinn.com/jekyll_plugins/jekyll_plugin_support.html' spec.license = 'MIT' spec.metadata = { 'allowed_push_host' => 'https://rubygems.org', 'bug_tracker_uri' => "#{github}/issues", 'changelog_uri' => "#{github}/CHANGELOG.md", 'homepage_uri' => spec.homepage, 'source_code_uri' => github, } spec.name = 'jekyll_plugin_support' spec.post_install_message = <<~END_MESSAGE Thanks for installing #{spec.name}! END_MESSAGE spec.require_paths = ['lib'] spec.required_ruby_version = '>= 2.6.0' spec.summary = 'Provides a framework for writing and testing Jekyll plugins' spec.test_files = spec.files.grep %r{^(test|spec|features)/} spec.version = JekyllPluginSupportVersion::VERSION spec.add_dependency 'facets' spec.add_dependency 'jekyll', '>= 3.5.0' spec.add_dependency 'jekyll_plugin_logger' spec.add_dependency 'key-value-parser' spec.add_dependency 'pry' end
A key feature of Ruby gems, essential to the code presented in this article,
is that gems are not stored in a compressed format.
Instead, they are stored as uncompressed files in a directory tree.
In contrast, most package management formats for other computer languages and OSes use a
compressed format to store dependencies.
For example:
Python wheel
,
Java jars
(also used by Maven),
and Debian deb
(also used by Ubuntu apt).
The following Ruby code was developed on StackOverflow.
The method returns the Gem::Specification
for a gem when pointed to a file within any directory within the gem.
# @param file must be a fully qualified file name # @return Gem::Specification of gem that file points into, # or nil if not called from a gem def current_spec(file) return nil unless file.exist? searcher = if Gem::Specification.respond_to?(:find) Gem::Specification elsif Gem.respond_to?(:searcher) Gem.searcher.init_gemspecs end searcher&.find do |spec| file.start_with? spec.full_gem_path end end
The current_spec
method in the above code uses the
safe navigation operator.
&.
is called the “safe navigation operator” because it suppresses method calls when the receiver is nil
.
It returns nil
and does not evaluate method arguments if the call is skipped.
If I paste the code that defines searcher
into irb
,
the result is a Gem::Specification
,
which is confusing because it is actually a Gem::Specification
that contains a collection of Gem::Specification
s.
$ $ irb irb(main):001:1* searcher = if Gem::Specification.respond_to?(:find) irb(main):002:1* Gem::Specification irb(main):003:1* elsif Gem.respond_to?(:searcher) irb(main):004:1* Gem.searcher.init_gemspecs irb(main):005:0> end => Gem::Specification
Let’s look at the first element contained within searcher
:
irb(main):006:0> searcher.first => Gem::Specification.new do |s| s.name = "error_highlight" s.version = Gem::Version.new("0.3.0") s.installed_by_version = Gem::Version.new("0") s.authors = ["Yusuke Endoh"] s.date = Time.utc(2023, 1, 24) s.description = "The gem enhances Exception#message by adding a short explanation where the exception is raised" s.email = ["mame@ruby-lang.org"] s.files = ["lib/error_highlight.rb", "lib/error_highlight/base.rb", "lib/error_highlight/core_ext.rb", "lib/error_highlight/formatter.rb", "lib/error_highlight/version.rb"] s.homepage = "https://github.com/ruby/error_highlight" s.licenses = ["MIT"] s.require_paths = ["lib"] s.required_ruby_version = Gem::Requirement.new([">= 3.1.0.dev"]) s.rubygems_version = "3.3.3" s.specification_version = 4 s.summary = "Shows a one-line code snippet with an underline in the error backtrace" end
Let’s search the list for the Gem::Specification
for the jekyll_plugin_support v0.6.0
gem into spec
.
irb(main):007:0> spec = searcher.find_by_name('jekyll_plugin_support', '0.6.0') => Gem::Specification.new do |s| ...
The directory that contains an installed Gem::Specification
can be obtained by calling full_gem_path
:
irb(main):008:0> spec.full_gem_path => "/home/mslinn/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/jekyll_plugin_support-0.6.0"
As most Ruby programmers know, a Ruby source file can discover its own location by examining __FILE__
.
If this is done by a file within a Ruby gem, then the location of the file is obtained,
and one of the parent directories of the location will be the location of the entire gem.
irb(main):008:0> file = __FILE__ => "(irb)"
Unfortunately, __FILE__
does not return anything useful from irb
, as you can see above.
Let’s cheat a little and set file
to a valid value for the gem we are looking at.
We’ll point at lib/jekyll_plugin_support.rb
within the gem:
irb(main):009:0> file = File.join(spec.full_gem_path, 'lib/jekyll_plugin_support.rb') => "/home/mslinn/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/jekyll_plugin_support-0.6.0/lib/jekyll_plugin_suppo..." irb(main):010:0> File.exist? file => true
Here is an quick test to see if the file
is within the gem directory tree:
irb(main):008:0> file.start_with? spec.full_gem_path => true
If a gem needs to know what its Gem::Specification
is,
it can iterate through each of the items within searcher
,
and compare the value from full_gem_path
against the value returned by __FILE__
.
irb(main):008:0> searcher&.find do |spec| file.start_with? spec.full_gem_path end => Gem::Specification.new do |s| ...
Now that we know how the current_spec
method works, we can invoke it from a Ruby gem like this:
spec = current_spec __FILE__
The gem has obtained its own Gem::
through digital navel-gazing (omphaloskepsis).
The gem can now retrieve its own properties from spec
.
@name = spec.name @authors = spec.authors @homepage = spec.homepage @published_date = spec.date.to_date.to_s @version = spec.version
... and that is the essence of how the
jekyll_plugin_support
subclass feature works.
I, Mike Slinn, have been working with Ruby for a long time now. Back in 2005, I was the product marketing manager at CodeGear (the company was formerly known as Borland) for their 3rd Rail IDE. 3rd Rail supported Ruby and Ruby on Rails at launch.
In 2006, I co-chaired the Silicon Valley Ruby Conference on behalf of the SD Forum in Silicon Valley. As you can see, I have the t-shirt. I was the sole chairman of the 2007 Silicon Valley Ruby Conference.
Several court cases have come my way over the years in my capacity as a software expert witness. The court cases featured questions about IP misappropriation for Ruby on Rails programs. You can read about my experience as a software expert if that interests you.
I currently enjoy writing Jekyll plugins in Ruby for this website and others, as well as Ruby utilities.
]]>