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
|
@@ -11,16 +11,82 @@ module Aspera
|
|
|
11
11
|
# register(spec) - store a CommandSpec; raises on duplicate full_path
|
|
12
12
|
# register_option(spec) - store an OptionSpec by name
|
|
13
13
|
# option_specs - Hash{Symbol => OptionSpec} of all registered options
|
|
14
|
-
# [](path) - retrieve a CommandSpec by full path
|
|
15
|
-
# children_of(path) - Hash{Symbol => CommandSpec} of direct children (
|
|
16
|
-
#
|
|
14
|
+
# [](path) - retrieve a CommandSpec by full path (follows mounts)
|
|
15
|
+
# children_of(path) - Hash{Symbol => CommandSpec} of direct children (follows mounts)
|
|
16
|
+
# resolve(path) - [registry, path] owning the spec at path (follows mounts)
|
|
17
|
+
# local?(path) - true if path is owned by this registry (not reached through a mount)
|
|
18
|
+
# mount_of(path) - MountSpec of the local node at path, if any
|
|
19
|
+
# mount_at(path) - MountSpec of the node at path, if any (follows mounts)
|
|
20
|
+
# own_children_of(path) - Hash{Symbol => CommandSpec} of direct children declared on the node, without mounted ones
|
|
21
|
+
# arguments_at(path) - Array of ArgumentSpec read by the node at path (mount arguments first)
|
|
22
|
+
# leaf_paths - Array of all leaf paths (follows mounts, or stops at mount nodes)
|
|
23
|
+
# command_path(words) - command path designated by command line words (aliases, arguments)
|
|
24
|
+
# all_paths - Array of all locally registered full paths
|
|
17
25
|
# any? - true if at least one spec has been registered
|
|
18
26
|
# validate! - cross-spec consistency checks; raises on violation
|
|
27
|
+
#
|
|
28
|
+
# Paths are always expressed in this registry's namespace: a path going through a
|
|
29
|
+
# mounted node (see MountSpec) is translated to the target registry transparently.
|
|
30
|
+
# Specs returned for mounted paths are the target's specs (their full_path is in the
|
|
31
|
+
# target namespace).
|
|
19
32
|
class CommandRegistry
|
|
20
33
|
# @param path [Array<Symbol>] full path to look up
|
|
21
34
|
# @return [CommandSpec, nil]
|
|
22
35
|
def [](path)
|
|
23
|
-
|
|
36
|
+
registry, local_path = resolve(path)
|
|
37
|
+
registry.equal?(self) ? @specs[local_path] : registry[local_path]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Find the registry owning `path`, following mounts.
|
|
41
|
+
# A host child always takes precedence over a mounted child with the same id.
|
|
42
|
+
# @param path [Array<Symbol>] path in this registry's namespace
|
|
43
|
+
# @return [Array(CommandRegistry, Array<Symbol>)] owning registry and path in its namespace
|
|
44
|
+
def resolve(path)
|
|
45
|
+
path = Array(path)
|
|
46
|
+
path.each_index do |i|
|
|
47
|
+
prefix = path[0, i]
|
|
48
|
+
mount = @specs[prefix]&.mount
|
|
49
|
+
next if mount.nil? || @children_index[prefix]&.key?(path[i]) || !mount.accepts?(path[i])
|
|
50
|
+
return mount.registry.resolve(mount.at + path[i..])
|
|
51
|
+
end
|
|
52
|
+
[self, path]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param path [Array<Symbol>] path in this registry's namespace
|
|
56
|
+
# @return [Boolean] true if the node at path is declared in this registry (not mounted)
|
|
57
|
+
def local?(path)
|
|
58
|
+
resolve(path).first.equal?(self)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param path [Array<Symbol>] local path of a node
|
|
62
|
+
# @return [MountSpec, nil] the mount declared on that node
|
|
63
|
+
def mount_of(path)
|
|
64
|
+
@specs[Array(path)]&.mount
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @param path [Array<Symbol>] path in this registry's namespace
|
|
68
|
+
# @return [MountSpec, nil] the mount declared on the node at path, following mounts
|
|
69
|
+
def mount_at(path)
|
|
70
|
+
registry, local_path = resolve(path)
|
|
71
|
+
registry.mount_of(local_path)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Children declared on the node itself, without the ones exposed by its mount.
|
|
75
|
+
# @param path [Array<Symbol>] path in this registry's namespace
|
|
76
|
+
# @return [Hash{Symbol => CommandSpec}]
|
|
77
|
+
def own_children_of(path)
|
|
78
|
+
registry, local_path = resolve(path)
|
|
79
|
+
return registry.own_children_of(local_path) unless registry.equal?(self)
|
|
80
|
+
@children_index[local_path] || {}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Arguments read by the node at path, in order.
|
|
84
|
+
# For a child exposed by a mount, the mount's arguments come first (read by the host).
|
|
85
|
+
# @param path [Array<Symbol>] path in this registry's namespace
|
|
86
|
+
# @return [Array<ArgumentSpec>]
|
|
87
|
+
def arguments_at(path)
|
|
88
|
+
path = Array(path)
|
|
89
|
+
(path.empty? ? [] : mount_arguments(path)) + (self[path]&.arguments || [])
|
|
24
90
|
end
|
|
25
91
|
|
|
26
92
|
# Register a CommandSpec. Raises if the full_path is already registered.
|
|
@@ -40,14 +106,67 @@ module Aspera
|
|
|
40
106
|
|
|
41
107
|
# Returns a Hash mapping each child id to its CommandSpec for all direct
|
|
42
108
|
# children of `path`. Empty hash if no children are registered.
|
|
43
|
-
#
|
|
109
|
+
# For a mount node: mounted children (filtered) followed by local children,
|
|
110
|
+
# local ones overriding mounted ones with the same id.
|
|
44
111
|
# @param path [Array<Symbol>] parent path ([] for root-level commands)
|
|
45
112
|
# @return [Hash{Symbol => CommandSpec}]
|
|
46
113
|
def children_of(path)
|
|
47
|
-
|
|
114
|
+
registry, local_path = resolve(path)
|
|
115
|
+
return registry.children_of(local_path) unless registry.equal?(self)
|
|
116
|
+
own = @children_index[local_path] || {}
|
|
117
|
+
mount = @specs[local_path]&.mount
|
|
118
|
+
return own if mount.nil?
|
|
119
|
+
mount.registry.children_of(mount.at).select { |id, _| mount.accepts?(id) }.merge(own)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# All leaf paths under path, in tree order, following mounts.
|
|
123
|
+
# A mount cycle (a sub-tree mounting one of its ancestors) is not expanded twice.
|
|
124
|
+
# @param path [Array<Symbol>] root of the listing ([] for all)
|
|
125
|
+
# @param expand_mounts [Boolean] `false`: a mount node is listed as a leaf, followed by its own children only
|
|
126
|
+
# @return [Array<Array<Symbol>>]
|
|
127
|
+
def leaf_paths(path = [], chain = [], expand_mounts: true)
|
|
128
|
+
children_of(path).keys.flat_map do |id|
|
|
129
|
+
child = path + [id]
|
|
130
|
+
key = subtree_key(child)
|
|
131
|
+
next [] if chain.include?(key)
|
|
132
|
+
if !expand_mounts && mount_at(child)
|
|
133
|
+
next [child] + own_children_of(child).keys.flat_map do |own_id|
|
|
134
|
+
own_child = child + [own_id]
|
|
135
|
+
children_of(own_child).empty? ? [own_child] : leaf_paths(own_child, chain + [key], expand_mounts: false)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
children_of(child).empty? ? [child] : leaf_paths(child, chain + [key], expand_mounts: expand_mounts)
|
|
139
|
+
end
|
|
48
140
|
end
|
|
49
141
|
|
|
50
|
-
#
|
|
142
|
+
# Command path designated by words of a command line: sub-commands (or their aliases),
|
|
143
|
+
# each possibly followed by its positional arguments, e.g. `packages receive ALL` designates `packages receive`.
|
|
144
|
+
# Words after a leaf command are its arguments.
|
|
145
|
+
# @param words [Array<Symbol>] words following the plugin name
|
|
146
|
+
# @return [Array(Array<Symbol>, Symbol)] command path, and the first word that is neither a sub-command
|
|
147
|
+
# nor an expected argument (`nil` if none)
|
|
148
|
+
def command_path(words)
|
|
149
|
+
path = []
|
|
150
|
+
# Number of positional arguments still accepted by the node at path
|
|
151
|
+
args_left = 0
|
|
152
|
+
words.each do |word|
|
|
153
|
+
children = children_of(path)
|
|
154
|
+
break if children.empty?
|
|
155
|
+
id = children.key?(word) ? word : children.find { |_, c| Array(c.aliases).include?(word) }&.first
|
|
156
|
+
if id
|
|
157
|
+
path += [id]
|
|
158
|
+
arguments = arguments_at(path)
|
|
159
|
+
args_left = arguments.any?(&:multiple) ? Float::INFINITY : arguments.length
|
|
160
|
+
elsif args_left.positive?
|
|
161
|
+
args_left -= 1
|
|
162
|
+
else
|
|
163
|
+
return [path, word]
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
[path, nil]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# @return [Array<Array<Symbol>>] all locally registered full paths
|
|
51
170
|
def all_paths
|
|
52
171
|
@specs.keys
|
|
53
172
|
end
|
|
@@ -77,60 +196,119 @@ module Aspera
|
|
|
77
196
|
end
|
|
78
197
|
|
|
79
198
|
# Cross-spec consistency checks.
|
|
80
|
-
# @param plugin_class [Class, nil] when given, also verify that
|
|
199
|
+
# @param plugin_class [Class, nil] when given, also verify that methods referenced by Symbol
|
|
200
|
+
# (implicit and explicit actions, `setup:`, `condition:`, `lookup:`, mount `instance:`) exist
|
|
81
201
|
# @raise [ArgumentError] on any violation
|
|
82
202
|
# @return [self]
|
|
83
203
|
def validate!(plugin_class: nil)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
unless @specs.key?(parent_path)
|
|
204
|
+
@children_index.each do |parent_path, children|
|
|
205
|
+
# Rule: every non-root parent path that appears in the children index must have
|
|
206
|
+
# a registered CommandSpec. A missing parent means commands_under(:x) was used
|
|
207
|
+
# without a matching command :x declaration.
|
|
208
|
+
unless parent_path.empty? || @specs.key?(parent_path)
|
|
90
209
|
raise ArgumentError,
|
|
91
210
|
"commands_under(#{parent_path.map(&:inspect).join(', ')}) used but #{parent_path.last.inspect} has no command declaration"
|
|
92
211
|
end
|
|
212
|
+
# Rule: an alias designates a single command, and does not hide a sibling command
|
|
213
|
+
aliases = children.values.flat_map { |c| Array(c.aliases) }
|
|
214
|
+
conflicts = aliases.select { |a| children.key?(a) || aliases.count(a) > 1 }.uniq
|
|
215
|
+
raise ArgumentError, "#{parent_path.inspect}: alias conflicts with a sibling command or alias: #{conflicts.inspect}" unless conflicts.empty?
|
|
93
216
|
end
|
|
94
217
|
|
|
95
218
|
@specs.each_value do |spec|
|
|
96
219
|
path = spec.full_path
|
|
97
220
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
when Array then spec.delegates_to
|
|
104
|
-
end
|
|
105
|
-
# An empty array [] means re-enter the root - always valid
|
|
106
|
-
unless dt_path.empty? || @specs.key?(dt_path)
|
|
107
|
-
raise ArgumentError,
|
|
108
|
-
"#{path.inspect}: delegates_to #{dt_path.inspect} points to unknown path"
|
|
221
|
+
validate_arguments(path, spec.arguments, plugin_class)
|
|
222
|
+
# Rule: methods called on the plugin instance exist
|
|
223
|
+
if plugin_class
|
|
224
|
+
{setup: spec.setup, condition: spec.condition}.each do |attribute, method_name|
|
|
225
|
+
raise ArgumentError, "#{path.inspect}: no method #{method_name} on #{plugin_class} for #{attribute}:" unless method_name.nil? || instance_method?(plugin_class, method_name)
|
|
109
226
|
end
|
|
110
227
|
end
|
|
111
228
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
raise ArgumentError,
|
|
115
|
-
|
|
229
|
+
if (mount = spec.mount)
|
|
230
|
+
# Rule: a mount needs an instance method, no action, and must point to existing target nodes
|
|
231
|
+
raise ArgumentError, "#{path.inspect}: mount requires instance:" if mount.instance.nil?
|
|
232
|
+
# Mount arguments precede the arguments of the mounted command: they cannot be optional
|
|
233
|
+
raise ArgumentError, "#{path.inspect}: mount arguments must be mandatory" unless mount.arguments.all?(&:mandatory)
|
|
234
|
+
validate_arguments(path, mount.arguments, plugin_class)
|
|
235
|
+
raise ArgumentError, "#{path.inspect}: mount and action: are exclusive" if spec.action
|
|
236
|
+
raise ArgumentError, "#{path.inspect}: mount at #{mount.at.inspect} not found in #{mount.plugin}" unless mount.at.empty? || mount.registry[mount.at]
|
|
237
|
+
target_ids = mount.registry.children_of(mount.at).keys
|
|
238
|
+
unknown = Array(mount.only) + Array(mount.except) - target_ids
|
|
239
|
+
raise ArgumentError, "#{path.inspect}: mount only/except unknown in #{mount.plugin}: #{unknown.inspect}" unless unknown.empty?
|
|
240
|
+
instance_defined = plugin_class.nil? || instance_method?(plugin_class, mount.instance)
|
|
241
|
+
raise ArgumentError, "#{path.inspect}: no method #{mount.instance} on #{plugin_class}" unless instance_defined
|
|
242
|
+
next
|
|
116
243
|
end
|
|
117
244
|
|
|
118
|
-
# Rule: leaf commands with no explicit action must have a matching instance method
|
|
119
|
-
next if spec.action # explicit action: skip
|
|
120
245
|
next if @children_index[path]&.any? # intermediate node: skip
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
246
|
+
action = spec.action
|
|
247
|
+
if action.nil?
|
|
248
|
+
next unless plugin_class
|
|
249
|
+
# Rule: leaf commands with no explicit action must have a matching instance method
|
|
250
|
+
action = CommandSpec.action_method(path)
|
|
251
|
+
unless instance_method?(plugin_class, action)
|
|
252
|
+
raise ArgumentError,
|
|
253
|
+
"#{path.inspect}: no action: and no method #{action} on #{plugin_class}"
|
|
254
|
+
end
|
|
127
255
|
end
|
|
256
|
+
action = plugin_class.instance_method(action) if action.is_a?(Symbol) && plugin_class && instance_method?(plugin_class, action)
|
|
257
|
+
# Rule: the action receives the whole dispatch context as keywords (setup results, arguments),
|
|
258
|
+
# so it must accept any keyword (`**`): a lambda or method with fixed arity would raise ArgumentError.
|
|
259
|
+
# A non-lambda Proc ignores extra keywords.
|
|
260
|
+
next if action.is_a?(Symbol) || (action.is_a?(Proc) && !action.lambda?)
|
|
261
|
+
raise ArgumentError, "#{path.inspect}: action must accept any keyword (**), parameters: #{action.parameters.inspect}" unless action.parameters.any? { |kind, _| kind.eql?(:keyrest) }
|
|
128
262
|
end
|
|
129
263
|
self
|
|
130
264
|
end
|
|
131
265
|
|
|
132
266
|
private
|
|
133
267
|
|
|
268
|
+
# @return [Boolean] true if plugin_class defines instance method name (public or private)
|
|
269
|
+
def instance_method?(plugin_class, name)
|
|
270
|
+
plugin_class.method_defined?(name) || plugin_class.private_method_defined?(name)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Consistency of the positional arguments of a node, in reading order.
|
|
274
|
+
# @param path [Array<Symbol>] node path, for error messages
|
|
275
|
+
# @param arg_specs [Array<ArgumentSpec>] arguments of the node (or of its mount)
|
|
276
|
+
# @param plugin_class [Class, nil] when given, verify that `lookup:` methods exist
|
|
277
|
+
# @raise [ArgumentError] on any violation
|
|
278
|
+
def validate_arguments(path, arg_specs, plugin_class)
|
|
279
|
+
previous = nil
|
|
280
|
+
Array(arg_specs).each do |arg|
|
|
281
|
+
where = "#{path.inspect}: argument #{arg.name}"
|
|
282
|
+
# Rule: an argument following an optional one, or one that takes all remaining arguments, is never read reliably
|
|
283
|
+
raise ArgumentError, "#{where}: mandatory after optional #{previous.name}" if previous && arg.mandatory && !previous.mandatory
|
|
284
|
+
raise ArgumentError, "#{where}: after #{previous.name}, which takes all remaining arguments" if previous&.multiple.eql?(true)
|
|
285
|
+
unless arg.lookup.nil?
|
|
286
|
+
# Rule: the percent-selector lookup is only used for identifiers
|
|
287
|
+
raise ArgumentError, "#{where}: lookup: requires type: :identifier" unless arg.type.eql?(:identifier)
|
|
288
|
+
raise ArgumentError, "#{where}: no method #{arg.lookup} on #{plugin_class} for lookup:" if arg.lookup.is_a?(Symbol) && plugin_class && !instance_method?(plugin_class, arg.lookup)
|
|
289
|
+
end
|
|
290
|
+
previous = arg
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# @param path [Array<Symbol>] non-empty path in this registry's namespace
|
|
295
|
+
# @return [Array<ArgumentSpec>] arguments of the mount exposing the last segment of path, if any
|
|
296
|
+
def mount_arguments(path)
|
|
297
|
+
registry, parent = resolve(path[0..-2])
|
|
298
|
+
return registry.send(:mount_arguments, parent + [path.last]) unless registry.equal?(self)
|
|
299
|
+
mount = @specs[parent]&.mount
|
|
300
|
+
return [] if mount.nil? || @children_index[parent]&.key?(path.last) || !mount.accepts?(path.last)
|
|
301
|
+
mount.arguments
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Identity of the sub-tree exposed at path: the mount point for a mount node, else the owning node.
|
|
305
|
+
# @return [Array(Integer, Array<Symbol>)]
|
|
306
|
+
def subtree_key(path)
|
|
307
|
+
registry, local_path = resolve(path)
|
|
308
|
+
mount = registry.mount_of(local_path)
|
|
309
|
+
mount ? [mount.registry.object_id, mount.at] : [registry.object_id, local_path]
|
|
310
|
+
end
|
|
311
|
+
|
|
134
312
|
def initialize
|
|
135
313
|
# Keyed by Array<Symbol> full path
|
|
136
314
|
@specs = {}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'aspera/schema/registry'
|
|
4
|
+
require 'aspera/cli/option_types'
|
|
4
5
|
|
|
5
6
|
module Aspera
|
|
6
7
|
module Cli
|
|
@@ -10,7 +11,8 @@ module Aspera
|
|
|
10
11
|
#
|
|
11
12
|
# @!attribute name [Symbol] Name used in help and error messages
|
|
12
13
|
# @!attribute description [String] User-facing description
|
|
13
|
-
# @!attribute type [Class, Array<Class>, :identifier] Validated type; :identifier triggers instance_identifier
|
|
14
|
+
# @!attribute type [Class, Array<Class>, :identifier, nil] Validated type; :identifier triggers instance_identifier.
|
|
15
|
+
# Default: String (unless allowed:); explicit nil accepts any value
|
|
14
16
|
# @!attribute mandatory [Boolean] Default true; optional args must come after all mandatory ones
|
|
15
17
|
# @!attribute multiple [Boolean, String] true: consume all remaining; String: consume until named marker
|
|
16
18
|
# @!attribute default [Object, nil] Default value when mandatory: false and no argument provided
|
|
@@ -21,7 +23,7 @@ module Aspera
|
|
|
21
23
|
# Proc/lambda → called via instance_exec(field, value, **ctx, &lookup).
|
|
22
24
|
# Style: use Symbol for named methods; ->(){} for 1-liners; lambda do…end for 2–3 statements.
|
|
23
25
|
# @!attribute allowed [Array<Symbol>, nil] Allowed Symbol values; when set, type is forced to Symbol and accept_list is applied
|
|
24
|
-
# @!attribute interactive [Boolean] When true,
|
|
26
|
+
# @!attribute interactive [Boolean] When true, prompts for this argument (only) when no CLI args are provided
|
|
25
27
|
ArgumentSpec = Struct.new(
|
|
26
28
|
:name,
|
|
27
29
|
:description,
|
|
@@ -41,8 +43,20 @@ module Aspera
|
|
|
41
43
|
kwargs[:multiple] = false if kwargs[:multiple].nil?
|
|
42
44
|
kwargs[:bulk] = false if kwargs[:bulk].nil?
|
|
43
45
|
kwargs[:interactive] = false if kwargs[:interactive].nil?
|
|
46
|
+
kwargs[:type] = String unless kwargs.key?(:type) || kwargs[:allowed]
|
|
44
47
|
super
|
|
45
48
|
end
|
|
49
|
+
|
|
50
|
+
# Syntax of argument for help, e.g. `<name>`, `[<name>]`, `<paths...>`, `<account:Hash>`.
|
|
51
|
+
# Type is shown only when the argument is not free text.
|
|
52
|
+
# @return [String]
|
|
53
|
+
def syntax
|
|
54
|
+
token = allowed ? allowed.join('|') : name.to_s
|
|
55
|
+
token += '...' if multiple
|
|
56
|
+
types = Array(type).grep(Class)
|
|
57
|
+
token += ":#{types.map(&:name).join('|')}" unless allowed || types.empty? || types.include?(String)
|
|
58
|
+
mandatory ? "<#{token}>" : "[<#{token}>]"
|
|
59
|
+
end
|
|
46
60
|
end
|
|
47
61
|
|
|
48
62
|
# Declares an option referenced by name from command declarations.
|
|
@@ -54,11 +68,14 @@ module Aspera
|
|
|
54
68
|
# @!attribute allowed [Array, nil] Allowed values (forwarded to options.declare)
|
|
55
69
|
# @!attribute default [Object, nil] Default value
|
|
56
70
|
# @!attribute short [String, nil] Single-character short form (e.g. 'x')
|
|
57
|
-
# @!attribute
|
|
58
|
-
#
|
|
59
|
-
# -
|
|
60
|
-
# -
|
|
61
|
-
#
|
|
71
|
+
# @!attribute on_set [Symbol, Proc, #call, nil] Called with the new value each time the value is set
|
|
72
|
+
# (for a flag: called without argument when the flag is found)
|
|
73
|
+
# - Symbol: method of the plugin instance
|
|
74
|
+
# - Proc: executed with instance_exec on the plugin instance
|
|
75
|
+
# - other: object responding to `call`, e.g. `Log.instance.method(:level=)`
|
|
76
|
+
# - nil: no callback, the value is read with `get_option`
|
|
77
|
+
# @!attribute shorthand [String, nil] For a `Hash` option: a `String` value is stored as `{shorthand => value}`
|
|
78
|
+
# @!attribute deprecation [Hash, nil] Forwarded to options.declare as deprecation: `{last:, message:}`
|
|
62
79
|
# @!attribute schema [String, nil] JSON schema name; also derives description when nil
|
|
63
80
|
OptionSpec = Struct.new(
|
|
64
81
|
:name,
|
|
@@ -66,43 +83,114 @@ module Aspera
|
|
|
66
83
|
:allowed,
|
|
67
84
|
:default,
|
|
68
85
|
:short,
|
|
69
|
-
:
|
|
86
|
+
:on_set,
|
|
87
|
+
:shorthand,
|
|
70
88
|
:deprecation,
|
|
71
89
|
:schema,
|
|
72
90
|
keyword_init: true
|
|
73
|
-
)
|
|
91
|
+
) do
|
|
92
|
+
# Declare this option on a parser, resolving the `on_set` callback.
|
|
93
|
+
# @param parser [Parser] Parser to declare the option on
|
|
94
|
+
# @param target [Object, nil] Object for Symbol and Proc `on_set` callbacks (plugin instance); nil: such callbacks are not bound
|
|
95
|
+
# @return [void]
|
|
96
|
+
def declare_on(parser, target: nil)
|
|
97
|
+
parser.declare(
|
|
98
|
+
name,
|
|
99
|
+
description: description,
|
|
100
|
+
short: short,
|
|
101
|
+
allowed: allowed,
|
|
102
|
+
default: default,
|
|
103
|
+
on_set: resolved_on_set(target),
|
|
104
|
+
shorthand: shorthand,
|
|
105
|
+
deprecation: deprecation,
|
|
106
|
+
schema: schema
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
# @param target [Object, nil] Object for Symbol and Proc `on_set` callbacks
|
|
113
|
+
# @return [#call, nil] `on_set` callback for `Parser#declare`
|
|
114
|
+
def resolved_on_set(target)
|
|
115
|
+
case on_set
|
|
116
|
+
when Symbol then target&.method(on_set)
|
|
117
|
+
when Proc
|
|
118
|
+
return if target.nil?
|
|
119
|
+
proc_on_set = on_set
|
|
120
|
+
->(*value) { target.instance_exec(*value, &proc_on_set) }
|
|
121
|
+
else on_set
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Declares that a command node exposes a sub-tree of another plugin class.
|
|
127
|
+
# The mounted children appear in the host registry (dispatch, --help, completion)
|
|
128
|
+
# as if they were declared by the host; host children with the same id take precedence.
|
|
129
|
+
#
|
|
130
|
+
# @!attribute plugin [Class] Target plugin class (subclass of Plugins::Base)
|
|
131
|
+
# @!attribute at [Array<Symbol>] Path in the target registry whose children are mounted ([] = root)
|
|
132
|
+
# @!attribute instance [Symbol] Host instance method called with **ctx, returning the target plugin
|
|
133
|
+
# instance, or [instance, ctx] where ctx seeds the target dispatch
|
|
134
|
+
# (it replaces what the setups of `at` and its ancestors would provide)
|
|
135
|
+
# @!attribute only [Array<Symbol>, nil] Restrict mounted children to these ids
|
|
136
|
+
# @!attribute except [Array<Symbol>, nil] Exclude these ids from mounted children
|
|
137
|
+
# @!attribute arguments [Array<ArgumentSpec>] Arguments read by the host after the mounted command, before its own
|
|
138
|
+
# arguments; resolved values are passed to `instance`
|
|
139
|
+
# (e.g. `packages ls <package_id> <path>`)
|
|
140
|
+
MountSpec = Struct.new(
|
|
141
|
+
:plugin,
|
|
142
|
+
:at,
|
|
143
|
+
:instance,
|
|
144
|
+
:only,
|
|
145
|
+
:except,
|
|
146
|
+
:arguments,
|
|
147
|
+
keyword_init: true
|
|
148
|
+
) do
|
|
149
|
+
def initialize(**kwargs)
|
|
150
|
+
kwargs[:at] = Array(kwargs[:at]).freeze
|
|
151
|
+
kwargs[:arguments] = Array(kwargs[:arguments]).map { |a| a.is_a?(Hash) ? ArgumentSpec.new(**a) : a }.freeze
|
|
152
|
+
super
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @return [CommandRegistry] registry of the target plugin class
|
|
156
|
+
def registry
|
|
157
|
+
plugin.command_registry
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# @param id [Symbol] id of a child of `at` in the target registry
|
|
161
|
+
# @return [Boolean] true if this child is exposed by the mount
|
|
162
|
+
def accepts?(id)
|
|
163
|
+
(only.nil? || only.include?(id)) && !except&.include?(id)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
74
166
|
|
|
75
167
|
# Declares a single command node in the flat registry.
|
|
76
168
|
#
|
|
77
169
|
# @!attribute id [Symbol] Unique identifier within its parent's namespace
|
|
78
170
|
# @!attribute parent [Symbol, Array<Symbol>, nil] Full path to parent; nil for root commands
|
|
79
171
|
# @!attribute description [String] User-facing help text
|
|
80
|
-
# @!attribute options [Array<Symbol>] Option names consumed by this command
|
|
81
172
|
# @!attribute arguments [Array<ArgumentSpec>] Positional arguments, in order.
|
|
82
173
|
# The first ArgumentSpec with type: :identifier is treated as the instance
|
|
83
174
|
# identifier for intermediate nodes (consumed in Phase A) and leaf nodes.
|
|
84
175
|
# @!attribute action [Symbol, Proc, nil] Instance method (Symbol) or inline block (Proc) called when this is a leaf command
|
|
85
176
|
# @!attribute setup [Symbol, nil] Instance method called before dispatching to children; returns Hash merged into ctx
|
|
86
|
-
# @!attribute delegates_to [Symbol, Array<Symbol>, nil] Re-enter the command tree at this path
|
|
87
|
-
# @!attribute delegate_instance [Symbol, nil] Instance method returning a different plugin object
|
|
88
177
|
# @!attribute aliases [Array<Symbol>, nil] Alternative names accepted for this command (each resolves to this command's id)
|
|
89
178
|
# @!attribute transfer_paths [:send, :receive, nil] File-list resolution delegated to TransferAgent; mutually exclusive with arguments
|
|
90
179
|
# @!attribute condition [Symbol, nil] Instance method returning Boolean; if false command is hidden from dispatch
|
|
91
180
|
# @!attribute query_schema [String, nil] Schema path for --query help; when set, the runner hints `--query=help`
|
|
181
|
+
# @!attribute mount [MountSpec, Hash, nil] Expose children of another plugin's registry under this node
|
|
92
182
|
CommandSpec = Struct.new(
|
|
93
183
|
:id,
|
|
94
184
|
:parent,
|
|
95
185
|
:description,
|
|
96
|
-
:options,
|
|
97
186
|
:arguments,
|
|
98
187
|
:action,
|
|
99
188
|
:setup,
|
|
100
|
-
:delegates_to,
|
|
101
|
-
:delegate_instance,
|
|
102
189
|
:aliases,
|
|
103
190
|
:transfer_paths,
|
|
104
191
|
:condition,
|
|
105
192
|
:query_schema,
|
|
193
|
+
:mount,
|
|
106
194
|
keyword_init: true
|
|
107
195
|
) do
|
|
108
196
|
def initialize(**kwargs)
|
|
@@ -112,6 +200,7 @@ module Aspera
|
|
|
112
200
|
a.is_a?(Hash) ? ArgumentSpec.new(**a) : a
|
|
113
201
|
end
|
|
114
202
|
end
|
|
203
|
+
kwargs[:mount] = MountSpec.new(**kwargs[:mount]) if kwargs[:mount].is_a?(Hash)
|
|
115
204
|
super
|
|
116
205
|
end
|
|
117
206
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Bash completion for ascli
|
|
2
|
+
# Activate: eval "$(ascli config completion bash)"
|
|
3
|
+
|
|
4
|
+
_ascli_complete() {
|
|
5
|
+
local cur="${COMP_WORDS[COMP_CWORD]}"
|
|
6
|
+
# COMP_WORDS[0] is the program name itself; pass only the words typed before the cursor
|
|
7
|
+
local candidates
|
|
8
|
+
candidates=$(ascli config completion words "${COMP_WORDS[@]:1:COMP_CWORD-1}" 2>/dev/null)
|
|
9
|
+
COMPREPLY=( $(compgen -W "${candidates}" -- "${cur}") )
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
complete -F _ascli_complete ascli
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Fish completion for ascli
|
|
2
|
+
# Activate: ascli config completion fish | source
|
|
3
|
+
# Or save as file ~/.config/fish/completions/ascli.fish
|
|
4
|
+
|
|
5
|
+
# Remove completions of a previous activation
|
|
6
|
+
complete --command ascli --erase
|
|
7
|
+
|
|
8
|
+
# Disable file completion entirely — ascli manages its own argument tree
|
|
9
|
+
complete --command ascli --no-files
|
|
10
|
+
|
|
11
|
+
# Dynamic completion: pass all words already typed (excluding 'ascli' itself)
|
|
12
|
+
# to `ascli config completion words` and return the result as completions.
|
|
13
|
+
complete --command ascli --arguments '(
|
|
14
|
+
set -l words (commandline -opc)
|
|
15
|
+
ascli config completion words $words[2..] 2>/dev/null
|
|
16
|
+
)'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#compdef ascli
|
|
2
|
+
# Zsh completion for ascli
|
|
3
|
+
# Activate (after compinit): eval "$(ascli config completion zsh)"
|
|
4
|
+
# Or save as file `_ascli` in a folder of $fpath
|
|
5
|
+
|
|
6
|
+
_ascli() {
|
|
7
|
+
local -a candidates
|
|
8
|
+
# words[1] is 'ascli' itself; pass only the words typed before the cursor
|
|
9
|
+
candidates=(${(f)"$(ascli config completion words "${(@)words[2,CURRENT-1]}" 2>/dev/null)"})
|
|
10
|
+
compadd -a candidates
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if [[ "${funcstack[1]}" == _ascli ]]; then
|
|
14
|
+
# Autoloaded from $fpath: this file is the body of function _ascli
|
|
15
|
+
_ascli "$@"
|
|
16
|
+
else
|
|
17
|
+
# Evaluated or sourced
|
|
18
|
+
compdef _ascli ascli
|
|
19
|
+
fi
|
data/lib/aspera/cli/context.rb
CHANGED
|
@@ -36,6 +36,8 @@ module Aspera
|
|
|
36
36
|
attr_accessor :progress_bar
|
|
37
37
|
# Optional: nil when no PAC script is configured
|
|
38
38
|
attr_accessor :pac_executor
|
|
39
|
+
# `true` when help is requested (`-h`)
|
|
40
|
+
attr_accessor :help_requested
|
|
39
41
|
|
|
40
42
|
# Initialize all members to nil, so that they are defined and can be validated later
|
|
41
43
|
# @return [nil]
|
|
@@ -43,6 +45,7 @@ module Aspera
|
|
|
43
45
|
MEMBERS.each { |i| instance_variable_set(:"@#{i}", nil) }
|
|
44
46
|
@progress_bar = nil
|
|
45
47
|
@pac_executor = nil
|
|
48
|
+
@help_requested = false
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
# Validate that all mandatory members are non-nil (detect bootstrap bugs)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/cli/version'
|
|
4
|
+
require 'aspera/assert'
|
|
5
|
+
|
|
6
|
+
module Aspera
|
|
7
|
+
module Cli
|
|
8
|
+
# Deprecation of a feature (e.g. an option).
|
|
9
|
+
# @!attribute last [String] Last released version supporting the feature without deprecation, e.g. `4.25.0`
|
|
10
|
+
# @!attribute message [String] What to use instead, e.g. `use --out.level`
|
|
11
|
+
Deprecation = Struct.new(:last, :message, keyword_init: true) do
|
|
12
|
+
def initialize(**kwargs)
|
|
13
|
+
super
|
|
14
|
+
Aspera.assert_type(last, String) { 'deprecation last' }
|
|
15
|
+
Aspera.assert(Gem::Version.correct?(last) && Gem::Version.new(last) < Gem::Version.new(VERSION)) do
|
|
16
|
+
"deprecation: last (#{last}) must be a released version, before #{VERSION}"
|
|
17
|
+
end
|
|
18
|
+
Aspera.assert_type(message, String) { 'deprecation message' }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [String] e.g. `deprecated after 4.25.0: use --out.level`
|
|
22
|
+
def to_s = "deprecated after #{last}: #{message}"
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# @param value [Deprecation, Hash, nil] Deprecation, or its attributes: `{last:, message:}`
|
|
26
|
+
# @return [Deprecation, nil]
|
|
27
|
+
def create(value)
|
|
28
|
+
case value
|
|
29
|
+
when nil, Deprecation then value
|
|
30
|
+
when Hash then new(**value)
|
|
31
|
+
else Aspera.error_unexpected_value(value.class) { 'deprecation, expect Hash {last:, message:}' }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -12,6 +12,8 @@ require 'base64'
|
|
|
12
12
|
require 'zlib'
|
|
13
13
|
require 'csv'
|
|
14
14
|
require 'singleton'
|
|
15
|
+
require 'aspera/rainbow'
|
|
16
|
+
using Rainbow
|
|
15
17
|
|
|
16
18
|
module Aspera
|
|
17
19
|
module Cli
|
|
@@ -75,7 +77,7 @@ module Aspera
|
|
|
75
77
|
case mode
|
|
76
78
|
when '' then $stdin.read
|
|
77
79
|
when 'bin' then $stdin.binmode.read
|
|
78
|
-
when 'chomp' then $stdin.chomp
|
|
80
|
+
when 'chomp' then $stdin.read.chomp
|
|
79
81
|
else raise BadArgument, "`stdin` supports only: '', 'bin' or 'chomp'"
|
|
80
82
|
end
|
|
81
83
|
end
|
|
@@ -116,10 +118,11 @@ module Aspera
|
|
|
116
118
|
end
|
|
117
119
|
|
|
118
120
|
# Update the Regex to match an extended value based on @handlers
|
|
121
|
+
# Anchored on the whole value (`\A`, `\z`), not on lines: a multi-line value is extended only if it starts with a modifier.
|
|
119
122
|
def update_regex
|
|
120
123
|
handler_regex = "#{MARKER_START}(#{modifiers.join('|')})#{MARKER_END}"
|
|
121
|
-
@regex_single = Regexp.new("
|
|
122
|
-
@regex_extend = Regexp.new("
|
|
124
|
+
@regex_single = Regexp.new("\\A#{handler_regex}(.*)\\z", Regexp::MULTILINE)
|
|
125
|
+
@regex_extend = Regexp.new("\\A(.*)#{handler_regex}([^#{MARKER_IN_END}]*)#{MARKER_IN_END}(.*)\\z", Regexp::MULTILINE)
|
|
123
126
|
end
|
|
124
127
|
|
|
125
128
|
public
|