How to add --help, -h flag to Thor command? - ruby

I created a Thor class inside a Ruby executable, and it correctly shows the help when using ./foo help bar.
To make it more intuitive (for the sanity of my users), I'd also like to support ./foo bar --help and ./foo bar -h. When I do that, I get:
ERROR: "foo bar" was called with arguments ["--help"]
Usage: "foo bar"
I could manually do method_option :help, ... and handle it inside the bar method, but I hope there would be an easier way to do that (redirecting that command to ./foo help bar).
Does anyone know a simple and easy way to do this?

Assuming Foo is your class that inherits from Thor, you can call the following somewhere before Foo.start:
help_commands = Thor::HELP_MAPPINGS + ["help"]
# => ["-h", "-?", "--help", "-D"]
if help_commands.any? { |cmd| ARGV.include? cmd }
help_commands.each do |cmd|
if match = ARGV.delete(cmd)
ARGV.unshift match
end
end
end
Rather than going into Thor and patching some method to have different ARGV-parsing behavior, this kind of cheats by moving any help commands to the front of the list.

You can achive this with class_option. If you set a class option this option is availiable for every method in your cli and you can just check if it is set and then call the help method.
Something like this:
class CLI < Thor
class_option :help, type: :boolean
desc "foo PARAM", "foo"
def foo(param)
handle_help_option(:foo)
# your logic
end
def handle_help_option(method_name)
help(method_name) if options[:help]
end
end

Building on what #max-pleaner listed. This will support subcommands also:
help_commands = Thor::HELP_MAPPINGS + ["help"]
(help_commands & ARGV).each do |cmd|
match = ARGV.delete(cmd)
ARGV.size > 1 ? ARGV.insert(-2, match) : ARGV.unshift(match)
end

To complement the answer from max pleaner, the following handles subcommands, because subcommands help is broken if the trick is applied to them.
Also, I choose to overload Thor start command.
def self.start(*args)
if (Thor::HELP_MAPPINGS & ARGV).any? and subcommands.grep(/^#{ARGV[0]}/).empty?
Thor::HELP_MAPPINGS.each do |cmd|
if match = ARGV.delete(cmd)
ARGV.unshift match
end
end
end
super
end

Related

Spec Testing a Ruby CLI

I am trying to test the first ruby CLI i've written (n00b alert) and need some help. All my code is within 1 file, this includes a Class, OptionParser and some basic class execution methods. Here's an idea of what that looks like
The *rb. file
require 'optparse'
require 'fileutils'
class Foo
attr_accessor :arg, :opt
def initialize(p={})
#opt = p[:opt] || false
end
def do_something(arg)
#arg = arg
end
#more methods...
end
# Options
#options={}
#opt_parser = OptionParser.new do |opt|
opt.banner = "<{ FooBar }>"
opt.separator "------------"
opt.on("-o", "--opt", "An Option" do
#options[:opt] = true
end
end
#opt_parser.parse!
#CLI Execution
#foo = Foo.new(#options)
#foo.do_something(ARGV[0])
So here is the problem, i know would like to run some rspec tests rspec spec/ that i've wrote for the class, however the lines outside the class get executed of course and im left with an ARGV error.
What im looking for
Is there a better way to organize my code so i can test all the pieces, or how could i write a test to accommodate this file, Any and all suggestions would be greatly appreciated.
One posible solution is to wrap your option parsing code with a conditional that checks if the file is being run directly or loaded by some other file.
if __FILE__ == $0
# option parsing code
end
If you do that then all the code inside the if __FILE__ == $0 will not run with your test, but the rest of the code will run normally.

Commands in ruby terminal application

I have just written my first terminal application in ruby. I use OptionParser to parse the options and their arguments. However I want to create commands. For example:
git add .
In the above line, add is the command which cannot occur anywhere else than immediately after the application. How do I create these.
I will appreciate if anyone could point me in the right direction. However, please do not reference any gems such as Commander. I already know about these. I want to understand how it is done.
The OptionParser's parse! takes an array of arguments. By default, it will take ARGV, but you can override this behaviour like so:
Basic Approach
def build_option_parser(command)
# depending on `command`, build your parser
OptionParser.new do |opt|
# ...
end
end
args = ARGV
command = args.shift # pick and remove the first option, do some validation...
#options = build_option_parser(command).parse!(args) # parse the rest of it
Advanced Approach
Instead of a build_option_parser method with a huge case-statement, consider an OO approach:
class AddCommand
attr_reader :options
def initialize(args)
#options = {}
#parser = OptionParser.new #...
#parser.parse(args)
end
end
class MyOptionParser
def initialize(command, args)
#parser = {
'add' => AddCommand,
'...' => DotsCommand
}[command.to_s].new(args)
end
def options
#parser.options
end
end
Alternatives
For sure, there exist tons of Rubygems (well, 20 in that list), which will take care of your problem. I'd like to mention Thor which powers, e.g. the rails command line tool.
You can retrieve the command with Array#shift prior invoking OptionParser.

Test output to command line with RSpec

I want to do is run ruby sayhello.rb on the command line, then receive Hello from Rspec.
I've got that with this:
class Hello
def speak
puts 'Hello from RSpec'
end
end
hi = Hello.new #brings my object into existence
hi.speak
Now I want to write a test in rspec to check that the command line output is in fact "Hello from RSpec"
and not "I like Unix"
NOT WORKING. I currently have this in my sayhello_spec.rb file
require_relative 'sayhello.rb' #points to file so I can 'see' it
describe "sayhello.rb" do
it "should say 'Hello from Rspec' when ran" do
STDOUT.should_receive(:puts).with('Hello from RSpec')
end
end
Can someone point me in the right direction please?
Here's a pretty good way to do this. Copied from the hirb test_helper source:
def capture_stdout(&block)
original_stdout = $stdout
$stdout = fake = StringIO.new
begin
yield
ensure
$stdout = original_stdout
end
fake.string
end
Use like this:
output = capture_stdout { Hello.new.speak }
output.should == "Hello from RSpec\n"
The quietly command is probably what you want (cooked into ActiveSupport, see docs at api.rubyonrails.org). This snippet of RSpec code below shows how to ensure there is no output on stderr while simultaneously silencing stdout.
quietly do # silence everything
commands.each do |c|
content = capture(:stderr) { # capture anything sent to :stderr
MyGem::Cli.start(c)
}
expect(content).to be_empty, "#{c.inspect} had output on stderr: #{content}"
end
end
So you don't have to change your main ruby code I just found out you can do something like this:
def my_meth
print 'Entering my method'
p 5 * 50
puts 'Second inside message'
end
describe '#my_meth' do
it 'puts a 2nd message to the console' do
expect{my_meth}.to output(/Second inside message/).to_stdout
end
end
When checking for a desired output text I used it inside / / like a Regexp because after many many maaany tests and looking around, the STDOUT is everything that is outputted so I found it to be better to use Regex so you could check the whole STDOUT for the exact text that you want.
Like I put it, it works in the terminal just perfect.
//Just had to share this, it took me days to figure it out.
it "should say 'Hello from Rspec' when run" do
output = `ruby sayhello.rb`
output.should == 'Hello from RSpec'
end

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