couch-shell 0.0.5 → 0.0.6

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.
@@ -0,0 +1,88 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "couch-shell/exceptions"
4
+
5
+ module CouchShell
6
+
7
+ # Requires an instance method named "shell" that returns a
8
+ # CouchShell::Shell compatible object.
9
+ module PluginUtils
10
+
11
+ class ShellError < ShellUserError
12
+ end
13
+
14
+ class VarNotSet < ShellError
15
+
16
+ attr_accessor :var
17
+
18
+ def message
19
+ "Variable @#{@var.plugin.plugin_name}.#{@var.name} not set."
20
+ end
21
+
22
+ end
23
+
24
+ # Get the first element of pathstack or raise an exception if pathstack is
25
+ # empty.
26
+ def dbname!
27
+ raise ShellError, "must cd into database" if shell.pathstack.empty?
28
+ shell.pathstack[0]
29
+ end
30
+
31
+ # Raise an exception unless pathstack size is 1.
32
+ def ensure_at_database
33
+ if shell.pathstack.size != 1
34
+ raise ShellError, "current directory must be database"
35
+ end
36
+ end
37
+
38
+ # Displays msg in the same style as the standard couch-shell prompt
39
+ # and raises ShellError unless the user hits ENTER and nothing else.
40
+ #
41
+ # Usage:
42
+ #
43
+ # # do some setup logic
44
+ # continue?("Changes made: foo replaced by bar\n" +
45
+ # "Press ENTER to save changes or CTRL+C to cancel")
46
+ # # save changes
47
+ def continue?(msg)
48
+ shell.prompt_msg(msg, false)
49
+ unless shell.stdin.gets.chomp.empty?
50
+ raise ShellError, "cancelled"
51
+ end
52
+ end
53
+
54
+ # Like shell.request, but raises a ShellError ("required request failed")
55
+ # if response.ok? is false.
56
+ def request!(*args)
57
+ shell.request(*args).tap do |res|
58
+ raise ShellError, "required request failed" unless res.ok?
59
+ end
60
+ end
61
+
62
+ # Opens editor with the given file path and returns after the user closes
63
+ # the editor or raises a ShellError if the editor doesn't exit with an exit
64
+ # status of 0.
65
+ def editfile!(path)
66
+ unless system(shell.editor_bin!, path)
67
+ raise ShellError, "editing command failed with exit status #{$?.exitstatus}"
68
+ end
69
+ end
70
+
71
+ # Writes content to a temporary file, calls editfile! on it and returns the
72
+ # new file content on success after unlinking the temporary file.
73
+ def edittext!(content, tempfile_name_ext = ".js", tempfile_name_part = "cs")
74
+ t = Tempfile.new([tempfile_name_part, tempfile_name_ext])
75
+ t.write(content)
76
+ t.close
77
+ editfile! t.path
78
+ t.open.read
79
+ ensure
80
+ if t
81
+ t.close
82
+ t.unlink
83
+ end
84
+ end
85
+
86
+ end
87
+
88
+ end
@@ -15,25 +15,30 @@ module CouchShell
15
15
  # response
16
16
  def initialize(response)
17
17
  @res = response
18
- @json = nil
19
- @computed_json = false
18
+ @json_value = nil
19
+ @computed_json_value = false
20
20
  end
21
21
 
22
- # Response body parsed as json. nil if body is empty, false if
23
- # parsing failed.
22
+ # Response body parsed as json, represented as a ruby data structure. nil
23
+ # if body is empty, false if parsing failed.
24
24
  def json
25
- unless @computed_json
25
+ json_value ? json_value.unwrapped! : json_value
26
+ end
27
+
28
+ # Like json, but wrapped in a CouchShell::JsonValue.
29
+ def json_value
30
+ unless @computed_json_value
26
31
  if JSON_CONTENT_TYPES.include?(content_type) &&
27
32
  !body.nil? && !body.empty?
28
33
  begin
29
- @json = JsonValue.wrap(JSON.parse(body))
34
+ @json_value = JsonValue.wrap(JSON.parse(body))
30
35
  rescue JSON::ParserError
31
- @json = false
36
+ @json_value = false
32
37
  end
