aspera-cli 4.24.2 → 4.25.0.pre

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 (65) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +1064 -758
  4. data/CONTRIBUTING.md +43 -100
  5. data/README.md +671 -419
  6. data/lib/aspera/api/aoc.rb +71 -43
  7. data/lib/aspera/api/cos_node.rb +3 -2
  8. data/lib/aspera/api/faspex.rb +6 -5
  9. data/lib/aspera/api/node.rb +10 -12
  10. data/lib/aspera/ascmd.rb +1 -2
  11. data/lib/aspera/ascp/installation.rb +53 -39
  12. data/lib/aspera/assert.rb +25 -3
  13. data/lib/aspera/cli/error.rb +4 -2
  14. data/lib/aspera/cli/extended_value.rb +84 -60
  15. data/lib/aspera/cli/formatter.rb +55 -22
  16. data/lib/aspera/cli/main.rb +21 -14
  17. data/lib/aspera/cli/manager.rb +348 -247
  18. data/lib/aspera/cli/plugins/alee.rb +3 -3
  19. data/lib/aspera/cli/plugins/aoc.rb +70 -14
  20. data/lib/aspera/cli/plugins/base.rb +57 -49
  21. data/lib/aspera/cli/plugins/config.rb +69 -84
  22. data/lib/aspera/cli/plugins/console.rb +13 -8
  23. data/lib/aspera/cli/plugins/cos.rb +1 -1
  24. data/lib/aspera/cli/plugins/faspex.rb +32 -26
  25. data/lib/aspera/cli/plugins/faspex5.rb +45 -43
  26. data/lib/aspera/cli/plugins/faspio.rb +5 -5
  27. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  28. data/lib/aspera/cli/plugins/node.rb +131 -120
  29. data/lib/aspera/cli/plugins/oauth.rb +1 -1
  30. data/lib/aspera/cli/plugins/orchestrator.rb +114 -32
  31. data/lib/aspera/cli/plugins/preview.rb +26 -46
  32. data/lib/aspera/cli/plugins/server.rb +6 -8
  33. data/lib/aspera/cli/plugins/shares.rb +27 -32
  34. data/lib/aspera/cli/sync_actions.rb +49 -38
  35. data/lib/aspera/cli/transfer_agent.rb +16 -34
  36. data/lib/aspera/cli/version.rb +1 -1
  37. data/lib/aspera/cli/wizard.rb +8 -5
  38. data/lib/aspera/command_line_builder.rb +20 -17
  39. data/lib/aspera/coverage.rb +1 -1
  40. data/lib/aspera/environment.rb +41 -34
  41. data/lib/aspera/faspex_gw.rb +1 -1
  42. data/lib/aspera/keychain/factory.rb +1 -2
  43. data/lib/aspera/markdown.rb +31 -0
  44. data/lib/aspera/nagios.rb +6 -5
  45. data/lib/aspera/oauth/base.rb +17 -27
  46. data/lib/aspera/oauth/factory.rb +1 -1
  47. data/lib/aspera/oauth/url_json.rb +2 -1
  48. data/lib/aspera/preview/file_types.rb +23 -37
  49. data/lib/aspera/products/connect.rb +3 -3
  50. data/lib/aspera/rest.rb +51 -39
  51. data/lib/aspera/rest_error_analyzer.rb +4 -4
  52. data/lib/aspera/ssh.rb +5 -2
  53. data/lib/aspera/ssl.rb +41 -0
  54. data/lib/aspera/sync/conf.schema.yaml +182 -34
  55. data/lib/aspera/sync/database.rb +2 -1
  56. data/lib/aspera/sync/operations.rb +125 -69
  57. data/lib/aspera/transfer/parameters.rb +3 -4
  58. data/lib/aspera/transfer/spec.rb +2 -3
  59. data/lib/aspera/transfer/spec.schema.yaml +48 -18
  60. data/lib/aspera/transfer/spec_doc.rb +14 -14
  61. data/lib/aspera/uri_reader.rb +1 -1
  62. data/lib/transferd_pb.rb +2 -2
  63. data.tar.gz.sig +0 -0
  64. metadata +19 -6
  65. metadata.gz.sig +3 -2
@@ -11,32 +11,128 @@ require 'optparse'
11
11
 
12
12
  module Aspera
13
13
  module Cli
