aspera-cli 4.13.0 → 4.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +81 -7
  4. data/CONTRIBUTING.md +22 -6
  5. data/README.md +2038 -1080
  6. data/bin/ascli +18 -9
  7. data/bin/asession +12 -14
  8. data/examples/dascli +1 -1
  9. data/examples/proxy.pac +1 -1
  10. data/examples/rubyc +24 -0
  11. data/lib/aspera/aoc.rb +219 -159
  12. data/lib/aspera/ascmd.rb +25 -14
  13. data/lib/aspera/cli/basic_auth_plugin.rb +12 -9
  14. data/lib/aspera/cli/error.rb +17 -0
  15. data/lib/aspera/cli/extended_value.rb +47 -12
  16. data/lib/aspera/cli/formatter.rb +260 -179
  17. data/lib/aspera/cli/hints.rb +80 -0
  18. data/lib/aspera/cli/main.rb +104 -156
  19. data/lib/aspera/cli/manager.rb +259 -209
  20. data/lib/aspera/cli/plugin.rb +123 -63
  21. data/lib/aspera/cli/plugins/alee.rb +2 -3
  22. data/lib/aspera/cli/plugins/aoc.rb +341 -261
  23. data/lib/aspera/cli/plugins/ats.rb +22 -21
  24. data/lib/aspera/cli/plugins/bss.rb +5 -5
  25. data/lib/aspera/cli/plugins/config.rb +578 -627
  26. data/lib/aspera/cli/plugins/console.rb +44 -6
  27. data/lib/aspera/cli/plugins/cos.rb +15 -17
  28. data/lib/aspera/cli/plugins/faspex.rb +114 -100
  29. data/lib/aspera/cli/plugins/faspex5.rb +411 -264
  30. data/lib/aspera/cli/plugins/node.rb +354 -259
  31. data/lib/aspera/cli/plugins/orchestrator.rb +61 -29
  32. data/lib/aspera/cli/plugins/preview.rb +82 -90
  33. data/lib/aspera/cli/plugins/server.rb +79 -32
  34. data/lib/aspera/cli/plugins/shares.rb +55 -42
  35. data/lib/aspera/cli/sync_actions.rb +68 -0
  36. data/lib/aspera/cli/transfer_agent.rb +66 -73
  37. data/lib/aspera/cli/transfer_progress.rb +74 -0
  38. data/lib/aspera/cli/version.rb +1 -1
  39. data/lib/aspera/colors.rb +12 -8
  40. data/lib/aspera/command_line_builder.rb +14 -11
  41. data/lib/aspera/cos_node.rb +3 -2
  42. data/lib/aspera/data/6 +0 -0
  43. data/lib/aspera/environment.rb +24 -9
  44. data/lib/aspera/fasp/agent_aspera.rb +126 -0
  45. data/lib/aspera/fasp/agent_base.rb +31 -77
  46. data/lib/aspera/fasp/agent_connect.rb +25 -21
  47. data/lib/aspera/fasp/agent_direct.rb +89 -103
  48. data/lib/aspera/fasp/agent_httpgw.rb +231 -149
  49. data/lib/aspera/fasp/agent_node.rb +41 -34
  50. data/lib/aspera/fasp/agent_trsdk.rb +75 -32
  51. data/lib/aspera/fasp/error_info.rb +4 -2
  52. data/lib/aspera/fasp/faux_file.rb +52 -0
  53. data/lib/aspera/fasp/installation.rb +53 -195
  54. data/lib/aspera/fasp/management.rb +244 -0
  55. data/lib/aspera/fasp/parameters.rb +71 -37
  56. data/lib/aspera/fasp/parameters.yaml +76 -8
  57. data/lib/aspera/fasp/products.rb +162 -0
  58. data/lib/aspera/fasp/resume_policy.rb +3 -3
  59. data/lib/aspera/fasp/transfer_spec.rb +7 -6
  60. data/lib/aspera/fasp/uri.rb +26 -24
  61. data/lib/aspera/faspex_gw.rb +2 -2
  62. data/lib/aspera/faspex_postproc.rb +2 -2
  63. data/lib/aspera/hash_ext.rb +14 -4
  64. data/lib/aspera/json_rpc.rb +49 -0
  65. data/lib/aspera/keychain/macos_security.rb +13 -13
  66. data/lib/aspera/line_logger.rb +23 -0
  67. data/lib/aspera/log.rb +58 -16
  68. data/lib/aspera/node.rb +157 -92
  69. data/lib/aspera/oauth.rb +37 -19
  70. data/lib/aspera/open_application.rb +4 -4
  71. data/lib/aspera/persistency_action_once.rb +1 -1
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/file_types.rb +4 -2
  74. data/lib/aspera/preview/generator.rb +22 -35
  75. data/lib/aspera/preview/options.rb +2 -0
  76. data/lib/aspera/preview/terminal.rb +73 -16
  77. data/lib/aspera/preview/utils.rb +21 -28
  78. data/lib/aspera/proxy_auto_config.js +2 -2
  79. data/lib/aspera/rest.rb +136 -68
  80. data/lib/aspera/rest_call_error.rb +1 -1
  81. data/lib/aspera/rest_error_analyzer.rb +15 -14
  82. data/lib/aspera/rest_errors_aspera.rb +37 -34
  83. data/lib/aspera/secret_hider.rb +18 -15
  84. data/lib/aspera/ssh.rb +5 -2
  85. data/lib/aspera/sync.rb +127 -119
  86. data/lib/aspera/temp_file_manager.rb +10 -3
  87. data/lib/aspera/web_auth.rb +10 -7
  88. data/lib/aspera/web_server_simple.rb +9 -4
  89. data.tar.gz.sig +0 -0
  90. metadata +34 -17
  91. metadata.gz.sig +0 -0
  92. data/docs/test_env.conf +0 -186
  93. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  94. data/lib/aspera/cli/listener/logger.rb +0 -22
  95. data/lib/aspera/cli/listener/progress.rb +0 -50
  96. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  97. data/lib/aspera/cli/plugins/sync.rb +0 -44
  98. data/lib/aspera/data/7 +0 -0
  99. data/lib/aspera/fasp/listener.rb +0 -13
