Writing a DSL like Thor gem in Ruby? - ruby

I'm trying to figure out how the Thor gem creates a DSL like this (first example from their README)
class App < Thor # [1]
map "-L" => :list # [2]
desc "install APP_NAME", "install one of the available apps" # [3]
method_options :force => :boolean, :alias => :string # [4]
def install(name)
user_alias = options[:alias]
if options.force?
# do something
end
# other code
end
desc "list [SEARCH]", "list all of the available apps, limited by SEARCH"
def list(search="")
# list everything
end
end
Specifically, how does it know which method to map the desc and method_options call to?

desc is pretty easy to implement, the trick is to use Module.method_added:
class DescMethods
def self.desc(m)
#last_message = m
end
def self.method_added(m)
puts "#{m} described as #{#last_message}"
end
end
any class that inherits from DescMethods will have a desc method like Thor. For each method a message will be printed with the method name and description. For example:
class Test < DescMethods
desc 'Hello world'
def test
end
end
when this class is defined the string "test described as Hello world" will be printed.

Related

Dynamically loading Thor options for a Ruby Gem

While trying to develop a simple gem to learn the process, I happened to stumble on this issue: Thor DSL takes in options to a command using the syntax: option :some_option, :type => :boolean, just prior to the method definition.
I am trying to have a dynamic set of options loaded from a file. I do this file read operation in the constructor, but it seems the option keyword for the Thor class is getting processed before the initialize method.
Any ideas to resolve this? Also it would be great if someone can explain how the option keyword works? I mean is option a method call? I don't get the design. (This is the first DSL I am trying out and am a total newbie to Ruby Gems)
#!/usr/bin/env ruby
require 'thor'
require 'yaml'
require 'tinynews'
class TinyNewsCLI < Thor
attr_reader :sources
#sources = {}
def initialize *args
super
f = File.open( "sources.yml", "r" ).read
#sources = YAML::load( f )
end
desc "list", "Lists the available news feeds."
def list
puts "List of news feed sources: "
#sources.each do |symbol, source|
puts "- #{source[:title]}"
end
end
desc "show --source SOURCE", "Show news from SOURCE feed"
option :source, :required => true
def show
if options[:source]
TinyNews.print_to_cli( options[:source].to_sym )
end
end
desc "tinynews --NEWS_SOURCE", "Show news for NEWS_SOURCE"
#sources.keys.each do |source_symbol| # ERROR: States that #sources.keys is nil
#[:hindu, :cnn, :bbc].each do |source_symbol| # I expected the above to work like this
option source_symbol, :type => :boolean
end
def news_from_option
p #sources.keys
TinyNews.print_to_cli( options.keys.last.to_sym )
end
default_task :news_from_option
end
TinyNewsCLI.start( ARGV )
After a bit of tweaking, I think I ended upon a solution that doesn't look too bad. But not sure placing code in module like that is a good practice. But anyways:
#!/usr/bin/env ruby
require 'thor'
require 'yaml'
require 'tinynews'
module TinyNews
# ***** SOLUTION *******
f = File.open( "../sources.yml", "r" ).read
SOURCES = YAML::load( f )
class TinyNewsCLI < Thor
default_task :news_from_source
desc "list", "Lists the available news feeds."
def list
puts "List of news feed sources: "
SOURCES.each do |symbol, source|
puts "- #{source[:title]}"
end
end
desc "--source NEWS_SOURCE", "Show news for NEWS_SOURCE"
option :source, :required => true, :aliases => :s
def news_from_source
TinyNews.print_to_cli( options[:source].to_sym )
end
end
end
TinyNews::TinyNewsCLI.start( ARGV )

Accessing command line arguments from outside of Thor class