14
- # option is retrieved from another object using accessor
15
- class AttrAccessor
16
- # attr_accessor :object
17
- # attr_accessor :method_name
18
- def initialize(object, method_name, option_name)
19
- @object = object
20
- @method = method_name
21
- @option_name = option_name
22
- @has_writer = @object.respond_to?(writer_method)
23
- Log.log.trace1{"AttrAccessor: #{@option_name}: #{@object.class}.#{@method}: writer=#{@has_writer}"}
24
- Aspera.assert(@object.respond_to?(@method)){"#{object} does not respond to #{method_name}"}
14
+ # Constants to be used as parameter `allowed:` for `OptionValue`
15
+ module Allowed
16
+ # This option can be set to a single string or array, multiple times, and gives Array of String
17
+ TYPES_STRING_ARRAY = [Array, String].freeze
18
+ # A list of symbols with constrained values
19
+ TYPES_SYMBOL_ARRAY = [Array, Symbol].freeze
20
+ # Value will be coerced to int
21
+ TYPES_INTEGER = [Integer].freeze
22
+ TYPES_BOOLEAN = [FalseClass, TrueClass].freeze
23
+ # no value at all, it's a switch
24
+ TYPES_NONE = [].freeze
25
+ TYPES_ENUM = [Symbol].freeze
26
+ TYPES_STRING = [String].freeze
27
+ end
28
+
29
+ # Description of option, how to manage
30
+ class OptionValue
31
+ # [Array(Class)] List of allowed types
32
+ attr_reader :types, :sensitive
33
+ # [Array] List of allowed values (Symbols and specific values)
34
+ attr_accessor :values
35
+
36
+ # @param option [Symbol] Name of option
37
+ # @param allowed [see below] Allowed values
38
+ # @param handler [Hash] Accessor: keys: :o(object) and :m(method)
39
+ # @param deprecation [String] Deprecation message
40
+ # `allowed`:
41
+ # - `nil` No validation, so just a string
42
+ # - `Class` The single allowed Class
43
+ # - `Array<Class>` Multiple allowed classes
44
+ # - `Array<Symbol>` List of allowed values
45
+ def initialize(option:, description:, allowed: Allowed::TYPES_STRING, handler: nil, deprecation: nil)
46
+ Log.log.trace1{"option: #{option}, allowed: #{allowed}"}
47
+ @option = option
48
+ @description = description
49
+ # by default passwords and secrets are sensitive, else specify when declaring the option
50
+ @sensitive = SecretHider.instance.secret?(@option, '')
51
+ # either the value, or object giving value
52
+ @object = handler&.[](:o)
53
+ @read_method = handler&.[](:m)
54
+ @write_method = @read_method ? "#{@read_method}=".to_sym : nil
55
+ @deprecation = deprecation
56
+ @access = if @object.nil?
57
+ :local
58
+ elsif @object.respond_to?(@write_method)
59
+ :write
60
+ else
61
+ :setter
62
+ end
63
+ Aspera.assert(@object.respond_to?(@read_method)){"#{@object} does not respond to #{method}"} unless @access.eql?(:local)
64
+ @types = nil
65
+ @values = nil
66
+ if !allowed.nil?
67
+ allowed = [allowed] if allowed.is_a?(Class)
68
+ Aspera.assert_type(allowed, Array)
69
+ if allowed.take(Allowed::TYPES_SYMBOL_ARRAY.length) == Allowed::TYPES_SYMBOL_ARRAY
70
+ # Special case: array of defined symbol values
71
+ @types = Allowed::TYPES_SYMBOL_ARRAY
72
+ @values = allowed[Allowed::TYPES_SYMBOL_ARRAY.length..-1]
73
+ elsif allowed.all?(Class)
74
+ @types = allowed
75
+ @values = Manager::BOOLEAN_VALUES if allowed.eql?(Allowed::TYPES_BOOLEAN)
76
+ # Default value for array
77
+ @object ||= [] if @types.first.eql?(Array) && !@types.include?(NilClass)
78
+ @object ||= {} if @types.first.eql?(Hash) && !@types.include?(NilClass)
79
+ elsif allowed.all?(Symbol)
80
+ @types = Allowed::TYPES_ENUM
81
+ @values = allowed
82
+ else
83
+ Aspera.error_unexpected_value(allowed)
84
+ end
85
+ end
86
+ Log.log.trace1{"declare: #{@option}: #{@access} #{@object.class}.#{@read_method}".green}
25
87
  end
26
88
 
27
- def value
28
- return @object.send(@method) if @has_writer
29
- return @object.send(@method, @option_name, :get)
89
+ def clear
90
+ @object = nil
30
91
  end
31
92
 
32
- def value=(val)
33
- Log.log.trace1{"AttrAccessor: = #{@method} #{@option_name} :set #{val}, writer=#{@has_writer}"}
34
- return @object.send(writer_method, val) if @has_writer
35
- return @object.send(@method, @option_name, :set, val)
93
+ def value(log: true)
94
+ current_value =
95
+ case @access
96
+ when :local then @object
97
+ when :write then @object.send(@read_method)
98
+ when :setter then @object.send(@read_method, @option, :get)
99
+ end
100
+ Log.log.trace1{"#{@option} -> (#{current_value.class})#{current_value}"} if log
101
+ return current_value
36
102
  end
37
103
 
38
- def writer_method
39
- return "#{@method}="
104
+ # Assign value to option.
105
+ # Value can be a String, then evaluated with ExtendedValue, or directly a value.
106
+ # @param value [String, Object] Value to assign to option
107
+ def assign_value(value, where:)
108
+ Aspera.assert(!@deprecation, type: warn){"Option #{@option} is deprecated: #{@deprecation}"}
109
+ new_value = ExtendedValue.instance.evaluate(value, context: "option: #{@option}", allowed: @types)
110
+ Log.log.trace1{"#{where}: #{@option} <- (#{new_value.class})#{new_value}"}
111
+ new_value = Manager.enum_to_bool(new_value) if @types.eql?(Allowed::TYPES_BOOLEAN)
112
+ new_value = Integer(new_value) if @types.eql?(Allowed::TYPES_INTEGER)
113
+ new_value = [new_value] if @types.eql?(Allowed::TYPES_STRING_ARRAY) && new_value.is_a?(String)
114
+ # Setting a Hash to null set an empty hash
115
+ new_value = {} if new_value.eql?(nil) && @types&.first.eql?(Hash)
116
+ # Setting a Array to null set an empty hash
117
+ new_value = [] if new_value.eql?(nil) && @types&.first.eql?(Array)
118
+ if @types.eql?(Aspera::Cli::Allowed::TYPES_SYMBOL_ARRAY)
119
+ new_value = [new_value] if new_value.is_a?(String)
120
+ Aspera.assert_type(new_value, Array, type: BadArgument)
121
+ Aspera.assert_array_all(new_value, String, type: BadArgument)
122
+ new_value = new_value.map{ |v| Manager.get_from_list(v, @option, @values)}
123
+ end
124
+ Aspera.assert_type(new_value, *@types, type: BadArgument){"Option #{@option}"} if @types
125
+ if new_value.is_a?(Hash) || new_value.is_a?(Array)
126
+ current_value = value(log: false)
127
+ new_value = current_value.deep_merge(new_value) if new_value.is_a?(Hash) && current_value.is_a?(Hash) && !current_value.empty?
128
+ new_value = current_value + new_value if new_value.is_a?(Array) && current_value.is_a?(Array) && !current_value.empty?
129
+ end
130
+ case @access
131
+ when :local then @object = new_value
132
+ when :write then @object.send(@write_method, new_value)
133
+ when :setter then @object.send(@read_method, @option, :set, new_value)
134
+ end
135
+ Log.log.trace1{v = value(log: false); "#{@option} <- (#{v.class})#{v}"} # rubocop:disable Style/Semicolon
40
136
  end
41
137
  end
42
138
 
@@ -44,49 +140,29 @@ module Aspera
44
140
  # arguments options start with '-', others are commands
45
141
  # resolves on extended value syntax
46
142
  class Manager
47
- # boolean options are set to true/false from the following values
48
143
  BOOLEAN_SIMPLE = %i[no yes].freeze
49
- FALSE_VALUES = [BOOLEAN_SIMPLE.first, false].freeze
50
- TRUE_VALUES = [BOOLEAN_SIMPLE.last, true].freeze
51
- BOOLEAN_VALUES = (TRUE_VALUES + FALSE_VALUES).freeze
52
-
53
- # option name separator on command line
54
- OPTION_SEP_LINE = '-'
55
- # option name separator in code (symbol)
56
- OPTION_SEP_SYMBOL = '_'
57
- OPTION_VALUE_SEPARATOR = '='
58
- # an option like --a.b.c=d does: a={"b":{"c":ext_val(d)}}
59
- # TODO: all Hash are additive, + way to reset Hash (e.g. --opt=@none:)
60
- OPTION_HASH_SEPARATOR = '.'
61
- # starts an option
62
- OPTION_PREFIX = '--'
63
- # when this is alone, this stops option processing
64
- OPTIONS_STOP = '--'
65
- SOURCE_USER = 'cmdline' # cspell:disable-line
66
-
67
- DEFAULT_PARSER_TYPES = [Array, Hash].freeze
68
-
69
- private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_HASH_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER, :DEFAULT_PARSER_TYPES
70
-
71
144
  class << self
145
+ # @return `true` if value is a value for `true` in BOOLEAN_VALUES
72
146
  def enum_to_bool(enum)
73
147
  Aspera.assert_values(enum, BOOLEAN_VALUES){'boolean'}
74
148
  return TRUE_VALUES.include?(enum)
75
149
  end
76
150
 
77
- def time_to_string(time)
78
- return time.strftime('%Y-%m-%d %H:%M:%S')
151
+ # @return :yes ot :no
152
+ def enum_to_yes_no(enum)
153
+ Aspera.assert_values(enum, BOOLEAN_VALUES){'boolean'}
154
+ return TRUE_VALUES.include?(enum) ? BOOL_YES : BOOL_NO
79
155
  end
80
156
 
81
- # find shortened string value in allowed symbol list
157
+ # Find shortened string value in allowed symbol list
82
158
  def get_from_list(short_value, descr, allowed_values)
83
159
  Aspera.assert_type(short_value, String)
84
160
  # we accept shortcuts
85
161
  matching_exact = allowed_values.select{ |i| i.to_s.eql?(short_value)}
86
162
  return matching_exact.first if matching_exact.length == 1
87
163
  matching = allowed_values.select{ |i| i.to_s.start_with?(short_value)}
88
- multi_choice_assert(!matching.empty?, "unknown value for #{descr}: #{short_value}", allowed_values)
89
- multi_choice_assert(matching.length.eql?(1), "ambiguous shortcut for #{descr}: #{short_value}", matching)
164
+ Aspera.assert(!matching.empty?, multi_choice_assert_msg("unknown value for #{descr}: #{short_value}", allowed_values), type: BadArgument)
165
+ Aspera.assert(matching.length.eql?(1), multi_choice_assert_msg("ambiguous shortcut for #{descr}: #{short_value}", matching), type: BadArgument)
90
166
  return enum_to_bool(matching.first) if allowed_values.eql?(BOOLEAN_VALUES)
91
167
  return matching.first
92
168
  end
@@ -94,8 +170,8 @@ module Aspera
94
170
  # Generates error message with list of allowed values
95
171
  # @param error_msg [String] error message
96
172
  # @param accept_list [Array] list of allowed values
97
- def multi_choice_assert(assertion, error_msg, accept_list)
98
- raise Cli::BadArgument, [error_msg, 'Use:'].concat(accept_list.map{ |c| "- #{c}"}.sort).join("\n") unless assertion
173
+ def multi_choice_assert_msg(error_msg, accept_list)
174
+ [error_msg, 'Use:'].concat(accept_list.map{ |c| "- #{c}"}.sort).join("\n")
99
175
  end
100
176
 
101
177
  # change option name with dash to name with underscore
@@ -106,22 +182,6 @@ module Aspera
106
182
  def option_name_to_line(name)
107
183
  return "#{OPTION_PREFIX}#{name.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)}"
108
184
  end
109
-
110
- # @param what [Symbol] :option or :argument
111
- # @param descr [String] description for help
112
- # @param to_check [Object] value to check
113
- # @param type_list [NilClass, Class, Array[Class]] accepted value type(s)
114
- # @param check_array [bool] set to true if it is a list of values to check
115
- def validate_type(what, descr, to_check, type_list, check_array: false)
116
- return if type_list.nil?
117
- Aspera.assert(type_list.is_a?(Array) && type_list.all?(Class)){'types must be a Class Array'}
118
- value_list = check_array ? to_check : [to_check]
119
- value_list.each do |value|
120
- raise Cli::BadArgument,
121
- "#{what.to_s.capitalize} #{descr} is a #{value.class} but must be #{'one of: ' if type_list.length > 1}#{type_list.map(&:name).join(', ')}" unless
122
- type_list.any?{ |t| value.is_a?(t)}
123
- end
124
- end
125
185
  end
126
186
 
127
187
  attr_reader :parser
@@ -135,7 +195,7 @@ module Aspera
135
195
  @unprocessed_cmd_line_options = []
136
196
  # a copy of all initial options
137
197
  @initial_cli_options = []
138
- # option description: key = option symbol, value=Hash, :read_write, :accessor, :value, :accepted
198
+ # option description: option_symbol => OptionValue
139
199
  @declared_options = {}
140
200
  # do we ask missing options and arguments to user ?
141
201
  @ask_missing_mandatory = false # STDIN.isatty
@@ -180,35 +240,94 @@ module Aspera
180
240
  Log.log.trace1{"add_cmd_line_options:commands/arguments=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
181
241
  @parser.separator('')
182
242
  @parser.separator('OPTIONS: global')
183
- declare(:interactive, 'Use interactive input of missing params', values: :bool, handler: {o: self, m: :ask_missing_mandatory})
184
- declare(:ask_options, 'Ask even optional options', values: :bool, handler: {o: self, m: :ask_missing_optional})
185
- declare(:struct_parser, 'Default parser when expected value is a struct', values: %i[json ruby])
243
+ declare(:interactive, 'Use interactive input of missing params', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :ask_missing_mandatory})
244
+ declare(:ask_options, 'Ask even optional options', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :ask_missing_optional})
186
245
  # do not parse options yet, let's wait for option `-h` to be overridden
187
246
  end
188
247
 
189
- # @param descr [String] description for help
190
- # @param mandatory [Boolean] if true, raise error if option not set
191
- # @param multiple [Boolean] if true, return remaining arguments (Array)
192
- # @param accept_list [Array] list of allowed values (Symbol)
193
- # @param validation [Class, Array] accepted value type(s) or list of Symbols
194
- # @param aliases [Hash] map of aliases: key = alias, value = real value
195
- # @param default [Object] default value
196
- # @return one value, list or nil (if optional and no default)
197
- def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: String, aliases: nil, default: nil)
198
- Aspera.assert(accept_list.nil? || (accept_list.is_a?(Array) && accept_list.all?(Symbol)))
199
- validation = Symbol if accept_list
200
- Aspera.assert(validation.nil? || validation.is_a?(Class) || (validation.is_a?(Array) && validation.all?(Class))){'validation must be Class or Array of Class'}
201
- Aspera.assert(aliases.nil? || (aliases.is_a?(Hash) && aliases.keys.all?(Symbol) && aliases.values.all?(Symbol))){'aliases must be Hash:Symbol: Symbol'}
202
- allowed_types = validation
203
- unless allowed_types.nil?
204
- allowed_types = [allowed_types] unless allowed_types.is_a?(Array)
205
- descr = "#{descr} (#{allowed_types.join(', ')})"
248
+ # Declare an option
249
+ # @param option_symbol [Symbol] option name
250
+ # @param description [String] description for help
251
+ # @param short [String] short option name
252
+ # @param allowed [Object] Allowed values, see `OptionValue`
253
+ # @param default [Object] default value
254
+ # @param handler [Hash] handler for option value: keys: :o(object) and :m(method)
255
+ # @param deprecation [String] deprecation
256
+ # @param block [Proc] Block to execute when option is found
257
+ def declare(option_symbol, description, short: nil, allowed: nil, default: nil, handler: nil, deprecation: nil, &block)
258
+ Aspera.assert_type(option_symbol, Symbol)
259
+ Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
260
+ Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
261
+ Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with an uppercase"}
262
+ Aspera.assert(!['hash', 'extended value'].any?{ |s| description.downcase.include?(s)}){"#{option_symbol} shall use :allowed"}
263
+ Aspera.assert_type(handler, Hash) if handler
264
+ Aspera.assert(handler.keys.sort.eql?(%i[m o])) if handler
265
+ option_attrs = @declared_options[option_symbol] = OptionValue.new(
266
+ option: option_symbol,
267
+ description: description,
268
+ allowed: allowed,
269
+ handler: handler,
270
+ deprecation: deprecation
271
+ )
272
+ real_types = option_attrs.types&.reject{ |i| [NilClass, String, Symbol].include?(i)}
273
+ description = "#{description} (#{real_types.map(&:name).join(', ')})" if real_types && !real_types.empty? && !real_types.eql?(Allowed::TYPES_ENUM) && !real_types.eql?(Allowed::TYPES_BOOLEAN) && !real_types.eql?(Allowed::TYPES_STRING)
274
+ description = "#{description} (#{'deprecated'.blue}: #{deprecation})" if deprecation
275
+ set_option(option_symbol, default, where: 'default') unless default.nil?
276
+ on_args = [description]
277
+ case option_attrs.types
278
+ when Allowed::TYPES_ENUM, Allowed::TYPES_BOOLEAN
279
+ # This option value must be a symbol (or array of symbols)
280
+ set_option(option_symbol, Manager.enum_to_bool(default), where: 'default') if option_attrs.values.eql?(BOOLEAN_VALUES) && !default.nil?
281
+ value = get_option(option_symbol)
282
+ help_values =
283
+ if option_attrs.types.eql?(Allowed::TYPES_BOOLEAN)
284
+ highlight_current_in_list(BOOLEAN_SIMPLE, self.class.enum_to_yes_no(value))
285
+ else
286
+ highlight_current_in_list(option_attrs.values, value)
287
+ end
288
+ on_args[0] = "#{description}: #{help_values}"
289
+ on_args.push(symbol_to_option(option_symbol, 'ENUM'))
290
+ # on_args.push(option_attrs.values)
291
+ @parser.on(*on_args) do |v|
292
+ set_option(option_symbol, self.class.get_from_list(v.to_s, description, option_attrs.values), where: SOURCE_USER)
293
+ end
294
+ when Allowed::TYPES_NONE
295
+ Aspera.assert_type(block, Proc){"missing execution block for #{option_symbol}"}
296
+ on_args.push(symbol_to_option(option_symbol))
297
+ on_args.push("-#{short}") if short.is_a?(String)
298
+ @parser.on(*on_args, &block)
299
+ else
300
+ on_args.push(symbol_to_option(option_symbol, 'VALUE'))
301
+ on_args.push("-#{short}VALUE") unless short.nil?
302
+ # coerce integer
303
+ on_args.push(Integer) if option_attrs.types.eql?(Allowed::TYPES_INTEGER)
304
+ @parser.on(*on_args) do |v|
305
+ set_option(option_symbol, v, where: SOURCE_USER)
306
+ end
206
307
  end
308
+ Log.log.trace1{"on_args=#{on_args}"}
309
+ end
310
+
311
+ # @param descr [String] description for help
312
+ # @param mandatory [Boolean] if true, raise error if option not set
313
+ # @param multiple [Boolean] if true, return remaining arguments (Array)
314
+ # @param accept_list [Array, NilClass] list of allowed values (Symbol)
315
+ # @param validation [Class, Array, NilClass] Accepted value type(s) or list of Symbols
316
+ # @param aliases [Hash] map of aliases: key = alias, value = real value
317
+ # @param default [Object] default value
318
+ # @return one value, list or nil (if optional and no default)
319
+ def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: Allowed::TYPES_STRING, aliases: nil, default: nil)
320
+ Aspera.assert_array_all(accept_list, Symbol) unless accept_list.nil?
321
+ Aspera.assert_hash_all(aliases, Symbol, Symbol) unless aliases.nil?
322
+ validation = Symbol unless accept_list.nil?
323
+ validation = [validation] unless validation.is_a?(Array) || validation.nil?
324
+ Aspera.assert_array_all(validation, Class){'validation'} unless validation.nil?
325
+ descr = "#{descr} (#{validation.join(', ')})" unless validation.nil? || validation.eql?(Allowed::TYPES_STRING)
207
326
  result =
