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.
- data/lib/couch-shell-plugin/core.rb +241 -0
- data/lib/couch-shell-plugin/core_designs.rb +24 -0
- data/lib/couch-shell-plugin/core_edit.rb +38 -0
- data/lib/couch-shell-plugin/core_lucene.rb +54 -0
- data/lib/couch-shell-plugin/core_views.rb +165 -0
- data/lib/couch-shell.rb +5 -0
- data/lib/couch-shell/eval_context.rb +26 -3
- data/lib/couch-shell/exceptions.rb +65 -0
- data/lib/couch-shell/json_value.rb +91 -19
- data/lib/couch-shell/plugin.rb +251 -0
- data/lib/couch-shell/plugin_utils.rb +88 -0
- data/lib/couch-shell/response.rb +19 -14
- data/lib/couch-shell/shell.rb +204 -415
- data/lib/couch-shell/version.rb +1 -1
- metadata +11 -3
@@ -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
|
data/lib/couch-shell/response.rb
CHANGED
@@ -15,25 +15,30 @@ module CouchShell
|
|
15
15
|
# response
|
16
16
|
def initialize(response)
|
17
17
|
@res = response
|
18
|
-
@
|
19
|
-
@
|
18
|
+
@json_value = nil
|
19
|
+
@computed_json_value = false
|
20
20
|
end
|
21
21
|
|
22
|
-
# Response body parsed as json
|
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
|
-
|
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
|
-
@
|
34
|
+
@json_value = JsonValue.wrap(JSON.parse(body))
|
30
35
|
rescue JSON::ParserError
|
31
|
-
@
|
36
|
+
@json_value = false
|
32
37
|
end
|
33
38
|
end
|
34
|
-
@
|
39
|
+
@computed_json_value = true
|
35
40
|
end
|
36
|
-
@
|
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
|
65
|
-
if
|
66
|
-
|
67
|
-
elsif altname &&
|
68
|
-
|
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
|
data/lib/couch-shell/shell.rb
CHANGED
@@ -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
|
26
|
-
end
|
46
|
+
class PluginLoadError < ShellUserError
|
27
47
|
|
28
|
-
|
29
|
-
|
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
|
51
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
105
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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.
|
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
|
-
|
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
|
-
|
167
|
-
|
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
|
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
|
-
|
222
|
-
|
223
|
-
|
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
|
-
|
271
|
-
|
272
|
-
|
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
|
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
|
-
|
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
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
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
|
-
|
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 <<
|
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
|
-
|
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
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
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
|
-
|
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
|
-
|
459
|
-
|
460
|
-
|
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
|
469
|
-
|
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
|