elasticshell 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Dhruv Bansal
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,207 @@
1
+ = Elasticshell
2
+
3
+ Elasticsearch[http://www.elasticsearch.org] is a wonderful database
4
+ for performing full-text on rich documents at terabyte-scale.
5
+
6
+ It's already pretty easy to talk to Elasticsearch. You can
7
+
8
+ - use the HTTP-based, {REST
9
+ API}[http://www.elasticsearch.org/guide/reference/api/] via
10
+ commmand-line tools like curl[http://en.wikipedia.org/wiki/CURL], your favorite HTTP library, or even
11
+ your browser's URL bar
12
+
13
+ - use the interface built on Apache Thrift
14
+
15
+ - use the native {Java classes}[http://www.elasticsearch.org/guide/reference/java-api/]
16
+
17
+ What's missing was a command-line shell that let you directly inspect
18
+ Elasticsearch's "filesystem" or "database schema", run queries, and in
19
+ general muck about. I got sick of writing things like
20
+
21
+ $ curl -s -X GET "http://localhost:9200/_status" | ruby -rjson -e 'puts JSON.parse($stdin.read)["indices"]["my_index"]["docs"]["num_docs"]'
22
+
23
+ How about
24
+
25
+ $ es /_status --only=indices.my_index.docs.num_docs
26
+
27
+ == Installation
28
+
29
+ Installing Elasticshell should be as simple as installing the gem:
30
+
31
+ $ sudo gem install elasticshell
32
+
33
+ This should install a binary program 'es' that you can run from the
34
+ command line to start Elasticshell. Try
35
+
36
+ $ es --help
37
+
38
+ right now to see that everything is properly installed. You'll also
39
+ see a brief survey of Elasticshell's startup options.
40
+
41
+ == Usage
42
+
43
+ To start an Elasticshell session, just run
44
+
45
+ $ es
46
+
47
+ Elasticshell will automatically try to connect to a local
48
+ Elasticsearch database running on the default port. You can modify
49
+ this with the startup options. Type +help+ at any time to get some
50
+ contextual help from Elasticshell.
51
+
52
+ Within Elasticshell, there are three variables whose values affect
53
+ behavior. These variables are reflected in the default prompt, for
54
+ example:
55
+
56
+ GET /my_index/my_type >
57
+
58
+ This prompt tells us three things:
59
+
60
+ 1. The default HTTP verb we're using for requests is +GET+.
61
+
62
+ 2. The default API "scope" we're in is <tt>/my_index/my_type</tt>.
63
+
64
+ 3. Elasticshell will print raw responses from the database -- this is the <tt>></tt> at the end of the prompt. If we were in pretty-print mode, this would become a <tt>$</tt>.
65
+
66
+ === Changing Scope
67
+
68
+ Use the +cd+ built-in to move between scopes:
69
+
70
+ GET /my_index/my_type > cd /other_index/other_type
71
+ GET /other_index/other_type > cd ..
72
+ GET /other_index > cd
73
+ GET / >
74
+
75
+ Tab-complete within a scope after typing +cd+ to see what other scopes
76
+ live under this one.
77
+
78
+ === Changing HTTP Verb
79
+
80
+ You can change Elasticsearch's default HTTP verb by giving it one.
81
+ Here's the same thing in two steps:
82
+
83
+ GET / > PUT
84
+ PUT / > /my_new_index
85
+
86
+ Non-ambiguous shortcuts for HTTP verbs will also work, e.g. - +pu+ in
87
+ this case for +PUT+.
88
+
89
+ === Changing Prettiness
90
+
91
+ Typing +pretty+ at any time will toggle Elasticsearch's
92
+ pretty-printing format on or off.
93
+
94
+ GET / > pretty
95
+ GET / $
96
+
97
+ The <tt>$</tt>-sign means it's pretty...
98
+
99
+ === Running Commands
100
+
101
+ There are a lot of different ways of telling Elasticsearch what you
102
+ want done.
103
+
104
+ ==== Named commands
105
+
106
+ Each scope has different commands, as per the {Elasticsearch API
107
+ documentation}[http://www.elasticsearch.org/guide/reference/api/].
108
+ Within a scope, tab-complete on the first word to see a list of
109
+ possible commands. Hit enter after a command to see output from
110
+ Elasticsearch.
111
+
112
+ Here's a command to get the status for the cluster:
113
+
114
+ GET / > _status
115
+
116
+ Here's a command to get the health of the cluster:
117
+
118
+ GET / > cd _cluster
119
+ GET /_cluster > health
120
+
121
+ ==== Commands with query strings
122
+
123
+ Commands will also accept a query string, as in this example of a
124
+ search through +my_index+:
125
+
126
+ GET /my_index > _search?q=foo+AND+bar
127
+
128
+ ==== Commands with query bodies
129
+
130
+ In this example the query <tt>foo AND bar</tt> was passed via the
131
+ query string part of a URL. Passing a more complex query requires we
132
+ put the query in the body of the request. If you're willing to forego
133
+ using spaces you can do this right on the same line:
134
+
135
+ GET /my_index > _search {"query":{"query_string":{"query":"foo"}}}
136
+
137
+ But if you want more expressiveness you can either name a file (with
138
+ tab-completion) that contains the body you want:
139
+
140
+ # in /tmp/query.json
141
+ {
142
+ "query": {
143
+ "query_string: {
144
+ "query": "foo AND bar"
145
+ }
146
+ }
147
+ }
148
+
149
+ followed by
150
+
151
+ GET /my_index > _search /tmp/query.json
152
+
153
+ Or you can do +cat+-style, pasting the query into the shell, by using
154
+ the <tt>-</tt> character:
155
+
156
+ GET /my_index > _search -
157
+ {
158
+ "query": {
159
+ "query_string: {
160
+ "query": "foo AND bar"
161
+ }
162
+ }
163
+ }
164
+
165
+ Don't forget to use <tt>Ctrl-D</tt> to send an +EOF+ to flush the
166
+ input of the query.
167
+
168
+ ==== Arbitrary commands
169
+
170
+ You can send an arbitrary HTTP request to Elasticsearch, just spell
171
+ the command with a leading slash:
172
+
173
+ GET / > /my_index/_search?q=foo+AND+bar
174
+
175
+ You can specify a different HTTP verb by prefixing it before the path
176
+ you send the request to. Here's how to create an index using a +PUT+
177
+ request:
178
+
179
+ GET / > PUT /my_new_index
180
+
181
+ You can also change Elasticsearch's default HTTP verb by giving it
182
+ one. Here's the same thing in two steps:
183
+
184
+ GET / > PUT
185
+ PUT / > /my_new_index
186
+
187
+ Non-ambiguous shortcuts for HTTP verbs will also work, e.g. - +pu+ in
188
+ this case for +PUT+.
189
+
190
+ === Running just a single command
191
+
192
+ Instead of running Elasticshell interactively, you can exit after
193
+ running only a single command by using the <tt>--only</tt> option on
194
+ startup. For example,
195
+
196
+ $ es --only /_cluster/health
197
+
198
+ will output the cluster health and exit immediately. This can be
199
+ combined with the <tt>--pretty</tt> option for readability.
200
+
201
+ The <tt>--only</tt> option can also be passed a <tt>.</tt>-separated
202
+ hierarchical list of keys to slice into the resulting object. This is
203
+ useful when trying to drill into a large amount of data returned by
204
+ Elasticsearch. The example from the start of this file is relevant
205
+ again here:
206
+
207
+ $ es /_status --only=indices.my_index.docs.num_docs
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/bin/es ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.expand_path('../../lib', __FILE__) unless $:.include?($: << File.expand_path('../../lib', __FILE__))
4
+ require 'elasticshell'
5
+
6
+ Elasticshell.start if __FILE__ == $0
@@ -0,0 +1,45 @@
1
+ require 'rubberband'
2
+ require 'elasticshell/error'
3
+ require 'elasticshell/log'
4
+
5
+ module Elasticshell
6
+
7
+ class Client
8
+
9
+ DEFAULT_SERVERS = ['http://localhost:9200']
10
+
11
+ def initialize options={}
12
+ @client ||= ::ElasticSearch::Client.new(options[:servers] || DEFAULT_SERVERS)
13
+ end
14
+
15
+ def request verb, params={}, options={}, body=''
16
+ safe = options.delete(:safely)
17
+ safe_return = options.delete(:return)
18
+ # log_request(verb, params, options)
19
+ begin
20
+ @client.execute(:standard_request, verb, params, options, body)
21
+ rescue ElasticSearch::RequestError, ArgumentError => e
22
+ if safe
23
+ safe_return
24
+ else
25
+ raise ClientError.new(e.message)
26
+ end
27
+ end
28
+ end
29
+
30
+ def log_request verb, params, options={}
31
+ # FIXME digging way too deep into rubberband here...is it really
32
+ # necessary?
33
+ uri = @client.instance_variable_get('@connection').send(:generate_uri, params)
34
+ query = @client.instance_variable_get('@connection').send(:generate_query_string, options)
35
+ path = [uri, query].reject { |s| s.nil? || s.strip.empty? }.join("?")
36
+ Elasticshell.log("#{verb.to_s.upcase} #{path}")
37
+ end
38
+
39
+ def safely verb, params={}, options={}, body=''
40
+ request(verb, params, options.merge(:safely => true))
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,191 @@
1
+ require 'elasticshell/error'
2
+ require 'uri'
3
+
4
+ module Elasticshell
5
+
6
+ class Command
7
+
8
+ HTTP_VERB_RE = "(?:G(?:ET?)?|PO(?:ST?)?|PUT?|D(?:E(?:L(?:E(?:TE?)?)?)?)?)"
9
+
10
+ attr_accessor :shell, :input
11
+
12
+ def initialize shell, input
13
+ self.shell = shell
14
+ self.input = input
15
+ end
16
+
17
+ def evaluate!
18
+ case
19
+ when setting_scope? then set_scope!
20
+ when setting_http_verb? then set_http_verb!
21
+ when making_explicit_req? then make_explicit_req!
22
+ when pretty? then pretty!
23
+ when help? then help!
24
+ when ls? then ls!
25
+ when blank? then nil
26
+ when scope_command? then run_scope_command!
27
+ else
28
+ raise ArgumentError.new("Unknown command '#{input}' for scope '#{shell.scope.path}'. Try typing 'help' for a list of available commands.")
29
+ end
30
+ end
31
+
32
+ def setting_scope?
33
+ input =~ /^cd/
34
+ end
35
+
36
+ def set_scope!
37
+ if input =~ /^cd$/
38
+ shell.scope = Scopes.global(:client => shell.client)
39
+ return
40
+ end
41
+
42
+ return unless input =~ /^cd\s+(.+)$/
43
+ scope = $1
44
+ if scope =~ %r!^/!
45
+ shell.scope = Scopes.from_path(scope, :client => shell.client)
46
+ else
47
+ shell.scope = Scopes.from_path(File.expand_path(File.join(shell.scope.path, scope)), :client => shell.client)
48
+ end
49
+ end
50
+
51
+ def setting_http_verb?
52
+ input =~ Regexp.new("^" + HTTP_VERB_RE + "$", true)
53
+ end
54
+
55
+ def canonicalize_http_verb v
56
+ case v
57
+ when /^G/i then "GET"
58
+ when /^PO/i then "POST"
59
+ when /^PU/i then "PUT"
60
+ when /^D/i then "DELETE"
61
+ end
62
+ end
63
+
64
+ def set_http_verb!
65
+ shell.verb = canonicalize_http_verb(input)
66
+ end
67
+
68
+ def scope_command?
69
+ shell.scope.command?(input)
70
+ end
71
+
72
+ def run_scope_command!
73
+ shell.scope.execute(input, shell)
74
+ end
75
+
76
+ def blank?
77
+ input.empty?
78
+ end
79
+
80
+ def help?
81
+ input =~ /^help/i
82
+ end
83
+
84
+ def help!
85
+ shell.scope.refresh
86
+ shell.print <<HELP
87
+
88
+ Globally available commands:
89
+
90
+ cd [PATH]
91
+ Change scope to the given path. Current path is reflected in the
92
+ prompt (it's '#{shell.scope.path}' right now).
93
+
94
+ Ex:
95
+ GET / > cd /my_index
96
+ GET /my_index > cd /other_index/some_type
97
+ GET /other_index/some_type
98
+
99
+ [get|post|put|delete]
100
+ Set the default HTTP verb (can use a non-ambiguous shortcut like 'g'
101
+ for 'GET' or 'pu' for 'PUT'). Current default HTTP verb is '#{shell.verb}'.
102
+
103
+ ls
104
+ Show what indices or mappings are within the current scope.
105
+
106
+ help
107
+ Show contextual help.
108
+
109
+ [VERB] PATH
110
+ Send an HTTP request with the given VERB to the given PATH
111
+ (including query string if given). If no verb is given, use the
112
+ default.
113
+
114
+ Ex: Simple search
115
+ GET / > /my_index/_search?q=query+string
116
+ {...}
117
+
118
+ Ex: Create an index
119
+ GET / > PUT /my_new_index
120
+ {...}
121
+
122
+ or
123
+
124
+ GET / > put
125
+ PUT / > /my_new_index
126
+ {...}
127
+
128
+ #{shell.scope.help}
129
+ HELP
130
+ end
131
+
132
+ def ls?
133
+ input =~ /^l(s|l|a)?$/i
134
+ end
135
+
136
+ def ls!
137
+ shell.scope.refresh!
138
+ case
139
+ when input =~ /ll/
140
+ shell.print shell.scope.contents.join("\n")
141
+ else
142
+ shell.print shell.scope.contents.join(' ')
143
+ end
144
+ end
145
+
146
+ def pretty?
147
+ input =~ /pretty/i
148
+ end
149
+
150
+ def pretty!
151
+ if shell.pretty?
152
+ shell.not_pretty!
153
+ else
154
+ shell.pretty!
155
+ end
156
+ end
157
+
158
+ def making_explicit_req?
159
+ input =~ Regexp.new("^(" + HTTP_VERB_RE + "\s+)?/", true)
160
+ end
161
+
162
+ def make_explicit_req!
163
+ if input =~ Regexp.new("^(" + HTTP_VERB_RE + ")\s+(.+)$", true)
164
+ verb, path_and_query = canonicalize_http_verb($1), $2
165
+ else
166
+ verb, path_and_query = shell.verb, input
167
+ end
168
+ path, query = path_and_query.split('?')
169
+
170
+ params = {}
171
+ keys = [:index, :type, :id, :op]
172
+ parts = path.gsub(%r!^/!,'').gsub(%r!/$!,'').split('/')
173
+ while parts.size > 0
174
+ part = parts.shift
175
+ key = (keys.shift or ArgumentError.new("The input '#{path}' has too many path components."))
176
+ params[key] = part
177
+ end
178
+
179
+ options = {}
180
+ URI.decode_www_form(query || '').each do |key, value|
181
+ options[key] = value
182
+ end
183
+
184
+ shell.print(shell.client.request(verb.downcase.to_sym, params, options))
185
+ end
186
+
187
+
188
+ end
189
+
190
+ end
191
+
@@ -0,0 +1,8 @@
1
+ module Elasticshell
2
+
3
+ Error = Class.new(StandardError)
4
+ ArgumentError = Class.new(Error)
5
+ NotImplementedError = Class.new(Error)
6
+ ClientError = Class.new(Error)
7
+
8
+ end
@@ -0,0 +1,9 @@
1
+ module Elasticshell
2
+
3
+ def self.log msg
4
+ $stderr.puts("\n" + msg + "\n")
5
+ end
6
+
7
+ end
8
+
9
+
@@ -0,0 +1,38 @@
1
+ require 'elasticshell/scopes'
2
+
3
+ module Elasticshell
4
+
5
+ module Scopes
6
+
7
+ class Cluster < Scope
8
+
9
+ def initialize options={}
10
+ super("/_cluster", options)
11
+ end
12
+
13
+ def commands
14
+ {
15
+ 'health' => "Retreive the health of the cluster.",
16
+ 'state' => "Retreive the state of the cluster.",
17
+ 'settings'=> "Retreive the settings for the cluster.",
18
+ }
19
+ end
20
+
21
+ def exists?
22
+ true
23
+ end
24
+
25
+ def execute command, shell
26
+ case
27
+ when command?(command)
28
+ shell.request(:get, :index => '_cluster')
29
+ else
30
+ super(command, shell)
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
37
+
38
+
@@ -0,0 +1,54 @@
1
+ require 'elasticshell/scopes'
2
+
3
+ module Elasticshell
4
+
5
+ module Scopes
6
+
7
+ class Global < Scope
8
+
9
+ def initialize options={}
10
+ super("/", options)
11
+ end
12
+
13
+ def commands
14
+ {
15
+ '_status' => "Retreive the status of all indices in the cluster."
16
+ }
17
+ end
18
+
19
+ def initial_contents
20
+ ['_cluster', '_nodes']
21
+ end
22
+
23
+ def fetch_contents
24
+ self.contents += client.safely(:get, {:index => '_status'}, :return => {"indices" => {}})["indices"].keys
25
+ end
26
+
27
+ def index name, options={}
28
+ Scopes.index(name, options, :client => client)
29
+ end
30
+
31
+ def exists?
32
+ true
33
+ end
34
+
35
+ def execute command, shell
36
+ case
37
+ when command =~ /^_cluster/
38
+ shell.scope = Scopes.cluster(:client => client)
39
+ when command =~ /^_nodes/
40
+ shell.scope = Scopes.nodes(:client => client)
41
+ when command?(command)
42
+ shell.request(:get)
43
+ when index_names.include?(command)
44
+ shell.scope = index(command)
45
+ else
46
+ super(command, shell)
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
53
+
54
+
@@ -0,0 +1,61 @@
1
+ require 'elasticshell/scopes'
2
+
3
+ module Elasticshell
4
+
5
+ module Scopes
6
+
7
+ class Index < Scope
8
+
9
+ VALID_INDEX_NAME_RE = %r![^/]!
10
+
11
+ def initialize name, options={}
12
+ self.name = name
13
+ super("/#{self.name}", options)
14
+ end
15
+
16
+ attr_reader :name
17
+ def name= name
18
+ raise ArgumentError.new("Invalid index name: '#{name}'") unless name =~ VALID_INDEX_NAME_RE
19
+ @name = name
20
+ end
21
+
22
+ def commands
23
+ {
24
+ "_aliases" => "Find the aliases for this index.",
25
+ "_status" => "Retrieve the status of this index.",
26
+ "_stats" => "Retrieve usage stats for this index.",
27
+ "_search" => "Search records within this index.",
28
+ }
29
+ end
30
+
31
+ def global
32
+ @global ||= Scopes.global(:client => client)
33
+ end
34
+
35
+ def exists?
36
+ global.refresh
37
+ global.contents.include?(name)
38
+ end
39
+
40
+ def fetch_contents
41
+ @contents = (client.safely(:get, {:index => name, :op => '_mapping'}, :return => { name => {}})[name] || {}).keys
42
+ end
43
+
44
+ def mapping mapping_name, options={}
45
+ Scopes.mapping(self.name, mapping_name, options.merge(:client => client))
46
+ end
47
+
48
+ def execute command, shell
49
+ case
50
+ when command?(command)
51
+ shell.request(:get, :index => name)
52
+ when mapping_names.include?(command)
53
+ shell.scope = mapping(command)
54
+ else
55
+ super(command, shell)
56
+ end
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,55 @@
1
+ require 'elasticshell/scopes'
2
+
3
+ module Elasticshell
4
+
5
+ module Scopes
6
+
7
+ class Mapping < Scope
8
+
9
+ VALID_MAPPING_NAME_RE = %r![^/]!
10
+
11
+ attr_accessor :index
12
+
13
+ def initialize index, name, options={}
14
+ self.index = index
15
+ self.name = name
16
+ super("/#{index.name}/#{self.name}", options)
17
+ end
18
+
19
+ attr_reader :name
20
+ def name= name
21
+ raise ArgumentError.new("Invalid mapping name: '#{name}'") unless name =~ VALID_MAPPING_NAME_RE
22
+ @name = name
23
+ end
24
+
25
+ def commands
26
+ {
27
+ "_search" => "Search records within this mapping.",
28
+ "_mapping" => "Retrieve the mapping settings for this mapping.",
29
+ }
30
+ end
31
+
32
+ def exists?
33
+ index.refresh
34
+ index.contents.include?(name)
35
+ end
36
+
37
+ def command? command
38
+ true
39
+ end
40
+
41
+ def execute command, shell
42
+ case
43
+ when command?(command)
44
+ shell.request(:get, :index => index.name, :type => name)
45
+ else
46
+ record = shell.request(:get, :index => index.name, :type => name)
47
+ shell.print(record) if record
48
+ end
49
+ end
50
+
51
+ end
52
+ end
53
+ end
54
+
55
+
@@ -0,0 +1,37 @@
1
+ require 'elasticshell/scopes'
2
+
3
+ module Elasticshell
4
+
5
+ module Scopes
6
+
7
+ class Nodes < Scope
8
+
9
+ def initialize options={}
10
+ super("/_nodes", options)
11
+ end
12
+
13
+ def commands
14
+ {
15
+ 'info' => "Retreive info about the cluster's ndoes.",
16
+ 'stats' => "Retreive stats for the cluter's nodes.",
17
+ }
18
+ end
19
+
20
+ def exists?
21
+ true
22
+ end
23
+
24
+ def execute command, shell
25
+ case
26
+ when command?(command)
27
+ shell.request(:get, :index => '_nodes')
28
+ else
29
+ super(command, shell)
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
36
+
37
+
@@ -0,0 +1,155 @@
1
+ require 'elasticshell/error'
2
+ require 'uri'
3
+
4
+ module Elasticshell
5
+
6
+ module Scopes
7
+
8
+ autoload :Global, 'elasticshell/scopes/global'
9
+ autoload :Cluster, 'elasticshell/scopes/cluster'
10
+ autoload :Nodes, 'elasticshell/scopes/nodes'
11
+ autoload :Index, 'elasticshell/scopes/index'
12
+ autoload :Mapping, 'elasticshell/scopes/mapping'
13
+
14
+ def self.global options={}
15
+ Global.new(options)
16
+ end
17
+
18
+ def self.cluster options={}
19
+ Cluster.new(options)
20
+ end
21
+
22
+ def self.nodes options={}
23
+ Nodes.new(options)
24
+ end
25
+
26
+ def self.index name, options={}
27
+ Index.new(name, options)
28
+ end
29
+
30
+ def self.mapping index_name, mapping_name, options={}
31
+ Mapping.new(index(index_name, options), mapping_name, options)
32
+ end
33
+
34
+ def self.from_path path, options={}
35
+ segments = path.to_s.strip.gsub(%r!^/!,'').gsub(%r!/$!,'').split('/')
36
+ case
37
+ when segments.length == 0
38
+ global(options)
39
+ when segments.length == 1 && segments.first == '_cluster'
40
+ cluster(options)
41
+ when segments.length == 1 && segments.first == '_nodes'
42
+ nodes(options)
43
+ when segments.length == 1
44
+ index(segments.first, options)
45
+ when segments.length == 2
46
+ mapping(segments[0], segments[1], options)
47
+ else
48
+ raise ArgumentError.new("'#{path}' does not define a valid path for a scope.")
49
+ end
50
+ end
51
+ end
52
+
53
+ class Scope
54
+
55
+ attr_accessor :path, :client, :last_refresh_at, :contents
56
+
57
+ def initialize path, options
58
+ self.path = path
59
+ self.client = options[:client]
60
+ self.contents = initial_contents
61
+ end
62
+
63
+ def to_s
64
+ self.path
65
+ end
66
+
67
+ def completion_proc
68
+ Proc.new do |prefix|
69
+ refresh
70
+ case
71
+ when Readline.line_buffer =~ /^\s*cd\s+\S*$/
72
+ contents.find_all do |content|
73
+ content[0...prefix.length] == prefix
74
+ end
75
+ when Readline.line_buffer =~ /^\s*\S*$/
76
+ command_names.find_all do |command_name|
77
+ command_name[0...prefix.length] == prefix
78
+ end
79
+ else
80
+ Dir[prefix + '*']
81
+ end
82
+ end
83
+ end
84
+
85
+ def command_names
86
+ refresh
87
+ commands.keys.sort
88
+ end
89
+
90
+ def commands
91
+ {}
92
+ end
93
+
94
+ def refresh
95
+ refresh! unless refreshed?
96
+ end
97
+
98
+ def refresh!
99
+ reset!
100
+ fetch_contents
101
+ self.last_refresh_at = Time.now
102
+ true
103
+ end
104
+
105
+ def fetch_contents
106
+ end
107
+
108
+ def initial_contents
109
+ []
110
+ end
111
+
112
+ def reset!
113
+ self.contents = initial_contents
114
+ true
115
+ end
116
+
117
+ def refreshed?
118
+ self.last_refresh_at
119
+ end
120
+
121
+ def exists?
122
+ false
123
+ end
124
+
125
+ def command? command
126
+ command_names.any? do |command_name|
127
+ command[0...command_name.length] == command_name
128
+ end
129
+ end
130
+
131
+ def execute command, shell
132
+ if command_names.include?(command)
133
+ raise NotImplementedError.new("Have not yet implemented '#{command}' for scope '#{path}'.")
134
+ else
135
+ raise ArgumentError.new("No such command '#{command}' in scope '#{path}'.")
136
+ end
137
+ end
138
+
139
+ def help
140
+ [].tap do |msg|
141
+ msg << "Commands specific to the scope '#{path}':"
142
+ msg << ''
143
+ commands.each_pair do |command_name, description|
144
+ msg << ' ' + command_name
145
+ msg << (' ' + description)
146
+ msg << ''
147
+ end
148
+ end.join("\n")
149
+ end
150
+
151
+ end
152
+
153
+ end
154
+
155
+
@@ -0,0 +1,189 @@
1
+ require 'readline'
2
+ require 'uri'
3
+
4
+ require 'elasticshell/command'
5
+ require 'elasticshell/scopes'
6
+ require 'elasticshell/client'
7
+
8
+ module Elasticshell
9
+
10
+ class Shell
11
+
12
+ VERBS = %w[GET POST PUT DELETE]
13
+
14
+ attr_accessor :client, :input, :command, :state, :only
15
+
16
+ attr_reader :verb
17
+ def verb= v
18
+ raise ArgumentError.new("'#{v}' is not a valid HTTP verb. Must be one of: #{VERBS.join(', ')}") unless VERBS.include?(v.upcase)
19
+ @verb = v.upcase
20
+ end
21
+
22
+ attr_reader :scope
23
+ def scope= scope
24
+ @scope = scope
25
+ proc = scope.completion_proc
26
+ Readline.completion_proc = Proc.new do |prefix|
27
+ self.state = :completion
28
+ proc.call(prefix)
29
+ end
30
+ end
31
+
32
+ def initialize options={}
33
+ self.state = :init
34
+ self.client = Client.new(options)
35
+ self.verb = (options[:verb] || 'GET')
36
+ self.scope = Scopes.from_path((options[:scope] || '/'), :client => self.client)
37
+ self.only = options[:only]
38
+ pretty! if options[:pretty]
39
+ end
40
+
41
+ def prompt
42
+ "\e[1m#{prompt_verb_color}#{verb} #{prompt_scope_color}#{scope.path} #{prompt_prettiness_indicator} \e[0m"
43
+ end
44
+
45
+ def prompt_scope_color
46
+ scope.exists? ? "\e[32m" : "\e[33m"
47
+ end
48
+
49
+ def prompt_verb_color
50
+ verb == "GET" ? "\e[34m" : "\e[31m"
51
+ end
52
+
53
+ def prompt_prettiness_indicator
54
+ pretty? ? '$' : '>'
55
+ end
56
+
57
+ def pretty?
58
+ @pretty
59
+ end
60
+
61
+ def pretty!
62
+ @pretty = true
63
+ end
64
+
65
+ def not_pretty!
66
+ @pretty = false
67
+ end
68
+
69
+ def setup
70
+ trap("INT") do
71
+ int
72
+ end
73
+
74
+ Readline.completer_word_break_characters = " \t\n\"\\'`$><=|&{("
75
+
76
+ puts <<EOF
77
+ Elasticshell v. #{Elasticshell.version}
78
+ Type "help" for contextual help.
79
+ EOF
80
+ end
81
+
82
+ def run
83
+ setup
84
+ loop
85
+ end
86
+
87
+ def loop
88
+ self.state = :read
89
+ while line = Readline.readline(prompt, true)
90
+ eval_line(line)
91
+ end
92
+ end
93
+
94
+ def eval_line line
95
+ begin
96
+ self.input = line.strip
97
+ self.command = Command.new(self, input)
98
+ self.state = :eval
99
+ self.command.evaluate!
100
+ rescue ::Elasticshell::Error => e
101
+ $stderr.puts e.message
102
+ end
103
+ self.state = :read
104
+ end
105
+
106
+ def print obj, ignore_only=false
107
+ if self.only && !ignore_only
108
+ if self.only == true
109
+ print(obj, true)
110
+ else
111
+ only_parts = self.only.to_s.split('.')
112
+ obj_to_print = obj
113
+ while obj_to_print && only_parts.size > 0
114
+ this_only = only_parts.shift
115
+ obj_to_print = (obj_to_print || {})[this_only]
116
+ end
117
+ print(obj_to_print, true)
118
+ end
119
+ else
120
+ case obj
121
+ when nil
122
+ when String, Fixnum
123
+ puts obj
124
+ else
125
+ if pretty?
126
+ puts JSON.pretty_generate(obj)
127
+ else
128
+ puts obj.to_json
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ def int
135
+ case self.state
136
+ when :read
137
+ $stdout.write("^C\n#{prompt}")
138
+ else
139
+ $stdout.write("^C...aborted\n#{prompt}")
140
+ end
141
+ end
142
+
143
+ def clear_line
144
+ while Readline.point > 0
145
+ $stdin.write("\b \b")
146
+ end
147
+ end
148
+
149
+ def die
150
+ puts "C-d"
151
+ print("C-d...quitting")
152
+ exit()
153
+ end
154
+
155
+ def command_and_query_and_body command
156
+ parts = command.split
157
+
158
+ c_and_q = parts[0]
159
+ c, q = c_and_q.split('?')
160
+ o = {}
161
+ URI.decode_www_form(q || '').each do |k, v|
162
+ o[k] = v
163
+ end
164
+
165
+ path = parts[1]
166
+ case
167
+ when path && File.exist?(path) && File.readable?(path)
168
+ b = File.read(path)
169
+ when path && path == '-'
170
+ b = $stdin.gets(nil)
171
+ when path
172
+ b = path
173
+ else
174
+ b = (command.split(' ', 2).last || '')
175
+ end
176
+
177
+ [c, o, b]
178
+ end
179
+
180
+ def request verb, params={}
181
+ c, o, b = command_and_query_and_body(input)
182
+ body = (params.delete(:body) || b || '')
183
+ print(client.request(verb, params.merge(:op => c), o, b))
184
+ end
185
+
186
+ end
187
+
188
+ end
189
+
@@ -0,0 +1,59 @@
1
+ require 'rubygems'
2
+ require 'json'
3
+ require 'configliere'
4
+
5
+ require 'elasticshell/shell'
6
+ require 'elasticshell/scopes'
7
+ require 'elasticshell/client'
8
+
9
+ Settings.use(:commandline)
10
+
11
+ Settings.define(:servers, :description => "A comma-separated list of Elasticsearch servers to connect to.", :type => Array, :default => Elasticshell::Client::DEFAULT_SERVERS)
12
+ Settings.define(:only, :description => "A dot-separated hierarchical key to extract from the output scope.")
13
+ Settings.define(:pretty, :description => "Pretty-print all output. ", :default => false, :type => :boolean)
14
+ Settings.define(:verb, :description => "Set the default HTTP verb. ", :default => "GET")
15
+ Settings.define(:version, :description => "Print Elasticshell version and exit. ", :default => false, :type => :boolean)
16
+ Settings.description = <<-DESC
17
+ Elasticshell is a command-line shell for interacting with an
18
+ Elasticsearch database. It has the following start-up options.
19
+ DESC
20
+
21
+ def Settings.usage
22
+ "usage: #{File.basename($0)} [OPTIONS] [SCOPE]"
23
+ end
24
+ Settings.resolve!
25
+
26
+ module Elasticshell
27
+
28
+ def self.version
29
+ @version ||= begin
30
+ File.read(File.expand_path('../../VERSION', __FILE__)).chomp
31
+ rescue => e
32
+ 'unknown'
33
+ end
34
+ end
35
+
36
+ def self.start *args
37
+ if Settings[:version]
38
+ puts version
39
+ exit()
40
+ end
41
+
42
+ es = Shell.new(Settings)
43
+ if Settings[:only]
44
+ if Settings.rest.length == 0
45
+ $stderr.puts "Starting with the --only option requires the first argument to name an API path (like `/_cluster/health')"
46
+ exit(1)
47
+ else
48
+ es.eval_line(Settings.rest.first)
49
+ exit()
50
+ end
51
+ else
52
+ if Settings.rest.length > 0
53
+ es.scope = Scopes.from_path(Settings.rest.first, :client => es.client)
54
+ end
55
+ es.run
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+
3
+ describe Command do
4
+
5
+ end
6
+
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Scopes do
4
+
5
+ it "should be able to recognize the global scope" do
6
+ Scopes::Global.should_receive(:new).with(kind_of(Hash))
7
+ Scopes.from_path("/")
8
+ end
9
+
10
+ it "should be able to recognize an index scope" do
11
+ Scopes::Index.should_receive(:new).with('foobar', kind_of(Hash))
12
+ Scopes.from_path("/foobar")
13
+ end
14
+
15
+ it "should be able to recognize a mapping scope" do
16
+ index = mock("Index /foobar")
17
+ Scopes::Index.should_receive(:new).with('foobar', kind_of(Hash)).and_return(index)
18
+ Scopes::Mapping.should_receive(:new).with(index, 'baz', kind_of(Hash))
19
+ Scopes.from_path("/foobar/baz")
20
+ end
21
+
22
+ end
@@ -0,0 +1,12 @@
1
+ require 'rspec'
2
+
3
+ ELASTICSHELL_ROOT = File.expand_path(__FILE__, '../../lib')
4
+ $: << ELASTICSHELL_ROOT unless $:.include?(ELASTICSHELL_ROOT)
5
+ require 'elasticshell'
6
+ include Elasticshell
7
+
8
+ Dir[File.expand_path('../support/**/*.rb', __FILE__)].each { |path| require path }
9
+
10
+ RSpec.configure do |config|
11
+ config.mock_with :rspec
12
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: elasticshell
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dhruv Bansal
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-05 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: configliere
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rubberband
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: Elasticshell provides a command-line shell 'es' for connecting to and
79
+ querying an Elasticsearch database. The shell will tab-complete Elasticsearch API
80
+ commands and index/mapping names.
81
+ email:
82
+ - dhruv@infochimps.com
83
+ executables:
84
+ - es
85
+ extensions: []
86
+ extra_rdoc_files: []
87
+ files:
88
+ - bin/es
89
+ - lib/elasticshell/scopes/cluster.rb
90
+ - lib/elasticshell/scopes/mapping.rb
91
+ - lib/elasticshell/scopes/global.rb
92
+ - lib/elasticshell/scopes/index.rb
93
+ - lib/elasticshell/scopes/nodes.rb
94
+ - lib/elasticshell/command.rb
95
+ - lib/elasticshell/log.rb
96
+ - lib/elasticshell/shell.rb
97
+ - lib/elasticshell/client.rb
98
+ - lib/elasticshell/error.rb
99
+ - lib/elasticshell/scopes.rb
100
+ - lib/elasticshell.rb
101
+ - spec/elasticshell/scopes_spec.rb
102
+ - spec/elasticshell/command_spec.rb
103
+ - spec/spec_helper.rb
104
+ - LICENSE
105
+ - README.rdoc
106
+ - VERSION
107
+ homepage: http://github.com/dhruvbansal/elasticshell
108
+ licenses: []
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ! '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ none: false
121
+ requirements:
122
+ - - ! '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubyforge_project:
127
+ rubygems_version: 1.8.23
128
+ signing_key:
129
+ specification_version: 3
130
+ summary: A command-line shell for Elasticsearch
131
+ test_files: []
132
+ has_rdoc: