Class: CouchShell::Shell

Inherits:
Object
  • Object
show all
Defined in:
lib/couch-shell/shell.rb

Overview

Starting a shell:

require "couch-shell/shell"

shell = CouchShell::Shell.new(STDIN, STDOUT, STDERR)
# returns at end of STDIN or on a quit command
shell.read_execute_loop

Defined Under Namespace

Classes: PluginLoadError

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(stdin, stdout, stderr) ⇒ Shell

Returns a new instance of Shell.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/couch-shell/shell.rb', line 68

def initialize(stdin, stdout, stderr)
  @stdin = stdin
  @stdout = stdout
  @stderr = stderr
  @server_url = nil
  @pathstack = []
  @highline = HighLine.new(@stdin, @stdout)
  @responses = RingBuffer.new(10)
  @eval_context = EvalContext.new(self)
  @username = nil
  @password = nil
  @plugins = {}
  @commands = {}
  @variables = {}
  @variable_prefixes = []
  @stdout.puts "couch-shell #{VERSION}"
end

Instance Attribute Details

#passwordObject

Returns the value of attribute password.



66
67
68
# File 'lib/couch-shell/shell.rb', line 66

def password
  @password
end

#pathstackObject (readonly)

Returns the value of attribute pathstack.



64
65
66
# File 'lib/couch-shell/shell.rb', line 64

def pathstack
  @pathstack
end

#responsesObject (readonly)

A CouchShell::RingBuffer holding CouchShell::Response instances.



60
61
62
# File 'lib/couch-shell/shell.rb', line 60

def responses
  @responses
end

#server_urlObject (readonly)

Returns the value of attribute server_url.



61
62
63
# File 'lib/couch-shell/shell.rb', line 61

def server_url
  @server_url
end

#stdinObject (readonly)

Returns the value of attribute stdin.



63
64
65
# File 'lib/couch-shell/shell.rb', line 63

def stdin
  @stdin
end

#stdoutObject (readonly)

Returns the value of attribute stdout.



62
63
64
# File 'lib/couch-shell/shell.rb', line 62

def stdout
  @stdout
end

#usernameObject

Returns the value of attribute username.



65
66
67
# File 'lib/couch-shell/shell.rb', line 65

def username
  @username
end

Instance Method Details

#cd(path, get = false) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/couch-shell/shell.rb', line 168

def cd(path, get = false)
  old_pathstack = @pathstack.dup

  if path
    @pathstack = [] if path.start_with?("/")
    path.split('/').each { |elem|
      case elem
      when ""
        # do nothing
      when "."
        # do nothing
      when ".."
        if @pathstack.empty?
          @pathstack = old_pathstack
          raise ShellUserError, "Already at server root, can't go up"
        end
        @pathstack.pop
      else
        @pathstack << elem
      end
    }
  else
    @pathstack = []
  end

  old_dbname = old_pathstack[0]
  new_dbname = @pathstack[0]
  getdb = false
  if new_dbname && (new_dbname != old_dbname)
    getdb = get && @pathstack.size == 1
    res = request("GET", "/#{new_dbname}", nil, getdb)
    unless res.ok? && (json = res.json_value) &&
        json.object? && json["db_name"] &&
        json["db_name"].unwrapped! == new_dbname
      @pathstack = old_pathstack
      raise ShellUserError, "not a database: #{new_dbname}"
    end
  end
  if get && !getdb
    if request("GET", nil).code != "200"
      @pathstack = old_pathstack
    end
  end
end

#editor_bin!Object



517
518
519
520
# File 'lib/couch-shell/shell.rb', line 517

def editor_bin!
  ENV["EDITOR"] or
    raise ShellUserError, "EDITOR environment variable not set"
end

#errmsg(str) ⇒ Object



222
223
224
# File 'lib/couch-shell/shell.rb', line 222

def errmsg(str)
  @stderr.puts @highline.color(str, :red)
end

#eval_expr(expr) ⇒ Object

Evaluate the given expression.



494
495
496
# File 'lib/couch-shell/shell.rb', line 494

def eval_expr(expr)
  @eval_context.instance_eval(expr)
end

#execute(input) ⇒ Object

When the user enters something, it is passed to this method for execution. You may call it programmatically to simulate user input.

