aspera-cli 4.27.2 → 4.27.3
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 +50 -0
- data/bin/ascli +2 -1
- data/docs/README.md +804 -746
- data/lib/aspera/agent/connect.rb +6 -4
- data/lib/aspera/agent/desktop.rb +2 -2
- data/lib/aspera/agent/direct.rb +3 -1
- data/lib/aspera/agent/node.rb +3 -3
- 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 +9 -7
- data/lib/aspera/api/httpgw.rb +37 -33
- data/lib/aspera/api/node.rb +38 -33
- data/lib/aspera/ascmd.rb +3 -1
- data/lib/aspera/ascp/installation.rb +62 -27
- 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 +2 -2
- data/lib/aspera/cli/bootstrapper.rb +11 -15
- data/lib/aspera/cli/command_line.rb +252 -0
- data/lib/aspera/cli/command_registry.rb +149 -33
- data/lib/aspera/cli/command_spec.rb +103 -14
- 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 +2 -0
- data/lib/aspera/cli/formatter.rb +87 -75
- data/lib/aspera/cli/gem_checker.rb +1 -1
- data/lib/aspera/cli/hints.rb +7 -6
- data/lib/aspera/cli/http.rb +21 -21
- 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 +103 -0
- data/lib/aspera/cli/option_value.rb +281 -0
- data/lib/aspera/cli/options.schema.yaml +38 -5
- data/lib/aspera/cli/parser.rb +307 -848
- data/lib/aspera/cli/plugins/alee.rb +7 -4
- data/lib/aspera/cli/plugins/aoc.rb +430 -380
- data/lib/aspera/cli/plugins/ats.rb +58 -73
- data/lib/aspera/cli/plugins/base.rb +190 -240
- data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
- data/lib/aspera/cli/plugins/config.rb +244 -178
- data/lib/aspera/cli/plugins/console.rb +102 -38
- 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 +176 -173
- data/lib/aspera/cli/plugins/faspio.rb +5 -10
- 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 +277 -311
- data/lib/aspera/cli/plugins/orchestrator.rb +90 -77
- data/lib/aspera/cli/plugins/preview.rb +79 -90
- data/lib/aspera/cli/plugins/server.rb +76 -50
- data/lib/aspera/cli/plugins/shares.rb +68 -116
- data/lib/aspera/cli/preset_actions.rb +17 -10
- data/lib/aspera/cli/preset_manager.rb +12 -2
- data/lib/aspera/cli/prompt.rb +35 -0
- data/lib/aspera/cli/result.rb +13 -18
- data/lib/aspera/cli/runner.rb +31 -54
- data/lib/aspera/cli/special_values.rb +5 -0
- data/lib/aspera/cli/sync_actions.rb +41 -37
- data/lib/aspera/cli/terminal_formatter.rb +9 -3
- data/lib/aspera/cli/transfer_actions.rb +0 -6
- data/lib/aspera/cli/transfer_agent.rb +29 -35
- data/lib/aspera/cli/vault_manager.rb +0 -17
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +4 -2
- data/lib/aspera/coverage.rb +1 -0
- data/lib/aspera/environment.rb +6 -0
- 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 +1 -1
- data/lib/aspera/keychain/one_password_api.rb +1 -1
- data/lib/aspera/link_header.rb +2 -2
- data/lib/aspera/log.rb +22 -25
- data/lib/aspera/markdown.rb +2 -0
- data/lib/aspera/mime.rb +25 -0
- data/lib/aspera/node_simulator.rb +1 -0
- data/lib/aspera/oauth/base.rb +35 -25
- data/lib/aspera/oauth/factory.rb +1 -0
- data/lib/aspera/oauth/generic.rb +1 -1
- data/lib/aspera/oauth/jwt.rb +1 -1
- data/lib/aspera/oauth/web.rb +9 -8
- data/lib/aspera/preview/file_types.rb +4 -4
- data/lib/aspera/preview/generator.rb +7 -0
- data/lib/aspera/preview/options.rb +4 -4
- 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 +60 -0
- data/lib/aspera/rest/call_error.rb +27 -0
- data/lib/aspera/rest/client.rb +514 -0
- data/lib/aspera/rest/error_analyzer.rb +113 -0
- data/lib/aspera/rest/list.rb +143 -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 on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
- data/lib/aspera/schema/documentation.rb +13 -3
- data/lib/aspera/schema/registry.rb +18 -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 +39 -7
- metadata.gz.sig +0 -0
- data/lib/aspera/colors.rb +0 -79
- 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
data/lib/aspera/cli/mcp_tool.rb
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
require 'json'
|
|
6
6
|
require 'aspera/log'
|
|
7
|
+
require 'aspera/secret_hider'
|
|
7
8
|
require 'aspera/cli/runner'
|
|
8
9
|
require 'aspera/cli/error'
|
|
9
10
|
require 'aspera/schema/reader'
|
|
@@ -36,100 +37,61 @@ module Aspera
|
|
|
36
37
|
SYNTAX
|
|
37
38
|
args is a JSON array of strings mirroring the CLI command line.
|
|
38
39
|
Element 0 : plugin name (aoc, faspex5, node, server, config, …).
|
|
39
|
-
Elements 1+: sub-commands,
|
|
40
|
-
|
|
41
|
-
"@json:{...}" — inline JSON object or array (preferred
|
|
42
|
-
"@preset:name" — expand a saved
|
|
40
|
+
Elements 1+: sub-commands, positional arguments, and --option=value flags in any order.
|
|
41
|
+
Structured values use an extended-value prefix on the relevant element:
|
|
42
|
+
"@json:{...}" — inline JSON object or array (preferred: no shell quoting)
|
|
43
|
+
"@preset:name" — expand a saved preset
|
|
43
44
|
"@env:VAR" — read value from environment variable
|
|
44
45
|
"@file:/path" — read value from a file
|
|
45
|
-
Note: the dot-path form (@: key=value ...) is designed for humans typing in a terminal shell.
|
|
46
|
-
Prefer @json: when building args programmatically or as an LLM.
|
|
47
46
|
|
|
48
47
|
AUTOMATIC FLAGS
|
|
49
|
-
The server
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
Syntax notation: <arg> mandatory, [<arg>] optional, <a|b> enum, <arg...> variadic.
|
|
63
|
-
This single call covers all 800+ commands — no other discovery step is needed
|
|
64
|
-
unless you want details about a specific command or its Hash arguments.
|
|
65
|
-
IMPORTANT: never guess command names from training data. If you are unsure of
|
|
66
|
-
the exact subcommand name (e.g. shared_folders vs shared_inboxes), always call
|
|
67
|
-
["config", "commands"] first to find the correct name.
|
|
68
|
-
|
|
69
|
-
Step 2 — inspect a Hash argument schema BEFORE calling any command with <data>:
|
|
70
|
-
MANDATORY: whenever the command syntax shows a <data> argument, you MUST call
|
|
71
|
-
"help" offline first. Never infer field names from server error messages.
|
|
48
|
+
The server prepends extra_args to every call (default: #{DEFAULT_EXTRA_ARGS.join(' ')}).
|
|
49
|
+
Do NOT repeat them. To override one, include it in args: later values take precedence.
|
|
50
|
+
Commands never prompt: if credentials are missing, an error is returned; report it and stop.
|
|
51
|
+
|
|
52
|
+
DISCOVERY — never guess command or field names from training data
|
|
53
|
+
Step 1 — commands of a plugin, with syntax:
|
|
54
|
+
["config", "commands", "<plugin>"]
|
|
55
|
+
Returns { syntax, description } for every command of that plugin.
|
|
56
|
+
Notation: <arg> mandatory, [<arg>] optional, <a|b> enum, <arg...> variadic, <arg:Hash> typed.
|
|
57
|
+
A line ending with <command...> and "(see: <plugin> <path>)" provides the commands of that path:
|
|
58
|
+
list them with ["config", "commands", "<plugin>", "<command>", ...] (command words only).
|
|
59
|
+
Omit <plugin> to list the commands of all plugins (large).
|
|
60
|
+
Step 2 — schema of a Hash argument (shown <name:Hash>), MANDATORY before calling such a command:
|
|
72
61
|
["<plugin>", "<cmd>", ..., "help"]
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Note: only works for Hash-typed arguments, not for plain String arguments.
|
|
77
|
-
|
|
78
|
-
Step 2b — discover available query/filter parameters for list commands:
|
|
62
|
+
Put the literal "help" in place of the Hash argument. Returns field names, types,
|
|
63
|
+
required flags and descriptions. Never infer fields from server error messages.
|
|
64
|
+
Step 2b — filter parameters of a list command:
|
|
79
65
|
["<plugin>", "<cmd>", ..., "--query=help"]
|
|
80
|
-
|
|
81
|
-
with their types and descriptions.
|
|
82
|
-
Example: ["aoc", "admin", "user", "list", "--query=help"]
|
|
83
|
-
Example: ["faspex5", "admin", "packages", "list", "--query=help"]
|
|
84
|
-
Note: only works on commands that support --query filtering (list/delete).
|
|
85
|
-
|
|
86
|
-
Step 3 — list all options for a plugin as structured data:
|
|
66
|
+
Step 3 — all --flags of a plugin, with allowed values:
|
|
87
67
|
["config", "options", "<plugin>"]
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
["config", "documentation", "toc"]
|
|
94
|
-
Returns the table of contents: { level, title, anchor } for every heading.
|
|
95
|
-
["config", "documentation", "local", "<anchor>"]
|
|
96
|
-
Returns only the section matching that anchor (same slugs as GitHub).
|
|
97
|
-
["config", "documentation", "local", "--ui=text"]
|
|
98
|
-
Returns the complete README (~300 KB). Use only when a specific section
|
|
99
|
-
is insufficient and you need broader narrative context.
|
|
68
|
+
Documentation:
|
|
69
|
+
["config", "documentation", "toc"] → { level, title, anchor } per heading
|
|
70
|
+
["config", "documentation", "local", "<anchor>"] → one README section
|
|
71
|
+
Presets (saved credentials):
|
|
72
|
+
["config", "preset", "overview"]
|
|
100
73
|
|
|
101
74
|
RESULT FORMAT
|
|
102
|
-
Structured data is always in structuredContent (a JSON object).
|
|
103
|
-
For
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
counts, totals, or search results from a truncated text block.
|
|
75
|
+
Structured data is always in structuredContent (a JSON object; lists are under "items").
|
|
76
|
+
For lists, text content is limited to #{DEFAULT_MAX_TEXT_BYTES} bytes (whole items only);
|
|
77
|
+
when truncated, a WARNING line gives the real total. Read structuredContent.items for
|
|
78
|
+
the full dataset — never report counts or search results from a truncated text block.
|
|
107
79
|
|
|
108
80
|
FILE LIST FOR TRANSFERS
|
|
109
|
-
For all transfers (upload, download, package send, …), append source
|
|
110
|
-
|
|
111
|
-
["server", "upload", "--to-folder=/dst", "/local/file1", "/local/file2"]
|
|
112
|
-
["aoc", "packages", "send", "@:", "name=pkg", "recipients.0=user@example.com", "END",
|
|
113
|
-
"/local/file1", "/local/file2"]
|
|
81
|
+
For all transfers (upload, download, package send, …), append source paths at the
|
|
82
|
+
end of args — no --sources flag needed.
|
|
114
83
|
|
|
115
84
|
EXAMPLES
|
|
116
|
-
["config", "commands"]
|
|
117
|
-
["aoc", "admin", "user", "create", "help"]
|
|
118
|
-
["config", "options", "aoc"]
|
|
119
|
-
["config", "documentation", "
|
|
120
|
-
["
|
|
121
|
-
|
|
122
|
-
["aoc", "
|
|
123
|
-
'@json:{"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
["server", "browse", "/",
|
|
127
|
-
"--url=https://host", "--username=user", "--password=secret"]
|
|
128
|
-
["server", "upload", "--url=https://host", "--username=user", "--password=secret",
|
|
129
|
-
"--to-folder=/uploads", "/local/file1.txt", "/local/file2.txt"]
|
|
130
|
-
["aoc", "packages", "list", "--workspace=MyWorkspace"]
|
|
131
|
-
["node", "info", "--url=https://node-host",
|
|
132
|
-
"--username=user", "--password=pass"]
|
|
85
|
+
["config", "commands", "aoc"]
|
|
86
|
+
["aoc", "admin", "user", "create", "help"]
|
|
87
|
+
["config", "options", "aoc"]
|
|
88
|
+
["config", "documentation", "local", "leveraging-ai-assistance"]
|
|
89
|
+
["aoc", "--preset=myaoc", "packages", "list"]
|
|
90
|
+
["aoc", "--preset=myaoc", "admin", "user", "create", '@json:{"email":"a@b.com","name":"Alice"}']
|
|
91
|
+
["aoc", "--preset=myaoc", "packages", "send",
|
|
92
|
+
'@json:{"name":"pkg","recipients":["user@example.com"]}', "/local/a.txt", "/local/b.txt"]
|
|
93
|
+
["server", "--preset=myserver", "upload", "--to-folder=/uploads", "/local/a.txt"]
|
|
94
|
+
["server", "browse", "/", "--url=ssh://host:33001", "--username=user", "--password=secret"]
|
|
133
95
|
DESC
|
|
134
96
|
|
|
135
97
|
input_schema(
|
|
@@ -156,6 +118,8 @@ module Aspera
|
|
|
156
118
|
when Result::Nothing, Result::Empty, NilClass
|
|
157
119
|
MCP::Tool::Response.new([{type: 'text', text: ''}])
|
|
158
120
|
when Result::SingleObject, Result::ObjectList, Result::ValueList
|
|
121
|
+
# Secrets never reach the AI client, whatever the output format requested in args.
|
|
122
|
+
SecretHider.instance.deep_remove_secret(result.data)
|
|
159
123
|
# Apply --select filter in place (affects both text and structuredContent).
|
|
160
124
|
runner.context.formatter.filter_columns_on_select(result.data) if result.data.is_a?(Array)
|
|
161
125
|
# MCP spec requires structuredContent to be a JSON object (not an array).
|
|
@@ -177,7 +141,7 @@ module Aspera
|
|
|
177
141
|
end
|
|
178
142
|
MCP::Tool::Response.new(content, structured_content: structured)
|
|
179
143
|
else
|
|
180
|
-
MCP::Tool::Response.new([{type: 'text', text: result.data.to_s}])
|
|
144
|
+
MCP::Tool::Response.new([{type: 'text', text: SecretHider.instance.hide_secrets_in_string(result.data.to_s, all: true)}])
|
|
181
145
|
end
|
|
182
146
|
rescue Cli::SchemaRequest => e
|
|
183
147
|
schema_path = e.path
|
|
@@ -208,7 +172,7 @@ module Aspera
|
|
|
208
172
|
end.join("\n")
|
|
209
173
|
end
|
|
210
174
|
|
|
211
|
-
# Returns the largest prefix of
|
|
175
|
+
# Returns the largest prefix of `items` whose JSON serialization fits within `max_bytes`.
|
|
212
176
|
# Items are appended whole — no item is ever split mid-JSON.
|
|
213
177
|
def truncate_items_by_bytes(items, max_bytes)
|
|
214
178
|
buf = +''
|
|
@@ -16,62 +16,53 @@ module Aspera
|
|
|
16
16
|
|
|
17
17
|
# Declare an option in this class's registry.
|
|
18
18
|
#
|
|
19
|
-
# @param name [Symbol]
|
|
20
|
-
# @param description [String, nil]
|
|
21
|
-
# @param short [String, nil]
|
|
22
|
-
# @param allowed [Object, nil]
|
|
23
|
-
# @param default [Object, nil]
|
|
24
|
-
# @param
|
|
25
|
-
# @param
|
|
26
|
-
# @param
|
|
19
|
+
# @param name [Symbol] Option name
|
|
20
|
+
# @param description [String, nil] User-facing description; if nil, derived from schema: title/description
|
|
21
|
+
# @param short [String, nil] Single-character short form (without leading '-')
|
|
22
|
+
# @param allowed [Object, nil] Allowed values (see OptionValue)
|
|
23
|
+
# @param default [Object, nil] Default value
|
|
24
|
+
# @param on_set [Symbol, Proc, #call, nil] `on_set` callback (see OptionSpec)
|
|
25
|
+
# @param shorthand [String, nil] For a `Hash` option: a `String` value is stored as `{shorthand => value}`
|
|
26
|
+
# @param deprecation [Hash, nil] Deprecation `{last:, message:}` forwarded to options.declare (see `Deprecation`)
|
|
27
|
+
# @param schema [String, nil] Schema reference (e.g. "opts:components.schemas.Foo");
|
|
28
|
+
# when description: is nil, the schema title or first description line is used
|
|
27
29
|
def option(name, description: nil,
|
|
28
30
|
short: nil, allowed: nil, default: nil,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
on_set: nil, shorthand: nil, deprecation: nil, schema: nil)
|
|
32
|
+
register_option_spec(
|
|
33
|
+
OptionSpec.new(
|
|
34
|
+
name: name,
|
|
35
|
+
description: description,
|
|
36
|
+
short: short,
|
|
37
|
+
allowed: allowed,
|
|
38
|
+
default: default,
|
|
39
|
+
on_set: on_set,
|
|
40
|
+
shorthand: shorthand,
|
|
41
|
+
deprecation: deprecation,
|
|
42
|
+
schema: schema
|
|
43
|
+
)
|
|
40
44
|
)
|
|
41
45
|
end
|
|
42
46
|
|
|
47
|
+
# Store an OptionSpec in this class's registry.
|
|
48
|
+
# @param spec [OptionSpec]
|
|
49
|
+
# @raise [ArgumentError] on duplicate option name
|
|
50
|
+
def register_option_spec(spec)
|
|
51
|
+
raise ArgumentError, "Duplicate option: #{spec.name.inspect}" if option_specs.key?(spec.name)
|
|
52
|
+
option_specs[spec.name] = spec
|
|
53
|
+
end
|
|
54
|
+
|
|
43
55
|
# Declare all options registered on this class onto a Parser instance.
|
|
44
56
|
# Skips options already declared on the parser.
|
|
45
57
|
#
|
|
46
58
|
# @param parser [Aspera::Cli::Parser]
|
|
47
|
-
# @param target [Object, nil] default target object for Symbol
|
|
59
|
+
# @param target [Object, nil] default target object for Symbol and Proc `on_set` callbacks (defaults to self)
|
|
48
60
|
# @return [void]
|
|
49
61
|
def declare_options(parser, target: self)
|
|
50
62
|
option_specs.each_value do |spec|
|
|
51
|
-
|
|
52
|
-
resolved_handler =
|
|
53
|
-
case spec.handler
|
|
54
|
-
when Symbol then {o: target, m: spec.handler}
|
|
55
|
-
when Hash then spec.handler
|
|
56
|
-
end
|
|
57
|
-
parser.declare(
|
|
58
|
-
spec.name,
|
|
59
|
-
description: spec.description,
|
|
60
|
-
short: spec.short,
|
|
61
|
-
allowed: spec.allowed,
|
|
62
|
-
default: spec.default,
|
|
63
|
-
handler: resolved_handler,
|
|
64
|
-
deprecation: spec.deprecation,
|
|
65
|
-
schema: schema_for_spec(spec)
|
|
66
|
-
)
|
|
63
|
+
spec.declare_on(parser, target: target) unless parser.option_declared?(spec.name)
|
|
67
64
|
end
|
|
68
65
|
end
|
|
69
|
-
|
|
70
|
-
private
|
|
71
|
-
|
|
72
|
-
def schema_for_spec(spec)
|
|
73
|
-
spec.schema
|
|
74
|
-
end
|
|
75
66
|
end
|
|
76
67
|
end
|
|
77
68
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/cli/option_value'
|
|
4
|
+
require 'aspera/cli/error'
|
|
5
|
+
require 'aspera/assert'
|
|
6
|
+
|
|
7
|
+
module Aspera
|
|
8
|
+
module Cli
|
|
9
|
+
# Declared options, and their lookup by name: long, short, or unique abbreviation.
|
|
10
|
+
class OptionRegistry
|
|
11
|
+
# @return [Hash{Symbol => OptionValue}] declared options, in declaration order
|
|
12
|
+
attr_reader :options
|
|
13
|
+
# @return [String] help section of options declared next
|
|
14
|
+
attr_accessor :group
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@options = {}
|
|
18
|
+
# Short option char -> option, e.g. {'h' => help option}
|
|
19
|
+
@short_options = {}
|
|
20
|
+
@group = 'global'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Register a new option
|
|
24
|
+
# @param option [OptionValue] option to register
|
|
25
|
+
# @param short [String, nil] short option char
|
|
26
|
+
# @return [OptionValue] the option
|
|
27
|
+
def add(option, short: nil)
|
|
28
|
+
Aspera.assert(!@options.key?(option.option)) { "#{option.option} already declared" }
|
|
29
|
+
option.group = @group
|
|
30
|
+
option.short = short
|
|
31
|
+
@options[option.option] = option
|
|
32
|
+
@short_options[short] = option unless short.nil?
|
|
33
|
+
option
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param option_symbol [Symbol] option name
|
|
37
|
+
# @return [Boolean] `true` if option is declared
|
|
38
|
+
def declared?(option_symbol) = @options.key?(option_symbol)
|
|
39
|
+
|
|
40
|
+
# @param option_symbol [Symbol] option name
|
|
41
|
+
# @return [OptionValue] declared option
|
|
42
|
+
# @raise [BadArgument] if option is not declared
|
|
43
|
+
def fetch(option_symbol)
|
|
44
|
+
Aspera.assert(@options.key?(option_symbol), type: BadArgument) { "Unknown option: #{option_symbol}" }
|
|
45
|
+
@options[option_symbol]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param char [String] short option char
|
|
49
|
+
# @return [OptionValue, nil] declared option, or `nil`
|
|
50
|
+
def by_short(char) = @short_options[char]
|
|
51
|
+
|
|
52
|
+
# Find long option by exact name, or by unique abbreviation.
|
|
53
|
+
# @param name [String] option name with underscores
|
|
54
|
+
# @param allow_abbreviation [Boolean] accept a unique prefix of a declared option
|
|
55
|
+
# @return [OptionValue, nil] declared option, or `nil` if none matches (yet)
|
|
56
|
+
# @raise [BadArgument] if abbreviation is ambiguous
|
|
57
|
+
def by_long(name, allow_abbreviation: true)
|
|
58
|
+
option = @options[name.to_sym]
|
|
59
|
+
return option if !option.nil? || !allow_abbreviation
|
|
60
|
+
candidates = @options.keys.select { |k| k.to_s.start_with?(name) }
|
|
61
|
+
return if candidates.empty?
|
|
62
|
+
Aspera.assert(candidates.length.eql?(1), type: BadArgument) do
|
|
63
|
+
Parser.multi_choice_assert_msg("Ambiguous option: #{Parser.option_name_to_line(name)}", candidates.map { |c| Parser.option_name_to_line(c) })
|
|
64
|
+
end
|
|
65
|
+
@options[candidates.first]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/cli/error'
|
|
4
|
+
require 'aspera/assert'
|
|
5
|
+
|
|
6
|
+
module Aspera
|
|
7
|
+
module Cli
|
|
8
|
+
# Exception raised when schema is asked (`help`)
|
|
9
|
+
class SchemaRequest < Error
|
|
10
|
+
# Value of option or argument that requests its schema
|
|
11
|
+
KEYWORD = 'help'
|
|
12
|
+
|
|
13
|
+
# @return [String, nil] path to schema file
|
|
14
|
+
attr_reader :path
|
|
15
|
+
|
|
16
|
+
# @param type [Symbol] :argument or :option
|
|
17
|
+
# @param name [String] name of the option/argument
|
|
18
|
+
# @param schema_path [String, nil] path to schema file, or `nil` if not available
|
|
19
|
+
def initialize(type, name, schema_path)
|
|
20
|
+
super("#{type}: #{name}")
|
|
21
|
+
@path = schema_path
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Values accepted for boolean options: `true`, `false`, `yes`, `no`
|
|
26
|
+
module BoolValue
|
|
27
|
+
# Symbol for `true`
|
|
28
|
+
YES_SYM = :yes
|
|
29
|
+
# Symbol for `false`
|
|
30
|
+
NO_SYM = :no
|
|
31
|
+
# Values meaning `false`
|
|
32
|
+
FALSE_VALUES = [NO_SYM, false].freeze
|
|
33
|
+
# Values meaning `true`
|
|
34
|
+
TRUE_VALUES = [YES_SYM, true].freeze
|
|
35
|
+
private_constant :FALSE_VALUES, :TRUE_VALUES
|
|
36
|
+
# Boolean values
|
|
37
|
+
# @return [Array<true, false, :yes, :no>]
|
|
38
|
+
ALL = (TRUE_VALUES + FALSE_VALUES).freeze
|
|
39
|
+
# `false` and `true`
|
|
40
|
+
TYPES = [FalseClass, TrueClass].freeze
|
|
41
|
+
# `:no` and `:yes`
|
|
42
|
+
SYMBOLS = [NO_SYM, YES_SYM].freeze
|
|
43
|
+
# @return [Boolean] `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 [Boolean] `true` if value is a value for `true` or `false` in ALL
|
|
50
|
+
def symbol?(sym)
|
|
51
|
+
ALL.include?(sym)
|
|
52
|
+
end
|
|
53
|
+
module_function :true?, :symbol?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Type specifiers for the `allowed:` parameter of option declarations.
|
|
57
|
+
# Public API: STRING_ARRAY, SYMBOL_ARRAY, INTEGER, BOOLEAN, NONE.
|
|
58
|
+
# Internal (do not pass as `allowed:`):
|
|
59
|
+
# ENUM - derived internally when `allowed:` is an Array<Symbol> (enum list)
|
|
60
|
+
# STRING - the implicit default; equivalent to omitting `allowed:` entirely
|
|
61
|
+
module Type
|
|
62
|
+
# Option value is a String or Array of Strings (cumulative)
|
|
63
|
+
STRING_ARRAY = [Array, String].freeze
|
|
64
|
+
# Option value is a Symbol from a constrained list; use as prefix: SYMBOL_ARRAY + [:val1, :val2]
|
|
65
|
+
SYMBOL_ARRAY = [Array, Symbol].freeze
|
|
66
|
+
# Option value is coerced to Integer
|
|
67
|
+
INTEGER = [Integer].freeze
|
|
68
|
+
# Option value is a Boolean
|
|
69
|
+
BOOLEAN = BoolValue::TYPES
|
|
70
|
+
# Option has no value — it is a flag switch (e.g. `-N`, `--help`)
|
|
71
|
+
NONE = [].freeze
|
|
72
|
+
# Internal: derived when allowed: is an Array<Symbol>; do not pass directly
|
|
73
|
+
ENUM = [Symbol].freeze
|
|
74
|
+
# Internal: implicit default (String); equivalent to omitting allowed: entirely
|
|
75
|
+
STRING = [String].freeze
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Sources of option values.
|
|
79
|
+
# A value is ignored if the option was already set from a source with higher priority.
|
|
80
|
+
module OptionSource
|
|
81
|
+
# Source -> priority (higher wins)
|
|
82
|
+
PRIORITY = {
|
|
83
|
+
default: 0,
|
|
84
|
+
# Default preset for plugin (added with `override: false`)
|
|
85
|
+
plugin_preset: 1,
|
|
86
|
+
preset: 2,
|
|
87
|
+
env: 3,
|
|
88
|
+
cmdline: 4,
|
|
89
|
+
# Set by code, or asked to user: same as command line
|
|
90
|
+
code: 4,
|
|
91
|
+
interactive: 4
|
|
92
|
+
}.freeze
|
|
93
|
+
|
|
94
|
+
class << self
|
|
95
|
+
# @param source [Symbol] one of `PRIORITY` keys
|
|
96
|
+
# @return [Integer] priority of source
|
|
97
|
+
def priority(source)
|
|
98
|
+
PRIORITY.fetch(source) { Aspera.error_unexpected_value(source) { 'option source' } }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|