MuranoCLI 3.2.0.beta.1 → 3.2.0.beta.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -1
- data/.trustme.plugin +137 -0
- data/.trustme.sh +217 -117
- data/.trustme.vim +9 -3
- data/Gemfile +9 -3
- data/MuranoCLI.gemspec +8 -5
- data/Rakefile +1 -0
- data/dockers/Dockerfile.2.2.9 +6 -3
- data/dockers/Dockerfile.2.3.6 +6 -3
- data/dockers/Dockerfile.2.4.3 +6 -3
- data/dockers/Dockerfile.2.5.0 +6 -3
- data/dockers/Dockerfile.GemRelease +10 -8
- data/dockers/Dockerfile.m4 +23 -5
- data/dockers/docker-test.sh +65 -28
- data/docs/completions/murano_completion-bash +751 -57
- data/docs/develop.rst +10 -9
- data/lib/MrMurano/AccountBase.rb +95 -6
- data/lib/MrMurano/Commander-Entry.rb +9 -4
- data/lib/MrMurano/Config-Migrate.rb +2 -0
- data/lib/MrMurano/Config.rb +94 -26
- data/lib/MrMurano/Content.rb +1 -1
- data/lib/MrMurano/Exchange.rb +77 -42
- data/lib/MrMurano/Gateway.rb +1 -1
- data/lib/MrMurano/HttpAuthed.rb +20 -7
- data/lib/MrMurano/Logs.rb +10 -1
- data/lib/MrMurano/ProjectFile.rb +1 -1
- data/lib/MrMurano/ReCommander.rb +129 -73
- data/lib/MrMurano/Solution-ServiceConfig.rb +18 -11
- data/lib/MrMurano/Solution-Services.rb +78 -50
- data/lib/MrMurano/Solution-Users.rb +1 -1
- data/lib/MrMurano/Solution.rb +13 -63
- data/lib/MrMurano/SyncUpDown-Core.rb +185 -77
- data/lib/MrMurano/SyncUpDown-Item.rb +29 -4
- data/lib/MrMurano/SyncUpDown.rb +11 -11
- data/lib/MrMurano/Webservice-Cors.rb +1 -1
- data/lib/MrMurano/Webservice-Endpoint.rb +28 -17
- data/lib/MrMurano/Webservice-File.rb +103 -43
- data/lib/MrMurano/commands/domain.rb +1 -0
- data/lib/MrMurano/commands/element.rb +585 -0
- data/lib/MrMurano/commands/exchange.rb +211 -204
- data/lib/MrMurano/commands/gb.rb +1 -0
- data/lib/MrMurano/commands/globals.rb +17 -7
- data/lib/MrMurano/commands/init.rb +115 -101
- data/lib/MrMurano/commands/keystore.rb +1 -1
- data/lib/MrMurano/commands/logs.rb +2 -1
- data/lib/MrMurano/commands/postgresql.rb +17 -7
- data/lib/MrMurano/commands/service.rb +572 -0
- data/lib/MrMurano/commands/show.rb +7 -3
- data/lib/MrMurano/commands/solution.rb +2 -1
- data/lib/MrMurano/commands/solution_picker.rb +31 -15
- data/lib/MrMurano/commands/status.rb +205 -169
- data/lib/MrMurano/commands/sync.rb +70 -38
- data/lib/MrMurano/commands/token.rb +59 -14
- data/lib/MrMurano/commands/usage.rb +1 -0
- data/lib/MrMurano/commands.rb +2 -0
- data/lib/MrMurano/hash.rb +91 -0
- data/lib/MrMurano/http.rb +55 -6
- data/lib/MrMurano/makePretty.rb +47 -0
- data/lib/MrMurano/optparse.rb +60 -45
- data/lib/MrMurano/variegated/TruthyFalsey.rb +48 -0
- data/lib/MrMurano/variegated/ruby_dig.rb +64 -0
- data/lib/MrMurano/verbosing.rb +113 -3
- data/lib/MrMurano/version.rb +1 -1
- data/spec/Account_spec.rb +34 -20
- data/spec/Business_spec.rb +12 -9
- data/spec/Config_spec.rb +7 -1
- data/spec/Content_spec.rb +17 -1
- data/spec/GatewayBase_spec.rb +5 -2
- data/spec/GatewayDevice_spec.rb +4 -2
- data/spec/GatewayResource_spec.rb +4 -1
- data/spec/GatewaySettings_spec.rb +4 -1
- data/spec/HttpAuthed_spec.rb +73 -0
- data/spec/Http_spec.rb +32 -35
- data/spec/ProjectFile_spec.rb +1 -1
- data/spec/Solution-ServiceConfig_spec.rb +4 -1
- data/spec/Solution-ServiceEventHandler_spec.rb +6 -3
- data/spec/Solution-ServiceModules_spec.rb +4 -1
- data/spec/Solution-UsersRoles_spec.rb +4 -1
- data/spec/Solution_spec.rb +4 -1
- data/spec/SyncUpDown_spec.rb +1 -1
- data/spec/Webservice-Cors_spec.rb +4 -1
- data/spec/Webservice-Endpoint_spec.rb +9 -6
- data/spec/Webservice-File_spec.rb +17 -4
- data/spec/Webservice-Setting_spec.rb +6 -2
- data/spec/_workspace.rb +2 -0
- data/spec/cmd_common.rb +42 -13
- data/spec/cmd_content_spec.rb +17 -7
- data/spec/cmd_device_spec.rb +1 -1
- data/spec/cmd_domain_spec.rb +2 -2
- data/spec/cmd_element_spec.rb +400 -0
- data/spec/cmd_exchange_spec.rb +2 -2
- data/spec/cmd_init_spec.rb +59 -25
- data/spec/cmd_keystore_spec.rb +6 -3
- data/spec/cmd_link_spec.rb +10 -5
- data/spec/cmd_logs_spec.rb +1 -1
- data/spec/cmd_setting_application_spec.rb +18 -15
- data/spec/cmd_setting_product_spec.rb +7 -7
- data/spec/cmd_status_spec.rb +27 -17
- data/spec/cmd_syncdown_application_spec.rb +30 -3
- data/spec/cmd_syncdown_both_spec.rb +72 -18
- data/spec/cmd_syncup_spec.rb +71 -5
- data/spec/cmd_token_spec.rb +2 -2
- data/spec/cmd_usage_spec.rb +2 -2
- data/spec/dry_run_formatter.rb +27 -0
- data/spec/fixtures/dumped_config +8 -0
- data/spec/fixtures/exchange_element/element-show.json +1 -0
- data/spec/fixtures/exchange_element/swagger-mur-6407__10k.yaml +282 -0
- data/spec/fixtures/exchange_element/swagger-mur-6407__20k.yaml +588 -0
- data/spec/variegated_TruthyFalsey_spec.rb +29 -0
- metadata +51 -25
@@ -24,7 +24,12 @@ def args_parse_user_host_pair(args)
|
|
24
24
|
[username, host]
|
25
25
|
end
|
26
26
|
|
27
|
-
def
|
27
|
+
def args_sole_token_maybe(args)
|
28
|
+
return unless args.length == 1
|
29
|
+
args.shift if MrMurano::HttpAuthed.instance.token_looks_legit(args[0])
|
30
|
+
end
|
31
|
+
|
32
|
+
def args_opts_parse_token_maybe(args, options, must: false)
|
28
33
|
n_specified = 0
|
29
34
|
token = args.shift
|
30
35
|
n_specified += 1 unless token.nil?
|
@@ -43,10 +48,9 @@ def args_opts_parse_token!(args, options)
|
|
43
48
|
if n_specified > 1
|
44
49
|
MrMurano::Verbose.error(%(Please specify only one token, --token, or --from-env))
|
45
50
|
exit 1
|
46
|
-
elsif n_specified == 0
|
47
|
-
token = ask('Token: ')
|
48
51
|
end
|
49
|
-
if token.to_s.empty?
|
52
|
+
token = nil if token.to_s.empty?
|
53
|
+
if must && token.nil?
|
50
54
|
MrMurano::Verbose.error(%(Please specify a token!))
|
51
55
|
unless ENV['MURANO_TOKEN'].to_s.empty?
|
52
56
|
MrMurano::Verbose.warning(
|
@@ -58,6 +62,20 @@ def args_opts_parse_token!(args, options)
|
|
58
62
|
token
|
59
63
|
end
|
60
64
|
|
65
|
+
def args_opts_parse_token(args, options)
|
66
|
+
args_opts_parse_token_maybe(args, options, must: false)
|
67
|
+
end
|
68
|
+
|
69
|
+
def args_opts_parse_token!(args, options)
|
70
|
+
args_opts_parse_token_maybe(args, options, must: true)
|
71
|
+
end
|
72
|
+
|
73
|
+
def cmd_option_token_options(c)
|
74
|
+
# We cannot use --token, because it's already a global_option.
|
75
|
+
c.option '--value TOKEN', String, %(The token to use)
|
76
|
+
c.option '--from-env', %(Use token in MURANO_TOKEN)
|
77
|
+
end
|
78
|
+
|
61
79
|
command :token do |c|
|
62
80
|
c.syntax = %(murano token)
|
63
81
|
c.summary = %(About token commands)
|
@@ -164,20 +182,20 @@ end
|
|
164
182
|
alias_command 'token fetch', 'token get'
|
165
183
|
|
166
184
|
command 'token set' do |c|
|
167
|
-
c.syntax = %(murano token set [<username>] [<host>] [<token>])
|
185
|
+
c.syntax = %(murano token set [[[<username>] [<host>] [<token>]] | [<token>]])
|
168
186
|
c.summary = %(Set token for username and host)
|
169
187
|
c.description = %(
|
170
188
|
Set token for username and host.
|
171
189
|
).strip
|
172
|
-
|
173
|
-
c.option '--value TOKEN', String, %(The token to use)
|
174
|
-
c.option '--from-env', %(Use token in MURANO_TOKEN)
|
190
|
+
cmd_option_token_options(c)
|
175
191
|
c.project_not_required = true
|
176
192
|
|
177
193
|
c.action do |args, options|
|
178
194
|
c.verify_arg_count!(args, 3)
|
195
|
+
# If there's just one arg, might be the token (otherwise it's the username).
|
196
|
+
token = args_sole_token_maybe(args)
|
179
197
|
username, host = args_parse_user_host_pair(args)
|
180
|
-
token = args_opts_parse_token!(args, options)
|
198
|
+
token = args_opts_parse_token!(args, options) if token.nil?
|
181
199
|
token_file = MrMurano::Tokens.new
|
182
200
|
token_file.load
|
183
201
|
token_file.set(host, username, token)
|
@@ -186,24 +204,51 @@ command 'token set' do |c|
|
|
186
204
|
end
|
187
205
|
|
188
206
|
command 'token delete' do |c|
|
189
|
-
c.syntax = %(murano token delete [<username>] [<host>])
|
207
|
+
c.syntax = %(murano token delete [[[<username>] [<host>]] | [<token>]])
|
190
208
|
c.summary = %(Invalidate and delete token for username and host)
|
191
209
|
c.description = %(
|
192
210
|
Invalidate and delete token for username and host.
|
193
211
|
).strip
|
194
212
|
c.option '--no-invalidate', %(Do not also invalidate token on server)
|
213
|
+
cmd_option_token_options(c)
|
195
214
|
c.project_not_required = true
|
196
215
|
|
216
|
+
# Note: We confirm deleting passwords, but not throwaway tokens.
|
197
217
|
c.action do |args, options|
|
198
218
|
c.verify_arg_count!(args, 2)
|
199
|
-
|
200
|
-
|
219
|
+
clear_cache = false
|
220
|
+
# If there's just one arg, might be the token (otherwise it's the username).
|
221
|
+
token = args_sole_token_maybe(args)
|
222
|
+
if token.nil?
|
223
|
+
n_args = args.length
|
224
|
+
# First see if the user specified a username and/or host.
|
225
|
+
username, host = args_parse_user_host_pair(args)
|
226
|
+
# See if the user used --value or --from-env.
|
227
|
+
alt_token = args_opts_parse_token(args, options)
|
228
|
+
# If the user specified a user or host, they should not specify the
|
229
|
+
# token (user and host are used to lookup in the tokens file).
|
230
|
+
if !alt_token.nil?
|
231
|
+
if n_args > 0
|
232
|
+
MrMurano::Verbose.error %(
|
233
|
+
Please specify either token, or user and host, but not both
|
234
|
+
).strip
|
235
|
+
break
|
236
|
+
end
|
237
|
+
token = alt_token
|
238
|
+
else
|
239
|
+
token = MrMurano::HttpAuthed.instance.token_from_file(host, username)
|
240
|
+
clear_cache = true
|
241
|
+
end
|
242
|
+
end
|
243
|
+
if token.nil?
|
244
|
+
MrMurano::Verbose.warning %(No token specified or none found for user)
|
245
|
+
break
|
246
|
+
end
|
201
247
|
# We can create an authless instance, which won't ask user for
|
202
248
|
# login creds, because the delete command does not need creds.
|
203
249
|
acc = MrMurano::Account.new(authless: true)
|
204
|
-
token = MrMurano::HttpAuthed.instance.token_from_file(host, username)
|
205
250
|
acc.invalidate_token(token) unless options.no_invalidate
|
206
|
-
MrMurano::HttpAuthed.instance.token_remove(host, username)
|
251
|
+
MrMurano::HttpAuthed.instance.token_remove(host, username) if clear_cache
|
207
252
|
end
|
208
253
|
end
|
209
254
|
|
data/lib/MrMurano/commands.rb
CHANGED
@@ -17,6 +17,7 @@ require 'MrMurano/commands/content'
|
|
17
17
|
require 'MrMurano/commands/cors'
|
18
18
|
require 'MrMurano/commands/devices'
|
19
19
|
require 'MrMurano/commands/domain'
|
20
|
+
require 'MrMurano/commands/element'
|
20
21
|
require 'MrMurano/commands/exchange'
|
21
22
|
require 'MrMurano/commands/globals'
|
22
23
|
require 'MrMurano/commands/keystore'
|
@@ -28,6 +29,7 @@ require 'MrMurano/commands/mock'
|
|
28
29
|
require 'MrMurano/commands/postgresql'
|
29
30
|
require 'MrMurano/commands/password'
|
30
31
|
require 'MrMurano/commands/settings'
|
32
|
+
require 'MrMurano/commands/service'
|
31
33
|
require 'MrMurano/commands/show'
|
32
34
|
require 'MrMurano/commands/solution'
|
33
35
|
require 'MrMurano/commands/status'
|
data/lib/MrMurano/hash.rb
CHANGED
@@ -42,6 +42,97 @@ class Hash
|
|
42
42
|
end
|
43
43
|
self
|
44
44
|
end
|
45
|
+
|
46
|
+
# h = { :a => { :b => { :c => 1, :d => 2 }, :e => 3 }, :f => 4 }
|
47
|
+
# flat_hash(h) #=> {[:a, :b, :c]=>1, [:a, :b, :d]=>2, [:a, :e]=>3, [:f]=>4}
|
48
|
+
def self.flat_hash(hash, long_key=[], flat={})
|
49
|
+
return flat.update(long_key => hash) unless hash.is_a? Hash
|
50
|
+
hash.each { |key, val| flat_hash(val, long_key + [key], flat) }
|
51
|
+
flat
|
52
|
+
end
|
53
|
+
|
54
|
+
# Get all hash and nested hash keys.
|
55
|
+
def self.nested_keys(obj)
|
56
|
+
obj.each_with_object([]) do |(key, val), keys|
|
57
|
+
keys << key
|
58
|
+
keys.concat(nested_keys(val)) if val.is_a? Hash
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# deep_symbolize_keys is from rails:
|
63
|
+
#
|
64
|
+
# https://github.com/rails/rails/blob/f213e926892020f9ab6c8974612c59e2ba959253/
|
65
|
+
# activesupport/lib/active_support/core_ext/hash/keys.rb
|
66
|
+
|
67
|
+
# Returns a new hash with all keys converted by the block operation.
|
68
|
+
# This includes the keys from the root hash and from all
|
69
|
+
# nested hashes and arrays.
|
70
|
+
#
|
71
|
+
# hash = { person: { name: 'Rob', age: '28' } }
|
72
|
+
#
|
73
|
+
# hash.deep_transform_keys{ |key| key.to_s.upcase }
|
74
|
+
# # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
|
75
|
+
def deep_transform_keys(&block)
|
76
|
+
_deep_transform_keys_in_object(self, &block)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Destructively converts all keys by using the block operation.
|
80
|
+
# This includes the keys from the root hash and from all
|
81
|
+
# nested hashes and arrays.
|
82
|
+
def deep_transform_keys!(&block)
|
83
|
+
_deep_transform_keys_in_object!(self, &block)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns a new hash with all keys converted to symbols, as long as
|
87
|
+
# they respond to +to_sym+. This includes the keys from the root hash
|
88
|
+
# and from all nested hashes and arrays.
|
89
|
+
#
|
90
|
+
# hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
|
91
|
+
#
|
92
|
+
# hash.deep_symbolize_keys
|
93
|
+
# # => {:person=>{:name=>"Rob", :age=>"28"}}
|
94
|
+
def deep_symbolize_keys
|
95
|
+
# rubocop:disable Style/RescueModifier
|
96
|
+
deep_transform_keys { |key| key.to_sym rescue key }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Destructively converts all keys to symbols, as long as they respond
|
100
|
+
# to +to_sym+. This includes the keys from the root hash and from all
|
101
|
+
# nested hashes and arrays.
|
102
|
+
def deep_symbolize_keys!
|
103
|
+
deep_transform_keys! { |key| key.to_sym rescue key }
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
# support methods for deep transforming nested hashes and arrays
|
109
|
+
def _deep_transform_keys_in_object(object, &block)
|
110
|
+
case object
|
111
|
+
when Hash
|
112
|
+
object.each_with_object({}) do |(key, value), result|
|
113
|
+
result[yield(key)] = _deep_transform_keys_in_object(value, &block)
|
114
|
+
end
|
115
|
+
when Array
|
116
|
+
object.map { |e| _deep_transform_keys_in_object(e, &block) }
|
117
|
+
else
|
118
|
+
object
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def _deep_transform_keys_in_object!(object, &block)
|
123
|
+
case object
|
124
|
+
when Hash
|
125
|
+
object.keys.each do |key|
|
126
|
+
value = object.delete(key)
|
127
|
+
object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
|
128
|
+
end
|
129
|
+
object
|
130
|
+
when Array
|
131
|
+
object.map! { |e| _deep_transform_keys_in_object!(e, &block) }
|
132
|
+
else
|
133
|
+
object
|
134
|
+
end
|
135
|
+
end
|
45
136
|
end
|
46
137
|
|
47
138
|
def ordered_hash(dict)
|
data/lib/MrMurano/http.rb
CHANGED
@@ -62,25 +62,72 @@ module MrMurano
|
|
62
62
|
)
|
63
63
|
ccmd << %(-F #{m[:name]}=@#{m[:filename]}) unless m.nil?
|
64
64
|
else
|
65
|
-
|
65
|
+
req_body = request.body
|
66
|
+
unless $cfg['tool.show-password']
|
67
|
+
req_body = req_body.gsub(
|
68
|
+
/"password":(["'])(?:(?=(\\?))\2.)*?\1/, '"password":"<redacted>"'
|
69
|
+
)
|
70
|
+
end
|
71
|
+
ccmd << %(-d '#{req_body}')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
MrMurano::Http.curldebug_log(ccmd.join(' '), stamp_it: true)
|
76
|
+
end
|
77
|
+
|
78
|
+
# (lb): Ideally, we'd refactor and these wouldn't be static class methods.
|
79
|
+
def self.curldebug_after(request, response)
|
80
|
+
@http_complete = [] unless defined? @http_complete
|
81
|
+
@http_complete.push([request, response])
|
82
|
+
return response unless $cfg['tool.curldebug'] && $cfg['tool.curlfancy']
|
83
|
+
MrMurano::Http.curldebug_elapsed
|
84
|
+
if response.nil?
|
85
|
+
formatted = '<nil>'
|
86
|
+
else
|
87
|
+
begin
|
88
|
+
formatted = "#{response.code} / #{response.body}"
|
89
|
+
rescue StandardError
|
90
|
+
formatted = response.to_s
|
66
91
|
end
|
67
92
|
end
|
68
|
-
|
93
|
+
formatted = "Response: #{formatted}"
|
94
|
+
MrMurano::Http.curldebug_log(formatted)
|
95
|
+
response
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.curldebug_elapsed
|
99
|
+
http_recv_at = Time.now
|
100
|
+
if !@http_sent_at.nil?
|
101
|
+
formatted = "ElapsedT: #{http_recv_at - @http_sent_at}"
|
102
|
+
else
|
103
|
+
formatted = 'ElapsedT: Overlapping command clobbered time.'
|
104
|
+
MrMurano::Verbose.warning(formatted) if $cfg['tool.debug']
|
105
|
+
end
|
106
|
+
MrMurano::Http.curldebug_log(formatted)
|
107
|
+
@http_sent_at = nil
|
69
108
|
end
|
70
109
|
|
71
|
-
def self.curldebug_log(
|
110
|
+
def self.curldebug_log(formatted, stamp_it: false)
|
72
111
|
if $cfg.curlfile_f.nil?
|
73
|
-
MrMurano::Progress.instance.whirly_interject { puts
|
112
|
+
MrMurano::Progress.instance.whirly_interject { puts formatted }
|
74
113
|
else
|
75
|
-
$cfg.curlfile_f <<
|
114
|
+
$cfg.curlfile_f << formatted + "\n\n"
|
76
115
|
$cfg.curlfile_f.flush
|
77
116
|
end
|
117
|
+
return unless stamp_it
|
118
|
+
@http_outstanding = [] unless defined? @http_outstanding
|
119
|
+
@http_outstanding.push(formatted)
|
120
|
+
@http_sent_at = Time.now
|
78
121
|
end
|
79
122
|
|
80
123
|
def http
|
81
124
|
uri = URI($cfg['net.protocol'] + '://' + $cfg['net.host'])
|
82
125
|
if !defined?(@http) || @http.nil?
|
83
126
|
@http = Net::HTTP.new(uri.host, uri.port)
|
127
|
+
# (lb): The HTTP timeouts default to 60 secs. But requests
|
128
|
+
# sometimes timeout, so experimenting with longer timeouts.
|
129
|
+
@http.open_timeout = 90 # default: 60
|
130
|
+
@http.read_timeout = 90 # default: 60
|
84
131
|
@http.use_ssl = true if $cfg['net.protocol'] == 'https'
|
85
132
|
begin
|
86
133
|
@http.start
|
@@ -148,9 +195,11 @@ module MrMurano
|
|
148
195
|
def workit(request)
|
149
196
|
curldebug(request)
|
150
197
|
if block_given?
|
151
|
-
yield request, http
|
198
|
+
response = yield request, http
|
199
|
+
MrMurano::Http.curldebug_after(request, response)
|
152
200
|
else
|
153
201
|
response = http.request(request)
|
202
|
+
MrMurano::Http.curldebug_after(request, response)
|
154
203
|
case response
|
155
204
|
when Net::HTTPSuccess
|
156
205
|
workit_response(response)
|
data/lib/MrMurano/makePretty.rb
CHANGED
@@ -307,6 +307,53 @@ module MrMurano
|
|
307
307
|
def self.body_prefix(options)
|
308
308
|
options.indent && ' ' || ''
|
309
309
|
end
|
310
|
+
|
311
|
+
def self.width_last_column(headers, elems)
|
312
|
+
return nil unless $stdout.tty?
|
313
|
+
# Calculate how much room (how many characters) are left for the
|
314
|
+
# description column.
|
315
|
+
width_taken = 0
|
316
|
+
# rubocop:disable Performance/FixedSize
|
317
|
+
# "Do not compute the size of statically sized objects."
|
318
|
+
width_taken += '| '.length
|
319
|
+
# Calculate the width of each column except the last (:description).
|
320
|
+
headers[0..-2].each do |key|
|
321
|
+
elem_with_max = elems.max { |a, b| a.send(key).length <=> b.send(key).length }
|
322
|
+
width_taken += elem_with_max.send(key).length unless elem_with_max.nil?
|
323
|
+
width_taken += ' | '.length
|
324
|
+
end
|
325
|
+
width_taken += ' | '.length
|
326
|
+
term_width, _rows = HighLine::SystemExtensions.terminal_size
|
327
|
+
width_avail = term_width - width_taken
|
328
|
+
# MAGIC_NUMBER: Tweak/change this if you want. 20 char min feels
|
329
|
+
# about right: don't wrap if column would be narrow or negative.
|
330
|
+
return nil if width_avail < 20
|
331
|
+
width_avail
|
332
|
+
end
|
333
|
+
|
334
|
+
def self.split_text_on_whitespace(full, width_avail)
|
335
|
+
# The easiest method is to split at the width, and not on closest whitespace:
|
336
|
+
# elem.meta[key].scan(/.{1,#{width_avail}}/).join("\n")
|
337
|
+
# We'll instead split on whitespace.
|
338
|
+
parts = []
|
339
|
+
split_posit = width_avail - 1
|
340
|
+
until full.empty?
|
341
|
+
# Split the description on a space before the max width.
|
342
|
+
part = full[0..split_posit]
|
343
|
+
full = full[(split_posit + 1)..-1] || ''
|
344
|
+
leftover = ''
|
345
|
+
part, _space, leftover = part.rpartition(' ') unless full.empty?
|
346
|
+
if part.empty?
|
347
|
+
part = leftover.to_s
|
348
|
+
leftover = ''
|
349
|
+
else
|
350
|
+
full = leftover.to_s + full
|
351
|
+
full = full
|
352
|
+
end
|
353
|
+
parts.push(part.strip)
|
354
|
+
end
|
355
|
+
parts.join("\n")
|
356
|
+
end
|
310
357
|
end
|
311
358
|
end
|
312
359
|
|
data/lib/MrMurano/optparse.rb
CHANGED
@@ -1559,58 +1559,73 @@ XXX
|
|
1559
1559
|
def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc:
|
1560
1560
|
opt, arg, val, rest = nil
|
1561
1561
|
nonopt ||= proc {|a| throw :terminate, a}
|
1562
|
+
end_of_options = false
|
1563
|
+
|
1562
1564
|
argv.unshift(arg) if arg = catch(:terminate) {
|
1563
1565
|
while arg = argv.shift
|
1564
|
-
|
1565
|
-
|
1566
|
-
|
1567
|
-
|
1568
|
-
|
1569
|
-
|
1570
|
-
|
1571
|
-
|
1572
|
-
|
1573
|
-
|
1574
|
-
|
1575
|
-
|
1576
|
-
|
1577
|
-
|
1578
|
-
|
1579
|
-
|
1566
|
+
is_nonopt = end_of_options
|
1567
|
+
|
1568
|
+
if !end_of_options
|
1569
|
+
case arg
|
1570
|
+
|
1571
|
+
# end of options
|
1572
|
+
when '--'
|
1573
|
+
end_of_options = true
|
1574
|
+
|
1575
|
+
# long option
|
1576
|
+
when /\A--([^=]*)(?:=(.*))?/m
|
1577
|
+
opt, rest = $1, $2
|
1578
|
+
begin
|
1579
|
+
sw, = complete(:long, opt, true)
|
1580
|
+
rescue ParseError
|
1581
|
+
raise $!.set_option(arg, true)
|
1582
|
+
end
|
1583
|
+
begin
|
1584
|
+
opt, cb, val = sw.parse(rest, argv) {|*exc| raise(*exc)}
|
1585
|
+
val = cb.call(val) if cb
|
1586
|
+
setter.call(sw.switch_name, val) if setter
|
1587
|
+
rescue ParseError
|
1588
|
+
raise $!.set_option(arg, rest)
|
1589
|
+
end
|
1580
1590
|
|
1581
|
-
|
1582
|
-
|
1583
|
-
|
1584
|
-
|
1585
|
-
|
1586
|
-
|
1587
|
-
|
1588
|
-
|
1589
|
-
|
1590
|
-
|
1591
|
-
|
1592
|
-
|
1593
|
-
|
1594
|
-
|
1595
|
-
|
1596
|
-
|
1591
|
+
# short option
|
1592
|
+
when /\A-(.)((=).*|.+)?/m
|
1593
|
+
opt, has_arg, eq, val, rest = $1, $3, $3, $2, $2
|
1594
|
+
begin
|
1595
|
+
sw, = search(:short, opt)
|
1596
|
+
unless sw
|
1597
|
+
begin
|
1598
|
+
sw, = complete(:short, opt)
|
1599
|
+
# short option matched.
|
1600
|
+
val = arg.sub(/\A-/, '')
|
1601
|
+
has_arg = true
|
1602
|
+
rescue InvalidOption
|
1603
|
+
# if no short options match, try completion with long
|
1604
|
+
# options.
|
1605
|
+
sw, = complete(:long, opt)
|
1606
|
+
eq ||= !rest
|
1607
|
+
end
|
1597
1608
|
end
|
1609
|
+
rescue ParseError
|
1610
|
+
raise $!.set_option(arg, true)
|
1598
1611
|
end
|
1599
|
-
|
1600
|
-
|
1601
|
-
|
1602
|
-
|
1603
|
-
|
1604
|
-
|
1605
|
-
|
1606
|
-
|
1607
|
-
|
1608
|
-
|
1609
|
-
|
1612
|
+
begin
|
1613
|
+
opt, cb, val = sw.parse(val, argv) {|*exc| raise(*exc) if eq}
|
1614
|
+
raise InvalidOption, arg if has_arg and !eq and arg == "-#{opt}"
|
1615
|
+
argv.unshift(opt) if opt and (!rest or (opt = opt.sub(/\A-*/, '-')) != '-')
|
1616
|
+
val = cb.call(val) if cb
|
1617
|
+
setter.call(sw.switch_name, val) if setter
|
1618
|
+
rescue ParseError
|
1619
|
+
raise $!.set_option(arg, arg.length > 2)
|
1620
|
+
end
|
1621
|
+
|
1622
|
+
# non-option argument
|
1623
|
+
else
|
1624
|
+
is_nonopt = true
|
1610
1625
|
end
|
1626
|
+
end
|
1611
1627
|
|
1612
|
-
|
1613
|
-
else
|
1628
|
+
if is_nonopt
|
1614
1629
|
catch(:prune) do
|
1615
1630
|
visit(:each_option) do |sw0|
|
1616
1631
|
sw = sw0
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Copyright © 2016-2017 Exosite LLC. All Rights Reserved
|
2
|
+
# License: PROPRIETARY. See LICENSE.txt.
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
# vim:tw=0:ts=2:sw=2:et:ai
|
6
|
+
# Unauthorized copying of this file is strictly prohibited.
|
7
|
+
|
8
|
+
class TruthyFalsey
|
9
|
+
attr_reader :rejoinder
|
10
|
+
|
11
|
+
def initialize(rejoinder)
|
12
|
+
@rejoinder = rejoinder
|
13
|
+
end
|
14
|
+
|
15
|
+
TRUTHY_WORDS = %w[1 on t true y yes +].freeze
|
16
|
+
|
17
|
+
FALSEY_WORDS = ['0', 'off', 'f', 'false', 'n', 'no', '-', ''].freeze
|
18
|
+
|
19
|
+
def truthy?
|
20
|
+
TRUTHY_WORDS.include? @rejoinder.to_s.downcase
|
21
|
+
end
|
22
|
+
|
23
|
+
def falsey?
|
24
|
+
FALSEY_WORDS.include? @rejoinder.to_s.downcase
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.coerce_if_boolean(value, reference)
|
28
|
+
# Ruby doesn't have a Boolean type, just TrueClass and FalseClass.
|
29
|
+
return value unless [true, false].include? reference
|
30
|
+
tfy = TruthyFalsey.new(value)
|
31
|
+
if tfy.truthy?
|
32
|
+
true
|
33
|
+
elsif tfy.falsey?
|
34
|
+
false
|
35
|
+
else
|
36
|
+
value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.coerce_boolean(value, reference, default: 'true')
|
41
|
+
if value.nil?
|
42
|
+
default
|
43
|
+
else
|
44
|
+
TruthyFalsey.coerce_if_boolean(value, reference)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# Copyright © 2016-2017 Exosite LLC. All Rights Reserved
|
2
|
+
# License: PROPRIETARY. See LICENSE.txt.
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
# vim:tw=0:ts=2:sw=2:et:ai
|
6
|
+
# Unauthorized copying of this file is strictly prohibited.
|
7
|
+
|
8
|
+
# The Safe Navigation Operator (&.) was introduced in Ruby 2.3.
|
9
|
+
#
|
10
|
+
# Here, we introduce a delegator that performs a similar dig operator,
|
11
|
+
# but it wraps each child hash in a delegator, and it allows one to
|
12
|
+
# safe-set a nested value.
|
13
|
+
#
|
14
|
+
# For more on the dig operator, see:
|
15
|
+
#
|
16
|
+
# http://mitrev.net/ruby/2015/11/13/the-operator-in-ruby/
|
17
|
+
|
18
|
+
# NOTE: (lb): Rather than deriving from the built-in SimpleDelegator class,
|
19
|
+
# from the 'delegate' library, we use a third-party package, because
|
20
|
+
# SimpleDelegator maps the class and is_a? methods to the delegator, i.e.,
|
21
|
+
# some_delegator.is_a? Delegator == true. But we'd prefer that the delegator
|
22
|
+
# map to the wrapped object, e.g., MyDelegator.new(Hash.new).class == Hash.
|
23
|
+
|
24
|
+
# From:
|
25
|
+
# https://github.com/stevenharman/dumb_delegator
|
26
|
+
require 'dumb_delegator'
|
27
|
+
|
28
|
+
class HashDiggable < DumbDelegator
|
29
|
+
def initialize(obj=nil)
|
30
|
+
super(obj || {})
|
31
|
+
end
|
32
|
+
|
33
|
+
def a_diggable_hash?
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
def fill_safe(value, *path)
|
38
|
+
sub_hash = self
|
39
|
+
sub_hash = dig_safe(*path[0..-2]) if path.length > 1
|
40
|
+
sub_hash[path[-1]] = value
|
41
|
+
end
|
42
|
+
|
43
|
+
def dig_safe(key, *rest)
|
44
|
+
value = self[key]
|
45
|
+
|
46
|
+
if value.nil?
|
47
|
+
value = self[key] = ::HashDiggable.new
|
48
|
+
else
|
49
|
+
begin
|
50
|
+
value.a_diggable_hash?
|
51
|
+
# Okay!
|
52
|
+
rescue ::NoMethodError => _err
|
53
|
+
value = self[key] = ::HashDiggable.new(value) if value.is_a? ::Hash
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
if rest.empty?
|
58
|
+
value
|
59
|
+
else
|
60
|
+
value.dig_safe(*rest)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|