aspera-cli 4.27.2 → 4.27.4
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 +133 -0
- data/CONTRIBUTING.md +5 -2
- data/bin/ascli +3 -1
- data/docs/README.md +1006 -766
- data/lib/aspera/agent/base.rb +7 -2
- data/lib/aspera/agent/connect.rb +6 -8
- data/lib/aspera/agent/desktop.rb +2 -6
- data/lib/aspera/agent/direct.rb +52 -22
- data/lib/aspera/agent/node.rb +9 -8
- data/lib/aspera/agent/transferd.rb +2 -2
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +14 -12
- data/lib/aspera/api/ats.rb +1 -1
- data/lib/aspera/api/cos_node.rb +2 -2
- data/lib/aspera/api/faspex.rb +11 -7
- data/lib/aspera/api/httpgw.rb +38 -35
- data/lib/aspera/api/node.rb +39 -34
- data/lib/aspera/ascmd.rb +3 -1
- data/lib/aspera/ascp/installation.rb +63 -28
- data/lib/aspera/ascp/management.rb +1 -0
- data/lib/aspera/assert.rb +4 -0
- data/lib/aspera/cli/ascp_actions.rb +20 -41
- data/lib/aspera/cli/async_transfer_store.rb +12 -11
- data/lib/aspera/cli/bootstrapper.rb +14 -16
- data/lib/aspera/cli/command_line.rb +252 -0
- data/lib/aspera/cli/command_registry.rb +215 -37
- data/lib/aspera/cli/command_spec.rb +104 -15
- data/lib/aspera/cli/completion/ascli.bash +12 -0
- data/lib/aspera/cli/completion/ascli.fish +16 -0
- data/lib/aspera/cli/completion/ascli.zsh +19 -0
- data/lib/aspera/cli/context.rb +3 -0
- data/lib/aspera/cli/deprecation.rb +37 -0
- data/lib/aspera/cli/extended_value.rb +6 -3
- data/lib/aspera/cli/formatter.rb +94 -80
- data/lib/aspera/cli/gem_checker.rb +1 -1
- data/lib/aspera/cli/hints.rb +7 -6
- data/lib/aspera/cli/http.rb +22 -34
- data/lib/aspera/cli/info.rb +3 -0
- data/lib/aspera/cli/mcp_tool.rb +47 -83
- data/lib/aspera/cli/option_declarator.rb +33 -42
- data/lib/aspera/cli/option_registry.rb +69 -0
- data/lib/aspera/cli/option_types.rb +105 -0
- data/lib/aspera/cli/option_value.rb +278 -0
- data/lib/aspera/cli/options.schema.yaml +124 -15
- data/lib/aspera/cli/parser.rb +333 -862
- data/lib/aspera/cli/plugins/alee.rb +7 -4
- data/lib/aspera/cli/plugins/aoc.rb +545 -518
- data/lib/aspera/cli/plugins/ats.rb +59 -80
- data/lib/aspera/cli/plugins/base.rb +221 -265
- data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
- data/lib/aspera/cli/plugins/config.rb +263 -184
- data/lib/aspera/cli/plugins/console.rb +103 -39
- data/lib/aspera/cli/plugins/cos.rb +6 -23
- data/lib/aspera/cli/plugins/factory.rb +3 -0
- data/lib/aspera/cli/plugins/faspex5.rb +204 -182
- data/lib/aspera/cli/plugins/faspio.rb +6 -11
- data/lib/aspera/cli/plugins/httpgw.rb +8 -11
- data/lib/aspera/cli/plugins/mcp.rb +20 -55
- data/lib/aspera/cli/plugins/node.rb +300 -327
- data/lib/aspera/cli/plugins/orchestrator.rb +152 -110
- data/lib/aspera/cli/plugins/preview.rb +96 -105
- data/lib/aspera/cli/plugins/server.rb +78 -53
- data/lib/aspera/cli/plugins/shares.rb +80 -131
- data/lib/aspera/cli/preset_actions.rb +44 -27
- data/lib/aspera/cli/preset_manager.rb +44 -19
- data/lib/aspera/cli/prompt.rb +36 -0
- data/lib/aspera/cli/result.rb +42 -36
- data/lib/aspera/cli/runner.rb +32 -59
- data/lib/aspera/cli/special_values.rb +5 -0
- data/lib/aspera/cli/sync_actions.rb +51 -46
- data/lib/aspera/cli/terminal_formatter.rb +9 -3
- data/lib/aspera/cli/transfer_actions.rb +14 -9
- data/lib/aspera/cli/transfer_agent.rb +34 -38
- data/lib/aspera/cli/transfer_progress.rb +290 -55
- data/lib/aspera/cli/vault_manager.rb +0 -17
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +5 -3
- data/lib/aspera/coverage.rb +1 -1
- data/lib/aspera/environment.rb +35 -5
- data/lib/aspera/faspex_gw.rb +2 -1
- data/lib/aspera/faspex_postproc.rb +1 -0
- data/lib/aspera/graphql.rb +5 -5
- data/lib/aspera/json_rpc/client.rb +5 -5
- data/lib/aspera/keychain/encrypted_hash.rb +2 -2
- data/lib/aspera/keychain/factory.rb +2 -1
- data/lib/aspera/keychain/one_password_api.rb +1 -1
- data/lib/aspera/link_header.rb +2 -2
- data/lib/aspera/log.rb +47 -27
- data/lib/aspera/markdown.rb +2 -0
- data/lib/aspera/mime.rb +25 -0
- data/lib/aspera/node_emulator.rb +759 -0
- data/lib/aspera/oauth/base.rb +37 -26
- data/lib/aspera/oauth/factory.rb +7 -3
- data/lib/aspera/oauth/generic.rb +1 -1
- data/lib/aspera/oauth/json_credentials.rb +34 -0
- data/lib/aspera/oauth/jwt.rb +4 -5
- data/lib/aspera/oauth/web.rb +9 -8
- data/lib/aspera/oauth.rb +1 -0
- data/lib/aspera/persistency_folder.rb +1 -3
- data/lib/aspera/preview/file_types.rb +4 -4
- data/lib/aspera/preview/generator.rb +11 -1
- data/lib/aspera/preview/options.schema.yaml +119 -0
- data/lib/aspera/preview/terminal.rb +4 -3
- data/lib/aspera/preview/utils.rb +9 -6
- data/lib/aspera/products/connect.rb +1 -1
- data/lib/aspera/rainbow.rb +7 -0
- data/lib/aspera/rest/aspera_errors.rb +72 -0
- data/lib/aspera/rest/call_error.rb +27 -0
- data/lib/aspera/rest/client.rb +523 -0
- data/lib/aspera/rest/error_analyzer.rb +113 -0
- data/lib/aspera/rest/list.rb +149 -0
- data/lib/aspera/rest/parameters.rb +55 -0
- data/lib/aspera/rest/util.rb +176 -0
- data/lib/aspera/rest.rb +7 -621
- data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
- data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
- data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
- data/lib/aspera/schema/documentation.rb +13 -3
- data/lib/aspera/schema/reader.rb +12 -18
- data/lib/aspera/schema/registry.rb +23 -1
- data/lib/aspera/schema/validator.rb +92 -0
- data/lib/aspera/secret_hider.rb +36 -25
- data/lib/aspera/string_ext.rb +15 -0
- data/lib/aspera/temp_file_manager.rb +6 -5
- data/lib/aspera/transfer/parameters.rb +2 -0
- data/lib/aspera/transfer/spec.rb +1 -0
- data/lib/aspera/uri_reader.rb +11 -11
- data/lib/aspera/web_auth/index.html +147 -0
- data/lib/aspera/web_auth/server.rb +81 -0
- data.tar.gz.sig +0 -0
- metadata +43 -9
- metadata.gz.sig +0 -0
- data/lib/aspera/colors.rb +0 -79
- data/lib/aspera/node_simulator.rb +0 -344
- data/lib/aspera/preview/options.rb +0 -45
- data/lib/aspera/rest_call_error.rb +0 -25
- data/lib/aspera/rest_error_analyzer.rb +0 -111
- data/lib/aspera/rest_errors_aspera.rb +0 -58
- data/lib/aspera/rest_list.rb +0 -136
- data/lib/aspera/web_auth.rb +0 -211
|
@@ -21,26 +21,31 @@ module Aspera
|
|
|
21
21
|
private_constant :AGENT_SCHEMA_KEY
|
|
22
22
|
|
|
23
23
|
# Set the SDK directory, checking default and former locations
|
|
24
|
+
# If a product is selected for `ascp`, the SDK directory is still needed (keys, conf, transferd)
|
|
24
25
|
def set_sdk_dir
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
sdk_folder = options.get_option(:sdk_folder)
|
|
27
|
+
product_selected = Ascp::Installation.product_selector?(sdk_folder)
|
|
28
|
+
return unless sdk_folder.nil? || product_selected
|
|
29
|
+
@sdk_default_location = true
|
|
30
|
+
Log.log.debug('SDK folder is not set, checking default')
|
|
31
|
+
sdk_dir = self.class.default_app_main_folder(app_name: TRANSFERD_APP_NAME)
|
|
32
|
+
Log.log.debug { "Checking: #{sdk_dir}" }
|
|
33
|
+
if !Dir.exist?(sdk_dir)
|
|
34
|
+
Log.log.debug { "No such folder: #{sdk_dir}" }
|
|
35
|
+
former_sdk_folder = File.join(self.class.default_app_main_folder(app_name: Info::CMD_NAME), TRANSFERD_APP_NAME)
|
|
36
|
+
Log.log.debug { "Checking: #{former_sdk_folder}" }
|
|
37
|
+
sdk_dir = former_sdk_folder if Dir.exist?(former_sdk_folder)
|
|
38
|
+
end
|
|
39
|
+
Log.log.debug { "Using: #{sdk_dir}" }
|
|
40
|
+
if product_selected
|
|
38
41
|
Products::Transferd.sdk_directory = sdk_dir
|
|
42
|
+
else
|
|
43
|
+
options.set_option(:sdk_folder, sdk_dir, source: :default)
|
|
39
44
|
end
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
# Install the transfer SDK (ascp + transferd) from a URL or using the default source.
|
|
43
|
-
# Version defaults to
|
|
48
|
+
# Version defaults to `Info::SDK_VERSION`; pass `LATEST` as argument to install the latest available version.
|
|
44
49
|
# @param version [String, nil] version to install; nil means use the default SDK version
|
|
45
50
|
# @return [Result::Status] installation result message
|
|
46
51
|
def install_transfer_sdk(version: nil)
|
|
@@ -52,27 +57,14 @@ module Aspera
|
|
|
52
57
|
return Result::Status.new("Installed #{name} version #{ver} in #{folder}")
|
|
53
58
|
end
|
|
54
59
|
|
|
55
|
-
def action_ascp_show(**)
|
|
56
|
-
Result::Text.new(Ascp::Installation.instance.path(:ascp))
|
|
57
|
-
end
|
|
58
|
-
|
|
59
60
|
def action_ascp_info(**)
|
|
61
|
+
SecretHider.instance.add_secret_keys(DataRepository::ELEMENTS)
|
|
60
62
|
data = Ascp::Installation.instance.ascp_info
|
|
61
63
|
data['ts'] = transfer.user_transfer_spec
|
|
62
64
|
DataRepository::ELEMENTS.each_with_object(data) { |i, h| h[i.to_s] = DataRepository.instance.item(i) }
|
|
63
|
-
SecretHider::ADDITIONAL_KEYS_TO_HIDE.concat(DataRepository::ELEMENTS.map(&:to_s))
|
|
64
65
|
Result::SingleObject.new(data)
|
|
65
66
|
end
|
|
66
67
|
|
|
67
|
-
def action_ascp_install(version: nil, **)
|
|
68
|
-
install_transfer_sdk(version: version)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def action_ascp_spec(**)
|
|
72
|
-
builder = Schema::Documentation.new(TerminalFormatter, Transfer::Spec::SCHEMA, include_option: true, agent_columns: true).build
|
|
73
|
-
Result::ObjectList.new(builder.rows, fields: builder.columns)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
68
|
def action_ascp_schema(agent_name: nil, **)
|
|
77
69
|
schema = Transfer::Spec::SCHEMA.current.merge({'$comment'=>'DO NOT EDIT, this file was generated from the YAML.'})
|
|
78
70
|
schema['properties'] = schema['properties'].select { |_k, v| CommandLineBuilder.supported_by_agent(agent_name, v) } unless agent_name.nil?
|
|
@@ -88,10 +80,6 @@ module Aspera
|
|
|
88
80
|
Result::ObjectList.new(error_data)
|
|
89
81
|
end
|
|
90
82
|
|
|
91
|
-
def action_ascp_products_list(**)
|
|
92
|
-
Result::ObjectList.new(Ascp::Installation.instance.installed_products, fields: %w[name app_root])
|
|
93
|
-
end
|
|
94
|
-
|
|
95
83
|
def action_agents_list(**)
|
|
96
84
|
rows = Agent::Factory::ALL.map do |sym, names|
|
|
97
85
|
schema_key = AGENT_SCHEMA_KEY[sym]
|
|
@@ -141,15 +129,6 @@ module Aspera
|
|
|
141
129
|
end
|
|
142
130
|
Result::ObjectList.new(rows, fields: %w[name type description])
|
|
143
131
|
end
|
|
144
|
-
|
|
145
|
-
def action_transferd_install(**)
|
|
146
|
-
install_transfer_sdk
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def action_transferd_list(**)
|
|
150
|
-
sdk_list = Ascp::Installation.instance.sdk_locations
|
|
151
|
-
Result::ObjectList.new(sdk_list, fields: sdk_list.first.keys - ['url'])
|
|
152
|
-
end
|
|
153
132
|
end
|
|
154
133
|
end
|
|
155
134
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'aspera/log'
|
|
4
|
+
require 'aspera/secret_hider'
|
|
4
5
|
require 'json'
|
|
5
6
|
require 'securerandom'
|
|
6
7
|
|
|
@@ -10,7 +11,7 @@ module Aspera
|
|
|
10
11
|
#
|
|
11
12
|
# Each entry is a JSON document identified by a `job_id` (UUID generated by ascli).
|
|
12
13
|
# The underlying storage uses PersistencyFolder with the category prefix
|
|
13
|
-
#
|
|
14
|
+
# `CATEGORY` so all async-transfer files are grouped together and can be
|
|
14
15
|
# garbage-collected independently.
|
|
15
16
|
#
|
|
16
17
|
# Schema of a stored entry:
|
|
@@ -59,14 +60,14 @@ module Aspera
|
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
# Persist (create or update) an async transfer entry.
|
|
62
|
-
# Keys whose name starts with '_' are in-process references (e.g. '_agent_ref')
|
|
63
|
-
#
|
|
63
|
+
# Keys whose name starts with '_' are in-process references (e.g. '_agent_ref'), and secrets (e.g. agent `password`)
|
|
64
|
+
# are not written on disk: they are intentionally excluded from the JSON serialization — at every nesting level.
|
|
64
65
|
# @param job_id [String] the ascli-generated UUID
|
|
65
66
|
# @param data [Hash] fields to store (will be JSON-serialised)
|
|
66
67
|
def write(job_id, data)
|
|
67
68
|
Aspera.assert_type(job_id, String) { 'job_id' }
|
|
68
69
|
Aspera.assert_type(data, Hash) { 'data' }
|
|
69
|
-
@persistency.put(store_key(job_id), JSON.generate(
|
|
70
|
+
@persistency.put(store_key(job_id), JSON.generate(persistable(data)))
|
|
70
71
|
nil
|
|
71
72
|
end
|
|
72
73
|
|
|
@@ -81,7 +82,7 @@ module Aspera
|
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
# List all async transfer entries.
|
|
84
|
-
# @return [Array<Hash>] each entry includes the
|
|
85
|
+
# @return [Array<Hash>] each entry includes the `job_id` field
|
|
85
86
|
def list
|
|
86
87
|
@persistency.current_items(CATEGORY).map do |key, raw|
|
|
87
88
|
data = JSON.parse(raw)
|
|
@@ -105,17 +106,17 @@ module Aspera
|
|
|
105
106
|
"#{CATEGORY}#{job_id}"
|
|
106
107
|
end
|
|
107
108
|
|
|
108
|
-
# Recursively remove keys
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
def
|
|
109
|
+
# Recursively remove from a Hash the keys that must never reach the file system:
|
|
110
|
+
# - keys whose name starts with '_': in-process Ruby object references (e.g. '_agent_ref') that cannot be serialized to JSON
|
|
111
|
+
# - secrets (e.g. 'password'): taken from current options when the transfer is queried
|
|
112
|
+
def persistable(value)
|
|
112
113
|
case value
|
|
113
114
|
when Hash
|
|
114
115
|
value.each_with_object({}) do |(k, v), h|
|
|
115
|
-
h[k] =
|
|
116
|
+
h[k] = persistable(v) unless k.to_s.start_with?('_') || SecretHider.instance.secret?(k, v)
|
|
116
117
|
end
|
|
117
118
|
when Array
|
|
118
|
-
value.map { |v|
|
|
119
|
+
value.map { |v| persistable(v) }
|
|
119
120
|
else
|
|
120
121
|
value
|
|
121
122
|
end
|
|
@@ -31,8 +31,8 @@ module Aspera
|
|
|
31
31
|
# - populate context.persistency, context.presets, context.http_config, context.progress_bar
|
|
32
32
|
# - register plugin lookup folders
|
|
33
33
|
# - register @preset / @vault extended-value handlers
|
|
34
|
-
# - configure global singletons:
|
|
35
|
-
#
|
|
34
|
+
# - configure global singletons: Rest::Parameters, OAuth::Factory, SSL, Transfer::Parameters,
|
|
35
|
+
# Rest::ErrorAnalyzer
|
|
36
36
|
# - set up the PAC proxy executor (option :fpac)
|
|
37
37
|
class Bootstrapper
|
|
38
38
|
# Folder name inside $HOME for all Aspera tool data (~/.aspera)
|
|
@@ -88,16 +88,13 @@ module Aspera
|
|
|
88
88
|
setup_rest_and_transfer_runtime
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
# Public accessor used as option handler for :config_file
|
|
92
|
-
attr_accessor :config_file_option
|
|
93
|
-
|
|
94
91
|
private
|
|
95
92
|
|
|
96
93
|
# Declare + parse :home option: sets context.main_folder
|
|
97
94
|
def setup_main_folder
|
|
98
95
|
@context.options.declare(
|
|
99
96
|
:home, description: 'Home folder for tool',
|
|
100
|
-
|
|
97
|
+
on_set: @context.method(:main_folder=),
|
|
101
98
|
default: default_app_main_folder(app_name: Info::CMD_NAME)
|
|
102
99
|
)
|
|
103
100
|
@context.options.parse_options!
|
|
@@ -115,11 +112,10 @@ module Aspera
|
|
|
115
112
|
def setup_config_file
|
|
116
113
|
@context.options.declare(
|
|
117
114
|
:config_file, description: 'Path to YAML file with preset configuration',
|
|
118
|
-
handler: {o: self, m: :config_file_option},
|
|
119
115
|
default: File.join(@context.main_folder, DEFAULT_CONFIG_FILENAME)
|
|
120
116
|
)
|
|
121
117
|
@context.options.parse_options!
|
|
122
|
-
@context.presets = PresetManager.new(config_file: @
|
|
118
|
+
@context.presets = PresetManager.new(config_file: @context.options.get_option(:config_file))
|
|
123
119
|
@context.http_config = Http.new
|
|
124
120
|
end
|
|
125
121
|
|
|
@@ -143,9 +139,11 @@ module Aspera
|
|
|
143
139
|
|
|
144
140
|
# Declare + parse :progress_bar: sets context.progress_bar
|
|
145
141
|
def setup_progress_bar
|
|
146
|
-
@context.options.declare(:progress_bar, description: 'Display progress bar', allowed: Type::BOOLEAN, default:
|
|
142
|
+
@context.options.declare(:progress_bar, description: 'Display progress bar', allowed: Type::BOOLEAN, default: $stderr.tty?)
|
|
147
143
|
@context.options.parse_options!
|
|
148
144
|
@context.progress_bar = TransferProgress.new if @context.options.get_option(:progress_bar)
|
|
145
|
+
# Log lines do not overwrite the progress bar
|
|
146
|
+
Log.instance.status_line = @context.progress_bar
|
|
149
147
|
end
|
|
150
148
|
|
|
151
149
|
# Declare + parse :fpac / :proxy_credentials: sets up PAC executor
|
|
@@ -165,18 +163,18 @@ module Aspera
|
|
|
165
163
|
end
|
|
166
164
|
end
|
|
167
165
|
|
|
168
|
-
# Configure global singletons:
|
|
166
|
+
# Configure global singletons: Rest::Parameters, SSL, Transfer, Rest::ErrorAnalyzer.
|
|
169
167
|
# OAuth persist_mgr is NOT set here: it depends on :cache_tokens which is parsed later
|
|
170
168
|
# by Config#initialize. Runner sets it after Config.new.
|
|
171
169
|
def setup_rest_and_transfer_runtime
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
170
|
+
Rest::Parameters.instance.user_agent = Info::CMD_NAME
|
|
171
|
+
Rest::Parameters.instance.progress_bar = @context.progress_bar
|
|
172
|
+
Rest::Parameters.instance.session_cb = ->(http_session) { @context.http_config.update_session(http_session) }
|
|
173
|
+
Rest::Parameters.instance.spinner_cb = ->(title = nil, action: :spin) { @context.formatter.long_operation(title, action: action) }
|
|
176
174
|
OAuth::Web.additional_info = "#{Info::CMD_NAME} v#{Cli::VERSION}"
|
|
177
175
|
Transfer::Parameters.file_list_folder = File.join(@context.main_folder, FILE_LIST_FOLDER_NAME)
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
Rest::ErrorAnalyzer.instance.log_file = File.join(@context.main_folder, REST_EXCEPTIONS_LOG_FILENAME)
|
|
177
|
+
Rest::AsperaErrors.register_handlers
|
|
180
178
|
end
|
|
181
179
|
|
|
182
180
|
# @return [String] ~/.aspera
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/cli/error'
|
|
4
|
+
require 'aspera/dot_container'
|
|
5
|
+
require 'aspera/log'
|
|
6
|
+
require 'aspera/assert'
|
|
7
|
+
|
|
8
|
+
module Aspera
|
|
9
|
+
module Cli
|
|
10
|
+
# Positional (non-option) command line token.
|
|
11
|
+
class Argument
|
|
12
|
+
# @return [String] the raw argument value
|
|
13
|
+
attr_reader :value
|
|
14
|
+
# @return [Option, nil] option token claiming this token as its value (`--opt value` form)
|
|
15
|
+
attr_accessor :owner
|
|
16
|
+
# @return [Boolean] `true` once used, as positional argument or as option value
|
|
17
|
+
attr_accessor :consumed
|
|
18
|
+
|
|
19
|
+
def initialize(value)
|
|
20
|
+
@value = value
|
|
21
|
+
@owner = nil
|
|
22
|
+
@consumed = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Boolean] `true` if available as positional argument
|
|
26
|
+
def positional? = !@consumed && @owner.nil?
|
|
27
|
+
|
|
28
|
+
# @return [String] raw argument value
|
|
29
|
+
def to_s = @value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Option command line token (long or short form).
|
|
33
|
+
class Option
|
|
34
|
+
# @return [String] raw token as it appeared in argv (e.g. "--log-level=debug", "-Pval")
|
|
35
|
+
attr_reader :raw
|
|
36
|
+
# @return [String, nil] option name with underscores (e.g. "log_level"), nil for short options
|
|
37
|
+
attr_reader :name
|
|
38
|
+
# @return [String, nil] single-char short option letter (e.g. "P"), nil for long options
|
|
39
|
+
attr_reader :short_char
|
|
40
|
+
# @return [Array<String>, nil] sub-keys for dot-path notation (e.g. ["field"] for --custom.field)
|
|
41
|
+
attr_reader :dot_path
|
|
42
|
+
# @return [String, nil] value given in the same token (`--opt=val` or `-Oval`)
|
|
43
|
+
attr_reader :inline_value
|
|
44
|
+
# @return [Argument, nil] next token, claimed as value when there is no inline value (`--opt val` or `-O val`)
|
|
45
|
+
attr_accessor :value_token
|
|
46
|
+
# @return [Boolean] `true` once applied to a declared option
|
|
47
|
+
attr_accessor :consumed
|
|
48
|
+
# @return [Symbol, nil] option this token was resolved to, when `name` is an abbreviation
|
|
49
|
+
attr_accessor :abbreviation_of
|
|
50
|
+
|
|
51
|
+
def initialize(raw:, name: nil, short_char: nil, dot_path: nil, inline_value: nil)
|
|
52
|
+
@raw = raw
|
|
53
|
+
@name = name
|
|
54
|
+
@short_char = short_char
|
|
55
|
+
@dot_path = dot_path
|
|
56
|
+
@inline_value = inline_value
|
|
57
|
+
@value_token = nil
|
|
58
|
+
@consumed = false
|
|
59
|
+
@abbreviation_of = nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Boolean] `true` if value is in the same token
|
|
63
|
+
def inline? = !@inline_value.nil?
|
|
64
|
+
|
|
65
|
+
# @return [String, nil] inline value, or value of next token
|
|
66
|
+
def value = @inline_value || @value_token&.value
|
|
67
|
+
|
|
68
|
+
# @return [String] raw token, followed by its separate value if any
|
|
69
|
+
def to_s = @value_token.nil? ? @raw : "#{@raw} #{@value_token.value}"
|
|
70
|
+
|
|
71
|
+
class << self
|
|
72
|
+
# @param token [String] command line token
|
|
73
|
+
# @return [Boolean] `true` if token is an option, i.e. not `-`, `--`, or a negative number
|
|
74
|
+
def option?(token)
|
|
75
|
+
token.match?(/\A-\D/) && !token.eql?(STOP)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Build an Option from a raw token
|
|
79
|
+
# @param raw [String] e.g. "--log-level=debug", "--custom.field", "-P", "-Pval"
|
|
80
|
+
# @return [Option]
|
|
81
|
+
def parse(raw)
|
|
82
|
+
if raw.start_with?(PREFIX)
|
|
83
|
+
name_raw, value = raw.delete_prefix(PREFIX).split(VALUE_SEP, 2)
|
|
84
|
+
root, *dot_path = name_raw.to_s.split(DotContainer::SEPARATOR)
|
|
85
|
+
new(raw: raw, name: root.to_s.gsub(NAME_SEP_LINE, NAME_SEP_SYMBOL), dot_path: dot_path.empty? ? nil : dot_path, inline_value: value)
|
|
86
|
+
else
|
|
87
|
+
new(raw: raw, short_char: raw[1], inline_value: raw.length > 2 ? raw[2..] : nil)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Option name separator on command line (e.g. `--option-name`, the `-` between words)
|
|
93
|
+
NAME_SEP_LINE = '-'
|
|
94
|
+
# Option name separator in code/symbol (e.g. `:option_name`, the `_` between words)
|
|
95
|
+
NAME_SEP_SYMBOL = '_'
|
|
96
|
+
# Separator between option name and its inline value (e.g. `--opt=val`, the `=`)
|
|
97
|
+
VALUE_SEP = '='
|
|
98
|
+
# Long-option prefix (e.g. `--opt`)
|
|
99
|
+
PREFIX = '--'
|
|
100
|
+
# When alone, stops option processing: following tokens are positional arguments
|
|
101
|
+
STOP = '--'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Command line split in tokens, in original order.
|
|
105
|
+
# Tokens are never removed, only marked as consumed, so that positions are kept.
|
|
106
|
+
#
|
|
107
|
+
# An option without inline value claims the next token as its value (`--opt val`, `-O val`),
|
|
108
|
+
# unless that token looks like an option.
|
|
109
|
+
# If the option turns out to be a flag, the claimed token is given back to positional arguments.
|
|
110
|
+
class CommandLine
|
|
111
|
+
# @param argv [Array<String>] command line arguments
|
|
112
|
+
def initialize(argv)
|
|
113
|
+
# @type [Array<Option, Argument>]
|
|
114
|
+
@tokens = []
|
|
115
|
+
# Option token being applied (used by `@:` extended value)
|
|
116
|
+
@current_option = nil
|
|
117
|
+
# When set, only positional arguments after this token are available
|
|
118
|
+
@arguments_after = nil
|
|
119
|
+
process_options = true
|
|
120
|
+
argv.each do |value|
|
|
121
|
+
if process_options && value.eql?(Option::STOP)
|
|
122
|
+
process_options = false
|
|
123
|
+
elsif process_options && Option.option?(value)
|
|
124
|
+
@tokens.push(Option.parse(value))
|
|
125
|
+
else
|
|
126
|
+
argument = Argument.new(value)
|
|
127
|
+
previous = @tokens.last
|
|
128
|
+
if process_options && previous.is_a?(Option) && !previous.inline? && previous.value_token.nil?
|
|
129
|
+
previous.value_token = argument
|
|
130
|
+
argument.owner = previous
|
|
131
|
+
end
|
|
132
|
+
@tokens.push(argument)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
Log.log.trace1 { "arguments=#{pending_arguments},options=#{pending_options}" }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @return [Array<Option>] option tokens whose name was resolved as an abbreviation
|
|
139
|
+
def abbreviated_option_tokens
|
|
140
|
+
@tokens.select { |t| t.is_a?(Option) && !t.abbreviation_of.nil? }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @return [Array<Option>] option tokens not applied yet
|
|
144
|
+
def pending_option_tokens
|
|
145
|
+
@tokens.select { |t| t.is_a?(Option) && !t.consumed }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Mark option token as applied, and get its value.
|
|
149
|
+
# @param tok [Option] option token
|
|
150
|
+
# @param takes_value [Boolean] `false` for flags: claimed token is given back to positional arguments
|
|
151
|
+
# @return [String, nil] value, or `nil` for flags
|
|
152
|
+
# @raise [BadArgument] if option takes a value and none was given
|
|
153
|
+
def consume(tok, takes_value:)
|
|
154
|
+
tok.consumed = true
|
|
155
|
+
return release(tok) unless takes_value
|
|
156
|
+
return tok.inline_value if tok.inline?
|
|
157
|
+
Aspera.assert(!tok.value_token.nil?, type: BadArgument) { "Option #{tok.raw} requires a value" }
|
|
158
|
+
tok.value_token.consumed = true
|
|
159
|
+
tok.value_token.value
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Execute block with `tok` as current option
|
|
163
|
+
# @param tok [Option] option token being applied
|
|
164
|
+
def with_current_option(tok)
|
|
165
|
+
@current_option = tok
|
|
166
|
+
yield
|
|
167
|
+
ensure
|
|
168
|
+
@current_option = nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Execute block with only positional arguments after current option available, if any
|
|
172
|
+
def with_arguments_after_current_option
|
|
173
|
+
@arguments_after = @current_option
|
|
174
|
+
yield
|
|
175
|
+
ensure
|
|
176
|
+
@arguments_after = nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# @return [Array<String>] values of available positional arguments
|
|
180
|
+
def pending_arguments
|
|
181
|
+
positional_tokens.map(&:value)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# @return [Array<String>] options not applied yet, with their value if separate
|
|
185
|
+
def pending_options
|
|
186
|
+
pending_option_tokens.map(&:to_s)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Consume positional arguments.
|
|
190
|
+
# @param multiple [false, true, String] consumption mode:
|
|
191
|
+
# false — consume exactly one token
|
|
192
|
+
# true — consume all remaining tokens
|
|
193
|
+
# String — consume up to the marker token (marker is consumed too, not returned), or all if absent
|
|
194
|
+
# @return [Array<String>] consumed values
|
|
195
|
+
def shift_arguments(multiple)
|
|
196
|
+
arg_tokens = positional_tokens
|
|
197
|
+
selected =
|
|
198
|
+
case multiple
|
|
199
|
+
when false then arg_tokens.first(1)
|
|
200
|
+
when true then arg_tokens
|
|
201
|
+
when String
|
|
202
|
+
index = arg_tokens.index { |t| t.value.eql?(multiple) }
|
|
203
|
+
arg_tokens[index].consumed = true unless index.nil?
|
|
204
|
+
arg_tokens.take(index || arg_tokens.length)
|
|
205
|
+
else Aspera.error_unexpected_value(multiple) { 'multiple' }
|
|
206
|
+
end
|
|
207
|
+
selected.each { |t| t.consumed = true }
|
|
208
|
+
selected.map(&:value)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Add an argument before the available positional arguments
|
|
212
|
+
# @param value [String] argument value
|
|
213
|
+
def unshift_argument(value)
|
|
214
|
+
index = @tokens.index { |t| t.is_a?(Argument) && t.positional? } || @tokens.length
|
|
215
|
+
@tokens.insert(index, Argument.new(value))
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Consume all long options with a value, whether applied or not.
|
|
219
|
+
# @yieldparam tok [Option] long option token with a value
|
|
220
|
+
def each_long_option_with_value
|
|
221
|
+
@tokens.each do |tok|
|
|
222
|
+
next unless tok.is_a?(Option) && tok.short_char.nil? && !tok.value.nil?
|
|
223
|
+
yield(tok)
|
|
224
|
+
tok.consumed = true
|
|
225
|
+
tok.value_token&.consumed = true
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
# Give back claimed token to positional arguments.
|
|
232
|
+
# @param tok [Option] flag option token
|
|
233
|
+
# @return [nil]
|
|
234
|
+
def release(tok)
|
|
235
|
+
value_token = tok.value_token
|
|
236
|
+
return if value_token.nil?
|
|
237
|
+
index = @tokens.index { |t| t.equal?(value_token) }
|
|
238
|
+
Aspera.assert(@tokens[index + 1..].none? { |t| t.is_a?(Argument) && t.consumed && t.owner.nil? }) do
|
|
239
|
+
"Flag #{tok.raw} declared after following positional arguments were used"
|
|
240
|
+
end
|
|
241
|
+
value_token.owner = nil
|
|
242
|
+
tok.value_token = nil
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# @return [Array<Argument>] available positional arguments, in order
|
|
246
|
+
def positional_tokens
|
|
247
|
+
start = @arguments_after.nil? ? 0 : @tokens.index { |t| t.equal?(@arguments_after) } + 1
|
|
248
|
+
@tokens[start..].select { |t| t.is_a?(Argument) && t.positional? }
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|