Pretty new to Ruby and OO. Studying the text books, and all the articles that google found on Thor.
I have Thor working to capture multiple command line arguments and options. I'd like to do the rest of my programming from outside the Cli < Thor class though, and am having trouble accessing the command line arguments from outside the Cli class.
Questions:
Q1. Can the Cli < Thor class be treated like any other ruby class, or does inheriting from Thor, or the "Cli.start" command, cripple certain functionality of the Cli class versus not using Thor? Asking because I may simply not know how to access an instance variable from outside a class that doesn't use the initialize method. Thor will not let me use the initialize method to bring in the command line variables, probably because initialize is a reserved method name in ruby.
Q2. How can I access the command line argument variables a and b from outside the Thor class?
Here's my code
#!/usr/bin/env ruby
require 'thor'
class Cli < Thor
attr_reader :a, :b
method_option :add, :type => :string, :desc => 'add servers'
method_option :prod, :type => :string, :desc => 'production stack'
desc "tier <stack folder name> <app | web>", "creates an app or web server tier for the stack"
def tier(a,b)
#a = a
#b = b
puts a
puts b
end
end
Cli.start
arguments = Cli.new
puts "the first argument is #{arguments.a}"
Here's the result. Close (maybe). No errors, but arguments.a is nil.
$ ./create.rb tier a b
a
b
the first argument is
--
puts arguments.tier.a
threw the error:
./create.rb:11:in `tier': wrong number of arguments (0 for 2) (ArgumentError)
from ./create.rb:23:in `<main>'
The following works without Thor and using an initialize method and attr_reader, straight out of the text books. Can't figure out how to access the variables from a non-initialize method though.
#!/usr/bin/env ruby
class Cli
attr_reader :a, :b
def initialize(a,b)
#a = a
#b = b
end
end
arguments = Cli.new("a","b")
puts arguments.a
outupt:
$ ./create_wo_thor.rb
a
Instantiating your Cli class doesn't make much sense; that's not how Thor is designed.
You have a few options to access internal data from outside the class. If there are only a few variables that you want access to, storing them as class variables and making them available through getters (and setters, if you need them) would work:
require 'thor'
class Cli < Thor
method_option :add, :type => :string, :desc => 'add servers'
method_option :prod, :type => :string, :desc => 'production stack'
desc "tier <stack folder name> <app | web>", "creates an app or web server tier for the stack"
def tier(a,b)
##a = a
##b = b
puts a
puts b
end
def self.get_a
##a
end
def self.get_b
##b
end
end
Cli.start
puts "the first argument is #{Cli.get_a}"
This works as you hope:
$ ./thor.rb tier a b
a
b
the first argument is a
I prefer the following, using a global Hash:
require 'thor'
$args = {}
class Cli < Thor
method_option :add, :type => :string, :desc => 'add servers'
method_option :prod, :type => :string, :desc => 'production stack'
desc "tier <stack folder name> <app | web>", "creates an app or web server tier for the stack"
def tier(a,b)
$args[:a] = a
$args[:b] = b
puts a
puts b
end
end
Cli.start
puts "the first argument is #{$args[:a]}"
Last, I'd be remiss not to point out that all the command line arguments are available in the global ARGV, anyway:
require 'thor'
class Cli < Thor
method_option :add, :type => :string, :desc => 'add servers'
method_option :prod, :type => :string, :desc => 'production stack'
desc "tier <stack folder name> <app | web>", "creates an app or web server tier for the stack"
def tier(a,b)
puts a
puts b
end
end
Cli.start
puts "The first argugment is #{ARGV[1]}"
What would be best depends on how you intend to use it. If you just want raw access to the command line arguments, ARGV is the way to go. If you want to access certain pieces after Thor has done some processing for you, one of the first two might be more helpful.
Here's my code with all three options included for accessing the command line arguments from outside the Cli < Thor class. Compliments to Darshan.
#!/usr/bin/env ruby
require 'thor'
$args = {}
class Cli < Thor
attr_reader :a, :b
method_option :add, :type => :string, :desc => 'add servers'
method_option :prod, :type => :string, :desc => 'production stack'
desc "tier <stack folder name> <app | web>", "creates an app or web server tier for the stack"
def tier(a,b)
# store a and b in a global hash
$args[:a] = a
$args[:b] = b
# store a and b in class variables
##a = a
##b = b
end
# getter methods, for access of the class variables from outside the class
def self.get_a
##a
end
def self.get_b
##b
end
end
Cli.start
# three ways now to access the command line arguments from outside the Cli < Thor class
puts "the first argument, from $args[:a], is #{$args[:a]}"
puts "the second argument, from Cli.get_b, is #{Cli.get_b}"
puts "the first argument, from ARGV[1], is #{ARGV[1]}"
Results:
$ ./create.rb tier a b
the first argument, from $args[:a], is a
the second argument, from Cli.get_b, is b
the first argument, from ARGV[1], is a

How do I get Thor to display options?

This is my Ruby code:
require 'thor'
require 'thor/group'
module CLI
class Greet < Thor
desc 'hi', 'Say hi!'
method_option :name, :type => :string, :description => 'Name to greet', :default => 'there'
def hi
puts "Hi, #{options[:name]}!"
end
desc 'bye', 'say bye!'
def bye
puts "Bye!"
end
end
class Root < Thor
register CLI::Greet, 'greet', 'greet [COMMAND]', 'Greet with a command'
end
end
CLI::Root.start
This is the output:
C:\temp>ruby greet.rb help greet
Usage:
greet.rb greet [COMMAND]
Greet with a command
How do I get that to look something like this?
C:\temp>ruby greet.rb help greet
Usage:
greet.rb greet [COMMAND]
--name Name to greet
Greet with a command
You've got two things going on here. First, you've assigned --name to a method, not to the entire CLI::Greet class. So if you use the command:
ruby greet.rb greet help hi
you get
Usage:
greet.rb hi
Options:
[--name=NAME]
# Default: there
Say hi!
Which, yes, is wrong-- it doesn't have the subcommand in the help. There's a bug filed for this in Thor. It, however, is showing the method option properly.
What it seems like you're looking for, however, is a class method. This is a method defined for the entire CLI::Greet class, not just for the #hi method.
You'd do this as such:
require 'thor'
require 'thor/group'
module CLI
class Greet < Thor
desc 'hi', 'Say hi!'
class_option :number, :type => :string, :description => 'Number to call', :default => '555-1212'
method_option :name, :type => :string, :description => 'Name to greet', :default => 'there'
def hi
puts "Hi, #{options[:name]}! Call me at #{options[:number]}"
end
desc 'bye', 'say bye!'
def bye
puts "Bye! Call me at #{options[:number]}"
end
end
class Root < Thor
register CLI::Greet, 'greet', 'greet [COMMAND]', 'Greet with a command'
tasks["greet"].options = CLI::Greet.class_options
end
end
CLI::Root.start
With this, ruby greet.rb help greet returns
Usage:
greet.rb greet [COMMAND]
Options:
[--number=NUMBER]
# Default: 555-1212
Greet with a command
Note there is still a hack needed here: the tasks["greet"].options = CLI::Greet.class_options line in CLI::Root. There's a bug filed for this in Thor, too.