208
327
  if !@unprocessed_cmd_line_arguments.empty?
209
328
  how_many = multiple ? @unprocessed_cmd_line_arguments.length : 1
210
329
  values = @unprocessed_cmd_line_arguments.shift(how_many)
211
- values = values.map{ |v| evaluate_extended_value(v, allowed_types)}
330
+ values = values.map{ |v| ExtendedValue.instance.evaluate(v, context: "argument: #{descr}", allowed: validation)}
212
331
  # if expecting list and only one arg of type array : it is the list
213
332
  values = values.first if multiple && values.length.eql?(1) && values.first.is_a?(Array)
214
333
  if accept_list
@@ -221,7 +340,7 @@ module Aspera
221
340
  # no value provided, either get value interactively, or exception
222
341
  elsif mandatory then get_interactive(descr, multiple: multiple, accept_list: accept_list)
223
342
  end
224
- if result.is_a?(String) && validation.eql?(Integer)
343
+ if result.is_a?(String) && validation&.eql?(Allowed::TYPES_INTEGER)
225
344
  int_result = Integer(result, exception: false)
226
345
  raise Cli::BadArgument, "Invalid integer: #{result}" if int_result.nil?
227
346
  result = int_result
@@ -229,8 +348,14 @@ module Aspera
229
348
  Log.log.debug{"#{descr}=#{result}"}
230
349
  result = aliases[result] if aliases&.key?(result)
231
350
  # if value comes from JSON/YAML, it may come as Integer
232
- result = result.to_s if result.is_a?(Integer) && validation.eql?(String)
233
- self.class.validate_type(:argument, descr, result, allowed_types, check_array: multiple) unless result.nil? && !mandatory
351
+ result = result.to_s if result.is_a?(Integer) && validation&.eql?(Allowed::TYPES_STRING)
352
+ if validation && (mandatory || !result.nil?)
353
+ value_list = multiple ? result : [result]
354
+ value_list.each do |value|
355
+ raise Cli::BadArgument,
356
+ "Argument #{descr} is a #{value.class} but must be #{'one of: ' if validation.length > 1}#{validation.map(&:name).join(', ')}" unless validation.any?{ |t| value.is_a?(t)}
357
+ end
358
+ end
234
359
  return result
235
360
  end
236
361
 
@@ -240,146 +365,47 @@ module Aspera
240
365
  # either return value or calls handler, can return nil
241
366
  # ask interactively if requested/required
242
367
  # @param mandatory [Boolean] if true, raise error if option not set
243
- def get_option(option_symbol, mandatory: false, default: nil)
368
+ def get_option(option_symbol, mandatory: false)
244
369
  Aspera.assert_type(option_symbol, Symbol)
245
- attributes = @declared_options[option_symbol]
246
- Aspera.assert(attributes){"option not declared: #{option_symbol}"}
247
- result = nil
248
- case attributes[:read_write]
249
- when :accessor
250
- result = attributes[:accessor].value
251
- when :value
252
- result = attributes[:value]
253
- else Aspera.error_unexpected_value(attributes[:read_write]){'attribute read/write'}
254
- end
255
- Log.log.trace1{"(#{attributes[:read_write]}) get #{option_symbol}=#{result}"}
256
- result = default if result.nil?
257
- # do not fail for manual generation if option mandatory but not set
258
- result = :skip_missing_mandatory if result.nil? && mandatory && !@fail_on_missing_mandatory
259
- # Log.log.debug{"interactive=#{@ask_missing_mandatory}"}
370
+ Aspera.assert(@declared_options.key?(option_symbol), type: Cli::BadArgument){"Unknown option: #{option_symbol}"}
371
+ option_attrs = @declared_options[option_symbol]
372
+ result = option_attrs.value
373
+ # Do not fail for manual generation if option mandatory but not set
374
+ return :skip_missing_mandatory if result.nil? && mandatory && !@fail_on_missing_mandatory
260
375
  if result.nil?
261
376
  if !@ask_missing_mandatory
262
- raise Cli::BadArgument, "Missing mandatory option: #{option_symbol}" if mandatory
377
+ Aspera.assert(!mandatory, type: Cli::BadArgument){"Missing mandatory option: #{option_symbol}"}
263
378
  elsif @ask_missing_optional || mandatory
264
379
  # ask_missing_mandatory
265
- accept_list = nil
266
- # print "please enter: #{option_symbol.to_s}"
267
- accept_list = attributes[:values] if @declared_options.key?(option_symbol) && attributes.key?(:values)
268
- result = get_interactive(option_symbol.to_s, option: true, accept_list: accept_list)
380
+ result = get_interactive(option_symbol.to_s, check_option: true, accept_list: option_attrs.values)
269
381
  set_option(option_symbol, result, where: 'interactive')
270
382
  end
271
383
  end
272
- self.class.validate_type(:option, option_symbol, result, attributes[:types]) unless result.nil? && !mandatory
273
384
  return result
274
385
  end
275
386
 
276
- # set an option value by name, either store value or call handler
387
+ # Set an option value by name, either store value or call handler
388
+ # String is given to extended value
277
389
  # @param option_symbol [Symbol] option name
278
- # @param value [String] value to set
279
- # @param where [String] where the value comes from
280
- # @param expect [Class, Array] expected value type(s)
390
+ # @param value [String] Value to set
391
+ # @param where [String] Where the value comes from
281
392
  def set_option(option_symbol, value, where: 'code override')
282
393
  Aspera.assert_type(option_symbol, Symbol)
