Created
July 31, 2013 00:19
-
-
Save jendiamond/6118261 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## [Thor](http://whatisthor.com/) - Interpreted by Jen Diamond | |
Thor is a toolkit for building powerful command-line interfaces. It is used in Bundler, Vagrant, Rails, and 197 other gems. | |
Getting Started | |
A simple Thor class exposes an executable with a number of subcommands, like git or bundler. In a Thor class, public methods become commands. | |
class MyCLI < Thor | |
desc "hello NAME", "say hello to NAME" | |
def hello(name) | |
puts "Hello #{name}" | |
end | |
end | |
You can start the CLI with a call to MyCLI.start(ARGV). Typically, you would do this in an executable in the bin directory of your gem. | |
If the arguments you pass to start are empty, Thor will print out a help listing for your class. | |
Throughout the rest of this guide, I will assume that you have a file called cli in the current directory that looks like this: | |
require "thor" | |
class MyCLI < Thor | |
# contents of the Thor class | |
end | |
MyCLI.start(ARGV) | |
Thor will automatically use the executable name in the help it generates for a Thor class. | |
$ ruby ./cli | |
Tasks: | |
cli hello NAME # say hello to NAME | |
cli help [TASK] # Describe available tasks or one specific task | |
If you execute the hello task with a parameter, it will call your method: | |
$ ruby ./cli hello Yehuda | |
Hello Yehuda | |
If you execute it without a parameter, Thor will automatically print a useful error message: | |
$ ruby ./cli hello | |
"hello" was called incorrectly. Call as "test.rb hello NAME". | |
You can also use Ruby’s optional arguments to make a CLI argument optional: | |
class MyCLI < Thor | |
desc "hello NAME", "say hello to NAME" | |
def hello(name, from=nil) | |
puts "from: #{from}" if from | |
puts "Hello #{name}" | |
end | |
end | |
When you execute it: | |
$ ruby ./cli hello "Yehuda Katz" | |
Hello Yehuda Katz | |
$ ruby ./cli hello "Yehuda Katz" "Carl Lerche" | |
from: Carl Lerche | |
Hello Yehuda Katz | |
This can be useful in some cases, but in most cases you will want to use Unix-style options. | |
Long Description | |
By default, Thor will use the short description provided to desc in long usage information. | |
$ ruby ./cli help hello | |
Usage: | |
test.rb hello NAME | |
say hello to NAME | |
In many cases, you will want to provide a longer description for use in the longer usage instructions. In this case, you can use long_desc to specify longer usage instructions. | |
class MyCLI < Thor | |
desc "hello NAME", "say hello to NAME" | |
long_desc <<-LONGDESC | |
`cli hello` will print out a message to a person of your | |
choosing. | |
You can optionally specify a second parameter, which will print | |
out a from message as well. | |
> $ cli hello "Yehuda Katz" "Carl Lerche" | |
> from: Carl Lerche | |
LONGDESC | |
def hello(name, from=nil) | |
puts "from: #{from}" if from | |
puts "Hello #{name}" | |
end | |
end | |
By default, the long description wraps lines at the size of the terminal and will group lines with a single line break together, just like Markdown. You can also use the \x5 escape sequence at the beginning of a line to force a single hard break between lines. | |
class MyCLI < Thor | |
desc "hello NAME", "say hello to NAME" | |
long_desc <<-LONGDESC | |
`cli hello` will print out a message to a person of your | |
choosing. | |
You can optionally specify a second parameter, which will print | |
out a from message as well. | |
> $ cli hello "Yehuda Katz" "Carl Lerche" | |
\x5> from: Carl Lerche | |
LONGDESC | |
def hello(name, from=nil) | |
puts "from: #{from}" if from | |
puts "Hello #{name}" | |
end | |
end | |
In many cases, you will want to store the long descriptions in separate files to keep your CLI description short and readable. You can then use File.read to pull in the contents of the file. | |
Options and Flags | |
Thor makes it easy to specify options and flags as metadata about a Thor command: | |
class MyCLI < Thor | |
desc "hello NAME", "say hello to NAME" | |
option :from | |
def hello(name) | |
puts "from: #{options[:from]}" if options[:from] | |
puts "Hello #{name}" | |
end | |
end | |
Now, your users can specify the from option as a flag: | |
$ ruby ./cli hello --from "Carl Lerche" Yehuda | |
from: Carl Lerche | |
Hello Yehuda | |
$ ruby ./cli hello Yehuda --from "Carl Lerche" | |
from: Carl Lerche | |
Hello Yehuda | |
$ ruby ./cli hello Yehuda --from="Carl Lerche" | |
from: Carl Lerche | |
Hello Yehuda | |
By default, options are Strings, but you can specify an alternate type for any options: | |
class MyCLI < Thor | |
option :from | |
option :yell, :type => :boolean | |
desc "hello NAME", "say hello to NAME" | |
def hello(name) | |
output = [] | |
output << "from: #{options[:from]}" if options[:from] | |
output << "Hello #{name}" | |
output = output.join("\n") | |
puts options[:yell] ? output.upcase : output | |
end | |
end | |
Now, you can make the output from your task all caps: | |
$ ./cli hello --yell Yehuda --from "Carl Lerche" | |
FROM: CARL LERCHE | |
HELLO YEHUDA | |
$ ./cli hello Yehuda --from "Carl Lerche" --yell | |
FROM: CARL LERCHE | |
HELLO YEHUDA | |
You can also specify that a particular option is required. | |
class MyCLI < Thor | |
option :from, :required => true | |
option :yell, :type => :boolean | |
desc "hello NAME", "say hello to NAME" | |
def hello(name) | |
output = [] | |
output << "from: #{options[:from]}" if options[:from] | |
output << "Hello #{name}" | |
output = output.join("\n") | |
puts options[:yell] ? output.upcase : output | |
end | |
end | |
Now, if I try to run the command without the required option: | |
$ ./cli hello Yehuda | |
No value provided for required options '--from' | |
The full list of metadata you can provide for an option: | |
:desc: A description for the option. When printing out full usage for a command using cli help hello, this description will appear next to the option. | |
:banner: The short description of the option, printed out in the usage description. By default, this is the upcase version of the flag (from=FROM). | |
:required: Indicates that an option is required | |
:default: The default value of this option if it is not provided. An option cannot be both :required and have a :default. | |
:type: :string, :hash, :array, :numeric, or :boolean | |
:aliases: A list of aliases for this option. Typically, you would use aliases to provide short versions of the option. | |
You can use a shorthand to specify a number of options at once if you just want to specify the type of the options. You could rewrite the previous example as: | |
class MyCLI < Thor | |
desc "hello NAME", "say hello to NAME" | |
options :from => :required, :yell => :boolean | |
def hello(name) | |
output = [] | |
output << "from: #{options[:from]}" if options[:from] | |
output << "Hello #{name}" | |
output = output.join("\n") | |
puts options[:yell] ? output.upcase : output | |
end | |
end | |
In the shorthand, you can specify :required as the type, and the option will become a required :string. | |
Class Options | |
You can specify an option that should exist for the entire class by using class_option. Class options take exactly the same parameters as options for individual commands, but apply across all commands for a class. | |
The options hash in a given task will include any class options. | |
class MyCLI < Thor | |
class_option :verbose, :type => :boolean | |
desc "hello NAME", "say hello to NAME" | |
options :from => :required, :yell => :boolean | |
def hello(name) | |
puts "> saying hello" if options[:verbose] | |
output = [] | |
output << "from: #{options[:from]}" if options[:from] | |
output << "Hello #{name}" | |
output = output.join("\n") | |
puts options[:yell] ? output.upcase : output | |
puts "> done saying hello" if options[:verbose] | |
end | |
desc "goodbye", "say goodbye to the world" | |
def goodbye | |
puts "> saying goodbye" if options[:verbose] | |
puts "Goodbye World" | |
puts "> done saying goodbye" if options[:verbose] | |
end | |
end | |
Subcommands | |
As your CLI becomes more complex, you might want to be able to specify a command that points at its own set of subcommands. One example of this is the git remote command, which exposes add, rename, rm, prune, set-head, and so in. | |
In Thor, you can achieve this easily by creating a new Thor class to represent the subcommand, and point to it from the parent class. Let’s take a look at how you would implement git remote. The example is intentionally simplified. | |
module GitCLI | |
class Remote < Thor | |
desc "add <name> <url>", "Adds a remote named <name> for the repository at <url>" | |
long_desc <<-LONGDESC | |
Adds a remote named <name> for the repository at <url>. The command git fetch <name> can then be used to create and update | |
remote-tracking branches <name>/<branch>. | |
With -f option, git fetch <name> is run immediately after the remote information is set up. | |
With --tags option, git fetch <name> imports every tag from the remote repository. | |
With --no-tags option, git fetch <name> does not import tags from the remote repository. | |
With -t <branch> option, instead of the default glob refspec for the remote to track all branches under $GIT_DIR/remotes/<name>/, a | |
refspec to track only <branch> is created. You can give more than one -t <branch> to track multiple branches without grabbing all | |
branches. | |
With -m <master> option, $GIT_DIR/remotes/<name>/HEAD is set up to point at remote's <master> branch. See also the set-head | |
command. | |
When a fetch mirror is created with --mirror=fetch, the refs will not be stored in the refs/remotes/ namespace, but rather | |
everything in refs/ on the remote will be directly mirrored into refs/ in the local repository. This option only makes sense in | |
bare repositories, because a fetch would overwrite any local commits. | |
When a push mirror is created with --mirror=push, then git push will always behave as if --mirror was passed. | |
LONGDESC | |
option :t, :banner => "<branch>" | |
option :m, :banner => "<master>" | |
options :f => :boolean, :tags => :boolean, :mirror => :string | |
def add(name, url) | |
# implement git remote add | |
end | |
desc "rename <old> <new>", "Rename the remote named <old> to <new>" | |
def rename(old, new) | |
end | |
end | |
class Git < Thor | |
desc "fetch <repository> [<refspec>...]", "Download objects and refs from another repository" | |
options :all => :boolean, :multiple => :boolean | |
option :append, :type => :boolean, :aliases => :a | |
def fetch(respository, *refspec) | |
# implement git fetch here | |
end | |
desc "remote SUBCOMMAND ...ARGS", "manage set of tracked repositories" | |
subcommand "remote", Remote | |
end | |
end | |
You can access the options from the parent command in a subcommand using the parent_options accessor. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment