aspera-cli 4.26.2 → 4.27.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 +310 -21
- data/CONTRIBUTING.md +99 -14
- data/TODO.md +50 -0
- data/bin/ascli +3 -3
- data/docs/README.md +10977 -0
- data/docs/test-mcp-with-ai.md +202 -0
- data/lib/aspera/agent/base.rb +29 -7
- data/lib/aspera/agent/connect.rb +58 -21
- data/lib/aspera/agent/desktop.rb +53 -18
- data/lib/aspera/agent/direct.rb +71 -40
- data/lib/aspera/agent/factory.rb +3 -3
- data/lib/aspera/agent/httpgw.rb +55 -14
- data/lib/aspera/agent/node.rb +34 -2
- data/lib/aspera/agent/transferd.rb +48 -12
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +42 -44
- data/lib/aspera/api/cos_node.rb +10 -9
- data/lib/aspera/api/faspex.rb +16 -15
- data/lib/aspera/api/httpgw.rb +23 -23
- data/lib/aspera/api/node.rb +51 -70
- data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
- data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
- data/lib/aspera/ascmd.rb +32 -30
- data/lib/aspera/ascp/installation.rb +75 -69
- data/lib/aspera/ascp/management.rb +7 -8
- data/lib/aspera/assert.rb +12 -13
- data/lib/aspera/cli/ascp_actions.rb +155 -0
- data/lib/aspera/cli/async_transfer_store.rb +125 -0
- data/lib/aspera/cli/bootstrapper.rb +197 -0
- data/lib/aspera/cli/command_registry.rb +145 -0
- data/lib/aspera/cli/command_spec.rb +141 -0
- data/lib/aspera/cli/context.rb +19 -6
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +27 -26
- data/lib/aspera/cli/formatter.rb +95 -59
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +3 -3
- data/lib/aspera/cli/http.rb +74 -30
- data/lib/aspera/cli/info.rb +2 -0
- data/lib/aspera/cli/mailer.rb +97 -0
- data/lib/aspera/cli/mcp_tool.rb +225 -0
- data/lib/aspera/cli/option_declarator.rb +77 -0
- data/lib/aspera/cli/options.schema.yaml +634 -7
- data/lib/aspera/cli/parser.rb +1217 -0
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1178 -881
- data/lib/aspera/cli/plugins/ats.rb +205 -162
- data/lib/aspera/cli/plugins/base.rb +703 -175
- data/lib/aspera/cli/plugins/basic_auth.rb +7 -9
- data/lib/aspera/cli/plugins/config.rb +434 -757
- data/lib/aspera/cli/plugins/console.rb +107 -65
- data/lib/aspera/cli/plugins/cos.rb +46 -33
- data/lib/aspera/cli/plugins/factory.rb +6 -6
- data/lib/aspera/cli/plugins/faspex5.rb +627 -388
- data/lib/aspera/cli/plugins/faspio.rb +54 -51
- data/lib/aspera/cli/plugins/httpgw.rb +18 -25
- data/lib/aspera/cli/plugins/mcp.rb +279 -0
- data/lib/aspera/cli/plugins/node.rb +988 -840
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +112 -135
- data/lib/aspera/cli/plugins/preview.rb +195 -157
- data/lib/aspera/cli/plugins/server.rb +141 -91
- data/lib/aspera/cli/plugins/shares.rb +343 -110
- data/lib/aspera/cli/preset_actions.rb +159 -0
- data/lib/aspera/cli/preset_manager.rb +82 -39
- data/lib/aspera/cli/result.rb +78 -26
- data/lib/aspera/cli/runner.rb +323 -153
- data/lib/aspera/cli/secret_finder.rb +40 -0
- data/lib/aspera/cli/special_values.rb +1 -0
- data/lib/aspera/cli/sync_actions.rb +89 -64
- data/lib/aspera/cli/terminal_formatter.rb +3 -3
- data/lib/aspera/cli/transfer_actions.rb +90 -0
- data/lib/aspera/cli/transfer_agent.rb +129 -64
- data/lib/aspera/cli/transfer_progress.rb +9 -9
- data/lib/aspera/cli/vault_manager.rb +79 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +28 -30
- data/lib/aspera/colors.rb +3 -3
- data/lib/aspera/command_line_builder.rb +34 -34
- data/lib/aspera/command_line_converter.rb +1 -1
- data/lib/aspera/coverage.rb +1 -2
- data/lib/aspera/data_repository.rb +2 -1
- data/lib/aspera/dot_container.rb +12 -12
- data/lib/aspera/environment.rb +30 -23
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +5 -5
- data/lib/aspera/faspex_postproc.rb +16 -10
- data/lib/aspera/graphql.rb +37 -0
- data/lib/aspera/hash_ext.rb +8 -2
- data/lib/aspera/json_rpc/client.rb +62 -0
- data/lib/aspera/json_rpc/version.rb +7 -0
- data/lib/aspera/keychain/base.rb +10 -3
- data/lib/aspera/keychain/encrypted_hash.rb +92 -17
- data/lib/aspera/keychain/factory.rb +20 -8
- data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +23 -25
- data/lib/aspera/keychain/one_password_api.rb +86 -0
- data/lib/aspera/keychain/one_password_base.rb +34 -0
- data/lib/aspera/keychain/one_password_cli.rb +98 -0
- data/lib/aspera/link_header.rb +82 -0
- data/lib/aspera/log.rb +29 -15
- data/lib/aspera/markdown.rb +90 -7
- data/lib/aspera/nagios.rb +8 -8
- data/lib/aspera/node_simulator.rb +35 -27
- data/lib/aspera/oauth/base.rb +9 -10
- data/lib/aspera/oauth/boot.rb +5 -5
- data/lib/aspera/oauth/factory.rb +12 -10
- data/lib/aspera/oauth/jwt.rb +9 -11
- data/lib/aspera/oauth/web.rb +6 -6
- data/lib/aspera/persistency_action_once.rb +12 -12
- data/lib/aspera/persistency_folder.rb +18 -11
- data/lib/aspera/preview/file_types.rb +5 -5
- data/lib/aspera/preview/generator.rb +52 -49
- data/lib/aspera/preview/options.rb +3 -2
- data/lib/aspera/preview/terminal.rb +5 -5
- data/lib/aspera/preview/utils.rb +66 -19
- data/lib/aspera/products/connect.rb +2 -2
- data/lib/aspera/products/desktop.rb +1 -1
- data/lib/aspera/products/other.rb +3 -3
- data/lib/aspera/products/transferd.rb +4 -4
- data/lib/aspera/proxy_auto_config.rb +15 -14
- data/lib/aspera/rest.rb +159 -151
- data/lib/aspera/rest_error_analyzer.rb +6 -6
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +11 -10
- data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
- data/lib/aspera/schema/documentation.rb +65 -28
- data/lib/aspera/schema/reader.rb +136 -13
- data/lib/aspera/schema/registry.rb +60 -8
- data/lib/aspera/secret_hider.rb +12 -7
- data/lib/aspera/ssh.rb +64 -31
- data/lib/aspera/ssl.rb +5 -5
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/database.rb +2 -2
- data/lib/aspera/sync/operations.rb +22 -25
- data/lib/aspera/temp_file_manager.rb +25 -5
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +36 -34
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +10 -10
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +12 -3
- data/lib/aspera/uri_reader.rb +52 -15
- data/lib/aspera/web_auth.rb +8 -7
- data/lib/aspera/web_server_simple.rb +15 -12
- data/lib/aspera/yaml.rb +5 -4
- data.tar.gz.sig +0 -0
- metadata +34 -6
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/manager.rb +0 -766
- data/lib/aspera/cli/plugins/faspex.rb +0 -539
- data/lib/aspera/json_rpc.rb +0 -52
- data/lib/aspera/transfer/uri.rb +0 -56
data/lib/aspera/cli/manager.rb
DELETED
|
@@ -1,766 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'aspera/cli/extended_value'
|
|
4
|
-
require 'aspera/cli/error'
|
|
5
|
-
require 'aspera/cli/terminal_formatter'
|
|
6
|
-
require 'aspera/colors'
|
|
7
|
-
require 'aspera/secret_hider'
|
|
8
|
-
require 'aspera/log'
|
|
9
|
-
require 'aspera/assert'
|
|
10
|
-
require 'aspera/dot_container'
|
|
11
|
-
require 'io/console'
|
|
12
|
-
require 'optparse'
|
|
13
|
-
|
|
14
|
-
module Aspera
|
|
15
|
-
module Cli
|
|
16
|
-
# Exception raised when schema is asked (`help`)
|
|
17
|
-
class SchemaRequest < Error
|
|
18
|
-
# @return [String, nil] path to schema file
|
|
19
|
-
attr_reader :path
|
|
20
|
-
|
|
21
|
-
# @param type [Symbol] :argument or :option
|
|
22
|
-
# @param name [String] name of the option/argument
|
|
23
|
-
# @param schema_path [String, nil] path to schema file, or `nil` if not available
|
|
24
|
-
def initialize(type, name, schema_path)
|
|
25
|
-
super("#{type}: #{name}")
|
|
26
|
-
@path = schema_path
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
module BoolValue
|
|
31
|
-
# boolean options are set to true/false from the following values
|
|
32
|
-
YES_SYM = :yes
|
|
33
|
-
NO_SYM = :no
|
|
34
|
-
FALSE_VALUES = [NO_SYM, false].freeze
|
|
35
|
-
TRUE_VALUES = [YES_SYM, true].freeze
|
|
36
|
-
private_constant :YES_SYM, :NO_SYM, :FALSE_VALUES, :TRUE_VALUES
|
|
37
|
-
# Boolean values
|
|
38
|
-
# @return [Array<true, false, :yes, :no>]
|
|
39
|
-
ALL = (TRUE_VALUES + FALSE_VALUES).freeze
|
|
40
|
-
# `false` and `true`
|
|
41
|
-
TYPES = [FalseClass, TrueClass].freeze
|
|
42
|
-
SYMBOLS = [NO_SYM, YES_SYM].freeze
|
|
43
|
-
# @return `true` if value is a value for `true` in ALL
|
|
44
|
-
def true?(enum)
|
|
45
|
-
Aspera.assert_values(enum, ALL){'boolean'}
|
|
46
|
-
TRUE_VALUES.include?(enum)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# @return [:yes, :no]
|
|
50
|
-
def to_sym(enum)
|
|
51
|
-
Aspera.assert_values(enum, ALL){'boolean'}
|
|
52
|
-
TRUE_VALUES.include?(enum) ? YES_SYM : NO_SYM
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# @return `true` if value is a value for `true` or `false` in ALL
|
|
56
|
-
def symbol?(sym)
|
|
57
|
-
ALL.include?(sym)
|
|
58
|
-
end
|
|
59
|
-
module_function :true?, :to_sym, :symbol?
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Constants to be used as parameter `allowed:` for `OptionValue`
|
|
63
|
-
module Allowed
|
|
64
|
-
# This option can be set to a single string or array, multiple times, and gives Array of String
|
|
65
|
-
TYPES_STRING_ARRAY = [Array, String].freeze
|
|
66
|
-
# A list of symbols with constrained values
|
|
67
|
-
TYPES_SYMBOL_ARRAY = [Array, Symbol].freeze
|
|
68
|
-
# Value will be coerced to int
|
|
69
|
-
TYPES_INTEGER = [Integer].freeze
|
|
70
|
-
TYPES_BOOLEAN = BoolValue::TYPES
|
|
71
|
-
# No value at all for the option, it's a switch, like `-N`
|
|
72
|
-
TYPES_NONE = [].freeze
|
|
73
|
-
# Symbol
|
|
74
|
-
TYPES_ENUM = [Symbol].freeze
|
|
75
|
-
# String
|
|
76
|
-
TYPES_STRING = [String].freeze
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Description of option, how to manage
|
|
80
|
-
class OptionValue
|
|
81
|
-
# [Array(Class)] List of allowed types
|
|
82
|
-
attr_reader :types, :sensitive, :schema, :option
|
|
83
|
-
# [Array] List of allowed values (Symbols and specific values)
|
|
84
|
-
attr_accessor :values
|
|
85
|
-
|
|
86
|
-
# @param option [Symbol] Name of option
|
|
87
|
-
# @param description [String] Description for help
|
|
88
|
-
# @param allowed [nil,Class,Array<Class>,Array<Symbol>] Allowed values
|
|
89
|
-
# @param handler [Hash] Accessor: keys: :o(object) and :m(method)
|
|
90
|
-
# @param deprecation [String] Deprecation message
|
|
91
|
-
# @param schema [String] Declaration of schema
|
|
92
|
-
# `allowed`:
|
|
93
|
-
# - `nil` No validation, so just a string
|
|
94
|
-
# - `Class` The single allowed Class
|
|
95
|
-
# - `Array<Class>` Multiple allowed classes
|
|
96
|
-
# - `Array<Symbol>` List of allowed values
|
|
97
|
-
def initialize(option:, description:, allowed: Allowed::TYPES_STRING, handler: nil, deprecation: nil, schema: nil)
|
|
98
|
-
Log.log.trace1{"option: #{option}, allowed: #{allowed}"}
|
|
99
|
-
@option = option
|
|
100
|
-
@description = description
|
|
101
|
-
# by default passwords and secrets are sensitive, else specify when declaring the option
|
|
102
|
-
@sensitive = SecretHider.instance.secret?(@option, '')
|
|
103
|
-
# either the value, or object giving value
|
|
104
|
-
@object = handler&.[](:o)
|
|
105
|
-
@read_method = handler&.[](:m)
|
|
106
|
-
@write_method = @read_method ? "#{@read_method}=".to_sym : nil
|
|
107
|
-
@deprecation = deprecation
|
|
108
|
-
@schema = schema
|
|
109
|
-
@access = if @object.nil?
|
|
110
|
-
:local
|
|
111
|
-
elsif @object.respond_to?(@write_method)
|
|
112
|
-
:write
|
|
113
|
-
else
|
|
114
|
-
:setter
|
|
115
|
-
end
|
|
116
|
-
Aspera.assert(@object.respond_to?(@read_method)){"#{@object} does not respond to #{@read_method}"} unless @access.eql?(:local)
|
|
117
|
-
@types = nil
|
|
118
|
-
@values = nil
|
|
119
|
-
if !allowed.nil?
|
|
120
|
-
allowed = [allowed] if allowed.is_a?(Class)
|
|
121
|
-
Aspera.assert_type(allowed, Array)
|
|
122
|
-
if allowed.take(Allowed::TYPES_SYMBOL_ARRAY.length) == Allowed::TYPES_SYMBOL_ARRAY
|
|
123
|
-
# Special case: array of defined symbol values
|
|
124
|
-
@types = Allowed::TYPES_SYMBOL_ARRAY
|
|
125
|
-
@values = allowed[Allowed::TYPES_SYMBOL_ARRAY.length..]
|
|
126
|
-
elsif allowed.all?(Class)
|
|
127
|
-
@types = allowed
|
|
128
|
-
@values = BoolValue::ALL if allowed.eql?(Allowed::TYPES_BOOLEAN)
|
|
129
|
-
# Default value for array
|
|
130
|
-
@object ||= [] if @types.first.eql?(Array) && !@types.include?(NilClass)
|
|
131
|
-
@object ||= {} if @types.first.eql?(Hash) && !@types.include?(NilClass)
|
|
132
|
-
elsif allowed.all?(Symbol)
|
|
133
|
-
@types = Allowed::TYPES_ENUM
|
|
134
|
-
@values = allowed
|
|
135
|
-
else
|
|
136
|
-
Aspera.error_unexpected_value(allowed)
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
Log.log.trace1{"declare: #{@option}: #{@access} #{@object.class}.#{@read_method}".green}
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def clear
|
|
143
|
-
@object = nil
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def value(log: true)
|
|
147
|
-
current_value =
|
|
148
|
-
case @access
|
|
149
|
-
when :local then @object
|
|
150
|
-
when :write then @object.send(@read_method)
|
|
151
|
-
when :setter then @object.send(@read_method, @option, :get)
|
|
152
|
-
end
|
|
153
|
-
Log.log.trace1{"#{@option} -> (#{current_value.class})#{current_value}"} if log
|
|
154
|
-
current_value
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# Assign value to option.
|
|
158
|
-
# Value can be a `String`, then evaluated with `ExtendedValue`, or directly a value.
|
|
159
|
-
# @param value [String, Object] Value to assign to option
|
|
160
|
-
# @param where [String] Where the value is assigned from
|
|
161
|
-
# @return [nil]
|
|
162
|
-
def assign_value(value, where:)
|
|
163
|
-
Aspera.assert(!@deprecation, type: warn){"Option #{@option} is deprecated: #{@deprecation}"}
|
|
164
|
-
new_value = ExtendedValue.instance.evaluate(value, context: "option: #{@option}", allowed: @types)
|
|
165
|
-
Log.log.trace1{"#{where}: #{@option} <- (#{new_value.class})#{new_value}"}
|
|
166
|
-
new_value = BoolValue.true?(new_value) if @types.eql?(Allowed::TYPES_BOOLEAN)
|
|
167
|
-
new_value = Integer(new_value) if @types.eql?(Allowed::TYPES_INTEGER)
|
|
168
|
-
new_value = [new_value] if @types.eql?(Allowed::TYPES_STRING_ARRAY) && new_value.is_a?(String)
|
|
169
|
-
# Setting a Hash to null set an empty hash
|
|
170
|
-
new_value = {} if new_value.eql?(nil) && @types&.first.eql?(Hash)
|
|
171
|
-
# Setting a Array to null set an empty array
|
|
172
|
-
new_value = [] if new_value.eql?(nil) && @types&.first.eql?(Array)
|
|
173
|
-
if @types.eql?(Aspera::Cli::Allowed::TYPES_SYMBOL_ARRAY)
|
|
174
|
-
new_value = [new_value] if new_value.is_a?(String)
|
|
175
|
-
Aspera.assert_array_all(new_value, String, type: BadArgument)
|
|
176
|
-
new_value = new_value.map{ |v| Manager.get_from_list(v, @option, @values)}
|
|
177
|
-
end
|
|
178
|
-
Aspera.assert_type(new_value, *@types, type: BadArgument){"Option #{@option}"} if @types
|
|
179
|
-
if new_value.is_a?(Hash) || new_value.is_a?(Array)
|
|
180
|
-
current_value = value(log: false)
|
|
181
|
-
new_value = current_value.deep_merge(new_value) if new_value.is_a?(Hash) && current_value.is_a?(Hash) && !current_value.empty?
|
|
182
|
-
new_value = current_value + new_value if new_value.is_a?(Array) && current_value.is_a?(Array) && !current_value.empty?
|
|
183
|
-
end
|
|
184
|
-
case @access
|
|
185
|
-
when :local then @object = new_value
|
|
186
|
-
when :write then @object.send(@write_method, new_value)
|
|
187
|
-
when :setter then @object.send(@read_method, @option, :set, new_value)
|
|
188
|
-
end
|
|
189
|
-
Log.log.trace1{v = value(log: false); "#{@option} <- (#{v.class})#{v}"} # rubocop:disable Style/Semicolon
|
|
190
|
-
nil
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# parse command line options
|
|
195
|
-
# arguments options start with '-', others are commands
|
|
196
|
-
# resolves on extended value syntax
|
|
197
|
-
class Manager
|
|
198
|
-
class << self
|
|
199
|
-
# Find shortened string value in allowed symbol list
|
|
200
|
-
def get_from_list(short_value, descr, allowed_values)
|
|
201
|
-
Aspera.assert_type(short_value, String)
|
|
202
|
-
# we accept shortcuts
|
|
203
|
-
matching_exact = allowed_values.select{ |i| i.to_s.eql?(short_value)}
|
|
204
|
-
return matching_exact.first if matching_exact.length == 1
|
|
205
|
-
matching = allowed_values.select{ |i| i.to_s.start_with?(short_value)}
|
|
206
|
-
Aspera.assert(!matching.empty?, multi_choice_assert_msg("unknown value for #{descr}: #{short_value}", allowed_values), type: BadArgument)
|
|
207
|
-
Aspera.assert(matching.length.eql?(1), multi_choice_assert_msg("ambiguous shortcut for #{descr}: #{short_value}", matching), type: BadArgument)
|
|
208
|
-
return BoolValue.true?(matching.first) if allowed_values.eql?(BoolValue::ALL)
|
|
209
|
-
matching.first
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
# Generates error message with list of allowed values
|
|
213
|
-
# @param error_msg [String] Error message
|
|
214
|
-
# @param accept_list [Array<Symbol>] List of allowed values
|
|
215
|
-
def multi_choice_assert_msg(error_msg, accept_list)
|
|
216
|
-
[error_msg, 'Use:', *accept_list.map{ |choice| "- #{choice}"}.sort].join("\n")
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
# Change option name with dash to name with underscore
|
|
220
|
-
# @param name [String] option name
|
|
221
|
-
# @return [String]
|
|
222
|
-
def option_line_to_name(name)
|
|
223
|
-
name.gsub(OPTION_SEP_LINE, OPTION_SEP_SYMBOL)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def option_name_to_line(name)
|
|
227
|
-
"#{OPTION_PREFIX}#{name.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)}"
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
# @return [Hash{Symbol => String}, nil] `{field:,value:}` if identifier is a percent selector, else `nil`
|
|
231
|
-
def percent_selector(identifier)
|
|
232
|
-
Aspera.assert_type(identifier, String)
|
|
233
|
-
if (m = identifier.match(REGEX_LOOKUP_ID_BY_FIELD))
|
|
234
|
-
return {field: m[1], value: ExtendedValue.instance.evaluate(m[2], context: "percent selector: #{m[1]}")}
|
|
235
|
-
end
|
|
236
|
-
nil
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
attr_reader :parser
|
|
241
|
-
attr_accessor :ask_missing_mandatory, :ask_missing_optional
|
|
242
|
-
attr_writer :fail_on_missing_mandatory
|
|
243
|
-
|
|
244
|
-
# @param program_name [String] Name of the program
|
|
245
|
-
# @param argv [Array<String>, nil] Command line arguments to parse
|
|
246
|
-
def initialize(program_name, argv = nil)
|
|
247
|
-
# command line values *not* starting with '-'
|
|
248
|
-
@unprocessed_cmd_line_arguments = []
|
|
249
|
-
# command line values starting with at least one '-'
|
|
250
|
-
@unprocessed_cmd_line_options = []
|
|
251
|
-
# a copy of all initial options
|
|
252
|
-
@initial_cli_options = []
|
|
253
|
-
# Option descriptions: maps option symbol to its OptionValue descriptor
|
|
254
|
-
# @type [Hash{Symbol => OptionValue}]
|
|
255
|
-
@declared_options = {}
|
|
256
|
-
# do we ask missing options and arguments to user ?
|
|
257
|
-
@ask_missing_mandatory = false # STDIN.isatty
|
|
258
|
-
# ask optional options if not provided and in interactive
|
|
259
|
-
@ask_missing_optional = false
|
|
260
|
-
# get_option fails if a mandatory parameter is asked
|
|
261
|
-
@fail_on_missing_mandatory = true
|
|
262
|
-
# Array of [key(sym), value]
|
|
263
|
-
# those must be set before parse
|
|
264
|
-
# parse consumes those defined only
|
|
265
|
-
@option_pairs_batch = {}
|
|
266
|
-
@option_pairs_env = {}
|
|
267
|
-
# NOTE: was initially inherited but it is preferred to have specific methods
|
|
268
|
-
@parser = OptionParser.new
|
|
269
|
-
@parser.program_name = program_name
|
|
270
|
-
# options can also be provided by env vars : --param-name -> ASCLI_PARAM_NAME
|
|
271
|
-
env_prefix = program_name.upcase + OPTION_SEP_SYMBOL
|
|
272
|
-
ENV.each do |k, v|
|
|
273
|
-
@option_pairs_env[k.delete_prefix(env_prefix).downcase.to_sym] = v if k.start_with?(env_prefix)
|
|
274
|
-
end
|
|
275
|
-
Log.log.debug{"env=#{@option_pairs_env}".red}
|
|
276
|
-
@unprocessed_cmd_line_options = []
|
|
277
|
-
@unprocessed_cmd_line_arguments = []
|
|
278
|
-
return if argv.nil?
|
|
279
|
-
# true until `--` is found (stop options)
|
|
280
|
-
process_options = true
|
|
281
|
-
until argv.empty?
|
|
282
|
-
value = argv.shift
|
|
283
|
-
if process_options && value.start_with?('-')
|
|
284
|
-
Log.log.trace1{"opt: #{value}"}
|
|
285
|
-
if value.eql?(OPTIONS_STOP)
|
|
286
|
-
process_options = false
|
|
287
|
-
else
|
|
288
|
-
@unprocessed_cmd_line_options.push(value)
|
|
289
|
-
end
|
|
290
|
-
else
|
|
291
|
-
Log.log.trace1{"arg: #{value}"}
|
|
292
|
-
@unprocessed_cmd_line_arguments.push(value)
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
|
-
@initial_cli_options = @unprocessed_cmd_line_options.dup.freeze
|
|
296
|
-
Log.log.trace1{"add_cmd_line_options:commands/arguments=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
|
|
297
|
-
@parser.separator('')
|
|
298
|
-
@parser.separator('OPTIONS: global')
|
|
299
|
-
declare(:interactive, 'Use interactive input of missing params', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :ask_missing_mandatory})
|
|
300
|
-
declare(:ask_options, 'Ask even optional options', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :ask_missing_optional})
|
|
301
|
-
# do not parse options yet, let's wait for option `-h` to be overridden
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
# Add a type to the message if not special types
|
|
305
|
-
# @param types [Array<Class>] types to add
|
|
306
|
-
# @return [String] Types if relevant
|
|
307
|
-
def add_types_info(types)
|
|
308
|
-
return '' if !types || types.empty? || types.eql?(Allowed::TYPES_ENUM) || types.eql?(Allowed::TYPES_BOOLEAN) || types.eql?(Allowed::TYPES_STRING)
|
|
309
|
-
" (#{types.map(&:name).join(', ')})"
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
# Declare an option
|
|
313
|
-
# @param option_symbol [Symbol] option name
|
|
314
|
-
# @param description [String] description for help
|
|
315
|
-
# @param short [String] short option name
|
|
316
|
-
# @param allowed [Object] Allowed values, see `OptionValue`
|
|
317
|
-
# @param default [Object] default value
|
|
318
|
-
# @param handler [Hash] handler for option value: keys: :o(object) and :m(method)
|
|
319
|
-
# @param deprecation [String] deprecation
|
|
320
|
-
# @param schema [String] Definition of schema for Hash parameters
|
|
321
|
-
# @param block [Proc] Block to execute when option is found
|
|
322
|
-
def declare(option_symbol, description, short: nil, allowed: nil, default: nil, handler: nil, deprecation: nil, schema: nil, &block)
|
|
323
|
-
Aspera.assert_type(option_symbol, Symbol)
|
|
324
|
-
Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
|
|
325
|
-
Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
|
|
326
|
-
Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with an uppercase"}
|
|
327
|
-
Aspera.assert(!['hash', 'extended value'].any?{ |s| description.downcase.include?(s)}){"#{option_symbol} shall use :allowed instead of hash/extended value in option description"}
|
|
328
|
-
Aspera.assert_type(handler, Hash) if handler
|
|
329
|
-
Aspera.assert(handler.keys.sort.eql?(%i[m o]), 'handler must have keys :m and :o') if handler
|
|
330
|
-
option_attrs = @declared_options[option_symbol] = OptionValue.new(
|
|
331
|
-
option: option_symbol,
|
|
332
|
-
description: description,
|
|
333
|
-
allowed: allowed,
|
|
334
|
-
handler: handler,
|
|
335
|
-
deprecation: deprecation,
|
|
336
|
-
schema: schema
|
|
337
|
-
)
|
|
338
|
-
real_types = option_attrs.types&.reject{ |i| [NilClass, String, Symbol].include?(i)}
|
|
339
|
-
description += add_types_info(real_types)
|
|
340
|
-
description = "#{description} (#{'deprecated'.blue}: #{deprecation})" if deprecation
|
|
341
|
-
set_option(option_symbol, default, where: 'default') unless default.nil?
|
|
342
|
-
on_args = [description]
|
|
343
|
-
case option_attrs.types
|
|
344
|
-
when Allowed::TYPES_ENUM, Allowed::TYPES_BOOLEAN
|
|
345
|
-
# This option value must be a symbol (or array of symbols)
|
|
346
|
-
set_option(option_symbol, BoolValue.true?(default), where: 'default') if option_attrs.values.eql?(BoolValue::ALL) && !default.nil?
|
|
347
|
-
value = get_option(option_symbol)
|
|
348
|
-
help_values =
|
|
349
|
-
if option_attrs.types.eql?(Allowed::TYPES_BOOLEAN)
|
|
350
|
-
highlight_current_in_list(BoolValue::SYMBOLS, BoolValue.to_sym(value))
|
|
351
|
-
else
|
|
352
|
-
highlight_current_in_list(option_attrs.values, value)
|
|
353
|
-
end
|
|
354
|
-
on_args[0] = "#{description}: #{help_values}"
|
|
355
|
-
on_args.push(symbol_to_option(option_symbol, 'ENUM'))
|
|
356
|
-
# on_args.push(option_attrs.values)
|
|
357
|
-
@parser.on(*on_args) do |v|
|
|
358
|
-
set_option(option_symbol, self.class.get_from_list(v.to_s, description, option_attrs.values), where: SOURCE_USER)
|
|
359
|
-
end
|
|
360
|
-
when Allowed::TYPES_NONE
|
|
361
|
-
Aspera.assert_type(block, Proc){"missing execution block for #{option_symbol}"}
|
|
362
|
-
on_args.push(symbol_to_option(option_symbol))
|
|
363
|
-
on_args.push("-#{short}") if short.is_a?(String)
|
|
364
|
-
@parser.on(*on_args, &block)
|
|
365
|
-
else
|
|
366
|
-
on_args.push(symbol_to_option(option_symbol, 'VALUE'))
|
|
367
|
-
on_args.push("-#{short}VALUE") unless short.nil?
|
|
368
|
-
# coerce integer
|
|
369
|
-
on_args.push(Integer) if option_attrs.types.eql?(Allowed::TYPES_INTEGER)
|
|
370
|
-
@parser.on(*on_args) do |v|
|
|
371
|
-
set_option(option_symbol, v, where: SOURCE_USER)
|
|
372
|
-
end
|
|
373
|
-
end
|
|
374
|
-
Log.log.trace1{"on_args=#{on_args}"}
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
# @param descr [String] description for help
|
|
378
|
-
# @param mandatory [Boolean] `true`: raise error no more argument
|
|
379
|
-
# @param multiple [Boolean] `true`: return all remaining arguments (Array). String: until marker
|
|
380
|
-
# @param accept_list [Array<Symbol>, NilClass] list of allowed values
|
|
381
|
-
# @param validation [Class, Array, NilClass] Accepted value type(s) or list of Symbols
|
|
382
|
-
# @param aliases [Hash] map of aliases: key = alias, value = real value
|
|
383
|
-
# @param default [Object] default value
|
|
384
|
-
# @return one value, list or nil (if optional and no default)
|
|
385
|
-
def get_next_argument(descr, mandatory: true, multiple: false, accept_list: nil, validation: Allowed::TYPES_STRING, aliases: nil, default: nil, schema: nil)
|
|
386
|
-
Aspera.assert_array_all(accept_list, Symbol) unless accept_list.nil?
|
|
387
|
-
Aspera.assert_hash_all(aliases, Symbol, Symbol) unless aliases.nil?
|
|
388
|
-
validation = Symbol unless accept_list.nil?
|
|
389
|
-
validation = [validation] unless validation.is_a?(Array) || validation.nil?
|
|
390
|
-
Aspera.assert_array_all(validation, Class){'validation'} unless validation.nil?
|
|
391
|
-
descr = "#{descr}#{add_types_info(validation)}"
|
|
392
|
-
result =
|
|
393
|
-
if !@unprocessed_cmd_line_arguments.empty?
|
|
394
|
-
case multiple
|
|
395
|
-
when true
|
|
396
|
-
values = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length)
|
|
397
|
-
when false
|
|
398
|
-
values = [@unprocessed_cmd_line_arguments.shift]
|
|
399
|
-
when String
|
|
400
|
-
index = @unprocessed_cmd_line_arguments.index(multiple)
|
|
401
|
-
if index
|
|
402
|
-
values = @unprocessed_cmd_line_arguments.shift(index)
|
|
403
|
-
@unprocessed_cmd_line_arguments.shift # remove end marker
|
|
404
|
-
else
|
|
405
|
-
values = @unprocessed_cmd_line_arguments.shift(@unprocessed_cmd_line_arguments.length)
|
|
406
|
-
end
|
|
407
|
-
else Aspera.error_unexpected_value(multiple){'multiple'}
|
|
408
|
-
end
|
|
409
|
-
values = values.map{ |v| ExtendedValue.instance.evaluate(v, context: "argument: #{descr}", allowed: validation)}
|
|
410
|
-
# If expecting list and only one arg of type array : it is the list
|
|
411
|
-
values = values.first if multiple && values.length.eql?(1) && values.first.is_a?(Array)
|
|
412
|
-
if accept_list
|
|
413
|
-
allowed_values = [].concat(accept_list)
|
|
414
|
-
allowed_values.concat(aliases.keys) unless aliases.nil?
|
|
415
|
-
values = values.map{ |v| self.class.get_from_list(v, descr, allowed_values)}
|
|
416
|
-
end
|
|
417
|
-
multiple ? values : values.first
|
|
418
|
-
elsif !default.nil? then default
|
|
419
|
-
# no value provided, either get value interactively, or exception
|
|
420
|
-
elsif mandatory then get_interactive(descr, multiple: multiple, accept_list: accept_list, schema: schema)
|
|
421
|
-
end
|
|
422
|
-
if result.is_a?(String) && validation&.eql?(Allowed::TYPES_INTEGER)
|
|
423
|
-
int_result = Integer(result, exception: false)
|
|
424
|
-
raise Cli::BadArgument, "Invalid integer: #{result}" if int_result.nil?
|
|
425
|
-
result = int_result
|
|
426
|
-
end
|
|
427
|
-
Log.log.trace1{"#{descr}=#{result}"}
|
|
428
|
-
result = aliases[result] if aliases&.key?(result)
|
|
429
|
-
# if value comes from JSON/YAML, it may come as Integer
|
|
430
|
-
result = result.to_s if result.is_a?(Integer) && validation&.eql?(Allowed::TYPES_STRING)
|
|
431
|
-
if validation && (mandatory || !result.nil?)
|
|
432
|
-
value_list = multiple ? result : [result]
|
|
433
|
-
value_list.each do |value|
|
|
434
|
-
raise SchemaRequest.new(:argument, descr, schema) if validation.include?(Hash) && value.eql?(HELP)
|
|
435
|
-
raise Cli::BadArgument,
|
|
436
|
-
"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)}
|
|
437
|
-
end
|
|
438
|
-
end
|
|
439
|
-
result
|
|
440
|
-
end
|
|
441
|
-
|
|
442
|
-
# Resource identifier as positional parameter
|
|
443
|
-
#
|
|
444
|
-
# @param description [String] description of the identifier
|
|
445
|
-
# @param block [Proc] block to search for identifier based on attribute value
|
|
446
|
-
# @return [String, Array<String>] identifier or list of IDs (if `bulk` option is set)
|
|
447
|
-
# @yieldparam field [String] The field name from percent selector
|
|
448
|
-
# @yieldparam value [String] The value from percent selector
|
|
449
|
-
# @yieldreturn [String] Resolved identifier
|
|
450
|
-
def instance_identifier(description: 'identifier', &block)
|
|
451
|
-
res_id = get_next_argument(description, multiple: get_option(:bulk)) if res_id.nil?
|
|
452
|
-
# Can be an Array
|
|
453
|
-
if res_id.is_a?(String) && (m = Manager.percent_selector(res_id))
|
|
454
|
-
Aspera.assert(block_given?, type: Cli::BadArgument){"Percent syntax for #{description} not supported in this context"}
|
|
455
|
-
res_id = yield(m[:field], m[:value])
|
|
456
|
-
end
|
|
457
|
-
res_id
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
def get_next_command(command_list, aliases: nil); get_next_argument('command', accept_list: command_list, aliases: aliases); end
|
|
461
|
-
|
|
462
|
-
# Get an option definition by name
|
|
463
|
-
# @param option_symbol [Symbol]
|
|
464
|
-
# @return [OptionValue] Option definition
|
|
465
|
-
# @raise [Cli::BadArgument] if option not found
|
|
466
|
-
def option_def(option_symbol)
|
|
467
|
-
Aspera.assert(@declared_options.key?(option_symbol), type: Cli::BadArgument){"Unknown option: #{option_symbol}"}
|
|
468
|
-
@declared_options[option_symbol]
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
# Get an option value by name
|
|
472
|
-
# either return value or calls handler, can return nil
|
|
473
|
-
# ask interactively if requested/required
|
|
474
|
-
# @param option_symbol [Symbol]
|
|
475
|
-
# @param mandatory [Boolean] if true, raise error if option not set
|
|
476
|
-
def get_option(option_symbol, mandatory: false)
|
|
477
|
-
Aspera.assert_type(option_symbol, Symbol)
|
|
478
|
-
option_attrs = option_def(option_symbol)
|
|
479
|
-
result = option_attrs.value
|
|
480
|
-
# Do not fail for manual generation if option mandatory but not set
|
|
481
|
-
return :skip_missing_mandatory if result.nil? && mandatory && !@fail_on_missing_mandatory
|
|
482
|
-
if result.nil?
|
|
483
|
-
if !@ask_missing_mandatory
|
|
484
|
-
Aspera.assert(!mandatory, type: Cli::BadArgument){"Missing mandatory option: #{option_symbol}"}
|
|
485
|
-
elsif @ask_missing_optional || mandatory
|
|
486
|
-
# ask_missing_mandatory
|
|
487
|
-
result = get_interactive(option_symbol.to_s, check_option: true, accept_list: option_attrs.values, schema: option_attrs.schema)
|
|
488
|
-
set_option(option_symbol, result, where: 'interactive')
|
|
489
|
-
end
|
|
490
|
-
end
|
|
491
|
-
result
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
# Set an option value by name, either store value or call handler
|
|
495
|
-
# String is given to extended value
|
|
496
|
-
# @param option_symbol [Symbol] option name
|
|
497
|
-
# @param value [String] Value to set
|
|
498
|
-
# @param where [String] Where the value comes from
|
|
499
|
-
def set_option(option_symbol, value, where: 'code override')
|
|
500
|
-
Aspera.assert_type(option_symbol, Symbol)
|
|
501
|
-
option = option_def(option_symbol)
|
|
502
|
-
raise SchemaRequest.new(:option, option.option, option.schema) if option.types&.include?(Hash) && value.eql?(HELP)
|
|
503
|
-
option.assign_value(value, where: where)
|
|
504
|
-
end
|
|
505
|
-
|
|
506
|
-
# Set option to `nil`
|
|
507
|
-
def clear_option(option_symbol)
|
|
508
|
-
Aspera.assert_type(option_symbol, Symbol)
|
|
509
|
-
option_def(option_symbol).clear
|
|
510
|
-
end
|
|
511
|
-
|
|
512
|
-
# Adds each of the keys of specified hash as an option
|
|
513
|
-
# @param preset_hash [Hash] Options to add
|
|
514
|
-
# @param where [String] Where the value comes from
|
|
515
|
-
# @param override [Boolean] Override if already present
|
|
516
|
-
def add_option_preset(preset_hash, where, override: true)
|
|
517
|
-
Aspera.assert_type(preset_hash, Hash)
|
|
518
|
-
Log.log.debug{"add_option_preset: #{preset_hash}, #{where}, #{override}"}
|
|
519
|
-
preset_hash.each do |k, v|
|
|
520
|
-
option_symbol = k.to_sym
|
|
521
|
-
@option_pairs_batch[option_symbol] = v if override || !@option_pairs_batch.key?(option_symbol)
|
|
522
|
-
end
|
|
523
|
-
end
|
|
524
|
-
|
|
525
|
-
# Allows a plugin to add an argument as next argument to process
|
|
526
|
-
def unshift_next_argument(argument)
|
|
527
|
-
@unprocessed_cmd_line_arguments.unshift(argument)
|
|
528
|
-
end
|
|
529
|
-
|
|
530
|
-
# Check if there were unprocessed values to generate error
|
|
531
|
-
def command_or_arg_empty?
|
|
532
|
-
@unprocessed_cmd_line_arguments.empty?
|
|
533
|
-
end
|
|
534
|
-
|
|
535
|
-
# Unprocessed options or arguments ?
|
|
536
|
-
def final_errors
|
|
537
|
-
result = []
|
|
538
|
-
result.push("unprocessed options: #{@unprocessed_cmd_line_options}") unless @unprocessed_cmd_line_options.empty?
|
|
539
|
-
result.push("unprocessed values: #{@unprocessed_cmd_line_arguments}") unless @unprocessed_cmd_line_arguments.empty?
|
|
540
|
-
result
|
|
541
|
-
end
|
|
542
|
-
|
|
543
|
-
# Get all original options on command line used to generate a config in config file
|
|
544
|
-
# @return [Hash] options as taken from config file and command line just before command execution
|
|
545
|
-
def unprocessed_options_with_value
|
|
546
|
-
result = {}
|
|
547
|
-
@initial_cli_options.each do |option_argument|
|
|
548
|
-
# ignore short options
|
|
549
|
-
next unless option_argument.start_with?(OPTION_PREFIX)
|
|
550
|
-
name, value = option_argument.delete_prefix(OPTION_PREFIX).split(OPTION_VALUE_SEPARATOR, 2)
|
|
551
|
-
# ignore options without value
|
|
552
|
-
next if value.nil?
|
|
553
|
-
Log.log.debug{"option #{name}=#{value}"}
|
|
554
|
-
path = name.split(DotContainer::SEPARATOR)
|
|
555
|
-
path[0] = self.class.option_line_to_name(path[0])
|
|
556
|
-
DotContainer.dotted_to_container(path, smart_convert(value), result)
|
|
557
|
-
@unprocessed_cmd_line_options.delete(option_argument)
|
|
558
|
-
end
|
|
559
|
-
result
|
|
560
|
-
end
|
|
561
|
-
|
|
562
|
-
# @param only_defined [Boolean] if true, only return options that were defined
|
|
563
|
-
# @return [Hash] options as taken from config file and command line just before command execution
|
|
564
|
-
def known_options(only_defined: false)
|
|
565
|
-
result = {}
|
|
566
|
-
@declared_options.each_key do |option_symbol|
|
|
567
|
-
v = get_option(option_symbol)
|
|
568
|
-
result[option_symbol] = v unless only_defined && v.nil?
|
|
569
|
-
rescue => e
|
|
570
|
-
result[option_symbol] = e.to_s
|
|
571
|
-
end
|
|
572
|
-
result
|
|
573
|
-
end
|
|
574
|
-
|
|
575
|
-
# Removes already known options from the list
|
|
576
|
-
def parse_options!
|
|
577
|
-
Log.log.trace1('parse_options!'.red)
|
|
578
|
-
# First options from conf file
|
|
579
|
-
consume_option_pairs(@option_pairs_batch, 'set')
|
|
580
|
-
# Then, env var (to override)
|
|
581
|
-
consume_option_pairs(@option_pairs_env, 'env')
|
|
582
|
-
# Then, command line override
|
|
583
|
-
unknown_options = []
|
|
584
|
-
begin
|
|
585
|
-
# remove known options one by one, exception if unknown
|
|
586
|
-
Log.log.trace1('Before parse')
|
|
587
|
-
Log.dump(:unprocessed_cmd_line_options, @unprocessed_cmd_line_options, level: :trace1)
|
|
588
|
-
@parser.parse!(@unprocessed_cmd_line_options)
|
|
589
|
-
Log.log.trace1('After parse')
|
|
590
|
-
rescue OptionParser::InvalidOption => e
|
|
591
|
-
Log.log.trace1{"InvalidOption #{e}".red}
|
|
592
|
-
# An option like --a.b.c=d does: a={"b":{"c":ext_val(d)}}
|
|
593
|
-
if e.args.first.start_with?(OPTION_PREFIX)
|
|
594
|
-
name, value = e.args.first.delete_prefix(OPTION_PREFIX).split(OPTION_VALUE_SEPARATOR, 2)
|
|
595
|
-
if !value.nil?
|
|
596
|
-
path = name.split(DotContainer::SEPARATOR)
|
|
597
|
-
option_sym = self.class.option_line_to_name(path.shift).to_sym
|
|
598
|
-
if @declared_options.key?(option_sym)
|
|
599
|
-
# it's a known option, so let's process it
|
|
600
|
-
set_option(option_sym, DotContainer.dotted_to_container(path, smart_convert(value), get_option(option_sym)), where: 'dotted')
|
|
601
|
-
# resume to next
|
|
602
|
-
retry
|
|
603
|
-
end
|
|
604
|
-
end
|
|
605
|
-
end
|
|
606
|
-
# Save for later processing
|
|
607
|
-
unknown_options.push(e.args.first)
|
|
608
|
-
retry
|
|
609
|
-
end
|
|
610
|
-
Log.log.trace1{"remains: #{unknown_options}"}
|
|
611
|
-
# Set unprocessed options for next time
|
|
612
|
-
@unprocessed_cmd_line_options = unknown_options
|
|
613
|
-
end
|
|
614
|
-
|
|
615
|
-
def prompt_user_input(prompt, sensitive: false)
|
|
616
|
-
return $stdin.getpass("#{prompt}> ") if sensitive
|
|
617
|
-
print("#{prompt}> ")
|
|
618
|
-
line = $stdin.gets
|
|
619
|
-
Aspera.assert_type(line, String){'Unexpected end of standard input'}
|
|
620
|
-
line.chomp
|
|
621
|
-
end
|
|
622
|
-
|
|
623
|
-
# prompt user for input in a list of symbols
|
|
624
|
-
# @param prompt [String] prompt to display
|
|
625
|
-
# @param sym_list [Array] list of symbols to select from
|
|
626
|
-
# @return [Symbol] selected symbol
|
|
627
|
-
def prompt_user_input_in_list(prompt, sym_list)
|
|
628
|
-
loop do
|
|
629
|
-
input = prompt_user_input(prompt).to_sym
|
|
630
|
-
if sym_list.any?{ |a| a.eql?(input)}
|
|
631
|
-
return input
|
|
632
|
-
else
|
|
633
|
-
$stderr.puts("No such #{prompt}: #{input}, select one of: #{sym_list.join(', ')}") # rubocop:disable Style/StderrPuts
|
|
634
|
-
end
|
|
635
|
-
end
|
|
636
|
-
end
|
|
637
|
-
|
|
638
|
-
# Prompt user for input in a list of symbols
|
|
639
|
-
# @param descr [String] description for help
|
|
640
|
-
# @param check_option [Boolean] Check attributes of option with name=descr
|
|
641
|
-
# @param multiple [Boolean, String] `true` if multiple values expected
|
|
642
|
-
# @param accept_list [Array<Symbol>,NilClass] List of expected values
|
|
643
|
-
# @return [String] user input
|
|
644
|
-
def get_interactive(descr, check_option: false, multiple: false, accept_list: nil, schema: nil)
|
|
645
|
-
option_attrs = @declared_options[descr.to_sym]
|
|
646
|
-
what = option_attrs ? 'option' : 'argument'
|
|
647
|
-
default_prompt = "#{what}: #{descr}"
|
|
648
|
-
if !@ask_missing_mandatory
|
|
649
|
-
message = "Missing #{default_prompt}"
|
|
650
|
-
message = self.class.multi_choice_assert_msg(message, accept_list) if accept_list
|
|
651
|
-
message += "\n#{TerminalFormatter::HINT}Give `#{HELP}` as argument to retrieve the schema of the missing argument." if schema
|
|
652
|
-
raise Cli::MissingArgument, message
|
|
653
|
-
end
|
|
654
|
-
# ask interactively
|
|
655
|
-
result = []
|
|
656
|
-
puts(' (one per line, end with empty line)') if multiple
|
|
657
|
-
loop do
|
|
658
|
-
prompt = default_prompt
|
|
659
|
-
prompt = "#{accept_list.join(' ')}\n#{default_prompt}" if accept_list
|
|
660
|
-
entry = prompt_user_input(prompt, sensitive: option_attrs&.sensitive)
|
|
661
|
-
break if entry.empty? && multiple
|
|
662
|
-
entry = ExtendedValue.instance.evaluate(entry, context: 'interactive input')
|
|
663
|
-
entry = self.class.get_from_list(entry, descr, accept_list) if accept_list
|
|
664
|
-
return entry unless multiple
|
|
665
|
-
result.push(entry)
|
|
666
|
-
end
|
|
667
|
-
result
|
|
668
|
-
end
|
|
669
|
-
|
|
670
|
-
# Read remaining args and build an `Array` or `Hash`
|
|
671
|
-
# @param value [String] Argument to `@:` extended value
|
|
672
|
-
# @return [Hash, Array] Object representing dot-path values
|
|
673
|
-
def args_as_extended(end_marker)
|
|
674
|
-
# This extended value does not take args (`@:`)
|
|
675
|
-
# ExtendedValue.assert_no_value(end_marker, :p)
|
|
676
|
-
end_marker = SpecialValues::EOA if end_marker.empty?
|
|
677
|
-
result = nil
|
|
678
|
-
get_next_argument('args', multiple: end_marker).each do |argument|
|
|
679
|
-
Aspera.assert(argument.include?(OPTION_VALUE_SEPARATOR)){"Positional argument: #{argument} does not include #{OPTION_VALUE_SEPARATOR}"}
|
|
680
|
-
path, value = argument.split(OPTION_VALUE_SEPARATOR, 2)
|
|
681
|
-
result = DotContainer.dotted_to_container(path.split(DotContainer::SEPARATOR), smart_convert(value), result)
|
|
682
|
-
end
|
|
683
|
-
result
|
|
684
|
-
end
|
|
685
|
-
|
|
686
|
-
# ======================================================
|
|
687
|
-
private
|
|
688
|
-
|
|
689
|
-
# Using dotted hash notation, convert value to bool, int, float or extended value
|
|
690
|
-
# @param value [String] The value to convert to appropriate type
|
|
691
|
-
# @return the converted value
|
|
692
|
-
def smart_convert(value)
|
|
693
|
-
case value
|
|
694
|
-
when 'true' then true
|
|
695
|
-
when 'false' then false
|
|
696
|
-
else
|
|
697
|
-
Integer(value, exception: false) ||
|
|
698
|
-
Float(value, exception: false) ||
|
|
699
|
-
ExtendedValue.instance.evaluate(value, context: 'dotted expression')
|
|
700
|
-
end
|
|
701
|
-
end
|
|
702
|
-
|
|
703
|
-
# generate command line option from option symbol
|
|
704
|
-
def symbol_to_option(symbol, opt_val = nil)
|
|
705
|
-
result = [OPTION_PREFIX, symbol.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)].join
|
|
706
|
-
result = [result, OPTION_VALUE_SEPARATOR, opt_val].join unless opt_val.nil?
|
|
707
|
-
result
|
|
708
|
-
end
|
|
709
|
-
|
|
710
|
-
# TODO: use formatter
|
|
711
|
-
# Highlight current value in list
|
|
712
|
-
# @param list [Array<Symbol>] List of possible values
|
|
713
|
-
# @param current [Symbol] Current value
|
|
714
|
-
# @return [String] comma separated sorted list of values, with the current value highlighted
|
|
715
|
-
def highlight_current_in_list(list, current)
|
|
716
|
-
list.sort.map do |i|
|
|
717
|
-
if i.eql?(current)
|
|
718
|
-
$stdout.isatty ? i.to_s.red.bold : "[#{i}]"
|
|
719
|
-
else
|
|
720
|
-
i
|
|
721
|
-
end
|
|
722
|
-
end.join(', ')
|
|
723
|
-
end
|
|
724
|
-
|
|
725
|
-
# Try to evaluate options set in batch
|
|
726
|
-
# @param unprocessed_options [Array] list of options to apply (key_sym,value)
|
|
727
|
-
# @param where [String] where the options come from
|
|
728
|
-
def consume_option_pairs(unprocessed_options, where)
|
|
729
|
-
Log.log.trace1{"consume_option_pairs: #{where}"}
|
|
730
|
-
options_to_set = {}
|
|
731
|
-
unprocessed_options.each do |k, v|
|
|
732
|
-
if @declared_options.key?(k)
|
|
733
|
-
# constrained parameters as string are revert to symbol
|
|
734
|
-
v = self.class.get_from_list(v, "#{k} in #{where}", @declared_options[k].values) if @declared_options[k].values && v.is_a?(String)
|
|
735
|
-
options_to_set[k] = v
|
|
736
|
-
else
|
|
737
|
-
Log.log.trace1{"unprocessed: #{k}: #{v}"}
|
|
738
|
-
end
|
|
739
|
-
end
|
|
740
|
-
options_to_set.each do |k, v|
|
|
741
|
-
set_option(k, v, where: where)
|
|
742
|
-
# keep only unprocessed values for next parse
|
|
743
|
-
unprocessed_options.delete(k)
|
|
744
|
-
end
|
|
745
|
-
end
|
|
746
|
-
|
|
747
|
-
# Option name separator on command line, e.g. in --option-blah, third "-"
|
|
748
|
-
OPTION_SEP_LINE = '-'
|
|
749
|
-
# Option name separator in code (symbol), e.g. in :option_blah, the "_"
|
|
750
|
-
OPTION_SEP_SYMBOL = '_'
|
|
751
|
-
# Option value separator on command line, e.g. in --option-blah=foo, the "="
|
|
752
|
-
OPTION_VALUE_SEPARATOR = '='
|
|
753
|
-
# Starts an option, e.g. in --option-blah, the two first "--"
|
|
754
|
-
OPTION_PREFIX = '--'
|
|
755
|
-
# when this is alone, this stops option processing
|
|
756
|
-
OPTIONS_STOP = '--'
|
|
757
|
-
SOURCE_USER = 'cmdline' # cspell:disable-line
|
|
758
|
-
# Percent selector: select by this field for this value
|
|
759
|
-
REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/
|
|
760
|
-
# Ask for schema of Extended value
|
|
761
|
-
HELP = 'help'
|
|
762
|
-
|
|
763
|
-
private_constant :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER, :REGEX_LOOKUP_ID_BY_FIELD
|
|
764
|
-
end
|
|
765
|
-
end
|
|
766
|
-
end
|