aspera-cli 4.24.1 → 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 (99) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +1064 -745
  4. data/CONTRIBUTING.md +43 -100
  5. data/README.md +1281 -720
  6. data/bin/ascli +20 -1
  7. data/bin/asession +23 -27
  8. data/lib/aspera/agent/base.rb +10 -21
  9. data/lib/aspera/agent/connect.rb +2 -3
  10. data/lib/aspera/agent/desktop.rb +2 -2
  11. data/lib/aspera/agent/direct.rb +49 -32
  12. data/lib/aspera/agent/factory.rb +31 -0
  13. data/lib/aspera/api/aoc.rb +134 -76
  14. data/lib/aspera/api/cos_node.rb +3 -2
  15. data/lib/aspera/api/faspex.rb +213 -0
  16. data/lib/aspera/api/node.rb +107 -94
  17. data/lib/aspera/ascmd.rb +1 -2
  18. data/lib/aspera/ascp/installation.rb +73 -58
  19. data/lib/aspera/ascp/management.rb +119 -23
  20. data/lib/aspera/assert.rb +39 -11
  21. data/lib/aspera/cli/error.rb +4 -2
  22. data/lib/aspera/cli/extended_value.rb +91 -67
  23. data/lib/aspera/cli/formatter.rb +62 -27
  24. data/lib/aspera/cli/hints.rb +8 -0
  25. data/lib/aspera/cli/info.rb +4 -4
  26. data/lib/aspera/cli/main.rb +76 -84
  27. data/lib/aspera/cli/manager.rb +352 -248
  28. data/lib/aspera/cli/plugins/alee.rb +5 -4
  29. data/lib/aspera/cli/plugins/aoc.rb +175 -195
  30. data/lib/aspera/cli/plugins/ats.rb +4 -4
  31. data/lib/aspera/cli/plugins/base.rb +343 -0
  32. data/lib/aspera/cli/plugins/basic_auth.rb +45 -0
  33. data/lib/aspera/cli/plugins/config.rb +283 -269
  34. data/lib/aspera/cli/plugins/console.rb +27 -22
  35. data/lib/aspera/cli/plugins/cos.rb +3 -3
  36. data/lib/aspera/cli/plugins/factory.rb +78 -0
  37. data/lib/aspera/cli/plugins/faspex.rb +49 -46
  38. data/lib/aspera/cli/plugins/faspex5.rb +113 -225
  39. data/lib/aspera/cli/plugins/faspio.rb +19 -18
  40. data/lib/aspera/cli/plugins/httpgw.rb +14 -13
  41. data/lib/aspera/cli/plugins/node.rb +162 -149
  42. data/lib/aspera/cli/plugins/oauth.rb +48 -0
  43. data/lib/aspera/cli/plugins/orchestrator.rb +129 -45
  44. data/lib/aspera/cli/plugins/preview.rb +30 -50
  45. data/lib/aspera/cli/plugins/server.rb +21 -21
  46. data/lib/aspera/cli/plugins/shares.rb +45 -47
  47. data/lib/aspera/cli/sync_actions.rb +50 -39
  48. data/lib/aspera/cli/transfer_agent.rb +35 -49
  49. data/lib/aspera/cli/transfer_progress.rb +6 -6
  50. data/lib/aspera/cli/version.rb +3 -3
  51. data/lib/aspera/cli/wizard.rb +70 -55
  52. data/lib/aspera/colors.rb +6 -0
  53. data/lib/aspera/command_line_builder.rb +59 -61
  54. data/lib/aspera/command_line_converter.rb +2 -1
  55. data/lib/aspera/coverage.rb +2 -2
  56. data/lib/aspera/data_repository.rb +1 -1
  57. data/lib/aspera/environment.rb +51 -41
  58. data/lib/aspera/faspex_gw.rb +7 -5
  59. data/lib/aspera/faspex_postproc.rb +1 -1
  60. data/lib/aspera/keychain/factory.rb +1 -2
  61. data/lib/aspera/keychain/macos_security.rb +1 -1
  62. data/lib/aspera/log.rb +37 -9
  63. data/lib/aspera/markdown.rb +31 -0
  64. data/lib/aspera/nagios.rb +7 -6
  65. data/lib/aspera/oauth/base.rb +25 -28
  66. data/lib/aspera/oauth/factory.rb +9 -9
  67. data/lib/aspera/oauth/url_json.rb +2 -1
  68. data/lib/aspera/oauth/web.rb +2 -2
  69. data/lib/aspera/preview/file_types.rb +23 -37
  70. data/lib/aspera/products/connect.rb +7 -6
  71. data/lib/aspera/products/desktop.rb +1 -4
  72. data/lib/aspera/products/other.rb +9 -1
  73. data/lib/aspera/products/transferd.rb +0 -1
  74. data/lib/aspera/rest.rb +168 -113
  75. data/lib/aspera/rest_error_analyzer.rb +4 -4
  76. data/lib/aspera/ssh.rb +7 -4
  77. data/lib/aspera/ssl.rb +41 -0
  78. data/lib/aspera/sync/args.schema.yaml +46 -3
  79. data/lib/aspera/sync/conf.schema.yaml +307 -123
  80. data/lib/aspera/sync/database.rb +2 -1
  81. data/lib/aspera/sync/operations.rb +135 -79
  82. data/lib/aspera/temp_file_manager.rb +17 -5
  83. data/lib/aspera/transfer/error.rb +16 -7
  84. data/lib/aspera/transfer/parameters.rb +35 -22
  85. data/lib/aspera/transfer/resumer.rb +74 -0
  86. data/lib/aspera/transfer/spec.rb +5 -5
  87. data/lib/aspera/transfer/spec.schema.yaml +170 -59
  88. data/lib/aspera/transfer/spec_doc.rb +49 -43
  89. data/lib/aspera/uri_reader.rb +2 -2
  90. data/lib/aspera/web_auth.rb +6 -6
  91. data/lib/transferd_pb.rb +2 -2
  92. data.tar.gz.sig +0 -0
  93. metadata +26 -11
  94. metadata.gz.sig +0 -0
  95. data/lib/aspera/cli/basic_auth_plugin.rb +0 -43
  96. data/lib/aspera/cli/plugin.rb +0 -333
  97. data/lib/aspera/cli/plugin_factory.rb +0 -81
  98. data/lib/aspera/resumer.rb +0 -77
  99. data/lib/aspera/transfer/error_info.rb +0 -91
