Toys-Core
Toys is a configurable command line tool. Write commands in Ruby using a simple DSL, and Toys will provide the command line executable and take care of all the details such as argument parsing, online help, and error reporting.
Toys-Core is the command line tool framework underlying Toys. It can be used to write command line executables using the Toys DSL and the power of the Toys classes.
For more detailed information about Toys-Core, see the Toys-Core User's Guide. For background information about Toys itself, see the Toys README and the Toys User Guide.
Introductory tutorial
Here's a tutorial to help you get a feel for how to write a basic command line executable using Toys-Core.
It assumes basic familiarity with Toys, so, if you have not done so, I recommend first walking through the tutorial in the Toys README. It also assumes you are running a unix-like system such as Linux or macOS. Some commands might need to be modified if you're running on Windows.
Install Toys
Install the toys-core gem using:
$ gem install toys-core
You may also install the toys gem, which brings in toys-core as a dependency.
Toys-Core requires Ruby 2.4 or later.
Most parts of Toys-Core work on JRuby. However, JRuby is not recommended because of JVM boot latency, lack of support for Kernel#fork, and other issues.
Most parts of Toys-Core work on TruffleRuby. However, TruffleRuby is not recommended because it has a few known bugs that affect Toys.
Create a new executable
We'll start by creating an executable Ruby script. Using your favorite text
editor, create new a file called mycmd
with the following contents:
#!/usr/bin/env ruby
require "toys-core"
cli = Toys::CLI.new
exit(cli.run(*ARGV))
Make sure the file's executable bit is set:
$ chmod a+x mycmd
That's it! This is a fully-functional Toys-based executable! Let's see what happens when you run it:
$ ./mycmd
Just as with Toys itself, you get a help screen by default (since we haven't
yet actually implemented any behavior.) As you can see, some of the same
features from Toys are present already: online help, and --verbose
and
--quiet
flags. These features can of course all be customized or disabled,
but they're often useful to have to start off.
Add some functionality
You implement the functionality of your executable using the same DSL that you use to write Toys files. You could point your executable at a directory containing actual Toys files, but the simplest option is to provide the information to the Toys CLI object in a block.
Let's add some functionality.
#!/usr/bin/env ruby
require "toys-core"
cli = Toys::CLI.new
#### Insert the following block ...
cli.add_config_block do
desc "My first executable!"
flag :whom, default: "world"
def run
puts "Hello, #{whom}!"
end
end
exit(cli.run(*ARGV))
If you went through the tutorial in the README for the Toys gem, this should look familiar. Let's run it now, and experiment with passing flags to it.
$ ./mycmd
$ ./mycmd --whom=ruby
$ ./mycmd --bye
$ ./mycmd --help
Notice that we did not create a tool
block, but instead set up description,
flags, and functionality directly in the configuration block. This configures
the "root tool", i.e. what happens when you run the executable without passing
a tool name to it. (Note, it's legal to do this in Toys as well, by setting
functionality at the "top level" of a .toys.rb
file without including any
tool
block.)
Tool-based executables
But perhaps you want your executable to have multiple "tools", similar to other
familiar executables like git or kubectl. You can define tools, including
nested tools, by writing tool
blocks in your config. Here's an example:
#!/usr/bin/env ruby
require "toys-core"
cli = Toys::CLI.new
#### Change the config block as follows ...
cli.add_config_block do
# Things outside any tool block still apply to the root
desc "My first executable with several tools"
# We'll put the greet function here
tool "greet" do
desc "My first tool!"
flag :whom, default: "world"
def run
puts "Hello, #{whom}!"
end
end
# Try writing a second tool here. You could use the "new-repo"
# example from the Toys tutorial.
end
exit(cli.run(*ARGV))
Now you can run greet
as a tool:
$ ./mycmd greet
The "root" functionality once again shows global help, including a list of the available tools.
$ ./mycmd
Notice that the description set at the "root" of the config block (outside the tool blocks) shows up here.
Configuring the CLI
So far, our executable behaves very similarly to Toys itself. Help screens are shown by default, flags for help and verbosity are provided automatically, and any exceptions are displayed to the terminal.
These and many more aspects of the behavior of our executable can be customized
by passing options to the Toys::CLI
constructor. Here's an example that
modifies error handling and delimiter parsing.
#!/usr/bin/env ruby
require "toys-core"
#### Pass some additional options to the CLI constructor ...
cli = Toys::CLI.new(
extra_delimiters: ":",
error_handler: ->(_err) {
puts "Dude, an error happened..."
return 1
}
)
#### Change the config block as follows ...
cli.add_config_block do
tool "example" do
tool "greet" do
def run
puts "Hello, world!"
end
end
tool "error" do
def run
raise "Whoops!"
end
end
end
end
exit(cli.run(*ARGV))
Try these runs. Do they behave as you expected?
$ ./mycmd example greet
$ ./mycmd example:greet
$ ./mycmd example.greet
$ ./mycmd example error
Configuring middleware
Toys middleware are objects that provide common functionality for all the
tools in your executable. For example, a middleware adds the --help
flag to
your tools by default.
The next example modifies the middleware stack to alter this common tool functionality.
#!/usr/bin/env ruby
require "toys-core"
#### Change the CLI construction again ...
middlewares = [
[:set_default_descriptions, default_tool_desc: "Hey look, a tool!"],
[:show_help, help_flags: true]
]
cli = Toys::CLI.new middleware_stack: middlewares
#### Use this config block ...
cli.add_config_block do
tool "greet" do
def run
puts "Hello, world!"
end
end
end
exit(cli.run(*ARGV))
We've now modified the default description applied to tools that don't provide their own description. See the effect with:
$ ./mycmd greet --help
We've also omitted some of the default middleware, including the one that adds
the --verbose
and --quiet
flags to all your tools. Notice those flags are
no longer present.
We've also omitted the middleware that provides default execution behavior
(i.e. displaying the help screen) when there is no run
method. Now, since we
haven't defined a toplevel run
method in this last example, invoking the root
tool will cause an error:
$ ./mycmd
It is even possible to write your own middleware. In general, while the
Toys::CLI
constructor provides defaults that should work for many use cases,
you can also customize it heavily to suit the needs of your executable.
Packaging as a gem
So far we've created simple one-file executables that you could distribute by
itself. However, the toys-core
gem is a dependency, and your users will need
to have it installed. You could alleviate this by wrapping your executable in a
gem that can declare toys-core
as a dependency explicitly.
The examples directory includes a few simple examples that you can use as a starting point.
To experiment with the examples, clone the Toys repo from GitHub:
$ git clone https://github.com/dazuma/toys.git
$ cd toys
Navigate to the simple-gem example:
$ cd toys-core/examples/simple-gem
This example wraps the simple "greet" executable that we covered earlier in a gem. You can see the executable file in the bin directory.
Try it out by building and installing the gem. From the examples/simple-gem
directory, run:
$ toys install
Once the gem has successfully installed, you can run the executable, which Rubygems should have added to your path. (Note: if you are using a ruby installation manager, you may need to "rehash" or "reshim" to gain access to the executable.)
$ toys-core-simple-example --whom=Toys
Clean up by uninstalling the gem:
$ gem uninstall toys-core-simple-example
If the implementation of your executable is more complex, you might want to break it up into multiple files. The multi-file gem example demonstrates this.
$ cd ../multi-file-gem
This executable's implementation resides in its
lib directory,
a technique that may be familiar to writers of command line executables. More
interestingly, the tools themselves are no longer defined in a block passed to
the CLI object, but have been moved into a separate
"tools" directory.
This directory has the same structure and supports the same features that are
available when writing complex sets of tools in a .toys
directory. You then
configure the CLI object to look in this directory for its tools definitions,
as you can see in
the code.
Try it out now. From the examples/multi-file-gem
directory, run:
$ toys install
Once the gem has successfully installed, you can run the executable, which Rubygems should have added to your path. (Note: if you are using a ruby installation manager, you may need to "rehash" or "reshim" to gain access to the executable.)
$ toys-core-multi-file-example greet
Clean up by uninstalling the gem:
$ gem uninstall toys-core-multi-file-example
Learning more
This introduction should be enough to get you started. However, Toys-Core is a deep framework with many more features. Learn about how to write tools using the Toys DSL, including validating and interpreting command line arguments, using templates and mixins, controlling subprocesses, and producing nice styled output, in the Toys User Guide. Learn more about how to customize and package your own executable, including handling errors, controlling log output, and providing your own mixins, templates, and middleware, in the Toys-Core User Guide.
License
Copyright 2019-2022 Daniel Azuma and the Toys contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.