miniparse 0.3.0
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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/FAQ.md +38 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +76 -0
- data/Rakefile +18 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/ex01_simple.rb +22 -0
- data/examples/ex02_options.rb +27 -0
- data/examples/ex03_commands.rb +24 -0
- data/examples/ex04_mixed.rb +37 -0
- data/examples/ex05_block.rb +31 -0
- data/examples/ex06_task_app.rb +36 -0
- data/examples/ex07_controls.rb +63 -0
- data/lib/miniparse.rb +51 -0
- data/lib/miniparse/app.rb +51 -0
- data/lib/miniparse/command.rb +210 -0
- data/lib/miniparse/commander.rb +143 -0
- data/lib/miniparse/control.rb +67 -0
- data/lib/miniparse/option_broker.rb +113 -0
- data/lib/miniparse/parser.rb +136 -0
- data/lib/miniparse/version.rb +9 -0
- data/lib/miniparse/word_wrap.rb +59 -0
- data/miniparse.gemspec +34 -0
- metadata +129 -0
data/lib/miniparse.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# TODO consider inserting here instructions about how to use or how to find help
|
|
2
|
+
|
|
3
|
+
# other modules
|
|
4
|
+
require "miniparse/word_wrap"
|
|
5
|
+
|
|
6
|
+
module Miniparse
|
|
7
|
+
# error exit codes
|
|
8
|
+
# module ErrorCodes
|
|
9
|
+
ERR_HELP_REQ = 1
|
|
10
|
+
ERR_ARGUMENT = 2
|
|
11
|
+
# end
|
|
12
|
+
# include ErrorCodes
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# module miniparse
|
|
16
|
+
require "miniparse/command"
|
|
17
|
+
require "miniparse/commander"
|
|
18
|
+
require "miniparse/control"
|
|
19
|
+
require "miniparse/option_broker"
|
|
20
|
+
require "miniparse/parser"
|
|
21
|
+
require "miniparse/version"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
module Miniparse
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def self.help_usage_format(right_text)
|
|
30
|
+
left_text = "usage: #{File.basename($PROGRAM_NAME)}"
|
|
31
|
+
if Miniparse.control(:formatted_help)
|
|
32
|
+
width_display = Miniparse.control(:width_display)
|
|
33
|
+
width_left = left_text.size
|
|
34
|
+
WordWrap.two_cols_word_wrap(left_text, ' ', right_text,
|
|
35
|
+
width_left, width_display - 1 - width_left)
|
|
36
|
+
else
|
|
37
|
+
left_text + " " + right_text
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def self.debug(msg)
|
|
43
|
+
puts "\nDEBUG #{caller[0]}: #{msg}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require 'miniparse'
|
|
2
|
+
|
|
3
|
+
module App
|
|
4
|
+
|
|
5
|
+
# error exit codes
|
|
6
|
+
ERR_NOT_FOUND = 3
|
|
7
|
+
ERR_IO = 4
|
|
8
|
+
|
|
9
|
+
@parser = nil
|
|
10
|
+
@do_configure_parser = lambda { |parser| parser.parse ARGV }
|
|
11
|
+
|
|
12
|
+
def self.error(msg)
|
|
13
|
+
$stderr.puts "error: #{msg}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.warn(msg)
|
|
17
|
+
$stderr.puts "warning: #{msg}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.info(msg)
|
|
21
|
+
$stdout.puts "info: #{msg}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.debug(msg)
|
|
25
|
+
$stdout.puts "debug: #{msg}" if App.options[:debug]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.options
|
|
29
|
+
parser.options
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.configure_parser(&block)
|
|
33
|
+
reset_parser
|
|
34
|
+
@do_configure_parser = block
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# lazy parser creation
|
|
38
|
+
def self.parser
|
|
39
|
+
return @parser if @parser
|
|
40
|
+
@parser = Miniparse::Parser.new
|
|
41
|
+
parser.add_option("--debug", nil, negatable: false)
|
|
42
|
+
@do_configure_parser.call(parser)
|
|
43
|
+
parser
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# unset the parser (but doesn't affect the configuration)
|
|
47
|
+
def self.reset_parser
|
|
48
|
+
@parser = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
end
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
module Miniparse
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Command
|
|
6
|
+
|
|
7
|
+
def self.spec_to_name(spec)
|
|
8
|
+
spec_pattern_to_name(spec, /\A(\w[\w-]*)\z/)
|
|
9
|
+
end
|
|
10
|
+
def self.valid_spec(*args); spec_to_name(*args); end
|
|
11
|
+
|
|
12
|
+
attr_reader :name, :desc
|
|
13
|
+
|
|
14
|
+
# uses args:
|
|
15
|
+
# :spec
|
|
16
|
+
# :desc
|
|
17
|
+
def initialize(args, &block)
|
|
18
|
+
@spec = args.fetch(:spec)
|
|
19
|
+
@desc = args[:desc]
|
|
20
|
+
@block = block
|
|
21
|
+
@name = self.class.spec_to_name(spec)
|
|
22
|
+
raise SyntaxError, "invalid specification '#{spec}'" if name.nil?
|
|
23
|
+
post_initialize(args)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# runs the associated block with specified arguments
|
|
27
|
+
#
|
|
28
|
+
# @param args is arguments passed to the block
|
|
29
|
+
def run(*args)
|
|
30
|
+
block.call(*args) if block
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param arg is like an ARGV element
|
|
34
|
+
# @return true if arg specifies this object
|
|
35
|
+
def check(arg)
|
|
36
|
+
arg.to_s == name.to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return text of an option specification and description
|
|
40
|
+
def help_desc
|
|
41
|
+
return nil unless desc
|
|
42
|
+
|
|
43
|
+
separator = ' '
|
|
44
|
+
width_indent = Miniparse.control(:width_indent)
|
|
45
|
+
width_left = Miniparse.control(:width_left) -
|
|
46
|
+
Miniparse.control(:width_indent)
|
|
47
|
+
width_right = Miniparse.control(:width_display) -
|
|
48
|
+
separator.size -
|
|
49
|
+
Miniparse.control(:width_left)
|
|
50
|
+
|
|
51
|
+
if Miniparse.control(:formatted_help)
|
|
52
|
+
lines = WordWrap.two_cols_word_wrap_lines(
|
|
53
|
+
spec.to_s + add_spec, separator,
|
|
54
|
+
desc + add_desc,
|
|
55
|
+
width_left, width_right)
|
|
56
|
+
lines.collect! { |line| " "*width_indent + line }
|
|
57
|
+
lines.join("\n")
|
|
58
|
+
else
|
|
59
|
+
s = "%*s" % [width_indent, separator]
|
|
60
|
+
s += "%-*s" % [width_left, spec.to_s + add_spec]
|
|
61
|
+
s += ' '
|
|
62
|
+
s += desc + add_desc
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
protected
|
|
67
|
+
|
|
68
|
+
def self.spec_pattern_to_name(spec, pattern)
|
|
69
|
+
(spec =~ pattern) ? $1.to_sym : nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private_class_method :spec_pattern_to_name
|
|
73
|
+
|
|
74
|
+
attr_reader :spec, :block
|
|
75
|
+
|
|
76
|
+
# subclass hook for initializing
|
|
77
|
+
def post_initialize(args); nil; end
|
|
78
|
+
|
|
79
|
+
# subclass hook for changing description/specification
|
|
80
|
+
def add_desc; ""; end
|
|
81
|
+
def add_spec; ""; end
|
|
82
|
+
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# TODO FEATURE consider doing unambiguous matches for shortened options
|
|
88
|
+
# TODO FEATURE consider the option default value setting the type
|
|
89
|
+
class Option < Command
|
|
90
|
+
|
|
91
|
+
attr_reader :value, :shortable
|
|
92
|
+
|
|
93
|
+
def check(arg)
|
|
94
|
+
arg_to_value(arg) != nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def arg_to_value(arg)
|
|
98
|
+
raise NotImplementedError, "#{self.class} cannot respond to '#{__method__}'"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def parse_value(arg)
|
|
102
|
+
val = arg_to_value(arg)
|
|
103
|
+
if val != nil
|
|
104
|
+
@value = val
|
|
105
|
+
run(val)
|
|
106
|
+
end
|
|
107
|
+
val
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def help_usage
|
|
111
|
+
raise NotImplementedError, "#{self.class} cannot respond to '#{__method__}'"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
protected
|
|
115
|
+
|
|
116
|
+
# uses args:
|
|
117
|
+
# :default
|
|
118
|
+
def post_initialize(args)
|
|
119
|
+
super(args)
|
|
120
|
+
@value = args[:default]
|
|
121
|
+
@shortable = args.fetch(:shortable, Miniparse.control(:autoshortable))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def add_spec
|
|
125
|
+
shortable ? ', -' + name.to_s[0] : ""
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class SwitchOption < Option
|
|
133
|
+
|
|
134
|
+
def self.spec_to_name(spec)
|
|
135
|
+
spec_pattern_to_name(spec, /\A--(\w[\w-]+)\z/)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def arg_to_value(arg)
|
|
139
|
+
if arg == "--#{name}"
|
|
140
|
+
true
|
|
141
|
+
elsif negatable && (arg == "--no-#{name}")
|
|
142
|
+
false
|
|
143
|
+
elsif shortable && (arg == '-' + name.to_s[0])
|
|
144
|
+
true
|
|
145
|
+
else
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def help_usage
|
|
151
|
+
negatable ? "[--[no-]#{name}]" : "[--#{name}]"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
protected
|
|
155
|
+
|
|
156
|
+
attr_reader :negatable
|
|
157
|
+
|
|
158
|
+
# uses args:
|
|
159
|
+
# negatable:true
|
|
160
|
+
def post_initialize(args)
|
|
161
|
+
super(args)
|
|
162
|
+
@negatable = args.fetch(:negatable, Miniparse.control(:autonegatable))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def add_desc
|
|
166
|
+
value ? " (on by default)" : ""
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class FlagOption < Option
|
|
174
|
+
|
|
175
|
+
def self.spec_to_name(spec)
|
|
176
|
+
spec_pattern_to_name(spec, /\A--(\w[\w-]+)[=| ]\S+\z/)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# @param arg is like an ARGV element
|
|
180
|
+
# @return true if arg specifies this option
|
|
181
|
+
def check(arg)
|
|
182
|
+
super(arg) || (arg =~ /\A--#{name}\z/) ||
|
|
183
|
+
(shortable && (arg =~ /\A-#{name.to_s[0]}\z/))
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def arg_to_value(arg)
|
|
187
|
+
if arg =~ /\A--#{name}[=| ](.+)\z/
|
|
188
|
+
$1
|
|
189
|
+
elsif shortable && (arg =~ /\A-#{name.to_s[0]}[=| ](.+)\z/)
|
|
190
|
+
$1
|
|
191
|
+
else
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def help_usage
|
|
197
|
+
"[#{spec}]"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
protected
|
|
201
|
+
|
|
202
|
+
def add_desc
|
|
203
|
+
value ? " (#{value})" : ""
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
module Miniparse
|
|
2
|
+
|
|
3
|
+
# TODO create external documentation, maybe auto
|
|
4
|
+
|
|
5
|
+
# TODO this class maybe does too much, separate a command broker or something similar
|
|
6
|
+
|
|
7
|
+
class Commander
|
|
8
|
+
|
|
9
|
+
attr_reader :current_command, :parsed_command, :parsed_args
|
|
10
|
+
|
|
11
|
+
def parsed_values
|
|
12
|
+
brokers[parsed_command].parsed_values if parsed_command
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return current command broker or nil
|
|
16
|
+
def current_broker
|
|
17
|
+
brokers[current_command]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@commands = {}
|
|
22
|
+
@brokers = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param name is the command name (ex. either "kill" or :kill)
|
|
26
|
+
#
|
|
27
|
+
# @param desc is a short description of the command
|
|
28
|
+
#
|
|
29
|
+
# @param opts are the options to apply to the command
|
|
30
|
+
# :no_options indicates the command has no command line options
|
|
31
|
+
def add_command(args, &block)
|
|
32
|
+
spec = args[:spec]
|
|
33
|
+
unless name = Command.spec_to_name(spec)
|
|
34
|
+
raise SyntaxError, "unknown or invalid command specification '#{spec}'"
|
|
35
|
+
end
|
|
36
|
+
# only run before first command get added
|
|
37
|
+
# and the user isn't trying to add his own help command
|
|
38
|
+
add_help_command if name != :help && commands.empty?
|
|
39
|
+
cmd = Command.new(args, &block)
|
|
40
|
+
# FEATURE if a command already exists it gets quietly overwritten (and its options lost)
|
|
41
|
+
@commands[name] = cmd
|
|
42
|
+
@brokers[name] = OptionBroker.new do
|
|
43
|
+
puts help_command_text(name)
|
|
44
|
+
exit ERR_HELP_REQ
|
|
45
|
+
end
|
|
46
|
+
@current_command = name unless args[:no_options]
|
|
47
|
+
cmd
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param argv is ARGV like
|
|
51
|
+
# @return an array of argv parts: [global_argv, command_name, command_argv]
|
|
52
|
+
def split_argv(argv)
|
|
53
|
+
index = index_command(argv)
|
|
54
|
+
if index
|
|
55
|
+
global_argv = (index == 0) ? [] : argv[0..index-1]
|
|
56
|
+
command_argv = argv[index+1..-1]
|
|
57
|
+
[global_argv, Command.spec_to_name(argv[index]), command_argv]
|
|
58
|
+
else
|
|
59
|
+
[argv, nil, []]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parse_argv(name, argv)
|
|
64
|
+
cmd = commands.fetch(name)
|
|
65
|
+
@parsed_command = cmd.name
|
|
66
|
+
@parsed_args = brokers[cmd.name].parse_argv(argv)
|
|
67
|
+
commands[cmd.name].run(parsed_args)
|
|
68
|
+
parsed_args
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return the command general help for the commands in the commander
|
|
72
|
+
def help_desc
|
|
73
|
+
text = ""
|
|
74
|
+
if current_command
|
|
75
|
+
names_wo_desc = []
|
|
76
|
+
desc_texts = commands.sort.collect do |name, cmd|
|
|
77
|
+
if cmd.desc
|
|
78
|
+
cmd.help_desc
|
|
79
|
+
else
|
|
80
|
+
names_wo_desc << name
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
unless desc_texts.compact!.empty?
|
|
85
|
+
text += "\nCommands:\n"
|
|
86
|
+
text += desc_texts.join("\n")
|
|
87
|
+
end
|
|
88
|
+
unless names_wo_desc.empty?
|
|
89
|
+
text += "\nMore commands: \n"
|
|
90
|
+
text += ' '*Miniparse.control(:width_indent)
|
|
91
|
+
text += names_wo_desc.join(", ")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
text
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
protected
|
|
98
|
+
|
|
99
|
+
attr_reader :commands, :brokers
|
|
100
|
+
|
|
101
|
+
def help_command_text(name)
|
|
102
|
+
header = "Command #{name}: #{commands[name].desc}"
|
|
103
|
+
text = "\n"
|
|
104
|
+
text += WordWrap.word_wrap(header, Miniparse.control(:width_display))
|
|
105
|
+
text += "\n\n"
|
|
106
|
+
text += Miniparse.help_usage_format(
|
|
107
|
+
"#{name} #{brokers[name].help_usage}")
|
|
108
|
+
if (options_desc = brokers[name].help_desc).size > 0
|
|
109
|
+
text += "\n\nOptions:\n"
|
|
110
|
+
text += options_desc
|
|
111
|
+
end
|
|
112
|
+
text += "\n\n"
|
|
113
|
+
text
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def add_help_command
|
|
117
|
+
add_command(spec: :help, desc: nil, no_options: true) do |args|
|
|
118
|
+
index = index_command(args)
|
|
119
|
+
if index
|
|
120
|
+
name = args[index].to_sym
|
|
121
|
+
puts help_command_text(name)
|
|
122
|
+
exit ERR_HELP_REQ
|
|
123
|
+
else
|
|
124
|
+
raise ArgumentError, "no command specified, use 'help <command>' to get help"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @param argv is like ARGV
|
|
130
|
+
# @return index number of the first found command, nil if not found
|
|
131
|
+
def index_command(argv)
|
|
132
|
+
commands.values.each do |cmd|
|
|
133
|
+
index = argv.find_index { |arg| cmd.check(arg) }
|
|
134
|
+
return index if index
|
|
135
|
+
end
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
end
|