@@ -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,12 +195,13 @@ 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
142
202
  # ask optional options if not provided and in interactive
143
203
  @ask_missing_optional = false
204
+ # get_option fails if a mandatory parameter is asked
144
205
  @fail_on_missing_mandatory = true
145
206
  # Array of [key(sym), value]
146
207
  # those must be set before parse
@@ -179,35 +240,94 @@ module Aspera
179
240
  Log.log.trace1{"add_cmd_line_options:commands/arguments=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
180
241
  @parser.separator('')
181
242
  @parser.separator('OPTIONS: global')
182
- declare(:interactive, 'Use interactive input of missing params', values: :bool, handler: {o: self, m: :ask_missing_mandatory})
183
- declare(:ask_options, 'Ask even optional options', values: :bool, handler: {o: self, m: :ask_missing_optional})
184
- declare(:struct_parser, 'Default parser when expected value is a struct', values: %i[json ruby])
185
- # do not parse options yet, let's wait for option `-h` to be overriden
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})
245
+ # do not parse options yet, let's wait for option `-h` to be overridden
186
246
  end
187
247
 
188
- # @param descr [String] description for help
189
- # @param mandatory [Boolean] if true, raise error if option not set
190
- # @param multiple [Boolean] if true, return remaining arguments (Array)
191
- # @param accept_list [Array] list of allowed values (Symbol)
192
- # @param validation [Class, Array] accepted value type(s) or list of Symbols
193
- # @param aliases [Hash] map of aliases: key = alias, value = real value
194
- # @param default [Object] default value
195
- # @return one value, list or nil (if optional and no default)
196
- def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: String, aliases: nil, default: nil)
197
- Aspera.assert(accept_list.nil? || (accept_list.is_a?(Array) && accept_list.all?(Symbol)))
198
- validation = Symbol if accept_list
199
- Aspera.assert(validation.nil? || validation.is_a?(Class) || (validation.is_a?(Array) && validation.all?(Class))){'validation must be Class or Array of Class'}
200
- Aspera.assert(aliases.nil? || (aliases.is_a?(Hash) && aliases.keys.all?(Symbol) && aliases.values.all?(Symbol))){'aliases must be Hash:Symbol: Symbol'}
201
- allowed_types = validation
202
- unless allowed_types.nil?
203
- allowed_types = [allowed_types] unless allowed_types.is_a?(Array)
204
- 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
205
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)
206
326
  result =