283
- raise Cli::BadArgument, "Unknown option: #{option_symbol}" unless @declared_options.key?(option_symbol)
284
- attributes = @declared_options[option_symbol]
285
- Log.log.warn("#{option_symbol}: Option is deprecated: #{attributes[:deprecation]}") if attributes[:deprecation]
286
- value = evaluate_extended_value(value, attributes[:types])
287
- value = Manager.enum_to_bool(value) if attributes[:values].eql?(BOOLEAN_VALUES)
288
- Log.log.trace1{"(#{attributes[:read_write]}/#{where}) set #{option_symbol}=#{value}"}
289
- self.class.validate_type(:option, option_symbol, value, attributes[:types])
290
- case attributes[:read_write]
291
- when :accessor
292
- attributes[:accessor].value = value
293
- when :value
294
- attributes[:value] = value
295
- else Aspera.error_unexpected_value(attributes[:read_write]){'attribute read/write'}
296
- end
394
+ Aspera.assert(@declared_options.key?(option_symbol), type: Cli::BadArgument){"Unknown option: #{option_symbol}"}
395
+ @declared_options[option_symbol].assign_value(value, where: where)
297
396
  end
298
397
 
299
- # declare an option
300
- # @param option_symbol [Symbol] option name
301
- # @param description [String] description for help
302
- # @param handler [Hash] handler for option value: keys: o (object) and m (method)
303
- # @param default [Object] default value
304
- # @param values [nil, Array, :bool, :date, :none] list of allowed values, :bool for true/false, :date for dates, :none for on/off switch
305
- # @param short [String] short option name
306
- # @param coerce [Class] one of the coerce types accepted by option parser
307
- # @param types [Class, Array] accepted value type(s)
308
- # @param block [Proc] block to execute when option is found
309
- def declare(option_symbol, description, handler: nil, default: nil, values: nil, short: nil, coerce: nil, types: nil, deprecation: nil, &block)
398
+ # Set option to `nil`
399
+ def clear_option(option_symbol)
310
400
  Aspera.assert_type(option_symbol, Symbol)
311
- Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
312
- Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
313
- Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with an uppercase"}
314
- Aspera.assert(!['hash', 'extended value'].any?{ |s| description.downcase.include?(s)}){"#{option_symbol} shall use :types"}
315
- opt = @declared_options[option_symbol] = {
316
- read_write: handler.nil? ? :value : :accessor,
317
- # by default passwords and secrets are sensitive, else specify when declaring the option
318
- sensitive: SecretHider.instance.secret?(option_symbol, '')
319
- }
320
- if !types.nil?
321
- types = [types] unless types.is_a?(Array)
322
- Aspera.assert(types.all?(Class)){"types must be (Array of) Class: #{types}"}
323
- opt[:types] = types
324
- description = "#{description} (#{types.map(&:name).join(', ')})"
325
- end
326
- if deprecation
327
- opt[:deprecation] = deprecation
328
- description = "#{description} (#{'deprecated'.blue}: #{deprecation})"
329
- end
330
- Log.log.trace1{"declare: #{option_symbol}: #{opt[:read_write]}".green}
331
- if opt[:read_write].eql?(:accessor)
332
- Aspera.assert_type(handler, Hash)
333
- Aspera.assert(handler.keys.sort.eql?(%i[m o]))
334
- Log.log.trace1{"set attr obj: #{option_symbol} (#{handler[:o]},#{handler[:m]})"}
335
- opt[:accessor] = AttrAccessor.new(handler[:o], handler[:m], option_symbol)
336
- end
337
- set_option(option_symbol, default, where: 'default') unless default.nil?
338
- on_args = [description]
339
- case values
340
- when nil
341
- on_args.push(symbol_to_option(option_symbol, 'VALUE'))
342
- on_args.push("-#{short}VALUE") unless short.nil?
343
- on_args.push(coerce) unless coerce.nil?
344
- @parser.on(*on_args){ |v| set_option(option_symbol, v, where: SOURCE_USER)}
345
- when Array, :bool
346
- if values.eql?(:bool)
347
- values = BOOLEAN_VALUES
348
- set_option(option_symbol, Manager.enum_to_bool(default), where: 'default') unless default.nil?
349
- end
350
- # this option value must be a symbol
351
- opt[:values] = values
352
- value = get_option(option_symbol)
353
- help_values = values.map{ |i| i.eql?(value) ? highlight_current(i) : i}.join(', ')
354
- if values.eql?(BOOLEAN_VALUES)
355
- help_values = BOOLEAN_SIMPLE.map{ |i| (i.eql?(:yes) && value) || (i.eql?(:no) && !value) ? highlight_current(i) : i}.join(', ')
356
- end
357
- on_args[0] = "#{description}: #{help_values}"
358
- on_args.push(symbol_to_option(option_symbol, 'ENUM'))
359
- on_args.push(values)
360
- @parser.on(*on_args){ |v| set_option(option_symbol, self.class.get_from_list(v.to_s, description, values), where: SOURCE_USER)}
361
- when :date
362
- on_args.push(symbol_to_option(option_symbol, 'DATE'))
363
- @parser.on(*on_args) do |v|
364
- time_string = case v
365
- when 'now' then Manager.time_to_string(Time.now)
366
- when /^-([0-9]+)h/ then Manager.time_to_string(Time.now - (Regexp.last_match(1).to_i * 3600))
367
- else v
368
- end
369
- set_option(option_symbol, time_string, where: SOURCE_USER)
370
- end
371
- when :none
372
- Aspera.assert(!block.nil?){"missing block for #{option_symbol}"}
373
- on_args.push(symbol_to_option(option_symbol))
374
- on_args.push("-#{short}") if short.is_a?(String)
375
- @parser.on(*on_args, &block)
376
- else Aspera.error_unexpected_value(values)
377
- end
378
- Log.log.trace1{"on_args=#{on_args}"}
401
+ Aspera.assert(@declared_options.key?(option_symbol), type: Cli::BadArgument){"Unknown option: #{option_symbol}"}
402
+ @declared_options[option_symbol].clear
379
403
  end
380
404
 
381
405
  # Adds each of the keys of specified hash as an option
382
- # @param preset_hash [Hash] hash of options to add
406
+ # @param preset_hash [Hash] Options to add
407
+ # @param where [String] Where the value comes from
408
+ # @param override [Boolean] Override if already present
383
409
  def add_option_preset(preset_hash, where, override: true)
384
410
  Aspera.assert_type(preset_hash, Hash)
385
411
  Log.log.debug{"add_option_preset: #{preset_hash}, #{where}, #{override}"}
@@ -389,17 +415,17 @@ module Aspera
389
415
  end
390
416
  end
391
417
 
