rb-cmdline 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/lib/cmdline.rb +433 -0
  3. data/lib/rb-cmdline.rb +1 -0
  4. metadata +45 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0ae06e2e709b3938b119266dba1c6ca6badf79628d0cc52d0fef8f62cd4a9d12
4
+ data.tar.gz: 3e867fec030c46b59f433ec577063f9059a7b99ce29d488f77df41832c824231
5
+ SHA512:
6
+ metadata.gz: 6a23d8867357b1c48c0bdd4c618f25d95553ab26f5a35517dc7528657fbd0901bd8c3ae90101a1815c5ee39c699bb4c3fdf0a555bb932ca2f7c9fcfde4227085
7
+ data.tar.gz: 88e53b7bec52da1b3796040d3ea1199807fff513ee0e8a4fd830075c652ff476c3c97796f82885d9db806b28c561a37653742ea76627c221a87a09d24b9d8225
data/lib/cmdline.rb ADDED
@@ -0,0 +1,433 @@
1
+ # Copyright 2019 Bryan Frimin
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Cmdline
16
+ Option = Struct.new(
17
+ :short_name,
18
+ :long_name,
19
+ :value_string,
20
+ :description,
21
+ :default,
22
+ :set,
23
+ :value,
24
+ keyword_init: true
25
+ )
26
+
27
+ class Option
28
+ def initialize(*)
29
+ super
30
+ self.short_name ||= ""
31
+ self.long_name ||= ""
32
+ self.value_string ||= ""
33
+ self.description ||= ""
34
+ self.default ||= ""
35
+ self.set ||= false
36
+ self.value ||= ""
37
+ end
38
+
39
+ def sort_key
40
+ return short_name unless short_name.empty?
41
+ return long_name unless long_name.empty?
42
+ ""
43
+ end
44
+ end
45
+
46
+ Argument = Struct.new(
47
+ :name,
48
+ :description,
49
+ :trailing,
50
+ :value,
51
+ :trailing_values,
52
+ keyword_init: true
53
+ )
54
+
55
+ class Argument
56
+ def initialize(*)
57
+ super
58
+ self.name ||= ""
59
+ self.description ||= ""
60
+ self.trailing ||= false
61
+ self.value ||= ""
62
+ self.trailing_values ||= []
63
+ end
64
+ end
65
+
66
+ Command = Struct.new(
67
+ :name,
68
+ :description,
69
+ keyword_init: true
70
+ )
71
+
72
+ class Command
73
+ def initialize(*)
74
+ super
75
+ self.name ||= ""
76
+ self.description ||= ""
77
+ end
78
+ end
79
+
80
+ CmdLine = Struct.new(
81
+ :options,
82
+ :arguments,
83
+ :commands,
84
+ :command,
85
+ :command_arguments,
86
+ :program_name,
87
+ keyword_init: true
88
+ )
89
+
90
+ class CmdLine
91
+ def initialize(*)
92
+ super
93
+ self.options ||= {}
94
+ self.commands ||= {}
95
+ self.command ||= ""
96
+ self.arguments ||= []
97
+ self.command_arguments ||= []
98
+ self.program_name ||= ""
99
+ end
100
+
101
+ def add_flag(short, long, description)
102
+ option = Option.new(
103
+ short_name: short.to_s,
104
+ long_name: long.to_s,
105
+ value_string: "",
106
+ description: description.to_s
107
+ )
108
+
109
+ addopt(option)
110
+ end
111
+
112
+ def add_option(short, long, value, description)
113
+ option = Option.new(
114
+ short_name: short.to_s,
115
+ long_name: long.to_s,
116
+ value_string: value.to_s,
117
+ description: description.to_s
118
+ )
119
+
120
+ addopt(option)
121
+ end
122
+
123
+ def set_option_default(name, value)
124
+ option = self.options[name]
125
+ raise(ArgumentError, "unknown option") if option.nil?
126
+ raise(ArgumentError, "flags cannot have a default value") if option.value_string.empty?
127
+
128
+ option.default = value
129
+ end
130
+
131
+ def add_argument(name, description)
132
+ argument = Argument.new(
133
+ name: name.to_s,
134
+ description: description.to_s
135
+ )
136
+
137
+ addarg(argument)
138
+ end
139
+
140
+ def add_trailing_arguments(name, description)
141
+ argument = Argument.new(
142
+ name: name.to_s,
143
+ description: description.to_s,
144
+ trailing: true
145
+ )
146
+
147
+ addarg(argument)
148
+ end
149
+
150
+ def add_command(name, description)
151
+ if self.arguments.size.zero?
152
+ add_argument("command", "the command to execute")
153
+ elsif self.arguments.first.name != "command"
154
+ raise(ArgumentError, "cannot have both arguments and commands")
155
+ end
156
+
157
+ cmd = Command.new(
158
+ name: name.to_s,
159
+ description: description.to_s
160
+ )
161
+
162
+ self.commands[cmd.name] = cmd
163
+ end
164
+
165
+ def die(format, *args)
166
+ msg = sprintf(format, *args)
167
+ STDERR.puts("error: #{msg}")
168
+ exit(1)
169
+ end
170
+
171
+ def parse(args)
172
+ die("empty argument array") if args.size == 0
173
+
174
+ self.program_name = args.shift
175
+
176
+ while args.size > 0
177
+ arg = args.first
178
+
179
+ if arg == "--"
180
+ args.shift
181
+ break
182
+ end
183
+
184
+ is_short = arg.size == 2 && arg[0] == "-" && arg[1] != "-"
185
+ is_long = arg.size > 2 && arg[0,2] == "--"
186
+
187
+ if is_short || is_long
188
+ key = if is_short
189
+ arg[1,2]
190
+ else
191
+ arg[2..]
192
+ end
193
+
194
+ opt = self.options[key]
195
+ die("unknown option \"%s\"", key) if opt.nil?
196
+
197
+ opt.set = true
198
+
199
+ if opt.value_string.empty?
200
+ args = args[1..]
201
+ else
202
+ die("missing value for option \"%s\"", key) if args.size < 2
203
+ opt.value = args[1]
204
+ args = args[2..]
205
+ end
206
+ else
207
+ break
208
+ end
209
+ end
210
+
211
+ if self.arguments.size > 0 && !is_option_set("help")
212
+ last = self.arguments.last
213
+
214
+ min = self.arguments.size
215
+ min -= 1 if last.trailing
216
+
217
+ die("missing argument(s)") if args.size < min
218
+
219
+ min.times do |i|
220
+ self.arguments[i].value = args[i]
221
+ end
222
+ args = args[min..]
223
+
224
+ if last.trailing
225
+ last.trailing_values = args
226
+ args = args[args.size..]
227
+ end
228
+ end
229
+
230
+ if self.commands.size > 0
231
+ self.command = self.arguments.first.value
232
+ self.command_arguments = args
233
+ end
234
+
235
+ if !is_option_set("help")
236
+ if self.commands.size > 0
237
+ cmd = self.commands[self.command]
238
+ if cmd.nil?
239
+ die("unknown command \"%s\"", self.command)
240
+ end
241
+ elsif args.size > 0
242
+ die("invalid extra argument(s)")
243
+ end
244
+ end
245
+
246
+ if is_option_set("help")
247
+ print_usage
248
+ exit(0)
249
+ end
250
+ end
251
+
252
+ def print_usage
253
+ usage = sprintf("Usage: %s OPTIONS", self.program_name)
254
+ if self.arguments.size > 0
255
+ self.arguments.each do |arg|
256
+ if arg.trailing
257
+ usage << sprintf(" [<%s> ...]", arg.name)
258
+ else
259
+ usage << sprintf(" <%s>", arg.name)
260
+ end
261
+ end
262
+ end
263
+
264
+ usage << "\n\n"
265
+
266
+ opt_strs = {}
267
+ max_width = 0
268
+
269
+ self.options.each do |_, opt|
270
+ next if opt_strs[opt]
271
+
272
+ buf = ""
273
+
274
+ if opt.short_name != ""
275
+ buf << sprintf("-%s", opt.short_name)
276
+ end
277
+
278
+ if opt.long_name != ""
279
+ if opt.short_name != ""
280
+ buf << ", "
281
+ end
282
+
283
+ buf << sprintf("--%s", opt.long_name)
284
+ end
285
+
286
+ if opt.value_string != ""
287
+ buf << sprintf(" <%s>", opt.value_string)
288
+ end
289
+
290
+ opt_strs[opt] = buf
291
+
292
+ if buf.size > max_width
293
+ max_width = buf.size
294
+ end
295
+ end
296
+
297
+ if self.commands.size > 0
298
+ self.commands.each do |name, _|
299
+ max_width = name.size if name.size > max_width
300
+ end
301
+ elsif self.arguments.size > 0
302
+ self.arguments.each do |arg|
303
+ max_width = arg.name.size if arg.name.size > max_width
304
+ end
305
+ end
306
+
307
+ # Print options
308
+ usage << "OPTIONS\n\n"
309
+
310
+ opts = []
311
+ opt_strs.each do |opt, _|
312
+ opts << opt
313
+ end
314
+
315
+ # TODO: sort options
316
+
317
+ opts.each do |opt|
318
+ usage << sprintf("%-*s %s", max_width, opt_strs[opt], opt.description)
319
+ usage << sprintf(" (default: %s)", opt.default) unless opt.default.empty?
320
+ usage << "\n"
321
+ end
322
+
323
+ if self.commands.size > 0
324
+ usage << "\nCOMMANDS\n\n"
325
+ names = []
326
+ self.commands.each do |name, _|
327
+ names << name
328
+ end
329
+ names.sort!
330
+
331
+ names.each do |name|
332
+ cmd = self.commands[name]
333
+ usage << sprintf("%-*s %s\n", max_width, cmd.name, cmd.description)
334
+ end
335
+ elsif self.arguments.size > 0
336
+ usage << "\nARGUMENTS\n\n"
337
+
338
+ self.arguments.each do |arg|
339
+ usage << sprintf("%-*s %s\n", max_width, arg.name, arg.description)
340
+ end
341
+ end
342
+
343
+ printf(usage)
344
+ end
345
+
346
+ def is_option_set(name)
347
+ opt = self.options[name]
348
+ raise(ArgumentError, "unknown option") if opt.nil?
349
+ opt.set
350
+ end
351
+
352
+ def option_value(name)
353
+ opt = self.options[name]
354
+ raise(ArgumentError, "unknown option") if opt.nil?
355
+
356
+ return opt.value if opt.set
357
+ opt.default
358
+ end
359
+
360
+ def argument_value(name)
361
+ self.arguments.each do |arg|
362
+ if arg.name == name
363
+ return arg.value
364
+ end
365
+ end
366
+ raise(ArgumentError, "unknown argument")
367
+ end
368
+
369
+ def trailing_arguments_values(name)
370
+ raise(ArgumentError, "empty argument array") if self.arguments.empty?
371
+ last = self.arguments.last
372
+ raise(ArgumentError, "no trailing arguments") unless last.trailing
373
+ last.trailing_values
374
+ end
375
+
376
+ def command_name
377
+ raise(RuntimeError, "no command defined") if self.commands.empty?
378
+ self.command
379
+ end
380
+
381
+ def command_arguments_values
382
+ raise(RuntimeError, "no command defined") if self.commands.empty?
383
+ self.command_arguments
384
+ end
385
+
386
+ def command_name_and_arguments
387
+ raise(RuntimeError, "no command defined") if self.commands.empty?
388
+ [self.command, *self.command_arguments]
389
+ end
390
+
391
+ private
392
+
393
+ def addopt(opt)
394
+ if !opt.short_name.empty?
395
+ if opt.short_name.size != 1
396
+ raise(ArgumentError, "option short names must be one character long")
397
+ end
398
+
399
+ self.options[opt.short_name] = opt
400
+ end
401
+
402
+ if !opt.long_name.empty?
403
+ if opt.long_name.size < 2
404
+ raise(ArgumentError, "option long names must be at least two characters long")
405
+ end
406
+
407
+ self.options[opt.long_name] = opt
408
+ end
409
+ end
410
+
411
+ def addarg(arg)
412
+ raise(ArgumentError, "cannot have both arguments and commands") if self.commands.size > 0
413
+ if self.arguments.size > 0
414
+ last = self.arguments.last
415
+ if last.trailing
416
+ raise(ArgumentError, "cannot add argument after trailing argument")
417
+ end
418
+ end
419
+
420
+ self.arguments << arg
421
+ end
422
+ end
423
+
424
+ def self.new
425
+ cmd = CmdLine.new
426
+ cmd.add_flag("h", "help", "print help and exit")
427
+ cmd
428
+ end
429
+
430
+ def self.argv
431
+ ARGV.dup.unshift($0)
432
+ end
433
+ end
data/lib/rb-cmdline.rb ADDED
@@ -0,0 +1 @@
1
+ require("cmdline")
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rb-cmdline
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - gearnode
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-09-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: bryan@frimin.fr
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/cmdline.rb
20
+ - lib/rb-cmdline.rb
21
+ homepage: https://github.com/gearnode/rb-cmdline
22
+ licenses:
23
+ - Apache-2.0
24
+ metadata:
25
+ source_code_uri: https://github.com/gearnode/rb-cmdline
26
+ post_install_message:
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ required_rubygems_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements: []
41
+ rubygems_version: 3.0.4
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: A command line parser written in Ruby
45
+ test_files: []