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