├── .gitignore ├── README.md ├── build.sbt ├── images └── shell-demo.gif └── src └── main └── scala ├── Client.scala ├── Completions.scala ├── Error.scala ├── Main.scala ├── Parse.scala └── Request.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .ensime 17 | .ensime_cache/ 18 | .scala_dependencies 19 | .worksheet 20 | 21 | # ENSIME specific 22 | .ensime_cache/ 23 | .ensime 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## elasticsearch-shell 2 | 3 | Like Console, but for the command-line. 4 | 5 | ![demo](images/shell-demo.gif) 6 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "Elasticsearch Shell" 2 | 3 | version := "1.0" 4 | 5 | scalaVersion := "2.12.1" 6 | 7 | libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.3.0" 8 | libraryDependencies += "org.jline" % "jline" % "3.1.3" 9 | 10 | -------------------------------------------------------------------------------- /images/shell-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmarz/elasticsearch-shell/c93c442737f671adc210d267c44347792663833c/images/shell-demo.gif -------------------------------------------------------------------------------- /src/main/scala/Client.scala: -------------------------------------------------------------------------------- 1 | import scalaj.http._ 2 | 3 | object Client { 4 | 5 | def response(r: Request, settings: Map[String, String]) : Either[HttpResponse[String], Error] = { 6 | try { 7 | val responseAsString = r.method.toUpperCase match { 8 | case "PUT" => put(r, settings) 9 | case "POST" => post(r, settings) 10 | case _ => request(r, settings).asString 11 | } 12 | Left(responseAsString) 13 | } catch { 14 | case e: Exception => Right(new ConnectionError(settings("host"), s"${e.getMessage}")) 15 | } 16 | } 17 | 18 | private def request(r: Request, s: Map[String, String]) : HttpRequest = { 19 | Http(s"${s("host")}/${r.path.stripPrefix("/")}") 20 | .method(r.method) 21 | .param("pretty", "true") 22 | .params(r.params) 23 | .header("content-type", "application/json") 24 | } 25 | 26 | private def put(r: Request, s: Map[String, String]) : HttpResponse[String] = request(r, s).put(r.body).asString 27 | 28 | private def post(r: Request, s: Map[String, String]) : HttpResponse[String] = request(r, s).postData(r.body).asString 29 | 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/main/scala/Completions.scala: -------------------------------------------------------------------------------- 1 | import org.jline.reader.impl.completer.StringsCompleter; 2 | 3 | object Completions { 4 | 5 | def completer() : StringsCompleter = { 6 | new StringsCompleter( 7 | "_cat", 8 | "_cat/aliases", 9 | "_cat/allocation", 10 | "_cat/count", 11 | "_cat/fielddata", 12 | "_cat/health", 13 | "_cat/help", 14 | "_cat/indices", 15 | "_cat/master", 16 | "_cat/nodeattrs", 17 | "_cat/nodes", 18 | "_cat/pending_tasks", 19 | "_cat/plugins", 20 | "_cat/recovery", 21 | "_cat/repositories", 22 | "_cat/segments", 23 | "_cat/shards", 24 | "_cat/snapshots", 25 | "_cat/tasks", 26 | "_cat/templates", 27 | "_cat/thread_pool", 28 | "_search", 29 | "_search/scroll", 30 | "_search_shards", 31 | "_search/template", 32 | "_cluster/allocation/explain", 33 | "_cluster/settings", 34 | "_cluster/health", 35 | "_cluster/pending_tasks", 36 | "_cluster/reroute", 37 | "_cluster/state", 38 | "_cluster/stats", 39 | "_count", 40 | "_scripts", 41 | "_field_stats", 42 | "_analyze", 43 | "_cache/clear", 44 | "_template", 45 | "_flush", 46 | "_flush/synced", 47 | "_forcemerge", 48 | "_alias", 49 | "_mapping", 50 | "_mapping/field", 51 | "_settings", 52 | "_upgrade", 53 | "_recovery", 54 | "_refresh", 55 | "_segments", 56 | "_shard_stores", 57 | "_stats", 58 | "_shard_stores", 59 | "_shard_stores", 60 | "_aliases", 61 | "_validate/query", 62 | "_ingest/pipeline", 63 | "_ingest/pipeline/simulate", 64 | "_mget", 65 | "_msearch", 66 | "_msearch/template", 67 | "_mtermvectors", 68 | "_nodes/hot_threads", 69 | "_cluster/nodes/hot_threads", 70 | "_nodes", 71 | "_nodes/stats", 72 | "_reindex", 73 | "_render/template", 74 | "_snapshot", 75 | "_snapshot/status", 76 | "_tasks", 77 | "GET", 78 | "PUT", 79 | "POST", 80 | "DELETE", 81 | "HEAD", 82 | "set", 83 | "host", 84 | "http://", 85 | "https://" 86 | ) 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/scala/Error.scala: -------------------------------------------------------------------------------- 1 | 2 | case class Error(message: String) { 3 | 4 | override def toString : String = { 5 | s"Error: ${message}" 6 | } 7 | 8 | } 9 | 10 | class ParseError(message: String) extends Error(s"Failed to parse command - ${message}") 11 | 12 | class UnknownSettingError(name: String) extends Error(s"Unknown setting: ${name}") 13 | 14 | class ConnectionError(host: String, message: String) extends Error(s"Could not connect to host ${host} - ${message}") 15 | -------------------------------------------------------------------------------- /src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | import org.jline.reader._ 2 | import org.jline.terminal._ 3 | 4 | object Main extends App { 5 | 6 | println("\n" 7 | + """ __ __ _ __ """ + "\n" 8 | + """ ___ / /___ _ ___ / /_ (_)____ ___ ___ ___ _ ____ ____ / / """ + "\n" 9 | + """ / -_)/ // _ `/(_- "http://localhost:9200" 15 | ) 16 | 17 | val terminal = TerminalBuilder.terminal() 18 | val reader = LineReaderBuilder.builder() 19 | .terminal(terminal) 20 | .completer(Completions.completer) 21 | .build() 22 | 23 | var read = true 24 | while(read) { 25 | try { 26 | eval(reader.readLine("elasticsearch> ")) 27 | } 28 | catch { 29 | case e: UserInterruptException => read = false 30 | } 31 | } 32 | 33 | def eval(input: String) { 34 | var command = input.split(" ")(0) 35 | command.toLowerCase match { 36 | case "set" => set(input) 37 | case "help" => usage() 38 | case "clear" => clear() 39 | case "exit" => exit() 40 | case _ => request(input) 41 | } 42 | } 43 | 44 | def request(input: String) { 45 | Parse.request(input) match { 46 | case Left(request) => { 47 | Client.response(request, settings.toMap) match { 48 | case Left(response) => { 49 | println(s"\n${request}") 50 | println(s"Status: ${response.code}\n") 51 | println(response.body) 52 | } 53 | case Right(connectionError) => { 54 | println(connectionError) 55 | } 56 | } 57 | } 58 | case Right(parseError) => { 59 | println(parseError) 60 | usage() 61 | } 62 | } 63 | } 64 | 65 | def set(input: String) { 66 | Parse.setting(input) match { 67 | case Left(s) => settings(s._1) = s._2 68 | case Right(error) => { 69 | println(error) 70 | usage() 71 | } 72 | } 73 | } 74 | 75 | def usage() = { 76 | println() 77 | println(" Usage:") 78 | println() 79 | println(" Example: PUT foo/bar/1 {\"foo\":\"bar\"}") 80 | println(" Example: set host http://mynode:9200") 81 | println() 82 | println(" [verb] [endpoint] [body] : Executes an HTTP request to the given endpoint with an optional body.") 83 | println(" set [setting] [value] : Sets the value for a given setting.") 84 | println() 85 | println(" Settings:") 86 | println() 87 | println(" host : The node URL to connect to. Must begin with http:// or https://.") 88 | println(" user : If using basic authenticatin, the user name to connect to Elasticsearch.") 89 | println(" password : The password for the given user.") 90 | println() 91 | } 92 | 93 | def clear() { 94 | val ANSI_CLS = "\u001b[2J"; 95 | val ANSI_HOME = "\u001b[H"; 96 | System.out.print(ANSI_CLS + ANSI_HOME); 97 | System.out.flush(); 98 | } 99 | 100 | def exit() = throw new UserInterruptException("") 101 | 102 | } 103 | 104 | -------------------------------------------------------------------------------- /src/main/scala/Parse.scala: -------------------------------------------------------------------------------- 1 | 2 | object Parse { 3 | 4 | def setting(value: String) : Either[(String, String), Error] = { 5 | val parts = value.split(" ") 6 | parts.size match { 7 | case 3 => { 8 | val k = parts(1) 9 | val v = parts(2) 10 | k match { 11 | case "host" => host(v) 12 | case _ => Right(new UnknownSettingError(k)) 13 | } 14 | } 15 | case _ => Right(new ParseError("Invalid setting")) 16 | } 17 | } 18 | 19 | def request(value: String) : Either[Request, ParseError] = { 20 | val parts = value.split(" ") 21 | if (parts.size >= 2) { 22 | val method = parts(0) 23 | if (validHttpVerb(method)) { 24 | val path = parts(1) 25 | if (validPath(path)) { 26 | val queryParams = params(path) 27 | val body = parts.drop(2).mkString("") 28 | Left(Request(method, path, queryParams, body)) 29 | } else Right(new ParseError("Invalid path")) 30 | } else Right(new ParseError("Invalid HTTP method")) 31 | } else Right(new ParseError("Invalid length")) 32 | } 33 | 34 | private def host(value: String) : Either[(String, String), ParseError] = { 35 | if (value.startsWith("http://") || value.startsWith ("https://")) { 36 | Left(("host", value)) 37 | } else { 38 | Right(new ParseError("Invalid host")) 39 | } 40 | } 41 | 42 | private def params(value: String) : Array[(String, String)] = { 43 | var position = value.split("\\?") 44 | if (position.size == 1) { 45 | Array[(String, String)]() 46 | } else { 47 | position(1).split("&").map(p => p.split("=")).map(p => { 48 | p.size match { 49 | case 1 => (p(0), "true") 50 | case 2 => (p(0), p(1)) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | private def validHttpVerb(value: String) : Boolean = { 57 | List("GET", "PUT", "POST", "DELETE", "HEAD").contains(value.toUpperCase) 58 | } 59 | 60 | // TODO 61 | private def validPath(value: String) : Boolean = true 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/scala/Request.scala: -------------------------------------------------------------------------------- 1 | 2 | case class Request (method: String, path: String, params: Array[(String, String)], body: String) { 3 | 4 | override def toString : String = { 5 | s"Method: ${method.toUpperCase}\nPath: ${path}\nParams: ${params.mkString(" ")}\nBody: ${body}" 6 | } 7 | 8 | } 9 | --------------------------------------------------------------------------------