207
327
  if !@unprocessed_cmd_line_arguments.empty?
208
328
  how_many = multiple ? @unprocessed_cmd_line_arguments.length : 1
209
329
  values = @unprocessed_cmd_line_arguments.shift(how_many)
210
- values = values.map{ |v| evaluate_extended_value(v, allowed_types)}
330
+ values = values.map{ |v| ExtendedValue.instance.evaluate(v, context: "argument: #{descr}", allowed: validation)}
211
331
  # if expecting list and only one arg of type array : it is the list
212
332
  values = values.first if multiple && values.length.eql?(1) && values.first.is_a?(Array)
213
333
  if accept_list
@@ -220,7 +340,7 @@ module Aspera
220
340
  # no value provided, either get value interactively, or exception
221
341
  elsif mandatory then get_interactive(descr, multiple: multiple, accept_list: accept_list)
222
342
  end
223
- if result.is_a?(String) && validation.eql?(Integer)
343
+ if result.is_a?(String) && validation&.eql?(Allowed::TYPES_INTEGER)
224
344
  int_result = Integer(result, exception: false)
225
345
  raise Cli::BadArgument, "Invalid integer: #{result}" if int_result.nil?
226
346
  result = int_result
@@ -228,8 +348,14 @@ module Aspera
228
348
  Log.log.debug{"#{descr}=#{result}"}
229
349
  result = aliases[result] if aliases&.key?(result)
230
350
  # if value comes from JSON/YAML, it may come as Integer
231
- result = result.to_s if result.is_a?(Integer) && validation.eql?(String)
232
- 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
233
359
  return result
234
360
  end
235
361
 
@@ -239,146 +365,47 @@ module Aspera
239
365
  # either return value or calls handler, can return nil
240
366
  # ask interactively if requested/required
241
367
  # @param mandatory [Boolean] if true, raise error if option not set
242
- def get_option(option_symbol, mandatory: false, default: nil)
368
+ def get_option(option_symbol, mandatory: false)
243
369
  Aspera.assert_type(option_symbol, Symbol)
244
- attributes = @declared_options[option_symbol]
245
- Aspera.assert(attributes){"option not declared: #{option_symbol}"}
246
- result = nil
247
- case attributes[:read_write]
248
- when :accessor
249
- result = attributes[:accessor].value
250
- when :value
251
- result = attributes[:value]
252
- else Aspera.error_unexpected_value(attributes[:read_write]){'attribute read/write'}
253
- end
254
- Log.log.trace1{"(#{attributes[:read_write]}) get #{option_symbol}=#{result}"}
255
- result = default if result.nil?
256
- # do not fail for manual generation if option mandatory but not set
257
- result = '' if result.nil? && mandatory && !@fail_on_missing_mandatory
258
- # 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
259
375
  if result.nil?
260
376
  if !@ask_missing_mandatory
261
- raise Cli::BadArgument, "Missing mandatory option: #{option_symbol}" if mandatory
377
+ Aspera.assert(!mandatory, type: Cli::BadArgument){"Missing mandatory option: #{option_symbol}"}
262
378
  elsif @ask_missing_optional || mandatory
263
379
  # ask_missing_mandatory
264
- accept_list = nil
265
- # print "please enter: #{option_symbol.to_s}"
266
- accept_list = attributes[:values] if @declared_options.key?(option_symbol) && attributes.key?(:values)
267
- 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)
268
381
  set_option(option_symbol, result, where: 'interactive')
269
382
  end
270
383
  end
271
- self.class.validate_type(:option, option_symbol, result, attributes[:types]) unless result.nil? && !mandatory
272
384
  return result
273
385
  end
274
386
 
275
- # 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
276
389
  # @param option_symbol [Symbol] option name
277
- # @param value [String] value to set
278
- # @param where [String] where the value comes from
279
- # @param expect [Class, Array] expected value type(s)
390
+ # @param value [String] Value to set
391
+ # @param where [String] Where the value comes from
280
392
  def set_option(option_symbol, value, where: 'code override')