@@ -1,41 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aspera/cli/extended_value'
4
+ require 'aspera/cli/error'
3
5
  require 'aspera/colors'
6
+ require 'aspera/secret_hider'
4
7
  require 'aspera/log'
5
- require 'aspera/cli/extended_value'
6
- require 'optparse'
7
8
  require 'io/console'
9
+ require 'optparse'
8
10
 
9
11
  module Aspera
10
12
  module Cli
11
- # raised by cli on error conditions
12
- class CliError < StandardError; end
13
-
14
- # raised when an unexpected argument is provided
15
- class CliBadArgument < Aspera::Cli::CliError; end
16
-
17
- class CliNoSuchId < Aspera::Cli::CliError
18
- def initialize(res_type, res_id)
19
- msg = "No such #{res_type} identifier: #{res_id}"
20
- super(msg)
21
- end
22
- end
23
-
24
13
  # option is retrieved from another object using accessor
25
14
  class AttrAccessor
26
15
  # attr_accessor :object
27
- # attr_accessor :attr_symb
28
- def initialize(object, attr_symb)
16
+ # attr_accessor :method_name
17
+ def initialize(object, method_name, option_name)
29
18
  @object = object
30
- @attr_symb = attr_symb
19
+ @method = method_name
20
+ @option_name = option_name
21
+ @has_writer = @object.respond_to?(writer_method)
22
+ Log.log.debug{"AttrAccessor: #{@option_name}: #{@object.class}.#{@method}: writer=#{@has_writer}"}
23
+ raise "internal error: #{object} does not respond to #{method_name}" unless @object.respond_to?(@method)
31
24
  end
32
25
 
33
26
  def value
34
- return @object.send(@attr_symb)
27
+ return @object.send(@method) if @has_writer
28
+ return @object.send(@method, @option_name, :get)
35
29
  end
36
30
 
37
31
  def value=(val)
38
- @object.send("#{@attr_symb}=", val)
32
+ Log.log.trace1{"AttrAccessor: = #{@method} #{@option_name} :set #{val}, writer=#{@has_writer}"}
33
+ return @object.send(writer_method, val) if @has_writer
34
+ return @object.send(@method, @option_name, :set, val)
35
+ end
36
+
37
+ def writer_method
38
+ return "#{@method}="
39
39
  end
40
40
  end
41
41
 
@@ -52,9 +52,10 @@ module Aspera
52
52
  # option name separator on command line
53
53
  OPTION_SEP_LINE = '-'
54
54
  # option name separator in code (symbol)
55
- OPTION_SEP_SYMB = '_'
55
+ OPTION_SEP_SYMBOL = '_'
56
+ SOURCE_USER = 'cmdline' # cspell:disable-line
56
57
 
57
- private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMB
58
+ private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :SOURCE_USER
58
59
 
59
60
  class << self
60
61
  def enum_to_bool(enum)
@@ -67,13 +68,13 @@ module Aspera
67
68
  end
68
69
 
69
70
  # find shortened string value in allowed symbol list
70
- def get_from_list(shortval, descr, allowed_values)
71
+ def get_from_list(short_value, descr, allowed_values)
71
72
  # we accept shortcuts
72
- matching_exact = allowed_values.select{|i| i.to_s.eql?(shortval)}
73
+ matching_exact = allowed_values.select{|i| i.to_s.eql?(short_value)}
73
74
  return matching_exact.first if matching_exact.length == 1
