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.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/.trustme.plugin +137 -0
  4. data/.trustme.sh +217 -117
  5. data/.trustme.vim +9 -3
  6. data/Gemfile +9 -3
  7. data/MuranoCLI.gemspec +8 -5
  8. data/Rakefile +1 -0
  9. data/dockers/Dockerfile.2.2.9 +6 -3
  10. data/dockers/Dockerfile.2.3.6 +6 -3
  11. data/dockers/Dockerfile.2.4.3 +6 -3
  12. data/dockers/Dockerfile.2.5.0 +6 -3
  13. data/dockers/Dockerfile.GemRelease +10 -8
  14. data/dockers/Dockerfile.m4 +23 -5
  15. data/dockers/docker-test.sh +65 -28
  16. data/docs/completions/murano_completion-bash +751 -57
  17. data/docs/develop.rst +10 -9
  18. data/lib/MrMurano/AccountBase.rb +95 -6
  19. data/lib/MrMurano/Commander-Entry.rb +9 -4
  20. data/lib/MrMurano/Config-Migrate.rb +2 -0
  21. data/lib/MrMurano/Config.rb +94 -26
  22. data/lib/MrMurano/Content.rb +1 -1
  23. data/lib/MrMurano/Exchange.rb +77 -42
  24. data/lib/MrMurano/Gateway.rb +1 -1
  25. data/lib/MrMurano/HttpAuthed.rb +20 -7
  26. data/lib/MrMurano/Logs.rb +10 -1
  27. data/lib/MrMurano/ProjectFile.rb +1 -1
  28. data/lib/MrMurano/ReCommander.rb +129 -73
  29. data/lib/MrMurano/Solution-ServiceConfig.rb +18 -11
  30. data/lib/MrMurano/Solution-Services.rb +78 -50
  31. data/lib/MrMurano/Solution-Users.rb +1 -1
  32. data/lib/MrMurano/Solution.rb +13 -63
  33. data/lib/MrMurano/SyncUpDown-Core.rb +185 -77
  34. data/lib/MrMurano/SyncUpDown-Item.rb +29 -4
  35. data/lib/MrMurano/SyncUpDown.rb +11 -11
  36. data/lib/MrMurano/Webservice-Cors.rb +1 -1
  37. data/lib/MrMurano/Webservice-Endpoint.rb +28 -17
  38. data/lib/MrMurano/Webservice-File.rb +103 -43
  39. data/lib/MrMurano/commands/domain.rb +1 -0
  40. data/lib/MrMurano/commands/element.rb +585 -0
  41. data/lib/MrMurano/commands/exchange.rb +211 -204
  42. data/lib/MrMurano/commands/gb.rb +1 -0
  43. data/lib/MrMurano/commands/globals.rb +17 -7
  44. data/lib/MrMurano/commands/init.rb +115 -101
  45. data/lib/MrMurano/commands/keystore.rb +1 -1
  46. data/lib/MrMurano/commands/logs.rb +2 -1
  47. data/lib/MrMurano/commands/postgresql.rb +17 -7
  48. data/lib/MrMurano/commands/service.rb +572 -0
  49. data/lib/MrMurano/commands/show.rb +7 -3
  50. data/lib/MrMurano/commands/solution.rb +2 -1
  51. data/lib/MrMurano/commands/solution_picker.rb +31 -15
  52. data/lib/MrMurano/commands/status.rb +205 -169
  53. data/lib/MrMurano/commands/sync.rb +70 -38
  54. data/lib/MrMurano/commands/token.rb +59 -14
  55. data/lib/MrMurano/commands/usage.rb +1 -0
  56. data/lib/MrMurano/commands.rb +2 -0
  57. data/lib/MrMurano/hash.rb +91 -0
  58. data/lib/MrMurano/http.rb +55 -6
  59. data/lib/MrMurano/makePretty.rb +47 -0
  60. data/lib/MrMurano/optparse.rb +60 -45
  61. data/lib/MrMurano/variegated/TruthyFalsey.rb +48 -0
  62. data/lib/MrMurano/variegated/ruby_dig.rb +64 -0
  63. data/lib/MrMurano/verbosing.rb +113 -3
  64. data/lib/MrMurano/version.rb +1 -1
  65. data/spec/Account_spec.rb +34 -20
  66. data/spec/Business_spec.rb +12 -9
  67. data/spec/Config_spec.rb +7 -1
  68. data/spec/Content_spec.rb +17 -1
  69. data/spec/GatewayBase_spec.rb +5 -2
  70. data/spec/GatewayDevice_spec.rb +4 -2
  71. data/spec/GatewayResource_spec.rb +4 -1
  72. data/spec/GatewaySettings_spec.rb +4 -1
  73. data/spec/HttpAuthed_spec.rb +73 -0
  74. data/spec/Http_spec.rb +32 -35
  75. data/spec/ProjectFile_spec.rb +1 -1
  76. data/spec/Solution-ServiceConfig_spec.rb +4 -1
  77. data/spec/Solution-ServiceEventHandler_spec.rb +6 -3
  78. data/spec/Solution-ServiceModules_spec.rb +4 -1
  79. data/spec/Solution-UsersRoles_spec.rb +4 -1
  80. data/spec/Solution_spec.rb +4 -1
  81. data/spec/SyncUpDown_spec.rb +1 -1
  82. data/spec/Webservice-Cors_spec.rb +4 -1
  83. data/spec/Webservice-Endpoint_spec.rb +9 -6
  84. data/spec/Webservice-File_spec.rb +17 -4
  85. data/spec/Webservice-Setting_spec.rb +6 -2
  86. data/spec/_workspace.rb +2 -0
  87. data/spec/cmd_common.rb +42 -13
  88. data/spec/cmd_content_spec.rb +17 -7
  89. data/spec/cmd_device_spec.rb +1 -1
  90. data/spec/cmd_domain_spec.rb +2 -2
  91. data/spec/cmd_element_spec.rb +400 -0
  92. data/spec/cmd_exchange_spec.rb +2 -2
  93. data/spec/cmd_init_spec.rb +59 -25
  94. data/spec/cmd_keystore_spec.rb +6 -3
  95. data/spec/cmd_link_spec.rb +10 -5
  96. data/spec/cmd_logs_spec.rb +1 -1
  97. data/spec/cmd_setting_application_spec.rb +18 -15
  98. data/spec/cmd_setting_product_spec.rb +7 -7
  99. data/spec/cmd_status_spec.rb +27 -17
  100. data/spec/cmd_syncdown_application_spec.rb +30 -3
  101. data/spec/cmd_syncdown_both_spec.rb +72 -18
  102. data/spec/cmd_syncup_spec.rb +71 -5
  103. data/spec/cmd_token_spec.rb +2 -2
  104. data/spec/cmd_usage_spec.rb +2 -2
  105. data/spec/dry_run_formatter.rb +27 -0
  106. data/spec/fixtures/dumped_config +8 -0
  107. data/spec/fixtures/exchange_element/element-show.json +1 -0
  108. data/spec/fixtures/exchange_element/swagger-mur-6407__10k.yaml +282 -0
  109. data/spec/fixtures/exchange_element/swagger-mur-6407__20k.yaml +588 -0
  110. data/spec/variegated_TruthyFalsey_spec.rb +29 -0
  111. 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 args_opts_parse_token!(args, options)
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
- # We cannot use --token, because it's already a global_option.
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
- # We confirm deleting passwords, but not throwaway tokens.
200
- username, host = args_parse_user_host_pair(args)
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
 
