aspera-cli 4.23.0 → 4.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +37 -1
- data/CONTRIBUTING.md +86 -29
- data/README.md +2109 -1300
- data/bin/ascli +2 -1
- data/bin/asession +4 -4
- data/lib/aspera/agent/base.rb +4 -0
- data/lib/aspera/agent/connect.rb +20 -18
- data/lib/aspera/agent/desktop.rb +14 -11
- data/lib/aspera/agent/direct.rb +39 -31
- data/lib/aspera/agent/httpgw.rb +2 -2
- data/lib/aspera/agent/node.rb +9 -11
- data/lib/aspera/agent/transferd.rb +18 -11
- data/lib/aspera/api/aoc.rb +44 -31
- data/lib/aspera/api/cos_node.rb +7 -5
- data/lib/aspera/api/httpgw.rb +15 -18
- data/lib/aspera/api/node.rb +104 -22
- data/lib/aspera/ascmd.rb +22 -16
- data/lib/aspera/ascp/installation.rb +37 -40
- data/lib/aspera/ascp/management.rb +5 -4
- data/lib/aspera/assert.rb +54 -23
- data/lib/aspera/cli/basic_auth_plugin.rb +8 -7
- data/lib/aspera/cli/error.rb +1 -1
- data/lib/aspera/cli/extended_value.rb +28 -29
- data/lib/aspera/cli/formatter.rb +191 -168
- data/lib/aspera/cli/hints.rb +29 -3
- data/lib/aspera/cli/main.rb +138 -107
- data/lib/aspera/cli/manager.rb +50 -30
- data/lib/aspera/cli/plugin.rb +148 -77
- data/lib/aspera/cli/plugin_factory.rb +2 -2
- data/lib/aspera/cli/plugins/aoc.rb +189 -70
- data/lib/aspera/cli/plugins/ats.rb +15 -13
- data/lib/aspera/cli/plugins/config.rb +100 -214
- data/lib/aspera/cli/plugins/console.rb +49 -18
- data/lib/aspera/cli/plugins/cos.rb +4 -4
- data/lib/aspera/cli/plugins/faspex.rb +45 -51
- data/lib/aspera/cli/plugins/faspex5.rb +164 -165
- data/lib/aspera/cli/plugins/faspio.rb +6 -5
- data/lib/aspera/cli/plugins/httpgw.rb +2 -2
- data/lib/aspera/cli/plugins/node.rb +144 -162
- data/lib/aspera/cli/plugins/orchestrator.rb +10 -14
- data/lib/aspera/cli/plugins/preview.rb +26 -29
- data/lib/aspera/cli/plugins/server.rb +28 -28
- data/lib/aspera/cli/plugins/shares.rb +40 -28
- data/lib/aspera/cli/sync_actions.rb +101 -80
- data/lib/aspera/cli/transfer_agent.rb +51 -50
- data/lib/aspera/cli/transfer_progress.rb +29 -20
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +157 -0
- data/lib/aspera/colors.rb +13 -8
- data/lib/aspera/command_line_builder.rb +28 -22
- data/lib/aspera/command_line_converter.rb +31 -0
- data/lib/aspera/environment.rb +145 -101
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/faspex_postproc.rb +3 -2
- data/lib/aspera/hash_ext.rb +1 -1
- data/lib/aspera/id_generator.rb +10 -10
- data/lib/aspera/keychain/base.rb +18 -0
- data/lib/aspera/keychain/encrypted_hash.rb +6 -12
- data/lib/aspera/keychain/factory.rb +9 -3
- data/lib/aspera/keychain/hashicorp_vault.rb +9 -6
- data/lib/aspera/keychain/macos_security.rb +13 -13
- data/lib/aspera/log.rb +91 -19
- data/lib/aspera/nagios.rb +5 -6
- data/lib/aspera/node_simulator.rb +12 -7
- data/lib/aspera/oauth/base.rb +5 -3
- data/lib/aspera/oauth/factory.rb +24 -18
- data/lib/aspera/oauth/jwt.rb +13 -1
- data/lib/aspera/oauth/url_json.rb +3 -3
- data/lib/aspera/oauth/web.rb +5 -3
- data/lib/aspera/persistency_folder.rb +2 -2
- data/lib/aspera/preview/file_types.rb +4 -3
- data/lib/aspera/preview/generator.rb +25 -12
- data/lib/aspera/preview/terminal.rb +10 -7
- data/lib/aspera/preview/utils.rb +11 -9
- data/lib/aspera/products/connect.rb +1 -1
- data/lib/aspera/products/desktop.rb +1 -1
- data/lib/aspera/products/other.rb +2 -2
- data/lib/aspera/products/transferd.rb +8 -6
- data/lib/aspera/proxy_auto_config.rb +1 -1
- data/lib/aspera/rest.rb +29 -22
- data/lib/aspera/rest_call_error.rb +1 -1
- data/lib/aspera/resumer.rb +1 -1
- data/lib/aspera/secret_hider.rb +46 -40
- data/lib/aspera/ssh.rb +13 -3
- data/lib/aspera/sync/args.schema.yaml +102 -0
- data/lib/aspera/sync/conf.schema.yaml +701 -0
- data/lib/aspera/sync/database.rb +83 -0
- data/lib/aspera/sync/operations.rb +296 -0
- data/lib/aspera/temp_file_manager.rb +3 -2
- data/lib/aspera/transfer/error.rb +1 -1
- data/lib/aspera/transfer/error_info.rb +1 -2
- data/lib/aspera/transfer/faux_file.rb +11 -10
- data/lib/aspera/transfer/parameters.rb +6 -5
- data/lib/aspera/transfer/spec.rb +15 -1
- data/lib/aspera/transfer/spec.schema.yaml +316 -293
- data/lib/aspera/transfer/spec_doc.rb +34 -16
- data/lib/aspera/transfer/uri.rb +5 -5
- data/lib/aspera/uri_reader.rb +14 -10
- data/lib/aspera/web_auth.rb +2 -2
- data/lib/aspera/web_server_simple.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +15 -13
- metadata.gz.sig +0 -0
- data/lib/aspera/transfer/async_conf.schema.yaml +0 -716
- data/lib/aspera/transfer/convert.rb +0 -29
- data/lib/aspera/transfer/sync.rb +0 -232
- data/lib/aspera/transfer/sync_instance.schema.yaml +0 -20
- data/lib/aspera/transfer/sync_session.schema.yaml +0 -86
data/lib/aspera/cli/main.rb
CHANGED
@@ -16,10 +16,14 @@ require 'aspera/assert'
|
|
16
16
|
module Aspera
|
17
17
|
module Cli
|
18
18
|
# Global objects shared with plugins
|
19
|
-
class
|
20
|
-
MEMBERS = %i[options transfer config formatter persistency].freeze
|
19
|
+
class Context
|
20
|
+
MEMBERS = %i[options transfer config formatter persistency man_header].freeze
|
21
21
|
attr_accessor(*MEMBERS)
|
22
22
|
|
23
|
+
def initialize
|
24
|
+
@man_header = true
|
25
|
+
end
|
26
|
+
|
23
27
|
def validate
|
24
28
|
MEMBERS.each do |i|
|
25
29
|
Aspera.assert(instance_variable_defined?(:"@#{i}"))
|
@@ -42,19 +46,42 @@ module Aspera
|
|
42
46
|
STATUS_FIELD = 'status'
|
43
47
|
COMMAND_CONFIG = :config
|
44
48
|
COMMAND_HELP = :help
|
49
|
+
# types that go to result of type = text
|
45
50
|
SCALAR_TYPES = [String, Integer, Symbol].freeze
|
51
|
+
USER_INTERFACES = %i[text graphical].freeze
|
46
52
|
|
47
|
-
private_constant :COMMAND_CONFIG, :COMMAND_HELP, :SCALAR_TYPES
|
53
|
+
private_constant :COMMAND_CONFIG, :COMMAND_HELP, :SCALAR_TYPES, :USER_INTERFACES
|
48
54
|
|
49
55
|
class << self
|
50
|
-
#
|
51
|
-
|
56
|
+
# early debug for parser
|
57
|
+
# Note: does not accept shortcuts
|
58
|
+
def early_debug_setup(argv)
|
59
|
+
Log.instance.program_name = Info::CMD_NAME
|
60
|
+
argv.each do |arg|
|
61
|
+
case arg
|
62
|
+
when '--' then break
|
63
|
+
when /^--log-level=(.*)/ then Log.instance.level = Regexp.last_match(1).to_sym
|
64
|
+
when /^--logger=(.*)/ then Log.instance.logger_type = Regexp.last_match(1).to_sym
|
65
|
+
end
|
66
|
+
rescue => e
|
67
|
+
$stderr.puts("Error: #{e}") # rubocop:disable Style/StderrPuts
|
68
|
+
Process.exit(1)
|
69
|
+
end
|
70
|
+
end
|
52
71
|
|
53
|
-
|
54
|
-
def result_nothing; return {type: :nothing, data: :nil}; end
|
72
|
+
def result_special(how); {type: :special, data: how}; end
|
55
73
|
|
74
|
+
# Expect some list, but nothing to display
|
75
|
+
def result_empty; result_special(:empty); end
|
76
|
+
|
77
|
+
# Nothing expected
|
78
|
+
def result_nothing; result_special(:nothing); end
|
79
|
+
|
80
|
+
# Result is some status, such as "complete", "deleted"...
|
81
|
+
# @param status [String] The status
|
56
82
|
def result_status(status); return {type: :status, data: status}; end
|
57
83
|
|
84
|
+
# text result coming from command result
|
58
85
|
def result_text(data); return {type: :text, data: data}; end
|
59
86
|
|
60
87
|
def result_success; return result_status('complete'); end
|
@@ -68,7 +95,7 @@ module Aspera
|
|
68
95
|
return Main.result_nothing
|
69
96
|
end
|
70
97
|
|
71
|
-
#
|
98
|
+
# Used when one command executes several transfer jobs (each job being possibly multi session)
|
72
99
|
# @param status_table [Array] [{STATUS_FIELD=>[status array],...},...]
|
73
100
|
# @return a status object suitable as command result
|
74
101
|
# each element has a key STATUS_FIELD which contains the result of possibly multiple sessions
|
@@ -84,32 +111,40 @@ module Aspera
|
|
84
111
|
return result_object_list(status_table)
|
85
112
|
end
|
86
113
|
|
87
|
-
|
88
|
-
|
114
|
+
# Display image for that URL or directly blob
|
115
|
+
def result_image(url_or_blob)
|
116
|
+
return {type: :image, data: url_or_blob}
|
89
117
|
end
|
90
118
|
|
119
|
+
# A single object, must be Hash
|
91
120
|
def result_single_object(data, fields: nil)
|
92
121
|
return {type: :single_object, data: data, fields: fields}
|
93
122
|
end
|
94
123
|
|
124
|
+
# An Array of Hash
|
95
125
|
def result_object_list(data, fields: nil, total: nil)
|
96
126
|
return {type: :object_list, data: data, fields: fields, total: total}
|
97
127
|
end
|
98
128
|
|
99
|
-
|
129
|
+
# A list of values
|
130
|
+
def result_value_list(data, name: 'id')
|
131
|
+
Aspera.assert_type(data, Array)
|
132
|
+
Aspera.assert_type(name, String)
|
100
133
|
return {type: :value_list, data: data, name: name}
|
101
134
|
end
|
102
135
|
|
103
136
|
# Determines type of result based on data
|
104
137
|
def result_auto(data)
|
105
138
|
case data
|
139
|
+
when NilClass
|
140
|
+
return result_special(:null)
|
106
141
|
when Hash
|
107
142
|
return result_single_object(data)
|
108
143
|
when Array
|
109
144
|
all_types = data.map(&:class).uniq
|
110
145
|
return result_object_list(data) if all_types.eql?([Hash])
|
111
146
|
unsupported_types = all_types - SCALAR_TYPES
|
112
|
-
return result_value_list(data, 'list') if unsupported_types.empty?
|
147
|
+
return result_value_list(data, name: 'list') if unsupported_types.empty?
|
113
148
|
Aspera.error_unexpected_value(unsupported_types){'list item types'}
|
114
149
|
when *SCALAR_TYPES
|
115
150
|
return result_text(data)
|
@@ -121,12 +156,11 @@ module Aspera
|
|
121
156
|
# minimum initialization, no exception raised
|
122
157
|
def initialize(argv)
|
123
158
|
@argv = argv
|
124
|
-
|
125
|
-
Log.log.trace2{Log.dump(:argv, @argv)}
|
159
|
+
Log.dump(:argv, @argv, level: :trace2)
|
126
160
|
@option_help = false
|
127
161
|
@option_show_config = false
|
128
162
|
@bash_completion = false
|
129
|
-
@
|
163
|
+
@context = Context.new
|
130
164
|
end
|
131
165
|
|
132
166
|
# this is the main function called by initial script just after constructor
|
@@ -137,41 +171,39 @@ module Aspera
|
|
137
171
|
execute_command = true
|
138
172
|
# catch exceptions
|
139
173
|
begin
|
140
|
-
|
141
|
-
# find plugins, shall be after parse! ?
|
142
|
-
PluginFactory.instance.add_plugins_from_lookup_folders
|
174
|
+
init_agents_options_plugins
|
143
175
|
# help requested without command ? (plugins must be known here)
|
144
|
-
|
176
|
+
show_usage if @option_help && @context.options.command_or_arg_empty?
|
145
177
|
generate_bash_completion if @bash_completion
|
146
|
-
@
|
178
|
+
@context.config.periodic_check_newer_gem_version
|
147
179
|
command_sym =
|
148
|
-
if @option_show_config && @
|
180
|
+
if @option_show_config && @context.options.command_or_arg_empty?
|
149
181
|
COMMAND_CONFIG
|
150
182
|
else
|
151
|
-
@
|
183
|
+
@context.options.get_next_command(PluginFactory.instance.plugin_list.unshift(COMMAND_HELP))
|
152
184
|
end
|
153
185
|
# command will not be executed, but we need manual
|
154
|
-
@
|
186
|
+
@context.options.fail_on_missing_mandatory = false if @option_help || @option_show_config
|
155
187
|
# main plugin is not dynamically instantiated
|
156
188
|
case command_sym
|
157
189
|
when COMMAND_HELP
|
158
|
-
|
190
|
+
show_usage
|
159
191
|
when COMMAND_CONFIG
|
160
|
-
command_plugin = @
|
192
|
+
command_plugin = @context.config
|
161
193
|
else
|
162
194
|
# get plugin, set options, etc
|
163
195
|
command_plugin = get_plugin_instance_with_options(command_sym)
|
164
196
|
# parse plugin specific options
|
165
|
-
@
|
197
|
+
@context.options.parse_options!
|
166
198
|
end
|
167
199
|
# help requested for current plugin
|
168
|
-
|
200
|
+
show_usage(all: false) if @option_help
|
169
201
|
if @option_show_config
|
170
|
-
@
|
202
|
+
@context.formatter.display_results(type: :single_object, data: @context.options.known_options(only_defined: true).stringify_keys)
|
171
203
|
execute_command = false
|
172
204
|
end
|
173
205
|
# locking for single execution (only after "per plugin" option, in case lock port is there)
|
174
|
-
lock_port = @
|
206
|
+
lock_port = @context.options.get_option(:lock_port)
|
175
207
|
if !lock_port.nil?
|
176
208
|
begin
|
177
209
|
# no need to close later, will be freed on process exit. must save in member else it is garbage collected
|
@@ -183,18 +215,18 @@ module Aspera
|
|
183
215
|
Log.log.warn{"Another instance is already running (#{e.message})."}
|
184
216
|
end
|
185
217
|
end
|
186
|
-
pid_file = @
|
218
|
+
pid_file = @context.options.get_option(:pid_file)
|
187
219
|
if !pid_file.nil?
|
188
220
|
File.write(pid_file, Process.pid)
|
189
221
|
Log.log.debug{"Wrote pid #{Process.pid} to #{pid_file}"}
|
190
222
|
at_exit{File.delete(pid_file)}
|
191
223
|
end
|
192
224
|
# execute and display (if not exclusive execution)
|
193
|
-
@
|
225
|
+
@context.formatter.display_results(**command_plugin.execute_action) if execute_command
|
194
226
|
# save config file if command modified it
|
195
|
-
@
|
227
|
+
@context.config.save_config_file_if_needed
|
196
228
|
# finish
|
197
|
-
@
|
229
|
+
@context.transfer.shutdown
|
198
230
|
rescue Net::SSH::AuthenticationFailed => e; exception_info = {e: e, t: 'SSH', security: true}
|
199
231
|
rescue OpenSSL::SSL::SSLError => e; exception_info = {e: e, t: 'SSL'}
|
200
232
|
rescue Cli::BadArgument => e; exception_info = {e: e, t: 'Argument', usage: true}
|
@@ -211,15 +243,15 @@ module Aspera
|
|
211
243
|
# 1- processing of error condition
|
212
244
|
unless exception_info.nil?
|
213
245
|
Log.log.warn(exception_info[:e].message) if Log.instance.logger_type.eql?(:syslog) && exception_info[:security]
|
214
|
-
@
|
215
|
-
@
|
246
|
+
@context.formatter.display_message(:error, "#{Formatter::ERROR_FLASH} #{exception_info[:t]}: #{exception_info[:e].message}")
|
247
|
+
@context.formatter.display_message(:error, 'Use option -h to get help.') if exception_info[:usage]
|
216
248
|
# Is that a known error condition with proposal for remediation ?
|
217
|
-
Hints.hint_for(exception_info[:e], @
|
249
|
+
Hints.hint_for(exception_info[:e], @context.formatter)
|
218
250
|
end
|
219
251
|
# 2- processing of command not processed (due to exception or bad command line)
|
220
252
|
if execute_command || @option_show_config
|
221
|
-
@
|
222
|
-
@
|
253
|
+
@context.options.final_errors.each do |msg|
|
254
|
+
@context.formatter.display_message(:error, "#{Formatter::ERROR_FLASH} Argument: #{msg}")
|
223
255
|
# add code as exception if there is not already an error
|
224
256
|
exception_info = {e: Exception.new(msg), t: 'UnusedArg'} if exception_info.nil?
|
225
257
|
end
|
@@ -229,10 +261,36 @@ module Aspera
|
|
229
261
|
# show stack trace in debug mode
|
230
262
|
raise exception_info[:e] if Log.log.debug?
|
231
263
|
# else give hint and exit
|
232
|
-
@
|
264
|
+
@context.formatter.display_message(:error, 'Use --log-level=debug to get more details.') if exception_info[:debug]
|
233
265
|
Process.exit(1)
|
234
266
|
end
|
235
|
-
return
|
267
|
+
return
|
268
|
+
end
|
269
|
+
|
270
|
+
def init_agents_options_plugins
|
271
|
+
init_agents_and_options
|
272
|
+
# find plugins, shall be after parse! ?
|
273
|
+
PluginFactory.instance.add_plugins_from_lookup_folders
|
274
|
+
end
|
275
|
+
|
276
|
+
def show_usage(all: true, exit: true)
|
277
|
+
# display main plugin options (+config)
|
278
|
+
@context.formatter.display_message(:error, @context.options.parser)
|
279
|
+
if all
|
280
|
+
@context.only_manual
|
281
|
+
# list plugins that have a "require" field, i.e. all but main plugin
|
282
|
+
PluginFactory.instance.plugin_list.each do |plugin_name_sym|
|
283
|
+
# config was already included in the global options
|
284
|
+
next if plugin_name_sym.eql?(COMMAND_CONFIG)
|
285
|
+
# override main option parser with a brand new, to avoid having global options
|
286
|
+
@context.options = Manager.new(Info::CMD_NAME)
|
287
|
+
@context.options.parser.banner = '' # remove default banner
|
288
|
+
get_plugin_instance_with_options(plugin_name_sym)
|
289
|
+
# display generated help for plugin options
|
290
|
+
@context.formatter.display_message(:error, @context.options.parser.help)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
Process.exit(0) if exit
|
236
294
|
end
|
237
295
|
|
238
296
|
private
|
@@ -240,30 +298,34 @@ module Aspera
|
|
240
298
|
# This can throw exception if there is a problem with the environment, needs to be caught by execute method
|
241
299
|
def init_agents_and_options
|
242
300
|
# create formatter, in case there is an exception, it is used to display.
|
243
|
-
@
|
301
|
+
@context.formatter = Formatter.new
|
244
302
|
# create command line manager with arguments
|
245
|
-
@
|
303
|
+
@context.options = Manager.new(Info::CMD_NAME, @argv)
|
246
304
|
# formatter adds options
|
247
|
-
@
|
248
|
-
ExtendedValue.instance.default_decoder = @
|
305
|
+
@context.formatter.declare_options(@context.options)
|
306
|
+
ExtendedValue.instance.default_decoder = @context.options.get_option(:struct_parser)
|
249
307
|
# compare $0 with expected name
|
250
308
|
current_prog_name = File.basename($PROGRAM_NAME)
|
251
|
-
@
|
309
|
+
@context.formatter.display_message(
|
252
310
|
:error,
|
253
|
-
"#{Formatter::WARNING_FLASH} Please use '#{Info::CMD_NAME}' instead of '#{current_prog_name}'"
|
311
|
+
"#{Formatter::WARNING_FLASH} Please use '#{Info::CMD_NAME}' instead of '#{current_prog_name}'"
|
312
|
+
) unless current_prog_name.eql?(Info::CMD_NAME)
|
254
313
|
# declare and parse global options
|
255
314
|
declare_global_options
|
315
|
+
# do not display config commands if help is asked
|
316
|
+
@context.man_header = false
|
256
317
|
# the Config plugin adds the @preset parser, so declare before TransferAgent which may use it
|
257
|
-
@
|
318
|
+
@context.config = Plugins::Config.new(context: @context)
|
319
|
+
@context.man_header = true
|
258
320
|
# data persistency is set in config
|
259
|
-
Aspera.assert(@
|
321
|
+
Aspera.assert(@context.persistency){'missing persistency object'}
|
260
322
|
# the TransferAgent plugin may use the @preset parser
|
261
|
-
@
|
323
|
+
@context.transfer = TransferAgent.new(@context.options, @context.config)
|
262
324
|
# add commands for config plugin after all options have been added
|
263
|
-
@
|
264
|
-
@
|
325
|
+
@context.config.add_manual_header(false)
|
326
|
+
@context.validate
|
265
327
|
# set banner when all environment is created so that additional extended value modifiers are known, e.g. @preset
|
266
|
-
@
|
328
|
+
@context.options.parser.banner = app_banner
|
267
329
|
end
|
268
330
|
|
269
331
|
def app_banner
|
@@ -303,24 +365,29 @@ module Aspera
|
|
303
365
|
# define header for manual
|
304
366
|
def declare_global_options
|
305
367
|
Log.log.debug('declare_global_options')
|
306
|
-
@
|
307
|
-
@
|
308
|
-
@
|
309
|
-
@
|
310
|
-
@
|
368
|
+
@context.options.declare(:help, 'Show this message', values: :none, short: 'h'){@option_help = true}
|
369
|
+
@context.options.declare(:bash_comp, 'Generate bash completion for command', values: :none){@bash_completion = true}
|
370
|
+
@context.options.declare(:show_config, 'Display parameters used for the provided action', values: :none){@option_show_config = true}
|
371
|
+
@context.options.declare(:version, 'Display version', values: :none, short: 'v'){@context.formatter.display_message(:data, Cli::VERSION); Process.exit(0)} # rubocop:disable Style/Semicolon
|
372
|
+
@context.options.declare(
|
311
373
|
:ui, 'Method to start browser',
|
312
|
-
values:
|
313
|
-
handler: {o: Environment.instance, m: :url_method}
|
314
|
-
|
315
|
-
@
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
@
|
320
|
-
@
|
321
|
-
@
|
374
|
+
values: USER_INTERFACES,
|
375
|
+
handler: {o: Environment.instance, m: :url_method}
|
376
|
+
)
|
377
|
+
@context.options.declare(
|
378
|
+
:invalid_characters, 'Replacement character and invalid filename characters',
|
379
|
+
handler: {o: Environment.instance, m: :file_illegal_characters}
|
380
|
+
)
|
381
|
+
@context.options.declare(:log_level, 'Log level', values: Log::LEVELS, handler: {o: Log.instance, m: :level})
|
382
|
+
@context.options.declare(:log_format, 'Log formatter', types: [Proc, Logger::Formatter, String], handler: {o: Log.instance, m: :formatter})
|
383
|
+
@context.options.declare(:logger, 'Logging method', values: Log::LOG_TYPES, handler: {o: Log.instance, m: :logger_type})
|
384
|
+
@context.options.declare(:lock_port, 'Prevent dual execution of a command, e.g. in cron', coerce: Integer, types: Integer)
|
385
|
+
@context.options.declare(:once_only, 'Process only new items (some commands)', values: :bool, default: false)
|
386
|
+
@context.options.declare(:log_secrets, 'Show passwords in logs', values: :bool, handler: {o: SecretHider.instance, m: :log_secrets})
|
387
|
+
@context.options.declare(:clean_temp, 'Cleanup temporary files on exit', values: :bool, handler: {o: TempFileManager.instance, m: :cleanup_on_exit})
|
388
|
+
@context.options.declare(:pid_file, 'Write process identifier to file, delete on exit', types: String)
|
322
389
|
# parse declared options
|
323
|
-
@
|
390
|
+
@context.options.parse_options!
|
324
391
|
end
|
325
392
|
|
326
393
|
# @return the plugin instance, based on name
|
@@ -329,55 +396,19 @@ module Aspera
|
|
329
396
|
def get_plugin_instance_with_options(plugin_name_sym)
|
330
397
|
Log.log.debug{"get_plugin_instance_with_options(#{plugin_name_sym})"}
|
331
398
|
# load default params only if no param already loaded before plugin instantiation
|
332
|
-
@
|
333
|
-
command_plugin = PluginFactory.instance.create(plugin_name_sym,
|
399
|
+
@context.config.add_plugin_default_preset(plugin_name_sym)
|
400
|
+
command_plugin = PluginFactory.instance.create(plugin_name_sym, context: @context)
|
334
401
|
return command_plugin
|
335
402
|
end
|
336
403
|
|
337
404
|
def generate_bash_completion
|
338
|
-
if @
|
405
|
+
if @context.options.get_next_argument('', multiple: true, mandatory: false).nil?
|
339
406
|
PluginFactory.instance.plugin_list.each{ |p| puts p}
|
340
407
|
else
|
341
408
|
Log.log.warn('only first level completion so far')
|
342
409
|
end
|
343
410
|
Process.exit(0)
|
344
411
|
end
|
345
|
-
|
346
|
-
def exit_with_usage(include_all_plugins)
|
347
|
-
Log.log.debug{"exit_with_usage(#{include_all_plugins})".bg_red}
|
348
|
-
# display main plugin options (+config)
|
349
|
-
@env.formatter.display_message(:error, @env.options.parser)
|
350
|
-
if include_all_plugins
|
351
|
-
@env.only_manual
|
352
|
-
# list plugins that have a "require" field, i.e. all but main plugin
|
353
|
-
PluginFactory.instance.plugin_list.each do |plugin_name_sym|
|
354
|
-
# config was already included in the global options
|
355
|
-
next if plugin_name_sym.eql?(COMMAND_CONFIG)
|
356
|
-
# override main option parser with a brand new, to avoid having global options
|
357
|
-
@env.options = Manager.new(Info::CMD_NAME)
|
358
|
-
@env.options.parser.banner = '' # remove default banner
|
359
|
-
get_plugin_instance_with_options(plugin_name_sym)
|
360
|
-
# display generated help for plugin options
|
361
|
-
@env.formatter.display_message(:error, @env.options.parser.help)
|
362
|
-
end
|
363
|
-
end
|
364
|
-
Process.exit(0)
|
365
|
-
end
|
366
|
-
|
367
|
-
# early debug for parser
|
368
|
-
# Note: does not accept shortcuts
|
369
|
-
def early_debug_setup
|
370
|
-
Log.instance.program_name = Info::CMD_NAME
|
371
|
-
@argv.each do |arg|
|
372
|
-
case arg
|
373
|
-
when '--' then break
|
374
|
-
when /^--log-level=(.*)/ then Log.instance.level = Regexp.last_match(1).to_sym
|
375
|
-
when /^--logger=(.*)/ then Log.instance.logger_type = Regexp.last_match(1).to_sym
|
376
|
-
end
|
377
|
-
rescue => e
|
378
|
-
$stderr.puts("Error: #{e}")
|
379
|
-
end
|
380
|
-
end
|
381
412
|
end
|
382
413
|
end
|
383
414
|
end
|
data/lib/aspera/cli/manager.rb
CHANGED
@@ -54,14 +54,19 @@ module Aspera
|
|
54
54
|
OPTION_SEP_LINE = '-'
|
55
55
|
# option name separator in code (symbol)
|
56
56
|
OPTION_SEP_SYMBOL = '_'
|
57
|
-
SOURCE_USER = 'cmdline' # cspell:disable-line
|
58
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
|
59
62
|
OPTION_PREFIX = '--'
|
63
|
+
# when this is alone, this stops option processing
|
60
64
|
OPTIONS_STOP = '--'
|
65
|
+
SOURCE_USER = 'cmdline' # cspell:disable-line
|
61
66
|
|
62
67
|
DEFAULT_PARSER_TYPES = [Array, Hash].freeze
|
63
68
|
|
64
|
-
private_constant :FALSE_VALUES, :TRUE_VALUES, :BOOLEAN_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :SOURCE_USER
|
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
|
65
70
|
|
66
71
|
class << self
|
67
72
|
def enum_to_bool(enum)
|
@@ -75,6 +80,7 @@ module Aspera
|
|
75
80
|
|
76
81
|
# find shortened string value in allowed symbol list
|
77
82
|
def get_from_list(short_value, descr, allowed_values)
|
83
|
+
Aspera.assert_type(short_value, String)
|
78
84
|
# we accept shortcuts
|
79
85
|
matching_exact = allowed_values.select{ |i| i.to_s.eql?(short_value)}
|
80
86
|
return matching_exact.first if matching_exact.length == 1
|
@@ -107,7 +113,7 @@ module Aspera
|
|
107
113
|
# @param type_list [NilClass, Class, Array[Class]] accepted value type(s)
|
108
114
|
# @param check_array [bool] set to true if it is a list of values to check
|
109
115
|
def validate_type(what, descr, to_check, type_list, check_array: false)
|
110
|
-
return
|
116
|
+
return if type_list.nil?
|
111
117
|
Aspera.assert(type_list.is_a?(Array) && type_list.all?(Class)){'types must be a Class Array'}
|
112
118
|
value_list = check_array ? to_check : [to_check]
|
113
119
|
value_list.each do |value|
|
@@ -147,14 +153,13 @@ module Aspera
|
|
147
153
|
# options can also be provided by env vars : --param-name -> ASCLI_PARAM_NAME
|
148
154
|
env_prefix = program_name.upcase + OPTION_SEP_SYMBOL
|
149
155
|
ENV.each do |k, v|
|
150
|
-
if k.start_with?(env_prefix)
|
151
|
-
@option_pairs_env[k[env_prefix.length..-1].downcase.to_sym] = v
|
152
|
-
end
|
156
|
+
@option_pairs_env[k[env_prefix.length..-1].downcase.to_sym] = v if k.start_with?(env_prefix)
|
153
157
|
end
|
154
158
|
Log.log.debug{"env=#{@option_pairs_env}".red}
|
155
159
|
@unprocessed_cmd_line_options = []
|
156
160
|
@unprocessed_cmd_line_arguments = []
|
157
161
|
return if argv.nil?
|
162
|
+
# true until `--` is found (stop options)
|
158
163
|
process_options = true
|
159
164
|
until argv.empty?
|
160
165
|
value = argv.shift
|
@@ -171,7 +176,7 @@ module Aspera
|
|
171
176
|
end
|
172
177
|
end
|
173
178
|
@initial_cli_options = @unprocessed_cmd_line_options.dup.freeze
|
174
|
-
Log.log.
|
179
|
+
Log.log.trace1{"add_cmd_line_options:commands/arguments=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
|
175
180
|
@parser.separator('')
|
176
181
|
@parser.separator('OPTIONS: global')
|
177
182
|
declare(:interactive, 'Use interactive input of missing params', values: :bool, handler: {o: self, m: :ask_missing_mandatory})
|
@@ -244,10 +249,9 @@ module Aspera
|
|
244
249
|
result = attributes[:accessor].value
|
245
250
|
when :value
|
246
251
|
result = attributes[:value]
|
247
|
-
else
|
248
|
-
raise 'unknown type'
|
252
|
+
else Aspera.error_unexpected_value(attributes[:read_write]){'attribute read/write'}
|
249
253
|
end
|
250
|
-
Log.log.
|
254
|
+
Log.log.trace1{"(#{attributes[:read_write]}) get #{option_symbol}=#{result}"}
|
251
255
|
result = default if result.nil?
|
252
256
|
# do not fail for manual generation if option mandatory but not set
|
253
257
|
result = '' if result.nil? && mandatory && !@fail_on_missing_mandatory
|
@@ -259,9 +263,7 @@ module Aspera
|
|
259
263
|
# ask_missing_mandatory
|
260
264
|
accept_list = nil
|
261
265
|
# print "please enter: #{option_symbol.to_s}"
|
262
|
-
if @declared_options.key?(option_symbol) && attributes.key?(:values)
|
263
|
-
accept_list = attributes[:values]
|
264
|
-
end
|
266
|
+
accept_list = attributes[:values] if @declared_options.key?(option_symbol) && attributes.key?(:values)
|
265
267
|
result = get_interactive(option_symbol.to_s, option: true, accept_list: accept_list)
|
266
268
|
set_option(option_symbol, result, where: 'interactive')
|
267
269
|
end
|
@@ -282,15 +284,14 @@ module Aspera
|
|
282
284
|
Log.log.warn("#{option_symbol}: Option is deprecated: #{attributes[:deprecation]}") if attributes[:deprecation]
|
283
285
|
value = evaluate_extended_value(value, attributes[:types])
|
284
286
|
value = Manager.enum_to_bool(value) if attributes[:values].eql?(BOOLEAN_VALUES)
|
285
|
-
Log.log.
|
287
|
+
Log.log.trace1{"(#{attributes[:read_write]}/#{where}) set #{option_symbol}=#{value}"}
|
286
288
|
self.class.validate_type(:option, option_symbol, value, attributes[:types])
|
287
289
|
case attributes[:read_write]
|
288
290
|
when :accessor
|
289
291
|
attributes[:accessor].value = value
|
290
292
|
when :value
|
291
293
|
attributes[:value] = value
|
292
|
-
else
|
293
|
-
raise 'error'
|
294
|
+
else Aspera.error_unexpected_value(attributes[:read_write]){'attribute read/write'}
|
294
295
|
end
|
295
296
|
end
|
296
297
|
|
@@ -308,12 +309,12 @@ module Aspera
|
|
308
309
|
Aspera.assert_type(option_symbol, Symbol)
|
309
310
|
Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
|
310
311
|
Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
|
311
|
-
Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with
|
312
|
+
Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with an uppercase"}
|
312
313
|
Aspera.assert(!['hash', 'extended value'].any?{ |s| description.downcase.include?(s)}){"#{option_symbol} shall use :types"}
|
313
314
|
opt = @declared_options[option_symbol] = {
|
314
315
|
read_write: handler.nil? ? :value : :accessor,
|
315
316
|
# by default passwords and secrets are sensitive, else specify when declaring the option
|
316
|
-
sensitive: SecretHider.secret?(option_symbol, '')
|
317
|
+
sensitive: SecretHider.instance.secret?(option_symbol, '')
|
317
318
|
}
|
318
319
|
if !types.nil?
|
319
320
|
types = [types] unless types.is_a?(Array)
|
@@ -325,7 +326,7 @@ module Aspera
|
|
325
326
|
opt[:deprecation] = deprecation
|
326
327
|
description = "#{description} (#{'deprecated'.blue}: #{deprecation})"
|
327
328
|
end
|
328
|
-
Log.log.
|
329
|
+
Log.log.trace1{"declare: #{option_symbol}: #{opt[:read_write]}".green}
|
329
330
|
if opt[:read_write].eql?(:accessor)
|
330
331
|
Aspera.assert_type(handler, Hash)
|
331
332
|
Aspera.assert(handler.keys.sort.eql?(%i[m o]))
|
@@ -449,11 +450,20 @@ module Aspera
|
|
449
450
|
unknown_options = []
|
450
451
|
begin
|
451
452
|
# remove known options one by one, exception if unknown
|
452
|
-
Log.log.trace1('
|
453
|
+
Log.log.trace1('Before parse')
|
453
454
|
@parser.parse!(@unprocessed_cmd_line_options)
|
454
|
-
Log.log.trace1('After parse'
|
455
|
+
Log.log.trace1('After parse')
|
455
456
|
rescue OptionParser::InvalidOption => e
|
456
457
|
Log.log.trace1{"InvalidOption #{e}".red}
|
458
|
+
if (m = e.args.first.match(/^--([a-z\-]+)\.([^=]+)=(.+)$/))
|
459
|
+
option, path, raw_value = m.captures
|
460
|
+
option_sym = self.class.option_line_to_name(option).to_sym
|
461
|
+
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')
|
464
|
+
retry
|
465
|
+
end
|
466
|
+
end
|
457
467
|
# save for later processing
|
458
468
|
unknown_options.push(e.args.first)
|
459
469
|
retry
|
@@ -467,7 +477,7 @@ module Aspera
|
|
467
477
|
return $stdin.getpass("#{prompt}> ") if sensitive
|
468
478
|
print("#{prompt}> ")
|
469
479
|
line = $stdin.gets
|
470
|
-
|
480
|
+
Aspera.assert_type(line, String){'Unexpected end of standard input'}
|
471
481
|
return line.chomp
|
472
482
|
end
|
473
483
|
|
@@ -481,7 +491,7 @@ module Aspera
|
|
481
491
|
if sym_list.any?{ |a| a.eql?(input)}
|
482
492
|
return input
|
483
493
|
else
|
484
|
-
$stderr.puts("No such #{prompt}: #{input}, select one of: #{sym_list.join(', ')}")
|
494
|
+
$stderr.puts("No such #{prompt}: #{input}, select one of: #{sym_list.join(', ')}") # rubocop:disable Style/StderrPuts
|
485
495
|
end
|
486
496
|
end
|
487
497
|
end
|
@@ -521,10 +531,21 @@ module Aspera
|
|
521
531
|
|
522
532
|
private
|
523
533
|
|
524
|
-
|
525
|
-
|
526
|
-
|
534
|
+
# Using dotted hash notation, convert value to bool, int, float or extended value
|
535
|
+
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)
|
527
544
|
end
|
545
|
+
end
|
546
|
+
|
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)})
|
528
549
|
return ExtendedValue.instance.evaluate(value)
|
529
550
|
end
|
530
551
|
|
@@ -535,11 +556,12 @@ module Aspera
|
|
535
556
|
return result
|
536
557
|
end
|
537
558
|
|
559
|
+
# TODO: use formatter
|
538
560
|
def highlight_current(value)
|
539
561
|
$stdout.isatty ? value.to_s.red.bold : "[#{value}]"
|
540
562
|
end
|
541
563
|
|
542
|
-
# try to evaluate options set
|
564
|
+
# try to evaluate options set in batch
|
543
565
|
# @param unprocessed_options [Array] list of options to apply (key_sym,value)
|
544
566
|
# @param where [String] where the options come from
|
545
567
|
def consume_option_pairs(unprocessed_options, where)
|
@@ -548,9 +570,7 @@ module Aspera
|
|
548
570
|
unprocessed_options.each do |k, v|
|
549
571
|
if @declared_options.key?(k)
|
550
572
|
# constrained parameters as string are revert to symbol
|
551
|
-
if @declared_options[k].key?(:values) && v.is_a?(String)
|
552
|
-
v = self.class.get_from_list(v, "#{k} in #{where}", @declared_options[k][:values])
|
553
|
-
end
|
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)
|
554
574
|
options_to_set[k] = v
|
555
575
|
else
|
556
576
|
Log.log.trace1{"unprocessed: #{k}: #{v}"}
|