If input is nil, it is interpreted as “end of input”, raising a CouchShell::Shell::Quit exception. This exception is also raised by other commands (e.g. “exit” and “quit”). All other exceptions are caught and displayed on stderr.



384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/couch-shell/shell.rb', line 384

def execute(input)
  begin
    execute!(input)
  rescue Quit => e
    raise e
  rescue Interrupt
    @stdout.puts
    errmsg "interrupted"
  rescue UndefinedVariable => e
    errmsg "Variable `" + e.varname + "' is not defined."
  rescue ShellUserError => e
    errmsg e.message
  rescue Errno::ETIMEDOUT => e
    errmsg "timeout: #{e.message}"
  rescue SocketError, Errno::ENOENT => e
    @stdout.puts
    errmsg "#{e.class}: #{e.message}"
  rescue Exception => e
    #p e.class.instance_methods - Object.instance_methods
    errmsg "#{e.class}: #{e.message}"
    errmsg e.backtrace[0..5].join("\n")
  end
end

#execute!(input) ⇒ Object

Basic execute without error handling. Raises various exceptions.



409
410
411
412
413
414
415
416
417
418
# File 'lib/couch-shell/shell.rb', line 409

def execute!(input)
  case input
  when nil
    raise Quit
  when ""
    # do nothing
  else
    execute_command! *input.split(/\s+/, 2)
  end
end

#execute_command!(commandref, argstr = nil) ⇒ Object



420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'lib/couch-shell/shell.rb', line 420

def execute_command!(commandref, argstr = nil)
  if commandref.start_with?("@")
    # qualified command
    if commandref =~ /\A@([^\.]+)\.([^\.]+)\z/
      plugin_name = $1
      command_name = $2
      plugin = @plugins[plugin_name]
      raise NoSuchPluginRegistered.new(plugin_name) unless plugin
      ci = plugin.plugin_info.commands[command_name]
      raise NoSuchCommandInPlugin.new(plugin_name, command_name) unless ci
      plugin.send ci.execute_message, argstr
    else
      raise ShellUserError, "invalid command syntax"
    end
  else
    # unqualified command
    ci = @commands[commandref]
    raise NoSuchCommand.new(commandref) unless ci
    @plugins[ci.plugin.plugin_name].send ci.execute_message, argstr
  end
end

#expand(url) ⇒ Object



325
326
327
328
# File 'lib/couch-shell/shell.rb', line 325

def expand(url)
  u = @server_url
  "#{u.scheme}://#{u.host}:#{u.port}#{full_path url}"
end

#full_path(path) ⇒ Object



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/couch-shell/shell.rb', line 330

def full_path(path)
  stack = []
  if path !~ %r{\A/}
    stack = @pathstack.dup
  end
  if @server_url.path && !@server_url.path.empty?
    stack.unshift @server_url.path
  end
  if path && !path.empty? && path != "/"
    stack.push path
  end
  fpath = stack.join("/")
  if fpath !~ %r{\A/}
    "/" + fpath
  else
    fpath
  end
end

#http_client_request(method, absolute_url, body) ⇒ Object



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/couch-shell/shell.rb', line 302

def http_client_request(method, absolute_url, body)
  file = nil
  headers = {}
  if body.kind_of?(FileToUpload)
    file_to_upload = body
    file = File.open(file_to_upload.filename, "rb")
    body = [{'Content-Type' => file_to_upload.content_type!,
             'Content-Transfer-Encoding' => 'binary',
             :content => file}]
    #body = {'upload' => file}
  elsif body && body =~ JSON_DOC_START_RX
    headers['Content-Type'] = "application/json"
  end
  hclient = HTTPClient.new
  if @username && @password
    hclient.set_auth lookup_var("server"), @username, @password
  end
  res = hclient.request(method, absolute_url, body, headers)
  Response.new(res)
ensure
  file.close if file
end

#interpolate(str) ⇒ Object



452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
# File 'lib/couch-shell/shell.rb', line 452