33
38
  end
34
- @computed_json = true
39
+ @computed_json_value = true
35
40
  end
36
- @json
41
+ @json_value
37
42
  end
38
43
 
39
44
  def code
@@ -61,11 +66,11 @@ module CouchShell
61
66
  def attr(name, altname = nil)
62
67
  name = name.to_sym
63
68
  altname = altname ? altname.to_sym : nil
64
- if json
65
- if json.respond_to?(name)
66
- json.__send__ name
67
- elsif altname && json.respond_to?(altname)
68
- json.__send__ altname
69
+ if json_value
70
+ if json_value.respond_to?(name)
71
+ json_value.__send__ name
72
+ elsif altname && json_value.respond_to?(altname)
73
+ json_value.__send__ altname
69
74
  end
70
75
  end
71
76
  end
@@ -3,15 +3,36 @@
3
3
  require "tempfile"
4
4
  require "uri"
5
5
  require "net/http"
6
+ require "socket"
6
7
  require "httpclient"
7
8
  require "highline"
9
+ require "couch-shell/exceptions"
8
10
  require "couch-shell/version"
9
11
  require "couch-shell/response"
10
12
  require "couch-shell/ring_buffer"
11
13
  require "couch-shell/eval_context"
14
+ require "couch-shell/plugin"
12
15
 
13
16
  module CouchShell
14
17
 
