aspera-cli 4.13.0 → 4.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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