@@ -7,6 +7,7 @@
7
7
 
8
8
  require 'MrMurano/ReCommander'
9
9
  require 'MrMurano/Solution'
10
+ require 'MrMurano/commands/solution_picker'
10
11
 
11
12
  command :usage do |c|
12
13
  c.syntax = %(murano usage)
@@ -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
- ccmd << %(-d '#{request.body}')
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
- MrMurano::Http.curldebug_log(ccmd.join(' '))
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(ccmd)
110
+ def self.curldebug_log(formatted, stamp_it: false)
72
111
  if $cfg.curlfile_f.nil?
73
- MrMurano::Progress.instance.whirly_interject { puts ccmd }
112
+ MrMurano::Progress.instance.whirly_interject { puts formatted }
74
113
  else
75
- $cfg.curlfile_f << ccmd + "\n\n"
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)
@@ -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
 
@@ -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
- case arg
1565
- # long option
1566
- when /\A--([^=]*)(?:=(.*))?/m
1567
- opt, rest = $1, $2
1568
- begin
1569
- sw, = complete(:long, opt, true)
1570
- rescue ParseError
1571
- raise $!.set_option(arg, true)
1572
- end
1573
- begin
1574
- opt, cb, val = sw.parse(rest, argv) {|*exc| raise(*exc)}
1575
- val = cb.call(val) if cb
1576
- setter.call(sw.switch_name, val) if setter
1577
- rescue ParseError
1578
- raise $!.set_option(arg, rest)
1579
- end
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
- # short option
1582
- when /\A-(.)((=).*|.+)?/m
1583
- opt, has_arg, eq, val, rest = $1, $3, $3, $2, $2
1584
- begin
1585
- sw, = search(:short, opt)
1586
- unless sw
1587
- begin
1588
- sw, = complete(:short, opt)
1589
- # short option matched.
1590
- val = arg.sub(/\A-/, '')
1591
- has_arg = true
1592
- rescue InvalidOption
1593
- # if no short options match, try completion with long
1594
- # options.
1595
- sw, = complete(:long, opt)
1596
- eq ||= !rest
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
- rescue ParseError
1600
- raise $!.set_option(arg, true)
1601
- end
1602
- begin
1603
- opt, cb, val = sw.parse(val, argv) {|*exc| raise(*exc) if eq}
1604
- raise InvalidOption, arg if has_arg and !eq and arg == "-#{opt}"
1605
- argv.unshift(opt) if opt and (!rest or (opt = opt.sub(/\A-*/, '-')) != '-')
1606
- val = cb.call(val) if cb
1607
- setter.call(sw.switch_name, val) if setter
1608
- rescue ParseError
1609
- raise $!.set_option(arg, arg.length > 2)
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
- # non-option argument
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
+