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