18
+ JSON_DOC_START_RX = /\A[ \t\n\r]*[\(\{]/
19
+
20
+ class FileToUpload
21
+
22
+ attr_reader :filename, :content_type
23
+
24
+ def initialize(filename, content_type = nil)
25
+ @filename = filename
26
+ @content_type = content_type
27
+ end
28
+
29
+ def content_type!
30
+ # TODO: use mime-types and/or file to guess mime type
31
+ content_type || "application/octet-stream"
32
+ end
33
+
34
+ end
35
+
15
36
  # Starting a shell:
16
37
  #
17
38
  # require "couch-shell/shell"
@@ -22,44 +43,27 @@ module CouchShell
22
43
  #
23
44
  class Shell
24
45
 
25
- class Quit < Exception
26
- end
46
+ class PluginLoadError < ShellUserError
27
47
 
28
- class ShellUserError < Exception
29
- end
30
-
31
- class UndefinedVariable < ShellUserError
32
-
33
- attr_reader :varname
34
-
35
- def initialize(varname)
36
- @varname = varname
37
- end
38
-
39
- end
40
-
41
- class FileToUpload
42
-
43
- attr_reader :filename, :content_type
44
-
45
- def initialize(filename, content_type = nil)
46
- @filename = filename
47
- @content_type = content_type
48
+ def initialize(plugin_name, reason)
49
+ @plugin_name = plugin_name
50
+ @reason = reason
48
51
  end
49
52
 
50
- def content_type!
51
- # TODO: use mime-types and/or file to guess mime type
52
- content_type || "application/octet-stream"
53
+ def message
54
+ "Failed to load plugin #@plugin_name: #@reason"
53
55
  end
54
56
 
55
57
  end
56
58
 
57
- PREDEFINED_VARS = [
58
- "uuid", "id", "rev", "idr",
59
- "content-type", "server"
60
- ].freeze
61
-
62
- JSON_DOC_START_RX = /\A[ \t\n\r]*[\(\{]/
59
+ # A CouchShell::RingBuffer holding CouchShell::Response instances.
60
+ attr_reader :responses
61
+ attr_reader :server_url
62
+ attr_reader :stdout
63
+ attr_reader :stdin
64
+ attr_reader :pathstack
65
+ attr_accessor :username
66
+ attr_accessor :password
63
67
 
64
68
  def initialize(stdin, stdout, stderr)
65
69
  @stdin = stdin
@@ -70,10 +74,72 @@ module CouchShell
70
74
  @highline = HighLine.new(@stdin, @stdout)
71
75
  @responses = RingBuffer.new(10)
72
76
  @eval_context = EvalContext.new(self)
73
- @viewtext = nil
74
- @stdout.puts "couch-shell #{VERSION}"
75
77
  @username = nil
76
78
  @password = nil
79
+ @plugins = {}
80
+ @commands = {}
81
+ @variables = {}
82
+ @variable_prefixes = []
83
+ @stdout.puts "couch-shell #{VERSION}"
84
+ end
85
+
86
+ def plugin(plugin_name)
87
+ # load and instantiate
88
+ feature = "couch-shell-plugin/#{plugin_name}"
89
+ begin
90
+ require feature
91
+ rescue LoadError
92
+ raise PluginLoadError, "feature #{feature} not found"
93
+ end
94
+ pi = PluginInfo[plugin_name]
95
+ raise PluginLoadError, "plugin class not found" unless pi
96
+ plugin = pi.plugin_class.new(self)
97
+
98
+ # integrate plugin variables
99
+ ## enable qualified reference via @PLUGIN.VAR syntax
100
+ @eval_context._instance_variable_set(
101
+ :"@#{plugin_name}", plugin.variables_object)
102
+ ## enable unqualified reference
103
+ pi.variables.each { |vi|
104
+ if vi.name
105
+ existing = @variables[vi.name]
106
+ if existing
107
+ warn "When loading plugin #{plugin_name}: " +
108
+ "Variable #{vi.name} already defined by plugin " +
109
+ "#{existing.plugin.plugin_name}\n" +
110
+ "You can access it explicitely via @#{plugin_name}.#{vi.name}"
111
+ else
112
+ @variables[vi.name] = vi
113
+ end
114
+ end
115
+ if vi.prefix
116
+ existing = @variable_prefixes.find { |e| e.prefix == vi.prefix }
117
+ if existing
118
+ warn "When loading plugin #{plugin_name}: " +
119
+ "Variable prefix #{vi.prefix} already defined by plugin " +
120
+ "#{existing.plugin.plugin_name}\n" +
121
+ "You can access it explicitely via @#{plugin_name}.#{vi.prefix}*"
122
+ else
123
+ @variable_prefixes << vi
124
+ end
125
+ end
126
+ }
127
+
128
+ # integrate plugin commands
129
+ pi.commands.each_value { |ci|
130
+ existing = @commands[ci.name]
131
+ if existing
132
+ warn "When loading plugin #{plugin_name}: " +
133
+ "Command #{ci.name} already defined by plugin " +
134
+ "#{existing.plugin.plugin_name}\n" +
135
+ "You can access it explicitely via @#{plugin_name}.#{ci.name}"
136
+ else
137
+ @commands[ci.name] = ci
138
+ end
139
+ }
140
+
141
+ @plugins[plugin_name] = plugin
142
+ plugin.plugin_initialization
77
143
  end
78
144
 
79
145
  def normalize_server_url(url)
@@ -81,7 +147,7 @@ module CouchShell
81
147
  # remove trailing slash
82
148
  url = url.sub(%r{/\z}, '')
83
149
  # prepend http:// if scheme is omitted
84
- if url =~ /\A\p{Alpha}(?:\p{Alpha}|\p{Digit}|\+|\-|\.)*:/
150
+ if url =~ /\A\p{Alpha}(?:\p{Alpha}|\p{Digit}|\+|\-|\.)*:\/\//
85
151
  url
86
152
  else
87
153
  "http://#{url}"
@@ -101,27 +167,44 @@ module CouchShell
101
167
 
102
168
  def cd(path, get = false)
103
169
  old_pathstack = @pathstack.dup
104
- case path
105
- when nil
170
+
171
+ if path
172
+ @pathstack = [] if path.start_with?("/")
173
+ path.split('/').each { |elem|
174
+ case elem
175
+ when ""
176
+ # do nothing
177
+ when "."
178
+ # do nothing
179
+ when ".."
180
+ if @pathstack.empty?
181
+ @pathstack = old_pathstack
182
+ raise ShellUserError, "Already at server root, can't go up"
183
+ end
184
+ @pathstack.pop
185
+ else
186
+ @pathstack << elem
187
+ end
188
+ }
189
+ else
106
190
  @pathstack = []
107
- when ".."
108
- if @pathstack.empty?
109
- errmsg "Already at server root, can't go up."
110
- else
111
- @pathstack.pop
191
+ end
192
+
193
+ old_dbname = old_pathstack[0]
194
+ new_dbname = @pathstack[0]
195
+ getdb = false
196
+ if new_dbname && (new_dbname != old_dbname)
197
+ getdb = get && @pathstack.size == 1
198
+ res = request("GET", "/#{new_dbname}", nil, getdb)
199
+ unless res.ok? && (json = res.json_value) &&
200
+ json.object? && json["db_name"] &&
201
+ json["db_name"].unwrapped! == new_dbname
202
+ @pathstack = old_pathstack
203
+ raise ShellUserError, "not a database: #{new_dbname}"
112
204
  end
113
- when %r{\A/\z}
114
- @pathstack = []
115
- when %r{\A/}
116
- @pathstack = []
117
- cd path[1..-1], false
118
- when %r{/}
119
- path.split("/").each { |elem| cd elem, false }
120
- else
121
- @pathstack << path
122
205
  end
123
- if get
124
- if request("GET", nil) != "200"
206
+ if get && !getdb
207
+ if request("GET", nil).code != "200"
125
208
  @pathstack = old_pathstack
126
209
  end
127
210
  end
@@ -140,13 +223,17 @@ module CouchShell
140
223
  @stderr.puts @highline.color(str, :red)
141
224
  end
142
225
 
226
+ def warn(str)
227
+ @stderr.print @highline.color("warning: ", :red)
228
+ @stderr.puts @highline.color(str, :blue)
229
+ end
143
230
 
144
231
  def print_response(res, label = "", show_body = true)
145
232
  @stdout.print @highline.color("#{res.code} #{res.message}", :cyan)
146
233
  msg " #{label}"
147
234
  if show_body
148
235
  if res.json
149
- @stdout.puts res.json.format
236
+ @stdout.puts res.json_value.to_s(true)
150
237
  elsif res.body
151
238
  @stdout.puts res.body
152
239
  end
@@ -155,16 +242,16 @@ module CouchShell
155
242
  end
156
243
  end
157
244
 
245
+ # Returns CouchShell::Response or raises an exception.
158
246
  def request(method, path, body = nil, show_body = true)
159
247
  unless @server_url
160
- errmsg "Server not set - can't perform request."
161
- return
248
+ raise ShellUserError, "Server not set - can't perform request."
162
249
  end
163
250
  fpath = URI.encode(full_path(path))
164
251
  msg "#{method} #{fpath} ", false
165
252
  if @server_url.scheme != "http"
166
- errmsg "Protocol #{@server_url.scheme} not supported, use http."
167
- return
253
+ raise ShellUserError,
254
+ "Protocol #{@server_url.scheme} not supported, use http."
168
255
  end
169
256
  # HTTPClient and CouchDB don't work together with simple put/post
170
257
  # requests to due some Keep-alive mismatch.
@@ -180,7 +267,7 @@ module CouchShell
180
267
  vars = ["r#{@responses.index}"]
181
268
  vars << ["j#{@responses.index}"] if res.json
182
269
  print_response res, " vars: #{vars.join(', ')}", show_body
183
- res.code
270
+ res
184
271
  end
185
272
 
186
273
  def net_http_request(method, fpath, body)
@@ -218,9 +305,10 @@ module CouchShell
218
305
  if body.kind_of?(FileToUpload)
219
306
  file_to_upload = body
220
307
  file = File.open(file_to_upload.filename, "rb")
221
- #body = [{'Content-Type' => file_to_upload.content_type!,
222
- # :content => file}]
223
- body = {'upload' => file}
308
+ body = [{'Content-Type' => file_to_upload.content_type!,
309
+ 'Content-Transfer-Encoding' => 'binary',
310
+ :content => file}]
311
+ #body = {'upload' => file}
224
312
  elsif body && body =~ JSON_DOC_START_RX
225
313
  headers['Content-Type'] = "application/json"
226
314
  end
@@ -267,19 +355,19 @@ module CouchShell
267
355
  end
268
356
  end
269
357
 
270
- def continue?(msg)
271
- prompt_msg(msg, false)
272
- unless @stdin.gets.chomp.empty?
273
- raise ShellUserError, "cancelled"
274
- end
275
- end
276
-
358
+ # Displays the standard couch-shell prompt and waits for the user to
359
+ # enter a command. Returns the user input as a string (which may be
360
+ # empty), or nil if the input stream is closed.
277
361
  def read
278
362
  lead = @pathstack.empty? ? ">>" : @pathstack.join("/") + " >>"
279
363
  begin
280
364
  @highline.ask(@highline.color(lead, :yellow) + " ") { |q|
281
365
  q.readline = true
282
366
  }
367
+ rescue Interrupt
368
+ @stdout.puts
369
+ errmsg "interrupted"
370
+ return ""
283
371
  rescue NoMethodError
284
372
  # this is BAD, but highline 1.6.1 reacts to CTRL+D with a NoMethodError
285
373
  return nil
@@ -287,7 +375,7 @@ module CouchShell
287
375
  end
288
376
 
289
377
  # When the user enters something, it is passed to this method for
290
- # execution. You may call if programmatically to simulate user input.
378
+ # execution. You may call it programmatically to simulate user input.
291
379
  #
292
380
  # If input is nil, it is interpreted as "end of input", raising a
293
381
  # CouchShell::Shell::Quit exception. This exception is also raised by other
@@ -305,8 +393,14 @@ module CouchShell
305
393
  errmsg "Variable `" + e.varname + "' is not defined."
306
394
  rescue ShellUserError => e
307
395
  errmsg e.message
396
+ rescue Errno::ETIMEDOUT => e
397
+ errmsg "timeout: #{e.message}"
398
+ rescue SocketError, Errno::ENOENT => e
399
+ @stdout.puts
400
+ errmsg "#{e.class}: #{e.message}"
308
401
  rescue Exception => e
309
- errmsg e.message
402
+ #p e.class.instance_methods - Object.instance_methods
403
+ errmsg "#{e.class}: #{e.message}"
310
404
  errmsg e.backtrace[0..5].join("\n")
311
405
  end
312
406
  end
@@ -319,13 +413,29 @@ module CouchShell
319
413
  when ""
320
414
  # do nothing
321
415
  else
322
- command, argstr = input.split(/\s+/, 2)
323
- command_message = :"command_#{command.downcase}"
324
- if self.respond_to?(command_message)
325
- send command_message, argstr
416
+ execute_command! *input.split(/\s+/, 2)
417
+ end
418
+ end
419
+
420
+ def execute_command!(commandref, argstr = nil)
421
+ if commandref.start_with?("@")
422
+ # qualified command
423
+ if commandref =~ /\A@([^\.]+)\.([^\.]+)\z/
424
+ plugin_name = $1
425
+ command_name = $2
426
+ plugin = @plugins[plugin_name]
427
+ raise NoSuchPluginRegistered.new(plugin_name) unless plugin
428
+ ci = plugin.plugin_info.commands[command_name]
429
+ raise NoSuchCommandInPlugin.new(plugin_name, command_name) unless ci
430
+ plugin.send ci.execute_message, argstr
326
431
  else
327
- errmsg "unknown command `#{command}'"
432
+ raise ShellUserError, "invalid command syntax"
328
433
  end
434
+ else
435
+ # unqualified command
436
+ ci = @commands[commandref]
437
+ raise NoSuchCommand.new(commandref) unless ci
438
+ @plugins[ci.plugin.plugin_name].send ci.execute_message, argstr
329
439
  end
330
440
  end
331
441
 
@@ -363,7 +473,7 @@ module CouchShell
363
473
  end
364
474
  elsif c == ')'
365
475
  if expr
366
- res << shell_eval(expr).to_s
476
+ res << eval_expr(expr).to_s
367
477
  expr = nil
368
478
  else
369
479
  res << c
@@ -380,84 +490,28 @@ module CouchShell
380
490
  }
381
491
  end
382
492
 
383
- def shell_eval(expr)
493
+ # Evaluate the given expression.
494
+ def eval_expr(expr)
384
495
  @eval_context.instance_eval(expr)
385
496
  end
386
497
 
498
+ # Lookup unqualified variable name.
387
499
  def lookup_var(var)
388
- case var
389
- when "uuid"
390
- command_uuids nil
391
- if @responses.current(&:ok?)
392
- json = @responses.current.json
393
- if json && (uuids = json.couch_shell_ruby_value!["uuids"]) &&
394
- uuids.kind_of?(Array) && uuids.size > 0
395
- uuids[0]
396
- else
397
- raise ShellUserError,
398
- "interpolation failed due to unkown json structure"
399
- end
400
- else
401
- raise ShellUserError, "interpolation failed"
402
- end
403
- when "id"
404
- @responses.current { |r| r.attr "id", "_id" } or
405
- raise ShellUserError, "variable `id' not set"
406
- when "rev"
407
- @responses.current { |r| r.attr "rev", "_rev" } or
408
- raise ShellUserError, "variable `rev' not set"
409
- when "idr"
410
- "#{lookup_var 'id'}?rev=#{lookup_var 'rev'}"
411
- when "content-type"
412
- @responses.current(&:content_type)
413
- when "server"
414
- if @server_url
415
- u = @server_url
416
- "#{u.scheme}://#{u.host}:#{u.port}#{u.path}"
417
- else
418
- raise ShellUserError, "variable `server' not set"
419
- end
420
- when /\Ar(\d)\z/
421
- i = $1.to_i
422
- if @responses.readable_index?(i)
423
- @responses[i]
424
- else
425
- raise ShellUserError, "no response index #{i}"
426
- end
427
- when /\Aj(\d)\z/
428
- i = $1.to_i
429
- if @responses.readable_index?(i)
430
- if @responses[i].json
431
- @responses[i].json
432
- else
433
- raise ShellUserError, "no json in response #{i}"
434
- end
435
- else
436
- raise ShellUserError, "no response index #{i}"
437
- end
438
- when "viewtext"
439
- @viewtext or
440
- raise ShellUserError, "viewtext not set"
441
- else
442
- raise UndefinedVariable.new(var)
443
- end
444
- end
445
-
446
- def request_command_with_body(method, argstr)
447
- if argstr =~ JSON_DOC_START_RX
448
- url, bodyarg = nil, argstr
449
- else
450
- url, bodyarg= argstr.split(/\s+/, 2)
451
- end
452
- if bodyarg && bodyarg.start_with?("@")
453
- filename, content_type = bodyarg[1..-1].split(/\s+/, 2)
454
- body = FileToUpload.new(filename, content_type)
500
+ vi = @variables[var]
501
+ if vi
502
+ plugin = @plugins[vi.plugin.plugin_name]
503
+ plugin.send vi.lookup_message
455
504
  else
456
- body = bodyarg
505
+ vi = @variable_prefixes.find { |v|
506
+ var.start_with?(v.prefix) && var.length > v.prefix.length
507
+ }
508
+ raise UndefinedVariable.new(var) unless vi
509
+ plugin = @plugins[vi.plugin.plugin_name]
510
+ plugin.send vi.lookup_message, var[vi.prefix.length]
457
511
  end
458
- real_url = interpolate(url)
459
- request method, real_url, body
460
- real_url
512
+ rescue Plugin::VarNotSet => e
513
+ e.var = vi
514
+ raise e
461
515
  end
462
516
 
463
517
  def editor_bin!
@@ -465,273 +519,8 @@ module CouchShell
465
519
  raise ShellUserError, "EDITOR environment variable not set"
466
520
  end
467
521
 
468
- def command_get(argstr)
469
- request "GET", interpolate(argstr)
470
- end
471
-
472
- def command_put(argstr)
473
- request_command_with_body("PUT", argstr)
474
- end
475
-
476
- def command_cput(argstr)
477
- url = request_command_with_body("PUT", argstr)
478
- cd url if @responses.current(&:ok?)
479
- end
480
-
481
- def command_post(argstr)
482
- request_command_with_body("POST", argstr)
483
- end
484
-
485
- def command_delete(argstr)
486
- request "DELETE", interpolate(argstr)
487
- end
488
-
489
- def command_cd(argstr)
490
- cd interpolate(argstr), false
491
- end
492
-
493
- def command_cg(argstr)
494
- cd interpolate(argstr), true
495
- end
496
-
497
- def command_exit(argstr)
498
- raise Quit
499
- end
500
-
501
- def command_quit(argstr)
502
- raise Quit
503
- end
504
-
505
- def command_uuids(argstr)
506
- count = argstr ? argstr.to_i : 1
507
- request "GET", "/_uuids?count=#{count}"
508
- end
509
-
510
- def command_echo(argstr)
511
- if argstr
512
- @stdout.puts interpolate(argstr)
513
- end
514
- end
515
-
516
- def command_print(argstr)
517
- unless argstr
518
- errmsg "expression required"
519
- return
520
- end
521
- @stdout.puts shell_eval(argstr)
522
- end
523
-
524
- def command_format(argstr)
525
- unless argstr
526
- errmsg "expression required"
527
- return
528
- end
529
- val = shell_eval(argstr)
530
- if val.respond_to?(:couch_shell_format_string)
531
- @stdout.puts val.couch_shell_format_string
532
- else
533
- @stdout.puts val
534
- end
535
- end
536
-
537
- def command_server(argstr)
538
- self.server = argstr
539
- end
540
-
541
- def command_expand(argstr)
542
- @stdout.puts expand(interpolate(argstr))
543
- end
544
-
545
- def command_sh(argstr)
546
- unless argstr
547
- errmsg "argument required"
548
- return
549
- end
550
- unless system(argstr)
551
- errmsg "command exited with status #{$?.exitstatus}"
552
- end
553
- end
554
-
555
- def command_editview(argstr)
556
- if @pathstack.size != 1
557
- raise ShellUserError, "current directory must be database"
558
- end
559
- design_name, view_name = argstr.split(/\s+/, 2)
560
- if design_name.nil? || view_name.nil?
561
- raise ShellUserError, "design and view name required"
562
- end
563
- request "GET", "_design/#{design_name}", nil, false
564
- return unless @responses.current(&:ok?)
565
- design = @responses.current.json
566
- view = nil
567
- if design.respond_to?(:views) &&
568
- design.views.respond_to?(view_name.to_sym)
569
- view = design.views.__send__(view_name.to_sym)
570
- end
571
- mapval = view && view.respond_to?(:map) && view.map
572
- reduceval = view && view.respond_to?(:reduce) && view.reduce
573
- t = Tempfile.new(["view", ".js"])
574
- t.puts("map")
575
- if mapval
576
- t.puts mapval
577
- else
578
- t.puts "function(doc) {\n emit(doc._id, doc);\n}"
579
- end
580
- if reduceval || view.nil?
581
- t.puts
582
- t.puts("reduce")
583
- if reduceval
584
- t.puts reduceval
585
- else
586
- t.puts "function(keys, values, rereduce) {\n\n}"
587
- end
588
- end
589
- t.close
590
- continue?(
591
- "Press ENTER to edit #{view ? 'existing' : 'new'} view, " +
592
- "CTRL+C to cancel ")
593
- unless system(editor_bin!, t.path)
594
- raise ShellUserError, "editing command failed with exit status #{$?.exitstatus}"
595
- end
596
- text = t.open.read
597
- @viewtext = text
598
- t.close
599
- mapf = nil
600
- reducef = nil
601
- inmap = false
602
- inreduce = false
603
- i = 0
604
- text.each_line { |line|
605
- i += 1
606
- case line
607
- when /^map\s*(.*)$/
608
- unless $1.empty?
609
- msg "recover view text with `print viewtext'"
610
- raise ShellUserError, "invalid map line at line #{i}"
611
- end
612
- unless mapf.nil?
613
- msg "recover view text with `print viewtext'"
614
- raise ShellUserError, "duplicate map line at line #{i}"
615
- end
616
- inreduce = false
617
- inmap = true
618
- mapf = ""
619
- when /^reduce\s*(.*)$/
620
- unless $1.empty?
621
- msg "recover view text with `print viewtext'"
622
- raise ShellUserError, "invalid reduce line at line #{i}"
623
- end
624
- unless reducef.nil?
625
- msg "recover view text with `print viewtext'"
626
- raise ShellUserError, "duplicate reduce line at line #{i}"
627
- end
628
- inmap = false
629
- inreduce = true
630
- reducef = ""
631
- else
632
- if inmap
633
- mapf << line
634
- elsif inreduce
635
- reducef << line
636
- elsif line =~ /^\s*$/
637
- # ignore
638
- else
639
- msg "recover view text with `print viewtext'"
640
- raise ShellUserError, "unexpected content at line #{i}"
641
- end
642
- end
643
- }
644
- mapf.strip! if mapf
645
- reducef.strip! if reducef
646
- mapf = nil if mapf && mapf.empty?
647
- reducef = nil if reducef && reducef.empty?
648
- prompt_msg "View parsed, following actions would be taken:"
649
- if mapf && mapval.nil?
650
- prompt_msg " Add map function."
651
- elsif mapf.nil? && mapval
652
- prompt_msg " Remove map function."
653
- elsif mapf && mapval && mapf != mapval
654
- prompt_msg " Update map function."
655
- end
656
- if reducef && reduceval.nil?
657
- prompt_msg " Add reduce function."
658
- elsif reducef.nil? && reduceval
659
- prompt_msg " Remove reduce function."
660
- elsif reducef && reduceval && reducef != reduceval
661
- prompt_msg " Update reduce function."
662
- end
663
- continue? "Press ENTER to submit, CTRL+C to cancel "
664
- if !design.respond_to?(:views)
665
- design.set_attr!("views", {})
666
- end
667
- if view.nil?
668
- design.views.set_attr!(view_name, {})
669
- view = design.views.__send__(view_name.to_sym)
670
- end
671
- if mapf.nil?
672
- view.delete_attr!("map")
673
- else
674
- view.set_attr!("map", mapf)
675
- end
676
- if reducef.nil?
677
- view.delete_attr!("reduce")
678
- else
679
- view.set_attr!("reduce", reducef)
680
- end
681
- request "PUT", "_design/#{design_name}", design.to_s
682
- unless @responses.current(&:ok?)
683
- msg "recover view text with `print viewtext'"
684
- end
685
- ensure
686
- if t
687
- t.close
688
- t.unlink
689
- end
690
- end
691
-
692
- def command_view(argstr)
693
- if @pathstack.size != 1
694
- raise ShellUserError, "current directory must be database"
695
- end
696
- design_name, view_name = argstr.split("/", 2)
697
- if design_name.nil? || view_name.nil?
698
- raise ShellUserError, "argument in the form DESIGN/VIEW required"
699
- end
700
- request "GET", "_design/#{design_name}/_view/#{view_name}"
701
- end
702
-
703
- def command_member(argstr)
704
- id, rev = nil, nil
705
- json = @responses.current(&:json)
706
- unless json && (id = json.attr_or_nil!("_id")) &&
707
- (rev = json.attr_or_nil!("_rev")) &&
708
- (@pathstack.size > 0) &&
709
- (@pathstack.last == id.to_s)
710
- raise ShellUserError,
711
- "`cg' the desired document first, e.g.: `cg /my_db/my_doc_id'"
712
- end
713
- # TODO: read json string as attribute name if argstr starts with double
714
- # quote
715
- attr_name, new_valstr = argstr.split(/\s+/, 2)
716
- unless attr_name && new_valstr
717
- raise ShellUserError,
718
- "attribute name and new value argument required"
719
- end
720
- if new_valstr == "remove"
721
- json.delete_attr!(attr_name)
722
- else
723
- new_val = JsonValue.parse(new_valstr)
724
- json.set_attr!(attr_name, new_val)
725
- end
726
- request "PUT", "?rev=#{rev}", json.to_s
727
- end
728
-
729
- def command_user(argstr)
730
- prompt_msg("Password:", false)
731
- @password = @highline.ask(" ") { |q| q.echo = "*" }
732
- # we save the username only after the password was entered
733
- # to allow cancellation during password input
734
- @username = argstr
522
+ def read_secret
523
+ @highline.ask(" ") { |q| q.echo = "*" }
735
524
  end
736
525
 
737
526
  end