rb-cmdline 1.0.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.
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: []