74
- matching = allowed_values.select{|i| i.to_s.start_with?(shortval)}
75
- raise CliBadArgument, bad_arg_message_multi("unknown value for #{descr}: #{shortval}", allowed_values) if matching.empty?
76
- raise CliBadArgument, bad_arg_message_multi("ambiguous shortcut for #{descr}: #{shortval}", matching) unless matching.length.eql?(1)
75
+ matching = allowed_values.select{|i| i.to_s.start_with?(short_value)}
76
+ raise Cli::BadArgument, bad_arg_message_multi("unknown value for #{descr}: #{short_value}", allowed_values) if matching.empty?
77
+ raise Cli::BadArgument, bad_arg_message_multi("ambiguous shortcut for #{descr}: #{short_value}", matching) unless matching.length.eql?(1)
77
78
  return enum_to_bool(matching.first) if allowed_values.eql?(BOOLEAN_VALUES)
78
79
  return matching.first
79
80
  end
@@ -84,11 +85,19 @@ module Aspera
84
85
 
85
86
  # change option name with dash to name with underscore
86
87
  def option_line_to_name(name)
87
- return name.gsub(OPTION_SEP_LINE, OPTION_SEP_SYMB)
88
+ return name.gsub(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
88
89
  end
89
90
 
90
91
  def option_name_to_line(name)
91
- return "--#{name.to_s.gsub(OPTION_SEP_SYMB, OPTION_SEP_LINE)}"
92
+ return "--#{name.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)}"
93
+ end
94
+
95
+ def validate_type(what, descr, value, type_list)
96
+ return nil if type_list.nil?
97
+ raise 'internal error: types must be a Class Array' unless type_list.is_a?(Array) && type_list.all?(Class)
98
+ raise Cli::BadArgument,
99
+ "#{what.to_s.capitalize} #{descr} is a #{value.class} but must be #{type_list.length > 1 ? 'one of ' : ''}#{type_list.map(&:name).join(',')}" unless \
100
+ type_list.any?{|t|value.is_a?(t)}
92
101
  end
93
102
  end
94
103
 
@@ -96,14 +105,14 @@ module Aspera
96
105
  attr_accessor :ask_missing_mandatory, :ask_missing_optional
97
106
  attr_writer :fail_on_missing_mandatory
98
107
 
99
- def initialize(program_name, argv: nil)
108
+ def initialize(program_name)
100
109
  # command line values not starting with '-'
101
110
  @unprocessed_cmd_line_arguments = []
102
111
  # command line values starting with '-'
103
112
  @unprocessed_cmd_line_options = []
104
113
  # a copy of all initial options
105
114
  @initial_cli_options = []
106
- # option description: key = option symbol, value=hash, :type, :accessor, :value, :accepted
115
+ # option description: key = option symbol, value=hash, :read_write, :accessor, :value, :accepted
107
116
  @declared_options = {}
108
117
  # do we ask missing options and arguments to user ?
109
118
  @ask_missing_mandatory = false # STDIN.isatty
@@ -117,7 +126,7 @@ module Aspera
117
126
  @parser = OptionParser.new
118
127
  @parser.program_name = program_name
119
128
  # options can also be provided by env vars : --param-name -> ASCLI_PARAM_NAME
120
- env_prefix = program_name.upcase + OPTION_SEP_SYMB
129
+ env_prefix = program_name.upcase + OPTION_SEP_SYMBOL
121
130
  ENV.each do |k, v|
122
131
  if k.start_with?(env_prefix)
123
132
  @unprocessed_env.push([k[env_prefix.length..-1].downcase.to_sym, v])
@@ -126,211 +135,234 @@ module Aspera
126
135
  Log.log.debug{"env=#{@unprocessed_env}".red}
127
136
  @unprocessed_cmd_line_options = []
128
137
  @unprocessed_cmd_line_arguments = []
129
- # argv is nil when help is generated for every plugin
130
- unless argv.nil?
131
- @parser.separator('')
132
- @parser.separator('OPTIONS: global')
133
- set_obj_attr(:interactive, self, :ask_missing_mandatory)
134
- set_obj_attr(:ask_options, self, :ask_missing_optional)
135
- add_opt_boolean(:interactive, 'use interactive input of missing params')
136
- add_opt_boolean(:ask_options, 'ask even optional options')
137
- parse_options!
138
- process_options = true
139
- until argv.empty?
140
- value = argv.shift
141
- if process_options && value.start_with?('-')
142
- if value.eql?('--')
143
- process_options = false
144
- else
145
- @unprocessed_cmd_line_options.push(value)
146
- end
138
+ end
139
+
140
+ def parse_command_line(argv)
141
+ @parser.separator('')
142
+ @parser.separator('OPTIONS: global')
143
+ declare(:interactive, 'Use interactive input of missing params', values: :bool, handler: {o: self, m: :ask_missing_mandatory})
144
+ declare(:ask_options, 'Ask even optional options', values: :bool, handler: {o: self, m: :ask_missing_optional})
145
+ parse_options!
146
+ process_options = true
147
+ until argv.empty?
148
+ value = argv.shift
149
+ if process_options && value.start_with?('-')
150
+ if value.eql?('--')
151
+ process_options = false
147
152
  else
148
- @unprocessed_cmd_line_arguments.push(value)
153
+ @unprocessed_cmd_line_options.push(value)
149
154
  end
155
+ else
156
+ @unprocessed_cmd_line_arguments.push(value)
150
157
  end
151
158
  end
152
159
  @initial_cli_options = @unprocessed_cmd_line_options.dup
153
- Log.log.debug{"add_cmd_line_options:commands/args=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
160
+ Log.log.debug{"add_cmd_line_options:commands/arguments=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
154
161
  end
155
162
 
156
- def get_next_command(command_list); return get_next_argument('command', expected: command_list); end
157
-
163
+ # @param descr [String] description for help
158
164
  # @param expected is
159
- # - Array of allowed value (single value)
160
- # - :multiple for remaining values
161
- # - :single for a single unconstrained value
162
- # @param mandatory true/false
163
- # @param type expected class for result
165
+ # - Array of allowed value (single value)
166
+ # - :multiple for remaining values
167
+ # - :single for a single unconstrained value
168
+ # - :integer for a single integer value
169
+ # @param mandatory [Boolean] if true, raise error if option not set
170
+ # @param type [Class, Array] accepted value type(s)
171
+ # @param aliases [Hash] map of aliases: key = alias, value = real value
172
+ # @param default [Object] default value
164
173
  # @return value, list or nil
165
- def get_next_argument(descr, expected: :single, mandatory: true, type: nil)
174
+ def get_next_argument(descr, expected: :single, mandatory: true, type: nil, aliases: nil, default: nil)
166
175
  unless type.nil?
167
- raise 'internal: type must be a Class' unless type.is_a?(Class)
176
+ type = [type] unless type.is_a?(Array)
177
+ raise "INTERNAL ERROR: type must be Array of Class: #{type}" unless type.all?(Class)
168
178
  descr = "#{descr} (#{type})"
169
179
  end
170
- result = nil
171
- if !@unprocessed_cmd_line_arguments.empty?
172
- # there are values
173
- case expected
174
- when :single
175
- result = ExtendedValue.instance.evaluate(@unprocessed_cmd_line_arguments.shift)
176
- when :multiple
177
- result = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length).map{|v|ExtendedValue.instance.evaluate(v)}
178
- # if expecting list and only one arg of type array : it is the list
179
- if result.length.eql?(1) && result.first.is_a?(Array)
180
- result = result.first
180
+ result =
181
+ if !@unprocessed_cmd_line_arguments.empty?
182
+ # there are values
183
+ case expected
184
+ when :single
185
+ ExtendedValue.instance.evaluate(@unprocessed_cmd_line_arguments.shift)
186
+ when :multiple
187
+ value = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length).map{|v|ExtendedValue.instance.evaluate(v)}
188
+ # if expecting list and only one arg of type array : it is the list
189
+ if value.length.eql?(1) && value.first.is_a?(Array)
190
+ value = value.first
191
+ end
192
+ value
193
+ when Array
194
+ allowed_values = [].concat(expected)
195
+ allowed_values.concat(aliases.keys) unless aliases.nil?
196
+ raise "internal error: only symbols allowed: #{allowed_values}" unless allowed_values.all?(Symbol)
197
+ self.class.get_from_list(@unprocessed_cmd_line_arguments.shift, descr, allowed_values)
198
+ else
199
+ raise 'Internal error: expected: must be single, multiple, or value array'
181
200
  end
182
- else
183
- result = self.class.get_from_list(@unprocessed_cmd_line_arguments.shift, descr, expected)
201
+ elsif !default.nil? then default
202
+ # no value provided, either get value interactively, or exception
203
+ elsif mandatory then get_interactive(:argument, descr, expected: expected)
184
204
  end
185
- elsif mandatory
186
- # no value provided
187
- result = get_interactive(:argument, descr, expected: expected)
205
+ if result.is_a?(String) && type.eql?([Integer])
206
+ str_result = result
207
+ result = Integer(str_result, exception: false)
208
+ raise Cli::BadArgument, "Invalid integer: #{str_result}" if result.nil?
188
209
  end
189
210
  Log.log.debug{"#{descr}=#{result}"}
190
- raise "argument shall be #{type.name}" unless type.nil? || result.is_a?(type)
211
+ result = aliases[result] if !aliases.nil? && aliases.key?(result)
212
+ self.class.validate_type(:argument, descr, result, type) unless result.nil? && !mandatory
191
213
  return result
192
214
  end
193
215
 
194
- # declare option of type :accessor, or :value
195
- def declare_option(option_symbol, type)
196
- Log.log.debug{"declare_option: #{option_symbol}: #{type}: skip=#{@declared_options.key?(option_symbol)}".green}
197
- if @declared_options.key?(option_symbol)
198
- raise "INTERNAL ERROR: option #{option_symbol} already declared. only accessor can be re-declared and ignored" \
199
- unless @declared_options[option_symbol][:type].eql?(:accessor)
200
- return
201
- end
202
- @declared_options[option_symbol] = {type: type}
203
- # by default passwords and secrets are sensitive, else specify when declaring the option
204
- @declared_options[option_symbol][:sensitive] = true if !%w[password secret key].select{|i| option_symbol.to_s.end_with?(i)}.empty?
205
- end
206
-
207
- # define option with handler
208
- def set_obj_attr(option_symbol, object, attr_symb, default_value=nil)
209
- Log.log.debug{"set attr obj #{option_symbol} (#{object},#{attr_symb})"}
210
- declare_option(option_symbol, :accessor)
211
- @declared_options[option_symbol][:accessor] = AttrAccessor.new(object, attr_symb)
212
- set_option(option_symbol, default_value, 'default obj attr') if !default_value.nil?
213
- end
214
-
215
- # set an option value by name, either store value or call handler
216
- def set_option(option_symbol, value, where='default')
217
- if !@declared_options.key?(option_symbol)
218
- Log.log.debug{"set unknown option: #{option_symbol}"}
219
- raise 'ERROR: cannot set undeclared option'
220
- # declare_option(option_symbol)
221
- end
222
- value = ExtendedValue.instance.evaluate(value)
223
- value = Manager.enum_to_bool(value) if @declared_options[option_symbol][:values].eql?(BOOLEAN_VALUES)
224
- Log.log.debug{"(#{@declared_options[option_symbol][:type]}/#{where}) set #{option_symbol}=#{value}"}
225
- case @declared_options[option_symbol][:type]
226
- when :accessor
227
- @declared_options[option_symbol][:accessor].value = value
228
- when :value
229
- @declared_options[option_symbol][:value] = value
230
- else # nil or other
231
- raise 'error'
232
- end
233
- end
216
+ def get_next_command(command_list, aliases: nil); return get_next_argument('command', expected: command_list, aliases: aliases); end
234
217
 
235
218
  # Get an option value by name
236
- # either return value or call handler, can return nil
219
+ # either return value or calls handler, can return nil
237
220
  # ask interactively if requested/required
238
- def get_option(option_symbol, is_type: :optional)
221
+ # @param mandatory [Boolean] if true, raise error if option not set
222
+ def get_option(option_symbol, mandatory: false, default: nil)
223
+ attributes = @declared_options[option_symbol]
224
+ raise "INTERNAL ERROR: option not declared: #{option_symbol}" unless attributes
239
225
  result = nil
240
- if @declared_options.key?(option_symbol)
241
- case @declared_options[option_symbol][:type]
242
- when :accessor
243
- result = @declared_options[option_symbol][:accessor].value
244
- when :value
245
- result = @declared_options[option_symbol][:value]
246
- else
247
- raise 'unknown type'
248
- end
249
- Log.log.debug{"(#{@declared_options[option_symbol][:type]}) get #{option_symbol}=#{result}"}
226
+ case attributes[:read_write]
227
+ when :accessor
228
+ result = attributes[:accessor].value
229
+ when :value
230
+ result = attributes[:value]
231
+ else
232
+ raise 'unknown type'
250
233
  end
234
+ Log.log.debug{"(#{attributes[:read_write]}) get #{option_symbol}=#{result}"}
235
+ result = default if result.nil?
251
236
  # do not fail for manual generation if option mandatory but not set
252
- result = '' if result.nil? && is_type.eql?(:mandatory) && !@fail_on_missing_mandatory
237
+ result = '' if result.nil? && mandatory && !@fail_on_missing_mandatory
253
238
  # Log.log.debug{"interactive=#{@ask_missing_mandatory}"}
254
239
  if result.nil?
255
240
  if !@ask_missing_mandatory
256
- raise CliBadArgument, "Missing mandatory option: #{option_symbol}" if is_type.eql?(:mandatory)
257
- elsif @ask_missing_optional || is_type.eql?(:mandatory)
241
+ raise Cli::BadArgument, "Missing mandatory option: #{option_symbol}" if mandatory
242
+ elsif @ask_missing_optional || mandatory
258
243
  # ask_missing_mandatory
259
244
  expected = :single
260
245
  # print "please enter: #{option_symbol.to_s}"
261
- if @declared_options.key?(option_symbol) && @declared_options[option_symbol].key?(:values)
262
- expected = @declared_options[option_symbol][:values]
246
+ if @declared_options.key?(option_symbol) && attributes.key?(:values)
247
+ expected = attributes[:values]
263
248
  end
264
249
  result = get_interactive(:option, option_symbol.to_s, expected: expected)
265
250
  set_option(option_symbol, result, 'interactive')
266
251
  end
267
252
  end
253
+ self.class.validate_type(:option, option_symbol, result, attributes[:types]) unless result.nil? && !mandatory
268
254
  return result
269
255
  end
270
256
 
271
- # param must be hash
272
- def add_option_preset(preset_hash, op: :push)
273
- Log.log.debug{"add_option_preset=#{preset_hash}"}
274
- raise "internal error: setting default with no hash: #{preset_hash.class}" if !preset_hash.is_a?(Hash)
275
- # incremental override
276
- preset_hash.each{|k, v|@unprocessed_defaults.send(op, [k.to_sym, v])}
277
- end
278
-
279
- # define an option with restricted values
280
- def add_opt_list(option_symbol, values, help, *on_args)
281
- declare_option(option_symbol, :value)
282
- Log.log.debug{"add_opt_list #{option_symbol}"}
283
- on_args.unshift(symbol_to_option(option_symbol, 'ENUM'))
284
- # this option value must be a symbol
285
- @declared_options[option_symbol][:values] = values
286
- value = get_option(option_symbol)
287
- help_values = values.map{|i|i.eql?(value) ? highlight_current(i) : i}.join(', ')
288
- if values.eql?(BOOLEAN_VALUES)
289
- help_values = BOOLEAN_SIMPLE.map{|i|(i.eql?(:yes) && value) || (i.eql?(:no) && !value) ? highlight_current(i) : i}.join(', ')
257
+ # set an option value by name, either store value or call handler
258
+ def set_option(option_symbol, value, where='code override')
259
+ raise Cli::BadArgument, "Unknown option: #{option_symbol}" unless @declared_options.key?(option_symbol)
260
+ attributes = @declared_options[option_symbol]
261
+ Log.log.warn("#{option_symbol}: Option is deprecated: #{attributes[:deprecation]}") if attributes[:deprecation]
262
+ value = ExtendedValue.instance.evaluate(value)
263
+ value = Manager.enum_to_bool(value) if attributes[:values].eql?(BOOLEAN_VALUES)
264
+ Log.log.debug{"(#{attributes[:read_write]}/#{where}) set #{option_symbol}=#{value}"}
265
+ self.class.validate_type(:option, option_symbol, value, attributes[:types])
266
+ case attributes[:read_write]
267
+ when :accessor
268
+ attributes[:accessor].value = value
269
+ when :value
270
+ attributes[:value] = value
271
+ else # nil or other
272
+ raise 'error'
290
273
  end
291
- on_args.push(values)
292
- on_args.push("#{help}: #{help_values}")
293
- Log.log.debug{"on_args=#{on_args}"}
294
- @parser.on(*on_args){|v|set_option(option_symbol, self.class.get_from_list(v.to_s, help, values), 'cmdline')}
295
- end
296
-
297
- def add_opt_boolean(option_symbol, help, *on_args)
298
- add_opt_list(option_symbol, BOOLEAN_VALUES, help, *on_args)
299
- # if default was defined for obj, it may still be enum (yes/no) instead of boolean
300
- default_value = get_option(option_symbol)
301
- set_option(option_symbol, default_value, 'opt boolean') unless default_value.nil?
302
274
  end
303
275
 
304
- # define an option with open values
305
- def add_opt_simple(option_symbol, *on_args)
306
- declare_option(option_symbol, :value)
307
- Log.log.debug{"add_opt_simple #{option_symbol}"}
308
- on_args.unshift(symbol_to_option(option_symbol, 'VALUE'))
309
- Log.log.debug{"on_args=#{on_args}"}
310
- @parser.on(*on_args) { |v| set_option(option_symbol, v, 'cmdline') }
311
- end
312
-
313
- # define an option with date format
314
- def add_opt_date(option_symbol, *on_args)
315
- declare_option(option_symbol, :value)
316
- Log.log.debug{"add_opt_date #{option_symbol}"}
317
- on_args.unshift(symbol_to_option(option_symbol, 'DATE'))
318
- Log.log.debug{"on_args=#{on_args}"}
319
- @parser.on(*on_args) do |v|
320
- case v
321
- when 'now' then set_option(option_symbol, Manager.time_to_string(Time.now), 'cmdline')
322
- when /^-([0-9]+)h/ then set_option(option_symbol, Manager.time_to_string(Time.now - (Regexp.last_match(1).to_i * 3600)), 'cmdline')
323
- else set_option(option_symbol, v, 'cmdline')
276
+ # declare an option
277
+ # @param option_symbol [Symbol] option name
278
+ # @param description [String] description for help
279
+ # @param handler [Hash] handler for option value: keys: o (object) and m (method)
280
+ # @param default [Object] default value
281
+ # @param values [nil, Array, :bool, :date, :none] list of allowed values, :bool for true/false, :date for dates, :none for on/off switch
282
+ # @param short [String] short option name
283
+ # @param coerce [Class] one of the coerce types accepted par option parser
284
+ # @param types [Class, Array] accepted value type(s)
285
+ # @param block [Proc] block to execute when option is found
286
+ def declare(option_symbol, description, handler: nil, default: nil, values: nil, short: nil, coerce: nil, types: nil, deprecation: nil, &block)
287
+ raise "INTERNAL ERROR: #{option_symbol} already declared" if @declared_options.key?(option_symbol)
288
+ # raise "INTERNAL ERROR: #{option_symbol} clash with another option" if
289
+ # @declared_options.keys.map(&:to_s).any?{|k|k.start_with?(option_symbol.to_s) || option_symbol.to_s.start_with?(k)}
290
+ raise "INTERNAL ERROR: #{option_symbol} ends with dot" unless description[-1] != '.'
291
+ raise "INTERNAL ERROR: #{option_symbol} description does not start with capital" unless description[0] == description[0].upcase
292
+ raise "INTERNAL ERROR: #{option_symbol} shall use :types" if ['hash', 'extended value'].any?{|s|description.downcase.include?(s) }
293
+ opt = @declared_options[option_symbol] = {
294
+ read_write: handler.nil? ? :value : :accessor,
295
+ # by default passwords and secrets are sensitive, else specify when declaring the option
296
+ sensitive: SecretHider.secret?(option_symbol, '')
297
+ }
298
+ if !types.nil?
299
+ types = [types] unless types.is_a?(Array)
300
+ raise "INTERNAL ERROR: types must be Array of Class: #{types}" unless types.all?(Class)
301
+ opt[:types] = types
302
+ description = "#{description} (#{types.map(&:name).join(', ')})"
303
+ end
304
+ if deprecation
305
+ opt[:deprecation] = deprecation
306
+ description = "#{description} (#{'deprecated'.blue}: #{deprecation})"
307
+ end
308
+ Log.log.debug{"declare: #{option_symbol}: #{opt[:read_write]}".green}
309
+ if opt[:read_write].eql?(:accessor)
310
+ raise 'internal error' unless handler.is_a?(Hash)
311
+ raise 'internal error' unless handler.keys.sort.eql?(%i[m o])
312
+ Log.log.debug{"set attr obj #{option_symbol} (#{handler[:o]},#{handler[:m]})"}
313
+ opt[:accessor] = AttrAccessor.new(handler[:o], handler[:m], option_symbol)
314
+ end
315
+ set_option(option_symbol, default, 'default') unless default.nil?
316
+ on_args = [description]
317
+ case values
318
+ when nil
319
+ on_args.push(symbol_to_option(option_symbol, 'VALUE'))
320
+ on_args.push("-#{short}VALUE") unless short.nil?
321
+ on_args.push(coerce) unless coerce.nil?
322
+ @parser.on(*on_args) { |v| set_option(option_symbol, v, SOURCE_USER) }
323
+ when Array, :bool
324
+ if values.eql?(:bool)
325
+ values = BOOLEAN_VALUES
326
+ set_option(option_symbol, Manager.enum_to_bool(default), 'default') unless default.nil?
327
+ end
328
+ # this option value must be a symbol
329
+ opt[:values] = values
330
+ value = get_option(option_symbol)
331
+ help_values = values.map{|i|i.eql?(value) ? highlight_current(i) : i}.join(', ')
332
+ if values.eql?(BOOLEAN_VALUES)
333
+ help_values = BOOLEAN_SIMPLE.map{|i|(i.eql?(:yes) && value) || (i.eql?(:no) && !value) ? highlight_current(i) : i}.join(', ')
334
+ end
335
+ on_args[0] = "#{description}: #{help_values}"
336
+ on_args.push(symbol_to_option(option_symbol, 'ENUM'))
337
+ on_args.push(values)
338
+ @parser.on(*on_args){|v|set_option(option_symbol, self.class.get_from_list(v.to_s, description, values), SOURCE_USER)}
339
+ when :date
340
+ on_args.push(symbol_to_option(option_symbol, 'DATE'))
341
+ @parser.on(*on_args) do |v|
342
+ time_string = case v
343
+ when 'now' then Manager.time_to_string(Time.now)
344
+ when /^-([0-9]+)h/ then Manager.time_to_string(Time.now - (Regexp.last_match(1).to_i * 3600))
345
+ else v
346
+ end
347
+ set_option(option_symbol, time_string, SOURCE_USER)
324
348
  end
349
+ when :none
350
+ raise "internal error: missing block for #{option_symbol}" if block.nil?
351
+ on_args.push(symbol_to_option(option_symbol, nil))
352
+ on_args.push("-#{short}") if short.is_a?(String)
353
+ @parser.on(*on_args, &block)
354
+ else raise "internal error: Unknown type for values: #{values} / #{values.class}"
325
355
  end
356
+ Log.log.debug{"on_args=#{on_args}"}
326
357
  end
327
358
 
328
- # define an option without value
329
- def add_opt_switch(option_symbol, *on_args, &block)
330
- Log.log.debug{"add_opt_on #{option_symbol}"}
331
- on_args.unshift(symbol_to_option(option_symbol, nil))
332
- Log.log.debug{"on_args=#{on_args}"}
333
- @parser.on(*on_args, &block)
359
+ # Adds each of the keys of specified hash as an option
360
+ # @param preset_hash [Hash] hash of options to add
361
+ def add_option_preset(preset_hash, op: :push)
362
+ Log.log.debug{"add_option_preset=#{preset_hash}"}
363
+ raise "internal error: default expects Hash: #{preset_hash.class}" unless preset_hash.is_a?(Hash)
364
+ # incremental override
365
+ preset_hash.each{|k, v|@unprocessed_defaults.send(op, [k.to_sym, v])}
334
366
  end
335
367
 
336
368
  # check if there were unprocessed values to generate error
@@ -346,34 +378,35 @@ module Aspera
346
378
  return result
347
379
  end
348
380
 
349
- # get all original options on command line used to generate a config in config file
381
+ # get all original options on command line used to generate a config in config file
350
382
  def get_options_table(remove_from_remaining: true)
351
383
  result = {}
352
- @initial_cli_options.each do |optionval|
353
- case optionval
384
+ @initial_cli_options.each do |option_value|
385
+ case option_value
354
386
  when /^--([^=]+)$/
355
387
  # ignore
356
388
  when /^--([^=]+)=(.*)$/
357
389
  name = Regexp.last_match(1)
358
390
  value = Regexp.last_match(2)
359
- name.gsub!(OPTION_SEP_LINE, OPTION_SEP_SYMB)
391
+ name.gsub!(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
360
392
  value = ExtendedValue.instance.evaluate(value)
361
393
  Log.log.debug{"option #{name}=#{value}"}
362
394
  result[name] = value
363
- @unprocessed_cmd_line_options.delete(optionval) if remove_from_remaining
395
+ @unprocessed_cmd_line_options.delete(option_value) if remove_from_remaining
364
396
  else
365
- raise CliBadArgument, "wrong option format: #{optionval}"
397
+ raise Cli::BadArgument, "wrong option format: #{option_value}"
366
398
  end
367
399
  end
368
400
  return result
369
401
  end
370
402
 
371
- # return options as taken from config file and command line just before command execution
372
- def declared_options(only_defined: false)
403
+ # @param only_defined [Boolean] if true, only return options that were defined
404
+ # @return [Hash] options as taken from config file and command line just before command execution
405
+ def known_options(only_defined: false)
373
406
  result = {}
374
- @declared_options.each_key do |option_symb|
375
- v = get_option(option_symb)
376
- result[option_symb.to_s] = v unless only_defined && v.nil?
407
+ @declared_options.each_key do |option_symbol|
408
+ v = get_option(option_symbol)
409
+ result[option_symbol] = v unless only_defined && v.nil?
377
410
  end
378
411
  return result
379
412
  end
@@ -402,21 +435,36 @@ module Aspera
402
435
  @unprocessed_cmd_line_options = unknown_options
403
436
  end
404
437
 
405
- private
406
-
407
438
  def prompt_user_input(prompt, sensitive)
408
439
  return $stdin.getpass("#{prompt}> ") if sensitive
409
440
  print("#{prompt}> ")
410
- return $stdin.gets.chomp
441
+ line = $stdin.gets
442
+ raise 'Unexpected end of standard input' if line.nil?
443
+ return line.chomp
444
+ end
445
+
446
+ # prompt user for input in a list of symbols
447
+ # @param prompt [String] prompt to display
448
+ # @param sym_list [Array] list of symbols to select from
449
+ # @return [Symbol] selected symbol
450
+ def prompt_user_input_in_list(prompt, sym_list)
451
+ loop do
452
+ input = prompt_user_input(prompt, false).to_sym
453
+ if sym_list.any?{|a|a.eql?(input)}
454
+ return input
455
+ else
456
+ $stderr.puts("No such #{prompt}: #{input}, select one of: #{sym_list.join(', ')}")
457
+ end
458
+ end
411
459
  end
412
460
 
413
461
  def get_interactive(type, descr, expected: :single)
414
462
  if !@ask_missing_mandatory
415
- raise CliBadArgument, self.class.bad_arg_message_multi("missing: #{descr}", expected) if expected.is_a?(Array)
416
- raise CliBadArgument, "missing argument (#{expected}): #{descr}"
463
+ raise Cli::BadArgument, self.class.bad_arg_message_multi("missing: #{descr}", expected) if expected.is_a?(Array)
464
+ raise Cli::BadArgument, "missing argument (#{expected}): #{descr}"
417
465
  end
418
466
  result = nil
419
- sensitive = type.eql?(:option) && @declared_options[descr.to_sym][:sensitive]
467
+ sensitive = type.eql?(:option) && @declared_options[descr.to_sym].is_a?(Hash) && @declared_options[descr.to_sym][:sensitive]
420
468
  default_prompt = "#{type}: #{descr}"
421
469
  # ask interactively
422
470
  case expected
@@ -436,9 +484,11 @@ module Aspera
436
484
  return result
437
485
  end
438
486
 
487
+ private
488
+
439
489
  # generate command line option from option symbol
440
490
  def symbol_to_option(symbol, opt_val)
441
- result = '--' + symbol.to_s.gsub(OPTION_SEP_SYMB, OPTION_SEP_LINE)
491
+ result = '--' + symbol.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)
442
492
  result = result + '=' + opt_val unless opt_val.nil?
443
493
  return result
444
494
  end