MuranoCLI 3.2.0.beta.1 → 3.2.0.beta.5

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.
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
+