elasticshell 0.0.1

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/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: