command_mapper 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d3986821a50009055f2fe58c4f69e7eb530b5463114f73f838997743c8227fe
4
- data.tar.gz: dc0ca8143917f12ef8cb72601f5a2ff44cccd6c76c0fdbc23fc75bafff571ff5
3
+ metadata.gz: 7f40de593c4e0124a5c3075b9e89ec08e38fe7451af1ede8fc62030b89b694ec
4
+ data.tar.gz: '0392b215a5dff4c4b2bffd34ec7df72fece82adbe7d260a975bd909edb9f962d'
5
5
  SHA512:
6
- metadata.gz: dfaccdfb0892218f9e3b8661d2c80227d492c292407989d3bb2b2ffabe45806898549d463a9bb0aa54fe05092753b0751e797460a94c19849552675b75309ec2
7
- data.tar.gz: 4bc76e0cd50aaaf790331ce2aab5acac98562c7ed63cd3979a90429cad836ee4e095ab765aa22b57c17eff9bc89fb92f9ed1b09f97d98fe4ab47cfa38dbcc99a
6
+ metadata.gz: f5c09b88caea6446ac20238bbba137accd62552c413e5fa67d9dce9a62f2e3f027624c6a951ce59fc6c720f2683e80dcbe5d3e8942d85f472e5d25e12f5e5a85
7
+ data.tar.gz: a8e1b267f3628fd23d2924f9d6f25534744af6c2af8a80d5cc3b3fa63bbdc1f35853dcda99871db67cbeea6f6824f3ed77a19b33bd8bd1bb1f5ca7750abf8a7b
data/.document ADDED
@@ -0,0 +1,3 @@
1
+ -
2
+ ChangeLog.*
3
+ LICENSE.txt
@@ -11,7 +11,8 @@ jobs:
11
11
  ruby:
12
12
  - 2.6
13
13
  - 2.7
14
- - 3.0
14
+ - '3.0'
15
+ - 3.1
15
16
  - jruby
16
17
  - truffleruby
17
18
  name: Ruby ${{ matrix.ruby }}