def interpolate(str)
  return nil if str.nil?
  String.new.force_encoding(str.encoding).tap { |res|
    escape = false
    dollar = false
    expr = nil
    str.each_char { |c|
      if escape
        res << c
        escape = false
        next
      elsif c == '\\'
        escape = true
      elsif c == '$'
        dollar = true
        next
      elsif c == '('
        if dollar
          expr = ""
        else
          res << c
        end
      elsif c == ')'
        if expr
          res << eval_expr(expr).to_s
          expr = nil
        else
          res << c
        end
      elsif dollar
        res << "$"
      elsif expr
        expr << c
      else
        res << c
      end
      dollar = false
    }
  }
end

#lookup_var(var) ⇒ Object

Lookup unqualified variable name.



499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
# File 'lib/couch-shell/shell.rb', line 499

def lookup_var(var)
  vi = @variables[var]
  if vi
    plugin = @plugins[vi.plugin.plugin_name]
    plugin.send vi.lookup_message
  else
    vi = @variable_prefixes.find { |v|
      var.start_with?(v.prefix) && var.length > v.prefix.length
    }
    raise UndefinedVariable.new(var) unless vi
    plugin = @plugins[vi.plugin.plugin_name]
    plugin.send vi.lookup_message, var[vi.prefix.length]
  end
rescue Plugin::VarNotSet => e
  e.var = vi
  raise e
end

#msg(str, newline = true) ⇒ Object



213
214
215
216
217
218
219
220
# File 'lib/couch-shell/shell.rb', line 213

def msg(str, newline = true)
  @stdout.print @highline.color(str, :blue)
  if newline
    @stdout.puts
  else
    @stdout.flush
  end
end

#net_http_request(method, fpath, body) ⇒ Object



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/couch-shell/shell.rb', line 273

def net_http_request(method, fpath, body)
  res = nil
  Net::HTTP.start(@server_url.host, @server_url.port) do |http|
    req = (case method
           when "GET"
             Net::HTTP::Get
           when "PUT"
             Net::HTTP::Put
           when "POST"
             Net::HTTP::Post
           when "DELETE"
             Net::HTTP::Delete
           else
             raise "unsupported http method: `#{method}'"
           end).new(fpath)
    if @username && @password
      req.basic_auth @username, @password
    end
    if body
      req.body = body
      if req.content_type.nil? && req.body =~ JSON_DOC_START_RX
        req.content_type = "application/json"
      end
    end
    res = Response.new(http.request(req))
  end
  res
end

#normalize_server_url(url) ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
# File 'lib/couch-shell/shell.rb', line 145

def normalize_server_url(url)
  return nil if url.nil?
  # remove trailing slash
  url = url.sub(%r{/\z}, '')
  # prepend http:// if scheme is omitted
  if url =~ /\A\p{Alpha}(?:\p{Alpha}|\p{Digit}|\+|\-|\.)*:\/\//
    url
  else
    "http://#{url}"
  end
end

#plugin(plugin_name) ⇒ Object

Raises:



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/couch-shell/shell.rb', line 86

def plugin(plugin_name)
  # load and instantiate
  feature = "couch-shell-plugin/#{plugin_name}"
  begin
    require feature
  rescue LoadError
    raise PluginLoadError, "feature #{feature} not found"
  end
  pi = PluginInfo[plugin_name]
  raise PluginLoadError, "plugin class not found" unless pi
  plugin = pi.plugin_class.new(self)

  # integrate plugin variables
  ## enable qualified reference via @PLUGIN.VAR syntax
  @eval_context._instance_variable_set(
    :"@#{plugin_name}", plugin.variables_object)
  ## enable unqualified reference
  pi.variables.each { |vi|
    if vi.name
      existing = @variables[vi.name]
      if existing
        warn "When loading plugin #{plugin_name}: " +
          "Variable #{vi.name} already defined by plugin " +
          "#{existing.plugin.plugin_name}\n" +
          "You can access it explicitely via @#{plugin_name}.#{vi.name}"
      else
        @variables[vi.name] = vi
      end
    end
    if vi.prefix
      existing = @variable_prefixes.find { |e| e.prefix == vi.prefix }
      if existing
        warn "When loading plugin #{plugin_name}: " +
          "Variable prefix #{vi.prefix} already defined by plugin " +
          "#{existing.plugin.plugin_name}\n" +
          "You can access it explicitely via @#{plugin_name}.#{vi.prefix}*"
      else
        @variable_prefixes << vi
      end
    end
  }

  # integrate plugin commands
  pi.commands.each_value { |ci|
    existing = @commands[ci.name]
    if existing
      warn "When loading plugin #{plugin_name}: " +
        "Command #{ci.name} already defined by plugin " +
        "#{existing.plugin.plugin_name}\n" +
        "You can access it explicitely via @#{plugin_name}.#{ci.name}"
    else
      @commands[ci.name] = ci
    end
  }

  @plugins[plugin_name] = plugin
  plugin.plugin_initialization
