jls-clamp 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|