jls-clamp 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/.travis.yml +5 -0
- data/Gemfile +9 -0
- data/README.markdown +274 -0
- data/Rakefile +12 -0
- data/clamp.gemspec +24 -0
- data/examples/flipflop +31 -0
- data/examples/fubar +23 -0
- data/examples/gitdown +61 -0
- data/examples/speak +33 -0
- data/lib/clamp/attribute.rb +40 -0
- data/lib/clamp/attribute_declaration.rb +40 -0
- data/lib/clamp/command.rb +142 -0
- data/lib/clamp/errors.rb +26 -0
- data/lib/clamp/help.rb +100 -0
- data/lib/clamp/option/declaration.rb +57 -0
- data/lib/clamp/option/parsing.rb +59 -0
- data/lib/clamp/option.rb +80 -0
- data/lib/clamp/parameter/declaration.rb +28 -0
- data/lib/clamp/parameter/parsing.rb +24 -0
- data/lib/clamp/parameter.rb +80 -0
- data/lib/clamp/subcommand/declaration.rb +44 -0
- data/lib/clamp/subcommand/parsing.rb +41 -0
- data/lib/clamp/subcommand.rb +23 -0
- data/lib/clamp/version.rb +3 -0
- data/lib/clamp.rb +3 -0
- data/spec/clamp/command_group_spec.rb +267 -0
- data/spec/clamp/command_spec.rb +766 -0
- data/spec/clamp/option_module_spec.rb +37 -0
- data/spec/clamp/option_spec.rb +149 -0
- data/spec/clamp/parameter_spec.rb +201 -0
- data/spec/spec_helper.rb +45 -0
- metadata +84 -0
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'clamp/errors'
|
2
|
+
require 'clamp/help'
|
3
|
+
require 'clamp/option/declaration'
|
4
|
+
require 'clamp/option/parsing'
|
5
|
+
require 'clamp/parameter/declaration'
|
6
|
+
require 'clamp/parameter/parsing'
|
7
|
+
require 'clamp/subcommand/declaration'
|
8
|
+
require 'clamp/subcommand/parsing'
|
9
|
+
|
10
|
+
module Clamp
|
11
|
+
|
12
|
+
# {Command} models a shell command. Each command invocation is a new object.
|
13
|
+
# Command options and parameters are represented as attributes
|
14
|
+
# (see {Command::Declaration}).
|
15
|
+
#
|
16
|
+
# The main entry-point is {#run}, which uses {#parse} to populate attributes based
|
17
|
+
# on an array of command-line arguments, then calls {#execute} (which you provide)
|
18
|
+
# to make it go.
|
19
|
+
#
|
20
|
+
class Command
|
21
|
+
|
22
|
+
# Create a command execution.
|
23
|
+
#
|
24
|
+
# @param [String] invocation_path the path used to invoke the command
|
25
|
+
# @param [Hash] context additional data the command may need
|
26
|
+
#
|
27
|
+
def initialize(invocation_path, context = {})
|
28
|
+
@invocation_path = invocation_path
|
29
|
+
@context = context
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [String] the path used to invoke this command
|
33
|
+
#
|
34
|
+
attr_reader :invocation_path
|
35
|
+
|
36
|
+
# @return [Array<String>] unconsumed command-line arguments
|
37
|
+
#
|
38
|
+
def remaining_arguments
|
39
|
+
@remaining_arguments
|
40
|
+
end
|
41
|
+
|
42
|
+
# Parse command-line arguments.
|
43
|
+
#
|
44
|
+
# @param [Array<String>] arguments command-line arguments
|
45
|
+
# @return [Array<String>] unconsumed arguments
|
46
|
+
#
|
47
|
+
def parse(arguments)
|
48
|
+
@remaining_arguments = arguments.dup
|
49
|
+
parse_environment
|
50
|
+
parse_options
|
51
|
+
parse_parameters
|
52
|
+
parse_subcommand
|
53
|
+
handle_remaining_arguments
|
54
|
+
end
|
55
|
+
|
56
|
+
# Run the command, with the specified arguments.
|
57
|
+
#
|
58
|
+
# This calls {#parse} to process the command-line arguments,
|
59
|
+
# then delegates to {#execute}.
|
60
|
+
#
|
61
|
+
# @param [Array<String>] arguments command-line arguments
|
62
|
+
#
|
63
|
+
def run(arguments)
|
64
|
+
parse(arguments)
|
65
|
+
execute
|
66
|
+
end
|
67
|
+
|
68
|
+
# Execute the command (assuming that all options/parameters have been set).
|
69
|
+
#
|
70
|
+
# This method is designed to be overridden in sub-classes.
|
71
|
+
#
|
72
|
+
def execute
|
73
|
+
if @subcommand
|
74
|
+
@subcommand.execute
|
75
|
+
else
|
76
|
+
raise "you need to define #execute"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [String] usage documentation for this command
|
81
|
+
#
|
82
|
+
def help
|
83
|
+
self.class.help(invocation_path)
|
84
|
+
end
|
85
|
+
|
86
|
+
include Clamp::Option::Parsing
|
87
|
+
include Clamp::Parameter::Parsing
|
88
|
+
include Clamp::Subcommand::Parsing
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
attr_accessor :context
|
93
|
+
|
94
|
+
def handle_remaining_arguments
|
95
|
+
unless remaining_arguments.empty?
|
96
|
+
signal_usage_error "too many arguments"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def signal_usage_error(message)
|
103
|
+
e = UsageError.new(message, self)
|
104
|
+
e.set_backtrace(caller)
|
105
|
+
raise e
|
106
|
+
end
|
107
|
+
|
108
|
+
def request_help
|
109
|
+
raise HelpWanted, self
|
110
|
+
end
|
111
|
+
|
112
|
+
class << self
|
113
|
+
|
114
|
+
include Clamp::Option::Declaration
|
115
|
+
include Clamp::Parameter::Declaration
|
116
|
+
include Clamp::Subcommand::Declaration
|
117
|
+
include Help
|
118
|
+
|
119
|
+
# Create an instance of this command class, and run it.
|
120
|
+
#
|
121
|
+
# @param [String] invocation_path the path used to invoke the command
|
122
|
+
# @param [Array<String>] arguments command-line arguments
|
123
|
+
# @param [Hash] context additional data the command may need
|
124
|
+
#
|
125
|
+
def run(invocation_path = File.basename($0), arguments = ARGV, context = {})
|
126
|
+
begin
|
127
|
+
new(invocation_path, context).run(arguments)
|
128
|
+
rescue Clamp::UsageError => e
|
129
|
+
$stderr.puts "ERROR: #{e.message}"
|
130
|
+
$stderr.puts ""
|
131
|
+
$stderr.puts "See: '#{e.command.invocation_path} --help'"
|
132
|
+
exit(1)
|
133
|
+
rescue Clamp::HelpWanted => e
|
134
|
+
puts e.command.help
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
data/lib/clamp/errors.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Clamp
|
2
|
+
|
3
|
+
class Error < StandardError
|
4
|
+
|
5
|
+
def initialize(message, command)
|
6
|
+
super(message)
|
7
|
+
@command = command
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :command
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
# raise to signal incorrect command usage
|
15
|
+
class UsageError < Error; end
|
16
|
+
|
17
|
+
# raise to request usage help
|
18
|
+
class HelpWanted < Error
|
19
|
+
|
20
|
+
def initialize(command)
|
21
|
+
super("I need help", command)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
data/lib/clamp/help.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module Clamp
|
4
|
+
|
5
|
+
module Help
|
6
|
+
|
7
|
+
def usage(usage)
|
8
|
+
@declared_usage_descriptions ||= []
|
9
|
+
@declared_usage_descriptions << usage
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :declared_usage_descriptions
|
13
|
+
|
14
|
+
def description=(description)
|
15
|
+
@description = description.dup
|
16
|
+
if @description =~ /^\A\n*( +)/
|
17
|
+
indent = $1
|
18
|
+
@description.gsub!(/^#{indent}/, '')
|
19
|
+
end
|
20
|
+
@description.strip!
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :description
|
24
|
+
|
25
|
+
def derived_usage_description
|
26
|
+
parts = ["[OPTIONS]"]
|
27
|
+
parts += parameters.map { |a| a.name }
|
28
|
+
if has_subcommands?
|
29
|
+
parts << "SUBCOMMAND"
|
30
|
+
parts << "[ARGS] ..."
|
31
|
+
end
|
32
|
+
parts.join(" ")
|
33
|
+
end
|
34
|
+
|
35
|
+
def usage_descriptions
|
36
|
+
declared_usage_descriptions || [derived_usage_description]
|
37
|
+
end
|
38
|
+
|
39
|
+
def help(invocation_path)
|
40
|
+
help = Builder.new
|
41
|
+
help.add_usage(invocation_path, usage_descriptions)
|
42
|
+
help.add_description(description)
|
43
|
+
if has_parameters?
|
44
|
+
help.add_list("Parameters", parameters)
|
45
|
+
end
|
46
|
+
if has_subcommands?
|
47
|
+
help.add_list("Subcommands", recognised_subcommands)
|
48
|
+
end
|
49
|
+
help.add_list("Options", recognised_options)
|
50
|
+
help.string
|
51
|
+
end
|
52
|
+
|
53
|
+
class Builder
|
54
|
+
|
55
|
+
def initialize
|
56
|
+
@out = StringIO.new
|
57
|
+
end
|
58
|
+
|
59
|
+
def string
|
60
|
+
@out.string
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_usage(invocation_path, usage_descriptions)
|
64
|
+
puts "Usage:"
|
65
|
+
usage_descriptions.each do |usage|
|
66
|
+
puts " #{invocation_path} #{usage}".rstrip
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_description(description)
|
71
|
+
if description
|
72
|
+
puts ""
|
73
|
+
puts description.gsub(/^/, " ")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
DETAIL_FORMAT = " %-29s %s"
|
78
|
+
|
79
|
+
def add_list(heading, items)
|
80
|
+
puts "\n#{heading}:"
|
81
|
+
items.each do |item|
|
82
|
+
label, description = item.help
|
83
|
+
description.each_line do |line|
|
84
|
+
puts DETAIL_FORMAT % [label, line]
|
85
|
+
label = ''
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def puts(*args)
|
93
|
+
@out.puts(*args)
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'clamp/attribute_declaration'
|
2
|
+
require 'clamp/option'
|
3
|
+
|
4
|
+
module Clamp
|
5
|
+
class Option
|
6
|
+
|
7
|
+
module Declaration
|
8
|
+
|
9
|
+
include Clamp::AttributeDeclaration
|
10
|
+
|
11
|
+
def option(switches, type, description, opts = {}, &block)
|
12
|
+
option = Clamp::Option.new(switches, type, description, opts)
|
13
|
+
declared_options << option
|
14
|
+
define_accessors_for(option, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_option(switch)
|
18
|
+
recognised_options.find { |o| o.handles?(switch) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def declared_options
|
22
|
+
@declared_options ||= []
|
23
|
+
end
|
24
|
+
|
25
|
+
def recognised_options
|
26
|
+
declare_implicit_options
|
27
|
+
effective_options
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def declare_implicit_options
|
33
|
+
return nil if defined?(@implicit_options_declared)
|
34
|
+
unless effective_options.find { |o| o.handles?("--help") }
|
35
|
+
help_switches = ["--help"]
|
36
|
+
help_switches.unshift("-h") unless effective_options.find { |o| o.handles?("-h") }
|
37
|
+
option help_switches, :flag, "print help" do
|
38
|
+
request_help
|
39
|
+
end
|
40
|
+
end
|
41
|
+
@implicit_options_declared = true
|
42
|
+
end
|
43
|
+
|
44
|
+
def effective_options
|
45
|
+
ancestors.inject([]) do |options, ancestor|
|
46
|
+
if ancestor.kind_of?(Clamp::Option::Declaration)
|
47
|
+
options + ancestor.declared_options
|
48
|
+
else
|
49
|
+
options
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Clamp
|
2
|
+
class Option
|
3
|
+
|
4
|
+
module Parsing
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
def parse_options
|
9
|
+
while remaining_arguments.first =~ /^-/
|
10
|
+
|
11
|
+
switch = remaining_arguments.shift
|
12
|
+
break if switch == "--"
|
13
|
+
|
14
|
+
case switch
|
15
|
+
when /^(-\w)(.+)$/ # combined short options
|
16
|
+
switch = $1
|
17
|
+
remaining_arguments.unshift("-#{$2}")
|
18
|
+
when /^(--[^=]+)=(.*)/
|
19
|
+
switch = $1
|
20
|
+
remaining_arguments.unshift($2)
|
21
|
+
end
|
22
|
+
|
23
|
+
option = find_option(switch)
|
24
|
+
value = option.extract_value(switch, remaining_arguments)
|
25
|
+
|
26
|
+
begin
|
27
|
+
send("#{option.attribute_name}=", value)
|
28
|
+
rescue ArgumentError => e
|
29
|
+
signal_usage_error "option '#{switch}': #{e.message}"
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_environment
|
36
|
+
self.class.recognised_options.each do |option|
|
37
|
+
next if option.env_var.nil?
|
38
|
+
next unless ENV.has_key?(option.env_var)
|
39
|
+
value = ENV[option.env_var]
|
40
|
+
if option.flag?
|
41
|
+
# Set true if the env var is "1" false otherwise.
|
42
|
+
send("#{option.attribute_name}=", value == "1")
|
43
|
+
else
|
44
|
+
send("#{option.attribute_name}=", value)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def find_option(switch)
|
52
|
+
self.class.find_option(switch) ||
|
53
|
+
signal_usage_error("Unrecognised option '#{switch}'")
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
data/lib/clamp/option.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'clamp/attribute'
|
2
|
+
|
3
|
+
module Clamp
|
4
|
+
|
5
|
+
class Option < Attribute
|
6
|
+
|
7
|
+
def initialize(switches, type, description, options = {})
|
8
|
+
@switches = Array(switches)
|
9
|
+
@type = type
|
10
|
+
@description = description
|
11
|
+
if options.has_key?(:attribute_name)
|
12
|
+
@attribute_name = options[:attribute_name].to_s
|
13
|
+
end
|
14
|
+
if options.has_key?(:default)
|
15
|
+
@default_value = options[:default]
|
16
|
+
end
|
17
|
+
if options.has_key?(:env)
|
18
|
+
@env_var = options[:env]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :switches, :type
|
23
|
+
|
24
|
+
def attribute_name
|
25
|
+
@attribute_name ||= long_switch.sub(/^--(\[no-\])?/, '').tr('-', '_')
|
26
|
+
end
|
27
|
+
|
28
|
+
def long_switch
|
29
|
+
switches.find { |switch| switch =~ /^--/ }
|
30
|
+
end
|
31
|
+
|
32
|
+
def handles?(switch)
|
33
|
+
recognised_switches.member?(switch)
|
34
|
+
end
|
35
|
+
|
36
|
+
def flag?
|
37
|
+
@type == :flag
|
38
|
+
end
|
39
|
+
|
40
|
+
def flag_value(switch)
|
41
|
+
!(switch =~ /^--no-(.*)/ && switches.member?("--\[no-\]#{$1}"))
|
42
|
+
end
|
43
|
+
|
44
|
+
def read_method
|
45
|
+
if flag?
|
46
|
+
super + "?"
|
47
|
+
else
|
48
|
+
super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def extract_value(switch, arguments)
|
53
|
+
if flag?
|
54
|
+
flag_value(switch)
|
55
|
+
else
|
56
|
+
arguments.shift
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def help_lhs
|
61
|
+
lhs = switches.join(", ")
|
62
|
+
lhs += " " + type unless flag?
|
63
|
+
lhs
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def recognised_switches
|
69
|
+
switches.map do |switch|
|
70
|
+
if switch =~ /^--\[no-\](.*)/
|
71
|
+
["--#{$1}", "--no-#{$1}"]
|
72
|
+
else
|
73
|
+
switch
|
74
|
+
end
|
75
|
+
end.flatten
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'clamp/attribute_declaration'
|
2
|
+
require 'clamp/parameter'
|
3
|
+
|
4
|
+
module Clamp
|
5
|
+
class Parameter
|
6
|
+
|
7
|
+
module Declaration
|
8
|
+
|
9
|
+
include Clamp::AttributeDeclaration
|
10
|
+
|
11
|
+
def parameters
|
12
|
+
@parameters ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
def has_parameters?
|
16
|
+
!parameters.empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
def parameter(name, description, options = {}, &block)
|
20
|
+
parameter = Parameter.new(name, description, options)
|
21
|
+
parameters << parameter
|
22
|
+
define_accessors_for(parameter, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Clamp
|
2
|
+
class Parameter
|
3
|
+
|
4
|
+
module Parsing
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
def parse_parameters
|
9
|
+
|
10
|
+
self.class.parameters.each do |parameter|
|
11
|
+
begin
|
12
|
+
value = parameter.consume(remaining_arguments)
|
13
|
+
send("#{parameter.attribute_name}=", value) unless value.nil?
|
14
|
+
rescue ArgumentError => e
|
15
|
+
signal_usage_error "parameter '#{parameter.name}': #{e.message}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'clamp/attribute'
|
2
|
+
|
3
|
+
module Clamp
|
4
|
+
|
5
|
+
class Parameter < Attribute
|
6
|
+
|
7
|
+
def initialize(name, description, options = {})
|
8
|
+
@name = name
|
9
|
+
@description = description
|
10
|
+
infer_attribute_name_and_multiplicity
|
11
|
+
if options.has_key?(:attribute_name)
|
12
|
+
@attribute_name = options[:attribute_name].to_s
|
13
|
+
end
|
14
|
+
if options.has_key?(:default)
|
15
|
+
@default_value = options[:default]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :name, :attribute_name
|
20
|
+
|
21
|
+
def help_lhs
|
22
|
+
name
|
23
|
+
end
|
24
|
+
|
25
|
+
def consume(arguments)
|
26
|
+
if required? && arguments.empty?
|
27
|
+
raise ArgumentError, "no value provided"
|
28
|
+
end
|
29
|
+
if multivalued?
|
30
|
+
if arguments.length > 0
|
31
|
+
arguments.shift(arguments.length)
|
32
|
+
end
|
33
|
+
else
|
34
|
+
arguments.shift
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def default_value
|
39
|
+
if defined?(@default_value)
|
40
|
+
@default_value
|
41
|
+
elsif multivalued?
|
42
|
+
[]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
NAME_PATTERN = "([A-Za-z0-9_-]+)"
|
49
|
+
|
50
|
+
def infer_attribute_name_and_multiplicity
|
51
|
+
case @name
|
52
|
+
when /^\[#{NAME_PATTERN}\]$/
|
53
|
+
@attribute_name = $1
|
54
|
+
when /^\[#{NAME_PATTERN}\] ...$/
|
55
|
+
@attribute_name = "#{$1}_list"
|
56
|
+
@multivalued = true
|
57
|
+
when /^#{NAME_PATTERN} ...$/
|
58
|
+
@attribute_name = "#{$1}_list"
|
59
|
+
@multivalued = true
|
60
|
+
@required = true
|
61
|
+
when /^#{NAME_PATTERN}$/
|
62
|
+
@attribute_name = @name
|
63
|
+
@required = true
|
64
|
+
else
|
65
|
+
raise "invalid parameter name: '#{name}'"
|
66
|
+
end
|
67
|
+
@attribute_name = @attribute_name.downcase.tr('-', '_')
|
68
|
+
end
|
69
|
+
|
70
|
+
def multivalued?
|
71
|
+
@multivalued
|
72
|
+
end
|
73
|
+
|
74
|
+
def required?
|
75
|
+
@required
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'clamp/subcommand'
|
2
|
+
|
3
|
+
module Clamp
|
4
|
+
class Subcommand
|
5
|
+
|
6
|
+
module Declaration
|
7
|
+
|
8
|
+
def recognised_subcommands
|
9
|
+
@recognised_subcommands ||= []
|
10
|
+
end
|
11
|
+
|
12
|
+
def subcommand(name, description, subcommand_class = self, &block)
|
13
|
+
if block
|
14
|
+
# generate a anonymous sub-class
|
15
|
+
subcommand_class = Class.new(subcommand_class, &block)
|
16
|
+
end
|
17
|
+
recognised_subcommands << Subcommand.new(name, description, subcommand_class)
|
18
|
+
end
|
19
|
+
|
20
|
+
def has_subcommands?
|
21
|
+
!recognised_subcommands.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_subcommand(name)
|
25
|
+
recognised_subcommands.find { |sc| sc.is_called?(name) }
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_writer :default_subcommand
|
29
|
+
|
30
|
+
def default_subcommand(*args, &block)
|
31
|
+
if args.empty?
|
32
|
+
@default_subcommand ||= nil
|
33
|
+
else
|
34
|
+
$stderr.puts "WARNING: Clamp default_subcommand syntax has changed; check the README."
|
35
|
+
$stderr.puts " (from #{caller.first})"
|
36
|
+
subcommand(*args, &block)
|
37
|
+
self.default_subcommand = args.first
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Clamp
|
2
|
+
class Subcommand
|
3
|
+
|
4
|
+
module Parsing
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
def parse_subcommand
|
9
|
+
return false unless self.class.has_subcommands?
|
10
|
+
subcommand_name = parse_subcommand_name
|
11
|
+
@subcommand = instatiate_subcommand(subcommand_name)
|
12
|
+
@subcommand.parse(remaining_arguments)
|
13
|
+
remaining_arguments.clear
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def parse_subcommand_name
|
19
|
+
remaining_arguments.shift || self.class.default_subcommand || request_help
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_subcommand(name)
|
23
|
+
self.class.find_subcommand(name) ||
|
24
|
+
signal_usage_error("No such sub-command '#{name}'")
|
25
|
+
end
|
26
|
+
|
27
|
+
def instatiate_subcommand(name)
|
28
|
+
subcommand_class = find_subcommand(name).subcommand_class
|
29
|
+
subcommand = subcommand_class.new("#{invocation_path} #{name}", context)
|
30
|
+
self.class.recognised_options.each do |option|
|
31
|
+
if instance_variable_defined?(option.ivar_name)
|
32
|
+
subcommand.instance_variable_set(option.ivar_name, instance_variable_get(option.ivar_name))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
subcommand
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Clamp
|
2
|
+
|
3
|
+
class Subcommand < Struct.new(:name, :description, :subcommand_class)
|
4
|
+
|
5
|
+
def initialize(names, description, subcommand_class)
|
6
|
+
@names = Array(names)
|
7
|
+
@description = description
|
8
|
+
@subcommand_class = subcommand_class
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :names, :description, :subcommand_class
|
12
|
+
|
13
|
+
def is_called?(name)
|
14
|
+
names.member?(name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def help
|
18
|
+
[names.join(", "), description]
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|