392
- # allows a plugin to add an argument as next argument to process
418
+ # Allows a plugin to add an argument as next argument to process
393
419
  def unshift_next_argument(argument)
394
420
  @unprocessed_cmd_line_arguments.unshift(argument)
395
421
  end
396
422
 
397
- # check if there were unprocessed values to generate error
423
+ # Check if there were unprocessed values to generate error
398
424
  def command_or_arg_empty?
399
425
  return @unprocessed_cmd_line_arguments.empty?
400
426
  end
401
427
 
402
- # unprocessed options or arguments ?
428
+ # Unprocessed options or arguments ?
403
429
  def final_errors
404
430
  result = []
405
431
  result.push("unprocessed options: #{@unprocessed_cmd_line_options}") unless @unprocessed_cmd_line_options.empty?
@@ -407,7 +433,7 @@ module Aspera
407
433
  return result
408
434
  end
409
435
 
410
- # get all original options on command line used to generate a config in config file
436
+ # Get all original options on command line used to generate a config in config file
411
437
  # @return [Hash] options as taken from config file and command line just before command execution
412
438
  def unprocessed_options_with_value
413
439
  result = {}
@@ -419,7 +445,7 @@ module Aspera
419
445
  name = Regexp.last_match(1)
420
446
  value = Regexp.last_match(2)