281
393
  Aspera.assert_type(option_symbol, Symbol)
282
- raise Cli::BadArgument, "Unknown option: #{option_symbol}" unless @declared_options.key?(option_symbol)
283
- attributes = @declared_options[option_symbol]
284
- Log.log.warn("#{option_symbol}: Option is deprecated: #{attributes[:deprecation]}") if attributes[:deprecation]
285
- value = evaluate_extended_value(value, attributes[:types])
286
- value = Manager.enum_to_bool(value) if attributes[:values].eql?(BOOLEAN_VALUES)
287
- Log.log.trace1{"(#{attributes[:read_write]}/#{where}) set #{option_symbol}=#{value}"}
288
- self.class.validate_type(:option, option_symbol, value, attributes[:types])
289
- case attributes[:read_write]
290
- when :accessor
291
- attributes[:accessor].value = value
292
- when :value
293
- attributes[:value] = value
294
- else Aspera.error_unexpected_value(attributes[:read_write]){'attribute read/write'}
295
- 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)
296
396
  end
297
397
 
298
- # declare an option
299
- # @param option_symbol [Symbol] option name
300
- # @param description [String] description for help
301
- # @param handler [Hash] handler for option value: keys: o (object) and m (method)
302
- # @param default [Object] default value
303
- # @param values [nil, Array, :bool, :date, :none] list of allowed values, :bool for true/false, :date for dates, :none for on/off switch
304
- # @param short [String] short option name
305
- # @param coerce [Class] one of the coerce types accepted by option parser
306
- # @param types [Class, Array] accepted value type(s)
307
- # @param block [Proc] block to execute when option is found
308
- 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)
309
400
  Aspera.assert_type(option_symbol, Symbol)
310
- Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
311
- Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
312
- Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with an uppercase"}
313
- Aspera.assert(!['hash', 'extended value'].any?{ |s| description.downcase.include?(s)}){"#{option_symbol} shall use :types"}
314
- opt = @declared_options[option_symbol] = {
315
- read_write: handler.nil? ? :value : :accessor,
316
- # by default passwords and secrets are sensitive, else specify when declaring the option
317
- sensitive: SecretHider.instance.secret?(option_symbol, '')
318
- }
319
- if !types.nil?
320
- types = [types] unless types.is_a?(Array)
321
- Aspera.assert(types.all?(Class)){"types must be (Array of) Class: #{types}"}
322
- opt[:types] = types
323
- description = "#{description} (#{types.map(&:name).join(', ')})"
324
- end
325
- if deprecation
326
- opt[:deprecation] = deprecation
327
- description = "#{description} (#{'deprecated'.blue}: #{deprecation})"
328
- end
329
- Log.log.trace1{"declare: #{option_symbol}: #{opt[:read_write]}".green}
330
- if opt[:read_write].eql?(:accessor)
331
- Aspera.assert_type(handler, Hash)
332
- Aspera.assert(handler.keys.sort.eql?(%i[m o]))
333
- Log.log.trace1{"set attr obj: #{option_symbol} (#{handler[:o]},#{handler[:m]})"}
334
- opt[:accessor] = AttrAccessor.new(handler[:o], handler[:m], option_symbol)
335
- end
336
- set_option(option_symbol, default, where: 'default') unless default.nil?
337
- on_args = [description]
338
- case values
339
- when nil
340
- on_args.push(symbol_to_option(option_symbol, 'VALUE'))
341
- on_args.push("-#{short}VALUE") unless short.nil?
342
- on_args.push(coerce) unless coerce.nil?
343
- @parser.on(*on_args){ |v| set_option(option_symbol, v, where: SOURCE_USER)}
344
- when Array, :bool
345
- if values.eql?(:bool)
346
- values = BOOLEAN_VALUES
347
- set_option(option_symbol, Manager.enum_to_bool(default), where: 'default') unless default.nil?
348
- end
349
- # this option value must be a symbol
350
- opt[:values] = values
351
- value = get_option(option_symbol)
352
- help_values = values.map{ |i| i.eql?(value) ? highlight_current(i) : i}.join(', ')
353
- if values.eql?(BOOLEAN_VALUES)
354
- help_values = BOOLEAN_SIMPLE.map{ |i| (i.eql?(:yes) && value) || (i.eql?(:no) && !value) ? highlight_current(i) : i}.join(', ')
355
- end
356
- on_args[0] = "#{description}: #{help_values}"
357
- on_args.push(symbol_to_option(option_symbol, 'ENUM'))
358
- on_args.push(values)
359
- @parser.on(*on_args){ |v| set_option(option_symbol, self.class.get_from_list(v.to_s, description, values), where: SOURCE_USER)}
360
- when :date
361
- on_args.push(symbol_to_option(option_symbol, 'DATE'))
362
- @parser.on(*on_args) do |v|
363
- time_string = case v
364
- when 'now' then Manager.time_to_string(Time.now)
365
- when /^-([0-9]+)h/ then Manager.time_to_string(Time.now - (Regexp.last_match(1).to_i * 3600))
366
- else v
367
- end
368
- set_option(option_symbol, time_string, where: SOURCE_USER)
369
- end
370
- when :none
371
- Aspera.assert(!block.nil?){"missing block for #{option_symbol}"}
372
- on_args.push(symbol_to_option(option_symbol))
373
- on_args.push("-#{short}") if short.is_a?(String)
374
- @parser.on(*on_args, &block)
375
- else Aspera.error_unexpected_value(values)
376
- end
377
- 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
378
403
  end
