Skip to content

Commit a52e7c7

Browse files
Allow multiple usages to be passed to commands
Sometimes, for mutually exclusive arguments, it is better to specify each of the command line versions on its own line. This is explained in http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html, bullet point 8, and it's a technique used by many CLI utils. For example, the following is the beginning of the man's man page: ``` NAME man - an interface to the on-line reference manuals SYNOPSIS man [-C file] [-d] [-D] [--warnings[=warnings]] [-R encoding] [-L locale] [-m system[,...]] [-M path] [-S list] [-e extension] [-i|-I] [--regex|--wildcard] [--names-only] [-a] [-u] [--no-subpages] [-P pager] [-r prompt] [-7] [-E encoding] [--no-hyphenation] [--no-justification] [-p string] [-t] [-T[device]] [-H[browser]] [-X[dpi]] [-Z] [[section] page[.section] ...] ... man -k [apropos options] regexp ... man -K [-w|-W] [-S list] [-i|-I] [--regex] [section] term ... man -f [whatis options] page ... man -l [-C file] [-d] [-D] [--warnings[=warnings]] [-R encoding] [-L locale] [-P pager] [-r prompt] [-7] [-E encoding] [-p string] [-t] [-T[device]] [-H[browser]] [-X[dpi]] [-Z] file ... man -w|-W [-C file] [-d] [-D] page ... man -c [-C file] [-d] [-D] page ... man [-?V] ``` This commit implements support for that, by passing an array of "usages" instead of a single one.
1 parent 5799866 commit a52e7c7

File tree

6 files changed

+47
-10
lines changed

6 files changed

+47
-10
lines changed

lib/thor.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def command_help(shell, command_name)
170170
handle_no_command_error(meth) unless command
171171

172172
shell.say "Usage:"
173-
shell.say " #{banner(command)}"
173+
shell.say " #{banner(command).split("\n").join("\n ")}"
174174
shell.say
175175
class_options_help(shell, nil => command.options.values)
176176
if command.long_description
@@ -393,7 +393,9 @@ def dispatch(meth, given_args, given_opts, config) #:nodoc: # rubocop:disable Me
393393
# the namespace should be displayed as arguments.
394394
#
395395
def banner(command, namespace = nil, subcommand = false)
396-
"#{basename} #{command.formatted_usage(self, $thor_runner, subcommand)}"
396+
command.formatted_usage(self, $thor_runner, subcommand).split("\n").map do |formatted_usage|
397+
"#{basename} #{formatted_usage}"
398+
end.join("\n")
397399
end
398400

399401
def baseclass #:nodoc:

lib/thor/base.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ def handle_argument_error(command, error, args, arity) #:nodoc:
502502
msg = "ERROR: \"#{basename} #{name}\" was called with ".dup
503503
msg << "no arguments" if args.empty?
504504
msg << "arguments " << args.inspect unless args.empty?
505-
msg << "\nUsage: #{banner(command).inspect}"
505+
msg << "\nUsage: \"#{banner(command).split("\n").join("\"\n \"")}\""
506506
raise InvocationError, msg
507507
end
508508

lib/thor/command.rb

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,17 @@ def formatted_usage(klass, namespace = true, subcommand = false)
4949

5050
formatted ||= "".dup
5151

52-
formatted << required_arguments_for(klass, specific_usage)
52+
Array(usage).map do |specific_usage|
53+
formatted_specific_usage = formatted
5354

54-
# Add required options
55-
formatted << " #{required_options}"
55+
formatted_specific_usage += required_arguments_for(klass, specific_usage)
5656

57-
# Strip and go!
58-
formatted.strip
57+
# Add required options
58+
formatted_specific_usage += " #{required_options}"
59+
60+
# Strip and go!
61+
formatted_specific_usage.strip
62+
end.join("\n")
5963
end
6064

6165
protected

spec/command_spec.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
require "helper"
22

33
describe Thor::Command do
4-
def command(options = {})
4+
def command(options = {}, usage = "can_has")
55
options.each do |key, value|
66
options[key] = Thor::Option.parse(key, value)
77
end
88

9-
@command ||= Thor::Command.new(:can_has, "I can has cheezburger", "I can has cheezburger\nLots and lots of it", "can_has", options)
9+
@command ||= Thor::Command.new(:can_has, "I can has cheezburger", "I can has cheezburger\nLots and lots of it", usage, options)
1010
end
1111

1212
describe "#formatted_usage" do
@@ -30,6 +30,11 @@ def command(options = {})
3030
object = Struct.new(:namespace, :arguments).new("foo", [Thor::Argument.new(:bar, options)])
3131
expect(command(:foo => :required).formatted_usage(object)).to eq("foo:can_has BAR --foo=FOO")
3232
end
33+
34+
it "allows multiple usages" do
35+
object = Struct.new(:namespace, :arguments).new("foo", [])
36+
expect(command({ :bar => :required }, ["can_has FOO", "can_has BAR"]).formatted_usage(object, false)).to eq("can_has FOO --bar=BAR\ncan_has BAR --bar=BAR")
37+
end
3338
end
3439

3540
describe "#dynamic" do

spec/fixtures/script.thor

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ END
5151
[bar, options]
5252
end
5353

54+
method_option :all, :desc => "Do bazing for all the things"
55+
desc ["baz THING", "baz --all"], "super cool"
56+
def baz(thing = nil)
57+
raise if thing.nil? && !options.include?(:all)
58+
end
59+
5460
desc "example_default_command", "example!"
5561
method_options :with => :string
5662
def example_default_command
@@ -215,6 +221,10 @@ module Scripts
215221
desc "optional_arg [ARG]", "takes an optional arg"
216222
def optional_arg(arg='default')
217223
end
224+
225+
desc ["multiple_usages ARG --foo", "multiple_usages ARG --bar"], "takes mutually exclusive combinations of args and flags"
226+
def multiple_usages(arg)
227+
end
218228
end
219229
end
220230

spec/thor_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,9 @@ def boring(*args)
440440
Usage: "thor scripts:arities:two_args ARG1 ARG2"'
441441
arity_asserter.call %w(optional_arg one two), 'ERROR: "thor optional_arg" was called with arguments ["one", "two"]
442442
Usage: "thor scripts:arities:optional_arg [ARG]"'
443+
arity_asserter.call %w(multiple_usages), 'ERROR: "thor multiple_usages" was called with no arguments
444+
Usage: "thor scripts:arities:multiple_usages ARG --foo"
445+
"thor scripts:arities:multiple_usages ARG --bar"'
443446
end
444447

445448
it "raises an error if the invoked command does not exist" do
@@ -552,6 +555,19 @@ def shell
552555
END
553556
end
554557

558+
it "provides full help info when talking about a specific command with multiple usages" do
559+
expect(capture(:stdout) { MyScript.command_help(shell, "baz") }).to eq(<<-END)
560+
Usage:
561+
thor my_script:baz THING
562+
thor my_script:baz --all
563+
564+
Options:
565+
[--all=ALL] # Do bazing for all the things
566+
567+
super cool
568+
END
569+
end
570+
555571
it "raises an error if the command can't be found" do
556572
expect do
557573
MyScript.command_help(shell, "unknown")

0 commit comments

Comments
 (0)