couch-shell 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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