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