end


231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/couch-shell/shell.rb', line 231

def print_response(res, label = "", show_body = true)
  @stdout.print @highline.color("#{res.code} #{res.message}", :cyan)
  msg " #{label}"
  if show_body
    if res.json
      @stdout.puts res.json_value.to_s(true)
    elsif res.body
      @stdout.puts res.body
    end
  elsif res.body
    msg "body has #{res.body.bytesize} bytes"
  end
end

#prompt_msg(msg, newline = true) ⇒ Object



349
350
351
352
353
354
355
356
# File 'lib/couch-shell/shell.rb', line 349

def prompt_msg(msg, newline = true)
  @stdout.print @highline.color(msg, :yellow)
  if newline
    @stdout.puts
  else
    @stdout.flush
  end
end

#readObject

Displays the standard couch-shell prompt and waits for the user to enter a command. Returns the user input as a string (which may be empty), or nil if the input stream is closed.



361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/couch-shell/shell.rb', line 361

def read
  lead = @pathstack.empty? ? ">>" : @pathstack.join("/") + " >>"
  begin
    @highline.ask(@highline.color(lead, :yellow) + " ") { |q|
      q.readline = true
    }
  rescue Interrupt
    @stdout.puts
    errmsg "interrupted"
    return ""
  rescue NoMethodError
    # this is BAD, but highline 1.6.1 reacts to CTRL+D with a NoMethodError
    return nil
  end
end

#read_execute_loopObject

Start regular shell operation, i.e. reading commands from stdin and executing them. Returns when the user issues a quit command.



444
445
446
447
448
449
450
# File 'lib/couch-shell/shell.rb', line 444

def read_execute_loop
  loop {
    execute(read)
  }
rescue Quit
  msg "bye"
end

#read_secretObject



522
523
524
# File 'lib/couch-shell/shell.rb', line 522

def read_secret
  @highline.ask(" ") { |q| q.echo = "*" }
end

#request(method, path, body = nil, show_body = true) ⇒ Object

Returns CouchShell::Response or raises an exception.



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/couch-shell/shell.rb', line 246

def request(method, path, body = nil, show_body = true)
  unless @server_url
    raise ShellUserError, "Server not set - can't perform request."
  end
  fpath = URI.encode(full_path(path))
  msg "#{method} #{fpath} ", false
  if @server_url.scheme != "http"
    raise ShellUserError,
      "Protocol #{@server_url.scheme} not supported, use http."
  end
  # HTTPClient and CouchDB don't work together with simple put/post
  # requests to due some Keep-alive mismatch.
  #
  # Net:HTTP doesn't support file upload streaming.
  if body.kind_of?(FileToUpload) || method == "GET"
    res = http_client_request(method, URI.encode(expand(path)), body)
  else
    res = net_http_request(method, fpath, body)
  end
  @responses << res
  rescode = res.code
  vars = ["r#{@responses.index}"]
  vars << ["j#{@responses.index}"] if res.json
  print_response res, "  vars: #{vars.join(', ')}", show_body
  res
end

#server=(url) ⇒ Object



157
158
159
160
161
162
163
164
165
166
# File 'lib/couch-shell/shell.rb', line 157

def server=(url)
  if url
    @server_url = URI.parse(normalize_server_url(url))
    msg "Set server to #{lookup_var 'server'}"
    request("GET", nil)
  else
    @server_url = nil
    msg "Set server to none."
  end
end

#warn(str) ⇒ Object



226
227
228
229
# File 'lib/couch-shell/shell.rb', line 226

def warn(str)
  @stderr.print @highline.color("warning: ", :red)
  @stderr.puts @highline.color(str, :blue)
end