Thor : How can I get my Thor task to display help when I have defined an argument?

I'm converting a small project to use Thor, and being rather absent-minded I want to document the available tasks using built-in help.
But if I define a task with an argument the task-level help reverts to help for the class - which means no task description or details of the expected parameters are displayed.
I want to be be able to have a method I can call with an argument and not a parameter so it can be used like this
$ thor broke:foo hello
in a.thor broke:foo arg1=hello
I've boiled the problem down to the following thorfile which works as I want except the broken help output. I've stripped out any other parameters as they make no difference to the problem.
The first task ok:foo will display help normally, the second task broke:foo is rather less helpful :
class Ok < Thor
desc "foo", "ok test2"
def foo
puts "in a.thor ok:foo\n"
end
end
class Broke < Thor
argument :arg1, :type=>:string, :desc => "arg1"
desc "foo", "broke test1"
def foo
puts "in a.thor broke:foo arg1=#{self.arg1}\n"
end
end
Requesting help for the ok:foo task method gives :
$ thor help ok:foo
Usage:
thor ok:foo
ok test
Requesting help for the broke:foo task is rather less helpful :
$ thor help broke:foo
Tasks:
thor broke:foo ARG1 # broke test1
thor broke:help ARG1 [TASK] # Describe available tasks or one specific task
How can I define an argument and get the proper task help displayed ?
You have small mistake in your thorfile.
You have argument instead of method_option.
Here correct version:
class Ok < Thor
desc "foo", "ok test2"
def foo
puts "in a.thor ok:foo\n"
end
end
class Broke < Thor
method_option :arg1, :type=>:string, :desc => "arg1"
desc "foo", "broke test1"
def foo
puts "in a.thor broke:foo arg1=#{self.arg1}\n"
end
end
Wiki about method_option on github.com

Namespacing thor commands in a standalone ruby executable

When calling thor commands on the command line, the methods are namespaced by their module/class structure, e.g.
class App < Thor
desc 'hello', 'prints hello'
def hello
puts 'hello'
end
end
would be run with the command
thor app:hello
However, if you make that self executable by putting
App.start
at the bottom you can run the command like:
app hello
Is there any way to namespace those commands? So that you could call, for example
app say:hello
app say:goodbye
Another way of doing this is to use register:
class CLI < Thor
register(SubTask, 'sub', 'sub <command>', 'Description.')
end
class SubTask < Thor
desc "bar", "..."
def bar()
# ...
end
end
CLI.start
Now - assuming your executable is called foo - you can call:
$ foo sub bar
In the current thor version (0.15.0.rc2) there is a bug though, which causes the help texts to skip the namespace of sub commands:
$ foo sub
Tasks:
foo help [COMMAND] # Describe subcommands or one specific subcommand
foo bar #
You can fix that by overriding self.banner and explicitly setting the namespace.
class SubTask < Thor
namespace :sub
def bar ...
def self.banner(task, namespace = true, subcommand = false)
"#{basename} #{task.formatted_usage(self, true, subcommand)}"
end
end
The second parameter of formatted_usage is the only difference to the original implemtation of banner. You can also do this once and have other sub command thor classes inherit from SubTask. Now you get:
$ foo sub
Tasks:
foo sub help [COMMAND] # Describe subcommands or one specific subcommand
foo sub bar #
Hope that helps.
This is one way with App as the default namespace (quite hacky though):
#!/usr/bin/env ruby
require "rubygems"
require "thor"
class Say < Thor
# ./app say:hello
desc 'hello', 'prints hello'
def hello
puts 'hello'
end
end
class App < Thor
# ./app nothing
desc 'nothing', 'does nothing'
def nothing
puts 'doing nothing'
end
end
begin
parts = ARGV[0].split(':')
namespace = Kernel.const_get(parts[0].capitalize)
parts.shift
ARGV[0] = parts.join
namespace.start
rescue
App.start
end
Or, also not ideal:
define_method 'say:hello'

Resources