command_mapper 0.1.0 → 0.2.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 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}.