data/ChangeLog.md CHANGED
@@ -1,3 +1,33 @@
1
+ ### 0.2.0 / 2022-04-18
2
+
3
+ * Added {CommandMapper::Command.spawn} and
4
+ {CommandMapper::Command#spawn_command}.
5
+ * Added checks to {CommandMapper::Command.option},
6
+ {CommandMapper::Command.argument}, and {CommandMapper::Command.subcommand} to
7
+ avoid overwriting an existing option/argument/subcommand with the same name.
8
+ * Added the `value_in_flag:` keyword argument to
9
+ {CommandMapper::Command.option} which indicates an option's value
10
+ should be appended to the flag (ex: `-Fvalue`).
11
+ * Added the `range:` keyword argument to {CommandMapper::Types::Num#initialize}
12
+ for specifying the acceptable range of numbers.
13
+ * Allow options with `equals: true` (aka `--opt=...`) or `value_in_flag: true`
14
+ (aka `-Fvalue`) to accept values that start with a `-` character.
15
+
16
+ ### 0.1.2 / 2021-11-29
17
+
18
+ * Fixed a bug where {CommandMapper::Command.command_name} was not checking the
19
+ superclass for the {CommandMapper::Command.command_name command_name}, if no
20
+ `command "..."` was defined in the subclass.
21
+
22
+ ### 0.1.1 / 2021-11-29
23
+
24
+ * Fixed a bug where {CommandMapper::Types::Num}, {CommandMapper::Types::Hex},
25
+ {CommandMapper::Types::Enum}, {CommandMapper::Types::InputPath},
26
+ {CommandMapper::Types::InputFile}, and {CommandMapper::Types::InputDir} were
27
+ not being required by default.
28
+ * Allow {CommandMapper::Types::Map} to accept values that have already been
29
+ mapped to a String.
30
+
1
31
  ### 0.1.0 / 2021-11-25
2
32
 
3
33
  * Initial release:
data/Gemfile CHANGED
@@ -7,7 +7,9 @@ group :development do
7
7
  gem 'rubygems-tasks', '~> 0.2'
8
8
  gem 'rspec', '~> 3.0'
9
9
  gem 'simplecov', '~> 0.20', require: false
10
+
10
11
  gem 'kramdown'
12
+ gem 'redcarpet', platform: :mri
11
13
  gem 'yard', '~> 0.9'
12
14
  gem 'yard-spellcheck'
13
15
 
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2021 Hal Brodigan
1
+ Copyright (c) 2021-2022 Hal Brodigan
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -18,27 +18,47 @@ allow safely and securely executing commands.
18
18
  * Supports defining commands as Ruby classes.
19
19
  * Supports mapping in options and additional arguments.
20
20
  * Supports common option types:
21
- * `Str`: string values
22
- * `Num`: numeric values
23
- * `Hex`: hexadecimal values
24
- * `Map`: maps `true`/`false` to `yes`/`no`, or `enabled`/`disabled`
25
- (aka `--opt=yes|no` or `--opt=enabled|disabled` values).
26
- * `Enum`: maps a finite set of Symbols to a finite set of Strings
27
- (aka `--opt={foo|bar|baz}` values).
28
- * `List`: comma-separated list (aka `--opt VALUE,...`).
29
- * `KeyValue`: maps a Hash or Array to key:value Strings
30
- (aka `--opt KEY:VALUE` or `--opt KEY=VALUE` values).
31
- * `KeyValueList`: a key-value list
21
+ * [Str][CommandMapper::Types::Str]: string values
22
+ * [Num][CommandMapper::Types::Num]: numeric values
23
+ * [Hex][CommandMapper::Types::Hex]: hexadecimal values
24
+ * [Map][CommandMapper::Types::Map]: maps `true`/`false` to `yes`/`no`, or
25
+ `enabled`/`disabled` (aka `--opt=yes|no` or
26
+ `--opt=enabled|disabled` values).
27
+ * [Enum][CommandMapper::Types::Enum]: maps a finite set of Symbols to a
28
+ finite set of Strings (aka `--opt={foo|bar|baz}` values).
29
+ * [List][CommandMapper::Types::List]: comma-separated list
30
+ (aka `--opt VALUE,...`).
31
+ * [KeyValue][CommandMapper::Types::KeyValue]: maps a Hash or Array to
32
+ key:value Strings (aka `--opt KEY:VALUE` or `--opt KEY=VALUE` values).
33
+ * [KeyValueList][CommandMapper::Types::KeyValueList]: a key-value list
32
34
  (aka `--opt KEY:VALUE,...` or `--opt KEY=VALUE;...` values).
33
- * `InputPath`: a path to a pre-existing file or directory
34
- * `InputFile`: a path to a pre-existing file
35
- * `InputDir`: a path to a pre-existing directory
35
+ * [InputPath][CommandMapper::Types::InputPath]: a path to a pre-existing
36
+ file or directory
37
+ * [InputFile][CommandMapper::Types::InputFile]: a path to a pre-existing
38
+ file
39
+ * [InputDir][CommandMapper::Types::InputDir]: a path to a pre-existing
40
+ directory
36
41
  * Supports mapping in sub-commands.
37
42
  * Allows running the command via `IO.popen` to read the command's output.
38
43
  * Allows running commands with additional environment variables.
39
44
  * Allows overriding the command name or path to the command.
40
45
  * Allows running commands via `sudo`.
41
- * Prevents command injection and option injection.
46
+ * Prevents [command injection] and [option injection].
47
+
48
+ [command injection]: https://owasp.org/www-community/attacks/Command_Injection
49
+ [option injection]: https://staaldraad.github.io/post/2019-11-24-argument-injection/
50
+
51
+ [CommandMapper::Types::Str]: https://rubydoc.info/gems/command_mapper/CommandMapper/Types/Str
52
+ [CommandMapper::Types::Num]: https://rubydoc.info/gems/command_mapper/CommandMapper/Types/Num
53
+ [CommandMapper::Types::Hex]: https://rubydoc.info/gems/command_mapper/CommandMapper/Types/Hex
54
+ [CommandMapper::Types::Map]: https://rubydoc.info/gems/command_mapper/CommandMapper/Types/Map
55
+ [CommandMapper::Types::Enum]: https://rubydoc.info/gems/command_mapper/CommandMapper/Types/Enum
56
+ [CommandMapper::Types::List]: https://rubydoc.info/gems/command_mapper/CommandMapper/Types/List
57
+ [CommandMapper::Types::KeyValue]: https://rubydoc.info/gems/command_mapper/CommandMapper/Types/KeyValue
58
+ [CommandMapper::Types::KeyValueList]: https://rubydoc.info/gems/command_mapper/CommandMapper/Types/KeyValueList
59
+ [CommandMapper::Types::InputPath]: https://rubydoc.info/gems/command_mapper/CommandMapper/Types/InputPath
60
+ [CommandMapper::Types::InputFile]: https://rubydoc.info/gems/command_mapper/CommandMapper/Types/InputFile
61
+ [CommandMapper::Types::InputDir]: https://rubydoc.info/gems/command_mapper/CommandMapper/Types/InputDir
42
62
 
43
63
  ## Examples
44
64
 
@@ -56,7 +76,7 @@ class Grep < CommandMapper::Command
56
76
  option "--basic-regexp"
57
77
  option "--perl-regexp"
58
78
  option "--regexp", equals: true, value: true
59
- option "--file", equals: true, value: true
79
+ option "--file", name: :patterns_file, equals: true, value: true
60
80
  option "--ignore-case"
61
81
  option "--no-ignore-case"
62
82
  option "--word-regexp"
@@ -125,6 +145,18 @@ Defines an option with a required value:
125
145
  option "--output", value: {required: true}
126
146
  ```
127
147
 
148
+ Defines an option that uses an equals sign (ex: `--output=value`):
149
+
150
+ ```ruby
151
+ option "--output", equals: true, value: {required: true}
152
+ ```
153
+
154
+ Defines an option where the value is embedded into the flag (ex: `-Ivalue`):
155
+
156
+ ```ruby
157
+ option "-I", value: {required: true}, value_in_flag: true
158
+ ```
159
+
128
160
  Defines an option that can be specified multiple times:
129
161
 
130
162
  ```ruby
@@ -137,6 +169,12 @@ Defines an option that accepts a numeric value:
137
169
  option "--count", value: {type: Num.new}
138
170
  ```
139
171
 
172
+ Define an option that only accepts a range of acceptable values:
173
+
174
+ ```ruby
175
+ option "--count", value: {type: Num.new(range: 1..100)}
176
+ ```
177
+
140
178
  Defines an option that accepts a comma-separated list:
141
179
 
142
180
  ```ruby
@@ -351,18 +389,18 @@ $ gem install command_mapper
351
389
  ### Gemfile
352
390
 
353
391
  ```ruby
354
- gem 'command_mapper', '~> 0.1'
392
+ gem 'command_mapper', '~> 0.2'
355
393
  ```
356
394
 
357
395
  ### gemspec
358
396
 
359
397
  ```ruby
360
- gemspec.add_dependency 'command_mapper', '~> 0.1'
398
+ gemspec.add_dependency 'command_mapper', '~> 0.2'
361
399
  ```
362
400
 
363
401
  ## License
364
402
 
365
- Copyright (c) 2021 Hal Brodigan
403
+ Copyright (c) 2021-2022 Hal Brodigan
366
404
 
367
405
  See {file:LICENSE.txt} for license information.
368
406
 
@@ -6,6 +6,7 @@ module CommandMapper
6
6
  # The base class for both {Option options} and {Argument arguments}.
7
7
  #
8
8
  class Arg
9
+
9
10
  # The argument's arg's type.
10
11
  #
11
12
  # @return [Types::Type, nil]
@@ -18,6 +19,7 @@ module CommandMapper
18
19
  # Specifies whether the argument is required or can be omitted.
19
20
  #
20
21
  # @param [Types::Type, Hash, nil] type
22
+ # The type of the arg's value.
21
23
  #
22
24
  # @raise [ArgumentError]
23
25
  # The `type` keyword argument was given a `nil` value.
@@ -54,6 +56,7 @@ module CommandMapper
54
56
  # Validates whether a given value is compatible with the arg.
55
57
  #
56
58
  # @param [Object] value
59
+ # The given value to validate.
57
60
  #
58
61
  # @return [true, (false, String)]
59
62
  # Returns true if the value is valid, or `false` and a validation error
@@ -50,6 +50,7 @@ module CommandMapper
50
50
  # Validates whether a given value is compatible with the arg.
51
51
  #
52
52
  # @param [Array<Object>, Object] value
53
+ # The given value to validate.
53
54
  #
54
55
  # @return [true, (false, String)]
55
56
  # Returns true if the value is valid, or `false` and a validation error
@@ -109,7 +109,33 @@ module CommandMapper
109
109
  end
110
110
 
111
111
  #
112
- # Runs the command in a shell and captures all stdout output.
112
+ # Initializes and spawns the command as a separate process, returning the
113
+ # PID of the process.
114
+ #
115
+ # @param [Hash{Symbol => Object}] params
116
+ # The option values.
117
+ #
118
+ # @yield [self]
119
+ # The newly initialized command.
120
+ #
121
+ # @yieldparam [Command] self
122
+ #
123
+ # @return [Integer]
124
+ # The PID of the new command process.
125
+ #
126
+ # @raise [Errno::ENOENT]
127
+ # The command could not be found.
128
+ #
129
+ # @since 0.2.0
130
+ #
131
+ def self.spawn(params={},**kwargs,&block)
132
+ command = new(params,**kwargs,&block)
133
+ command.spawn_command
134
+ end
135
+
136
+ #
137
+ # Initializes and runs the command in a shell and captures all stdout
138
+ # output.
113
139
  #
114
140
  # @param [Hash{Symbol => Object}] params
115
141
  # The option values.
@@ -128,7 +154,7 @@ module CommandMapper
128
154
  end
129
155
 
130
156
  #
131
- # Executes the command and returns an IO object to it.
157
+ # Initializes and executes the command and returns an IO object to it.
132
158
  #
133
159
  # @param [Hash{Symbol => Object}] params
134
160
  # The option values.
@@ -146,7 +172,7 @@ module CommandMapper
146
172
  end
147
173
 
148
174
  #
149
- # Initializes and runs the command through sudo.
175
+ # Initializes and runs the command through `sudo`.
150
176
  #
151
177
  # @param [Hash{Symbol => Object}] params
152
178
  # The option values.
@@ -181,7 +207,11 @@ module CommandMapper
181
207
  # @api semipublic
182
208
  #
183
209
  def self.command_name
184
- @command_name || raise(NotImplementedError,"#{self} did not call command(...)")
210
+ @command_name || if superclass < Command
211
+ superclass.command_name
212
+ else
213
+ raise(NotImplementedError,"#{self} did not call command(...)")
214
+ end
185
215
  end
186
216
 
187
217
  #
@@ -221,6 +251,23 @@ module CommandMapper
221
251
  end
222
252
  end
223
253
 
254
+ #
255
+ # Determines if an option with the given name has been defined.
256
+ #
257
+ # @param [Symbol] name
258
+ # The given name.
259
+ #
260
+ # @return [Boolean]
261
+ # Specifies whether an option with the given name has been defined.
262
+ #
263
+ # @api semipublic
264
+ #
265
+ # @since 0.2.0
266
+ #
267
+ def self.has_option?(name)
268
+ options.has_key?(name)
269
+ end
270
+
224
271
  #
225
272
  # Defines an option for the command.
226
273
  #
@@ -230,10 +277,6 @@ module CommandMapper
230
277
  # @param [Symbol, nil] name
231
278
  # The option's name.
232
279
  #
233
- # @param [Boolean] equals
234
- # Specifies whether the option's flag and value should be separated with a
235
- # `=` character.
236
- #
237
280
  # @param [Hash, nil] value
238
281
  # The option's value.
239
282
  #
@@ -246,6 +289,14 @@ module CommandMapper
246
289
  # @param [Boolean] repeats
247
290
  # Specifies whether the option can be given multiple times.
248
291
  #
292
+ # @param [Boolean] equals
293
+ # Specifies whether the option's flag and value should be separated with a
294
+ # `=` character.
295
+ #
296
+ # @param [Boolean] value_in_flag
297
+ # Specifies that the value should be appended to the option's flag
298
+ # (ex: `-Fvalue`).
299
+ #
249
300
  # @api public
250
301
  #
251
302
  # @example Defining an option:
@@ -260,6 +311,9 @@ module CommandMapper
260
311
  # @example Defining an option who's value is optional:
261
312
  # option '--file', value: {required: false}
262
313
  #
314
+ # @example Defining an `-Fvalue` option:
315
+ # option '--foo', value: true, value_in_flag: true
316
+ #
263
317
  # @example Defining an `--opt=value` option:
264
318
  # option '--foo', equals: true, value: true
265
319
  #
@@ -270,25 +324,35 @@ module CommandMapper
270
324
  # option '--list', value: List.new
271
325
  #
272
326
  # @raise [ArgumentError]
273
- # The option flag conflicts with a pre-existing internal method.
274
- #
275
- def self.option(flag, name: nil, equals: nil, value: nil, repeats: false, &block)
327
+ # The option flag conflicts with a pre-existing internal method, or
328
+ # another argument or subcommand.
329
+ #
330
+ def self.option(flag, name: nil, value: nil, repeats: false,
331
+ # formatting options
332
+ equals: nil,
333
+ value_in_flag: nil,
334
+ &block)
276
335
  option = Option.new(flag, name: name,
277
- equals: equals,
278
336
  value: value,
279
337
  repeats: repeats,
338
+ # formatting options
339
+ equals: equals,
340
+ value_in_flag: value_in_flag,
280
341
  &block)
281
342
 
282
- self.options[option.name] = option
283
-
284
343
  if is_internal_method?(option.name)
285
344
  if name
286
345
  raise(ArgumentError,"option #{flag.inspect} with name #{name.inspect} cannot override the internal method with same name: ##{option.name}")
287
346
  else
288
347
  raise(ArgumentError,"option #{flag.inspect} maps to method name ##{option.name} and cannot override the internal method with same name: ##{option.name}")
289
348
  end
349
+ elsif has_argument?(option.name)
350
+ raise(ArgumentError,"option #{flag.inspect} with name #{option.name.inspect} conflicts with another argument with the same name")
351
+ elsif has_subcommand?(option.name)
352
+ raise(ArgumentError,"option #{flag.inspect} with name #{option.name.inspect} conflicts with another subcommand with the same name")
290
353
  end
291
354
 
355
+ self.options[option.name] = option
292
356
  attr_accessor option.name
293
357
  end
294
358
 
@@ -307,6 +371,23 @@ module CommandMapper
307
371
  end
308
372
  end
309
373
 
374
+ #
375
+ # Determines if an argument with the given name has been defined.
376
+ #
377
+ # @param [Symbol] name
378
+ # The given name.
379
+ #
380
+ # @return [Boolean]
381
+ # Specifies whether an argument with the given name has been defined.
382
+ #
383
+ # @api semipublic
384
+ #
385
+ # @since 0.2.0
386
+ #
387
+ def self.has_argument?(name)
388
+ arguments.has_key?(name)
389
+ end
390
+
310
391
  #
311
392
  # Defines an option for the command.
312
393
  #
@@ -333,7 +414,8 @@ module CommandMapper
333
414
  # argument :file, required: false
334
415
  #
335
416
  # @raise [ArgumentError]
336
- # The argument name conflicts with a pre-existing internal method.
417
+ # The argument name conflicts with a pre-existing internal method, or
418
+ # another option or subcommand.
337
419
  #
338
420
  def self.argument(name, required: true, type: Str.new, repeats: false)
339
421
  name = name.to_sym
@@ -341,12 +423,15 @@ module CommandMapper
341
423
  type: type,
342
424
  repeats: repeats)
343
425
 
344
- self.arguments[argument.name] = argument
345
-
346
426
  if is_internal_method?(argument.name)
347
427
  raise(ArgumentError,"argument #{name.inspect} cannot override internal method with same name: ##{argument.name}")
428
+ elsif has_option?(argument.name)
429
+ raise(ArgumentError,"argument #{name.inspect} conflicts with another option with the same name")
430
+ elsif has_subcommand?(argument.name)
431
+ raise(ArgumentError,"argument #{name.inspect} conflicts with another subcommand with the same name")
348
432
  end
349
433
 
434
+ self.arguments[argument.name] = argument
350
435
  attr_accessor name
351
436
  end
352
437
 
@@ -365,6 +450,23 @@ module CommandMapper
365
450
  end
366
451
  end
367
452
 
453
+ #
454
+ # Determines if a subcommand with the given name has been defined.
455
+ #
456
+ # @param [Symbol] name
457
+ # The given name.
458
+ #
459
+ # @return [Boolean]
460
+ # Specifies whether a subcommand with the given name has been defined.
461
+ #
462
+ # @api semipublic
463
+ #
464
+ # @since 0.2.0
465
+ #
466
+ def self.has_subcommand?(name)
467
+ subcommands.has_key?(name)
468
+ end
469
+
368
470
  #
369
471
  # Defines a subcommand.
370
472
  #
@@ -392,25 +494,30 @@ module CommandMapper
392
494
  # end
393
495
  #
394
496
  # @raise [ArgumentError]
395
- # The subcommand name conflicts with a pre-existing internal method.
497
+ # The subcommand name conflicts with a pre-existing internal method, or
498
+ # another option or argument.
396
499
  #
397
500
  def self.subcommand(name,&block)
398
- name = name.to_s
501
+ name = name.to_s
502
+ method_name = name.tr('-','_')
503
+ class_name = name.split(/[_-]+/).map(&:capitalize).join
504
+ subcommand_name = method_name.to_sym
505
+
506
+ if is_internal_method?(method_name)
507
+ raise(ArgumentError,"subcommand #{name.inspect} maps to method name ##{method_name} and cannot override the internal method with same name: ##{method_name}")
508
+ elsif has_option?(subcommand_name)
509
+ raise(ArgumentError,"subcommand #{name.inspect} conflicts with another option with the same name")
510
+ elsif has_argument?(subcommand_name)
511
+ raise(ArgumentError,"subcommand #{name.inspect} conflicts with another argument with the same name")
512
+ end
399
513
 
400
514
  subcommand_class = Class.new(Command)
401
515
  subcommand_class.command(name)
402
516
  subcommand_class.class_eval(&block)
403
517
 
404
- method_name = name.tr('-','_')
405
- class_name = name.split(/[_-]+/).map(&:capitalize).join
406
-
407
- self.subcommands[method_name.to_sym] = subcommand_class
518
+ self.subcommands[subcommand_name] = subcommand_class
408
519
  const_set(class_name,subcommand_class)
409
520
 
410
- if is_internal_method?(method_name)
411
- raise(ArgumentError,"subcommand #{name.inspect} maps to method name ##{method_name} and cannot override the internal method with same name: ##{method_name}")
412
- end
413
-
414
521
  define_method(method_name) do |&block|
415
522
  if block then @command_subcommand = subcommand_class.new(&block)
416
523
  else @command_subcommand
@@ -529,12 +636,28 @@ module CommandMapper
529
636
  end
530
637
 
531
638
  #
532
- # Initializes and runs the command.
639
+ # Runs the command.
533
640
  #
534
641
  # @return [Boolean, nil]
535
642
  #
536
643
  def run_command
537
- system(@command_env,*command_argv)
644
+ Kernel.system(@command_env,*command_argv)
645
+ end
646
+
647
+ #
648
+ # Spawns the command as a separate process, returning the PID of the
649
+ # process.
650
+ #
651
+ # @return [Integer]
652
+ # The PID of the new command process.
653
+ #
654
+ # @raise [Errno::ENOENT]
655
+ # The command could not be found.
656
+ #
657
+ # @since 0.2.0
658
+ #
659
+ def spawn_command
660
+ Process.spawn(@command_env,*command_argv)
538
661
  end
539
662
 
540
663
  #
@@ -559,7 +682,7 @@ module CommandMapper
559
682
  end
560
683
 
561
684
  #
562
- # Initializes and runs the command through sudo.
685
+ # Runs the command through `sudo`.
563
686
  #
564
687
  # @param [Hash{Symbol => Object}] sudo_params
565
688
  # Additional keyword arguments for {Sudo#initialize}.