421
447
  name.gsub!(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
422
- value = ExtendedValue.instance.evaluate(value)
448
+ value = ExtendedValue.instance.evaluate(value, context: "option: #{name}")
423
449
  Log.log.debug{"option #{name}=#{value}"}
424
450
  result[name] = value
425
451
  @unprocessed_cmd_line_options.delete(option_value)
@@ -443,27 +469,28 @@ module Aspera
443
469
  return result
444
470
  end
445
471
 
446
- # removes already known options from the list
472
+ # Removes already known options from the list
447
473
  def parse_options!
448
474
  Log.log.trace1('parse_options!'.red)
449
- # first conf file, then env var
475
+ # First options from conf file
450
476
  consume_option_pairs(@option_pairs_batch, 'set')
477
+ # Then, env var (to override)
451
478
  consume_option_pairs(@option_pairs_env, 'env')
452
- # command line override
479
+ # Then, command line override
453
480
  unknown_options = []
454
481
  begin
455
482
  # remove known options one by one, exception if unknown
456
483
  Log.log.trace1('Before parse')
484
+ Log.dump(:unprocessed_cmd_line_options, @unprocessed_cmd_line_options)
457
485
  @parser.parse!(@unprocessed_cmd_line_options)
458
486
  Log.log.trace1('After parse')
459
487
  rescue OptionParser::InvalidOption => e
460
488
  Log.log.trace1{"InvalidOption #{e}".red}
461
489
  if (m = e.args.first.match(/^--([a-z\-]+)\.([^=]+)=(.+)$/))
462
- option, path, raw_value = m.captures
490
+ option, path, value = m.captures
463
491
  option_sym = self.class.option_line_to_name(option).to_sym
464
492
  if @declared_options.key?(option_sym)
465
- value = path.split(OPTION_HASH_SEPARATOR).reverse.inject(smart_convert(raw_value)){ |v, k| {k => v}}
466
- set_option(option_sym, value, where: 'dotted')
493
+ set_option(option_sym, dotted_to_extended(path, value), where: 'dotted')
467
494
  retry
468
495
  end
469
496
  end
@@ -500,21 +527,21 @@ module Aspera
500
527
  end
501
528
 
502
529
  # Prompt user for input in a list of symbols
503
- # @param descr [String] description for help
504
- # @param option [Boolean] true if command line option
505
- # @param multiple [Boolean] true if multiple values expected
506
- # @param accept_list [Array] list of expected values
507
- def get_interactive(descr, option: false, multiple: false, accept_list: nil)
508
- what = option ? 'option' : 'argument'
530
+ # @param descr [String] description for help
531
+ # @param check_option [Boolean] Check attributes of option with name=descr
532
+ # @param multiple [Boolean] true if multiple values expected
533
+ # @param accept_list [Array] list of expected values
534
+ def get_interactive(descr, check_option: false, multiple: false, accept_list: nil)
535
+ option_attrs = @declared_options[descr.to_sym]
536
+ what = option_attrs ? 'option' : 'argument'
509
537
  if !@ask_missing_mandatory
510
538
  message = "missing #{what}: #{descr}"
511
539
  if accept_list.nil?
512
540
  raise Cli::BadArgument, message
513
541
  else
514
- self.class.multi_choice_assert(false, message, accept_list)
542
+ Aspera.assert(false, self.class.multi_choice_assert_msg(message, accept_list), type: Cli::MissingArgument)
515
543
  end
516
544
  end
517
- sensitive = option && @declared_options[descr.to_sym].is_a?(Hash) && @declared_options[descr.to_sym][:sensitive]
518
545
  default_prompt = "#{what}: #{descr}"
519
546
  # ask interactively
520
547
  result = []
@@ -522,9 +549,9 @@ module Aspera
522
549
  loop do
523
550
  prompt = default_prompt
524
551
  prompt = "#{accept_list.join(' ')}\n#{default_prompt}" if accept_list
525
- entry = prompt_user_input(prompt, sensitive: sensitive)
552
+ entry = prompt_user_input(prompt, sensitive: option_attrs&.sensitive)
526
553
  break if entry.empty? && multiple
527
- entry = ExtendedValue.instance.evaluate(entry)
554
+ entry = ExtendedValue.instance.evaluate(entry, context: 'interactive input')
528
555
  entry = self.class.get_from_list(entry, descr, accept_list) if accept_list
529
556
  return entry unless multiple
530
557
  result.push(entry)
@@ -532,24 +559,69 @@ module Aspera
532
559
  return result
533
560
  end
534
561
 
562
+ # Read remaining args and build an Array or Hash
563
+ # @param value [nil] Argument to `@:` extended value
564
+ def args_as_extended(value)
565
+ # This extended value does not take args (`@:`)
566
+ ExtendedValue.assert_no_value(value, :p)
567
+ result = nil
568
+ get_next_argument(:args, multiple: true).each do |arg|
569
+ Aspera.assert(arg.include?(OPTION_VALUE_SEPARATOR)){"Positional argument: #{arg} does not inlude #{OPTION_VALUE_SEPARATOR}"}
570
+ path, raw = arg.split(OPTION_VALUE_SEPARATOR, 2)
571
+ result = dotted_to_extended(path, raw, result)
572
+ end
573
+ result
574
+ end
575
+
576
+ # ======================================================
535
577
  private
536
578
 
537
579
  # Using dotted hash notation, convert value to bool, int, float or extended value
580
+ # @param value [String] The value to convert to appropriate type
581
+ # @return the converted value
538
582
  def smart_convert(value)
539
- return true if value == 'true'
540
- return false if value == 'false'
541
- Integer(value)
542
- rescue ::ArgumentError
543
- begin
544
- Float(value)
545
- rescue ::ArgumentError
546
- evaluate_extended_value(value, nil)
583
+ case value
584
+ when 'true' then true
585
+ when 'false' then false
586
+ else
587
+ Integer(value, exception: false) ||
588
+ Float(value, exception: false) ||
589
+ ExtendedValue.instance.evaluate(value, context: 'dotted expression')
547
590
  end
548
591
  end
549
592
 
550
- def evaluate_extended_value(value, types)
551
- return ExtendedValue.instance.evaluate_with_default(value) if DEFAULT_PARSER_TYPES.include?(types) || (types.is_a?(Array) && types.all?{ |t| DEFAULT_PARSER_TYPES.include?(t)})
552
- return ExtendedValue.instance.evaluate(value)
593
+ # Convert `String` to `Integer`, or keep `String` if not `Integer`
594
+ def int_or_string(value)
595
+ Integer(value, exception: false) || value
596
+ end
597
+
598
+ def new_hash_or_array_from_key(key)
599
+ key.is_a?(Integer) ? [] : {}
600
+ end
601
+
602
+ def array_requires_integer_index!(container, index)
603
+ Aspera.assert(container.is_a?(Hash) || index.is_a?(Integer)){'Using String index when Integer index used previously'}
604
+ end
605
+
606
+ # Insert extended value `value` into struct `result` at `path`
607
+ # @param path [String]
608
+ # @param value [String]
609
+ # @param result [NilClass, Hash, Array]
610
+ # @return [Hash, Array]
611
+ def dotted_to_extended(path, value, result = nil)
612
+ # Typed keys
613
+ keys = path.split(OPTION_DOTTED_SEPARATOR).map{ |k| int_or_string(k)}
614
+ # Create, or re-used higher level container
615
+ current = (result ||= new_hash_or_array_from_key(keys.first))
616
+ # walk the path, and create sub-containers if necessary
617
+ keys.each_cons(2) do |k, next_k|
618
+ array_requires_integer_index!(current, k)
619
+ current = (current[k] ||= new_hash_or_array_from_key(next_k))
620
+ end
621
+ # Assign value at last index
622
+ array_requires_integer_index!(current, keys.last)
623
+ current[keys.last] = smart_convert(value)
624
+ result
553
625
  end
554
626
 
555
627
  # generate command line option from option symbol
@@ -560,11 +632,18 @@ module Aspera
560
632
  end
561
633
 
562
634
  # TODO: use formatter
563
- def highlight_current(value)
564
- $stdout.isatty ? value.to_s.red.bold : "[#{value}]"
635
+ # @return [String] comma separated list of values, with the current value highlighted
636
+ def highlight_current_in_list(list, current)
637
+ list.map do |i|
638
+ if i.eql?(current)
639
+ $stdout.isatty ? i.to_s.red.bold : "[#{i}]"
640
+ else
641
+ i
642
+ end
643
+ end.join(', ')
565
644
  end
566
645
 
567
- # try to evaluate options set in batch
646
+ # Try to evaluate options set in batch
568
647
  # @param unprocessed_options [Array] list of options to apply (key_sym,value)
569
648
  # @param where [String] where the options come from
570
649
  def consume_option_pairs(unprocessed_options, where)
@@ -573,7 +652,7 @@ module Aspera
573
652
  unprocessed_options.each do |k, v|
574
653
  if @declared_options.key?(k)
575
654
  # constrained parameters as string are revert to symbol
576
- v = self.class.get_from_list(v, "#{k} in #{where}", @declared_options[k][:values]) if @declared_options[k].key?(:values) && v.is_a?(String)
655
+ v = self.class.get_from_list(v, "#{k} in #{where}", @declared_options[k].values) if @declared_options[k].values && v.is_a?(String)
577
656
  options_to_set[k] = v
578
657
  else
579
658
  Log.log.trace1{"unprocessed: #{k}: #{v}"}
@@ -585,6 +664,28 @@ module Aspera
585
664
  unprocessed_options.delete(k)
586
665
  end
587
666
  end
667
+ # boolean options are set to true/false from the following values
668
+ BOOL_YES = BOOLEAN_SIMPLE.last
669
+ BOOL_NO = BOOLEAN_SIMPLE.first
670
+ FALSE_VALUES = [BOOL_NO, false].freeze
671
+ TRUE_VALUES = [BOOL_YES, true].freeze
672
+ BOOLEAN_VALUES = (TRUE_VALUES + FALSE_VALUES).freeze
673
+
674
+ # Option name separator on command line, e.g. in --option-blah, third "-"
675
+ OPTION_SEP_LINE = '-'
676
+ # Option name separator in code (symbol), e.g. in :option_blah, the "_"
677
+ OPTION_SEP_SYMBOL = '_'
678
+ # Option value separator on command line, e.g. in --option-blah=foo, the "="
679
+ OPTION_VALUE_SEPARATOR = '='
680
+ # "." : An option like --a.b.c=d does: a={"b":{"c":ext_val(d)}}
681
+ OPTION_DOTTED_SEPARATOR = '.'
682
+ # Starts an option, e.g. in --option-blah, the two first "--"
683
+ OPTION_PREFIX = '--'
684
+ # when this is alone, this stops option processing
685
+ OPTIONS_STOP = '--'
686
+ SOURCE_USER = 'cmdline' # cspell:disable-line
687
+
688
+ private_constant :BOOL_YES, :BOOL_NO, :FALSE_VALUES, :TRUE_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_DOTTED_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER
588
689
  end
589
690
  end
590
691
  end