How to use or add subcommand feature with thor? - ruby

I am creating a CLI app using thor. Its going well but now I'm stuck with the sub-command feature.
There ain't anything in its github wiki and googled around, but nothing helpful.
So, can someone show or point me out how to implement the subcommand feature?

Check out: http://whatisthor.com/
From that site (edited a bit to save space and highlight subcommand usage):
module GitCLI
class Remote ", "Adds a remote named for the repository at "
option :t, :banner => ""
option :m, :banner => ""
options :f => :boolean, :tags => :boolean, :mirror => :string
def add(name, url)
# implement git remote add
end
desc "rename ", "Rename the remote named to "
def rename(old, new)
end
end
class Git [...]", "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 ### SUBCOMMAND USED HERE...
end
end
hth...

Try something like this (file test.rb):
#!/usr/bin/env ruby
require 'rubygems'
require 'thor'
require 'thor/group' # This is required -- it's not a bug, it's a feature!
class Bar < Thor
desc "baz", "Whatever"
def baz
puts "Hello from Bar"
end
end
class Foo < Thor
desc "go", "Do something"
def go
puts "Hello there!"
end
register Bar, :bar, "bar", "Do something else"
end
if __FILE__ == $0
Foo.start
end
This behaves as follows:
> test.rb
Tasks:
test.rb bar # Do something else
test.rb go # Do something
test.rb help [TASK] # Describe available tasks or one specific task
> test.rb go
Hello there!
> test.rb bar
Tasks:
test.rb baz # Whatever
test.rb help [COMMAND] # Describe subcommands or one specific subcommand
> test.rb bar baz
Hello from Bar
> test.rb baz
Could not find task "baz".
>
(This mostly works as expected, except the help information for "test.rb bar" isn't quite right, IMHO. I think it should say "test.rb bar baz ...", instead of "test.rb baz ...".)
Hope this helps!

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 )

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'

Writing a DSL like Thor gem in 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.

Resources