379
404
 
380
405
  # Adds each of the keys of specified hash as an option
381
- # @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
382
409
  def add_option_preset(preset_hash, where, override: true)
383
410
  Aspera.assert_type(preset_hash, Hash)
384
411
  Log.log.debug{"add_option_preset: #{preset_hash}, #{where}, #{override}"}
@@ -388,17 +415,17 @@ module Aspera
388
415
  end
389
416
  end
390
417
 
391
- # 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
392
419
  def unshift_next_argument(argument)
393
420
  @unprocessed_cmd_line_arguments.unshift(argument)
394
421
  end
395
422
 
396
- # check if there were unprocessed values to generate error
423
+ # Check if there were unprocessed values to generate error
397
424
  def command_or_arg_empty?
398
425
  return @unprocessed_cmd_line_arguments.empty?
399
426
  end
400
427
 
401
- # unprocessed options or arguments ?
428
+ # Unprocessed options or arguments ?
402
429
  def final_errors
403
430
  result = []
404
431
  result.push("unprocessed options: #{@unprocessed_cmd_line_options}") unless @unprocessed_cmd_line_options.empty?
@@ -406,7 +433,7 @@ module Aspera
406
433
  return result
407
434
  end
408
435
 
409
- # 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
410
437
  # @return [Hash] options as taken from config file and command line just before command execution
411
438
  def unprocessed_options_with_value
412
439
  result = {}
@@ -418,7 +445,7 @@ module Aspera
418
445
  name = Regexp.last_match(1)
419
446
  value = Regexp.last_match(2)
