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.
@@ -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