420
447
  name.gsub!(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
421
- value = ExtendedValue.instance.evaluate(value)
448
+ value = ExtendedValue.instance.evaluate(value, context: "option: #{name}")
422
449
  Log.log.debug{"option #{name}=#{value}"}
423
450
  result[name] = value
424
451
  @unprocessed_cmd_line_options.delete(option_value)
@@ -436,31 +463,34 @@ module Aspera
436
463
  @declared_options.each_key do |option_symbol|
437
464
  v = get_option(option_symbol)
438
465
  result[option_symbol] = v unless only_defined && v.nil?
466
+ rescue => e
467
+ result[option_symbol] = e.to_s
439
468
  end
440
469
  return result
441
470
  end
442
471
 
443
- # removes already known options from the list
472
+ # Removes already known options from the list
444
473
  def parse_options!
445
474
  Log.log.trace1('parse_options!'.red)
446
- # first conf file, then env var
475
+ # First options from conf file
447
476
  consume_option_pairs(@option_pairs_batch, 'set')
477
+ # Then, env var (to override)
448
478
  consume_option_pairs(@option_pairs_env, 'env')
449
- # command line override
479
+ # Then, command line override
450
480
  unknown_options = []
451
481
  begin
452
482
  # remove known options one by one, exception if unknown
453
483
  Log.log.trace1('Before parse')
484
+ Log.dump(:unprocessed_cmd_line_options, @unprocessed_cmd_line_options)
454
485
  @parser.parse!(@unprocessed_cmd_line_options)
455
486
  Log.log.trace1('After parse')
456
487
  rescue OptionParser::InvalidOption => e
457
488
  Log.log.trace1{"InvalidOption #{e}".red}
458
489
  if (m = e.args.first.match(/^--([a-z\-]+)\.([^=]+)=(.+)$/))
459
- option, path, raw_value = m.captures
490
+ option, path, value = m.captures
460
491
  option_sym = self.class.option_line_to_name(option).to_sym
461
492
  if @declared_options.key?(option_sym)
462
- value = path.split(OPTION_HASH_SEPARATOR).reverse.inject(smart_convert(raw_value)){ |v, k| {k => v}}
463
- set_option(option_sym, value, where: 'dotted')
493
+ set_option(option_sym, dotted_to_extended(path, value), where: 'dotted')
464
494
  retry
465
495
  end
466
496
  end
@@ -497,21 +527,21 @@ module Aspera
497
527
  end
498
528
 
499
529
  # Prompt user for input in a list of symbols
500
- # @param descr [String] description for help
501
- # @param option [Boolean] true if command line option
502
- # @param multiple [Boolean] true if multiple values expected
503
- # @param accept_list [Array] list of expected values
504
- def get_interactive(descr, option: false, multiple: false, accept_list: nil)
505
- 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'
506
537
  if !@ask_missing_mandatory
507
538
  message = "missing #{what}: #{descr}"
508
539
  if accept_list.nil?
509
540
  raise Cli::BadArgument, message
510
541
  else
511
- 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)
512
543
  end
513
544
  end
514
- sensitive = option && @declared_options[descr.to_sym].is_a?(Hash) && @declared_options[descr.to_sym][:sensitive]
515
545
  default_prompt = "#{what}: #{descr}"
516
546
  # ask interactively
517
547
  result = []
@@ -519,9 +549,9 @@ module Aspera
519
549
  loop do
520
550
  prompt = default_prompt
521
551
  prompt = "#{accept_list.join(' ')}\n#{default_prompt}" if accept_list
522
- entry = prompt_user_input(prompt, sensitive: sensitive)
552
+ entry = prompt_user_input(prompt, sensitive: option_attrs&.sensitive)
523
553
  break if entry.empty? && multiple
524
- entry = ExtendedValue.instance.evaluate(entry)
554
+ entry = ExtendedValue.instance.evaluate(entry, context: 'interactive input')
525
555
  entry = self.class.get_from_list(entry, descr, accept_list) if accept_list
526
556
  return entry unless multiple
527
557
  result.push(entry)
@@ -529,24 +559,69 @@ module Aspera
529
559
  return result
530
560
  end
531
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
+ # ======================================================
532
577
  private
533
578
 
534
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
535
582
  def smart_convert(value)
536
- return true if value == 'true'
537
- return false if value == 'false'
538
- Integer(value)
539
- rescue ArgumentError
540
- begin
541
- Float(value)
542
- rescue ArgumentError
543
- 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')
544
590
  end
545
591
  end
546
592
 
547
- def evaluate_extended_value(value, types)
548
- 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)})
549
- 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
550
625
  end
551
626
 
552
627
  # generate command line option from option symbol
@@ -557,11 +632,18 @@ module Aspera
557
632
  end
558
633
 
559
634
  # TODO: use formatter
560
- def highlight_current(value)
561
- $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(', ')
562
644
  end
563
645
 
564
- # try to evaluate options set in batch
646
+ # Try to evaluate options set in batch
565
647
  # @param unprocessed_options [Array] list of options to apply (key_sym,value)
566
648
  # @param where [String] where the options come from
567
649
  def consume_option_pairs(unprocessed_options, where)
@@ -570,7 +652,7 @@ module Aspera
570
652
  unprocessed_options.each do |k, v|
571
653
  if @declared_options.key?(k)
572
654
  # constrained parameters as string are revert to symbol
573
- 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)
574
656
  options_to_set[k] = v
575
657
  else
576
658
  Log.log.trace1{"unprocessed: #{k}: #{v}"}
@@ -582,6 +664,28 @@ module Aspera
582
664
  unprocessed_options.delete(k)
583
665
  end
584
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
585
689
  end
586
690
  end
587
691
  end