├── .gitignore ├── LICENSE.txt ├── README.md ├── influx-cli.go └── influx-cli_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | influx-cli.test 2 | influx-cli 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, and 11 | distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 14 | owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all other entities 17 | that control, are controlled by, or are under common control with that entity. 18 | For the purposes of this definition, "control" means (i) the power, direct or 19 | indirect, to cause the direction or management of such entity, whether by 20 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity exercising 24 | permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, including 27 | but not limited to software source code, documentation source, and configuration 28 | files. 29 | 30 | "Object" form shall mean any form resulting from mechanical transformation or 31 | translation of a Source form, including but not limited to compiled object code, 32 | generated documentation, and conversions to other media types. 33 | 34 | "Work" shall mean the work of authorship, whether in Source or Object form, made 35 | available under the License, as indicated by a copyright notice that is included 36 | in or attached to the work (an example is provided in the Appendix below). 37 | 38 | "Derivative Works" shall mean any work, whether in Source or Object form, that 39 | is based on (or derived from) the Work and for which the editorial revisions, 40 | annotations, elaborations, or other modifications represent, as a whole, an 41 | original work of authorship. For the purposes of this License, Derivative Works 42 | shall not include works that remain separable from, or merely link (or bind by 43 | name) to the interfaces of, the Work and Derivative Works thereof. 44 | 45 | "Contribution" shall mean any work of authorship, including the original version 46 | of the Work and any modifications or additions to that Work or Derivative Works 47 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 48 | by the copyright owner or by an individual or Legal Entity authorized to submit 49 | on behalf of the copyright owner. For the purposes of this definition, 50 | "submitted" means any form of electronic, verbal, or written communication sent 51 | to the Licensor or its representatives, including but not limited to 52 | communication on electronic mailing lists, source code control systems, and 53 | issue tracking systems that are managed by, or on behalf of, the Licensor for 54 | the purpose of discussing and improving the Work, but excluding communication 55 | that is conspicuously marked or otherwise designated in writing by the copyright 56 | owner as "Not a Contribution." 57 | 58 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 59 | of whom a Contribution has been received by Licensor and subsequently 60 | incorporated within the Work. 61 | 62 | 2. Grant of Copyright License. 63 | 64 | Subject to the terms and conditions of this License, each Contributor hereby 65 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 66 | irrevocable copyright license to reproduce, prepare Derivative Works of, 67 | publicly display, publicly perform, sublicense, and distribute the Work and such 68 | Derivative Works in Source or Object form. 69 | 70 | 3. Grant of Patent License. 71 | 72 | Subject to the terms and conditions of this License, each Contributor hereby 73 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 74 | irrevocable (except as stated in this section) patent license to make, have 75 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 76 | such license applies only to those patent claims licensable by such Contributor 77 | that are necessarily infringed by their Contribution(s) alone or by combination 78 | of their Contribution(s) with the Work to which such Contribution(s) was 79 | submitted. If You institute patent litigation against any entity (including a 80 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 81 | Contribution incorporated within the Work constitutes direct or contributory 82 | patent infringement, then any patent licenses granted to You under this License 83 | for that Work shall terminate as of the date such litigation is filed. 84 | 85 | 4. Redistribution. 86 | 87 | You may reproduce and distribute copies of the Work or Derivative Works thereof 88 | in any medium, with or without modifications, and in Source or Object form, 89 | provided that You meet the following conditions: 90 | 91 | You must give any other recipients of the Work or Derivative Works a copy of 92 | this License; and 93 | You must cause any modified files to carry prominent notices stating that You 94 | changed the files; and 95 | You must retain, in the Source form of any Derivative Works that You distribute, 96 | all copyright, patent, trademark, and attribution notices from the Source form 97 | of the Work, excluding those notices that do not pertain to any part of the 98 | Derivative Works; and 99 | If the Work includes a "NOTICE" text file as part of its distribution, then any 100 | Derivative Works that You distribute must include a readable copy of the 101 | attribution notices contained within such NOTICE file, excluding those notices 102 | that do not pertain to any part of the Derivative Works, in at least one of the 103 | following places: within a NOTICE text file distributed as part of the 104 | Derivative Works; within the Source form or documentation, if provided along 105 | with the Derivative Works; or, within a display generated by the Derivative 106 | Works, if and wherever such third-party notices normally appear. The contents of 107 | the NOTICE file are for informational purposes only and do not modify the 108 | License. You may add Your own attribution notices within Derivative Works that 109 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 110 | provided that such additional attribution notices cannot be construed as 111 | modifying the License. 112 | You may add Your own copyright statement to Your modifications and may provide 113 | additional or different license terms and conditions for use, reproduction, or 114 | distribution of Your modifications, or for any such Derivative Works as a whole, 115 | provided Your use, reproduction, and distribution of the Work otherwise complies 116 | with the conditions stated in this License. 117 | 118 | 5. Submission of Contributions. 119 | 120 | Unless You explicitly state otherwise, any Contribution intentionally submitted 121 | for inclusion in the Work by You to the Licensor shall be under the terms and 122 | conditions of this License, without any additional terms or conditions. 123 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 124 | any separate license agreement you may have executed with Licensor regarding 125 | such Contributions. 126 | 127 | 6. Trademarks. 128 | 129 | This License does not grant permission to use the trade names, trademarks, 130 | service marks, or product names of the Licensor, except as required for 131 | reasonable and customary use in describing the origin of the Work and 132 | reproducing the content of the NOTICE file. 133 | 134 | 7. Disclaimer of Warranty. 135 | 136 | Unless required by applicable law or agreed to in writing, Licensor provides the 137 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 138 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 139 | including, without limitation, any warranties or conditions of TITLE, 140 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 141 | solely responsible for determining the appropriateness of using or 142 | redistributing the Work and assume any risks associated with Your exercise of 143 | permissions under this License. 144 | 145 | 8. Limitation of Liability. 146 | 147 | In no event and under no legal theory, whether in tort (including negligence), 148 | contract, or otherwise, unless required by applicable law (such as deliberate 149 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 150 | liable to You for damages, including any direct, indirect, special, incidental, 151 | or consequential damages of any character arising as a result of this License or 152 | out of the use or inability to use the Work (including but not limited to 153 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 154 | any and all other commercial damages or losses), even if such Contributor has 155 | been advised of the possibility of such damages. 156 | 157 | 9. Accepting Warranty or Additional Liability. 158 | 159 | While redistributing the Work or Derivative Works thereof, You may choose to 160 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 161 | other liability obligations and/or rights consistent with this License. However, 162 | in accepting such obligations, You may act only on Your own behalf and on Your 163 | sole responsibility, not on behalf of any other Contributor, and only if You 164 | agree to indemnify, defend, and hold each Contributor harmless for any liability 165 | incurred by, or claims asserted against, such Contributor by reason of your 166 | accepting any such warranty or additional liability. 167 | 168 | END OF TERMS AND CONDITIONS 169 | 170 | APPENDIX: How to apply the Apache License to your work 171 | 172 | To apply the Apache License to your work, attach the following boilerplate 173 | notice, with the fields enclosed by brackets "{}" replaced with your own 174 | identifying information. (Don't include the brackets!) The text should be 175 | enclosed in the appropriate comment syntax for the file format. We also 176 | recommend that a file or class name and description of purpose be included on 177 | the same "printed page" as the copyright notice for easier identification within 178 | third-party archives. 179 | 180 | Copyright {yyyy} {name of copyright owner} 181 | 182 | Licensed under the Apache License, Version 2.0 (the "License"); 183 | you may not use this file except in compliance with the License. 184 | You may obtain a copy of the License at 185 | 186 | http://www.apache.org/licenses/LICENSE-2.0 187 | 188 | Unless required by applicable law or agreed to in writing, software 189 | distributed under the License is distributed on an "AS IS" BASIS, 190 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 191 | See the License for the specific language governing permissions and 192 | limitations under the License. 193 | 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | commandline client for influxdb, in Go 2 | similar to mysql, pgsql, etc. 3 | 4 | features 5 | -------- 6 | 7 | * implements allmost all available influxdb api features 8 | * makes influxdb features available through the query language, even when influxdb itself only supports them as API calls. 9 | * readline (history searching and navigation. uses ~/.influx_history) 10 | * ability to read commands from stdin, pipe command/query out to external process or redirect to a file 11 | * apache2 licensed, see included license file 12 | 13 | 14 | installation 15 | ------------ 16 | 17 | ``` 18 | go get github.com/Dieterbe/influx-cli 19 | ``` 20 | 21 | configuration 22 | ------------- 23 | 24 | The commandline options (see below) can also be stored in `~/.influxrc` 25 | Although this is entirely optional. 26 | 27 | For example: 28 | 29 | ``` 30 | host = "localhost" 31 | port = 8086 32 | user = "root" 33 | pass = "root" 34 | db = "" 35 | asyncCapacity = 100 # in datapoints 36 | asyncMaxWait = 1000 # in ms 37 | ``` 38 | 39 | The values in use at runtime follow this order of preference: 40 | 41 | defaults -> influxrc -> commandline args -> interactive updates 42 | 43 | Pro-tip: you can use the `writerc` command at runtime to generate this file, 44 | it will export the current runtime values. 45 | 46 | 47 | running 48 | ------- 49 | 50 | `$GOPATH/bin/influx-cli` or just `influx-cli` if you put `$GOPATH/bin` in your `$PATH` 51 | 52 | ``` 53 | Usage: influx-cli [flags] [query to execute on start] 54 | 55 | Flags: 56 | -async=false: when enabled, asynchronously flushes inserts 57 | -db="": database to use 58 | -host="localhost": host to connect to 59 | -pass="root": influxdb password 60 | -port=8086: port to connect to 61 | -recordsOnly=false: when enabled, doesn't display header 62 | -user="root": influxdb username 63 | 64 | Note: you can also pipe queries into stdin, one line per query 65 | ``` 66 | 67 | usage 68 | ----- 69 | 70 | ``` 71 | 72 | options & current session 73 | ------------------------- 74 | 75 | \r : show records only, no headers 76 | \t : toggle timing, which displays timing of 77 | query execution + network and output displaying 78 | (default: false) 79 | \async : asynchronously flush inserts 80 | \comp : disable compression (client lib doesn't support enabling) 81 | \db : switch to databasename (requires a bind call to be effective) 82 | \user : switch to different user (requires a bind call to be effective) 83 | \pass : update password (requires a bind call to be effective) 84 | 85 | bind : bind again, possibly after updating db, user or pass 86 | ping : ping the server 87 | 88 | 89 | admin 90 | ----- 91 | 92 | 93 | create admin : add given admin user 94 | delete admin : delete admin user 95 | update admin : update the password for given admin user 96 | list admin : list admins 97 | 98 | create db : create database 99 | delete db : drop database 100 | list db : list databases 101 | 102 | list series [/regex/[i]] : list series, optionally filtered by regex 103 | 104 | delete server : delete server by id 105 | list servers : list servers 106 | 107 | list shardspaces : list shardspaces 108 | 109 | 110 | data i/o 111 | -------- 112 | 113 | insert into [(col1[,col2[...]])] values (val1[,val2[,val3[...]]]) 114 | : insert values into the given columns for given series name. 115 | columns is optional and defaults to (time, sequence_number, value) 116 | (timestamp is assumed to be in ms. ms/u/s prefixes don't work yet) 117 | select ... : select statement for data retrieval 118 | 119 | 120 | misc 121 | ---- 122 | 123 | conn : display info about current connection 124 | raw : execute query raw (fallback for unsupported queries) 125 | echo : echo string + newline. 126 | this is useful when the input is not visible, i.e. from scripts 127 | writerc : write current parameters to ~/.influxrc file 128 | commands : this menu 129 | help : this menu 130 | exit / ctrl-D : exit the program 131 | 132 | modifiers 133 | --------- 134 | 135 | ANY command above can be subject to piping to another command or writing output to a file, like so: 136 | 137 | command; | : pipe the output into an external command (example: list series; | sort) 138 | note: currently you can only pipe into one external command at a time 139 | command; > : redirect the output into a file 140 | 141 | ``` 142 | -------------------------------------------------------------------------------- /influx-cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/BurntSushi/toml" 5 | "github.com/andrew-d/go-termutil" 6 | "github.com/davecgh/go-spew/spew" 7 | "github.com/gobs/readline" 8 | "github.com/influxdb/influxdb/client" 9 | "github.com/rcrowley/go-metrics" 10 | // "log" 11 | "bufio" 12 | "encoding/csv" 13 | "flag" 14 | "fmt" 15 | "io" 16 | "os" 17 | "os/exec" 18 | usr "os/user" 19 | "regexp" 20 | "strconv" 21 | "strings" 22 | "time" 23 | "net/url" 24 | ) 25 | 26 | // the following client methods are not implemented yet. 27 | // CreateDatabaseUser 28 | // ChangeDatabaseUser 29 | // UpdateDatabaseUser 30 | // UpdateDatabaseUserPermissions 31 | // DeleteDatabaseUser 32 | // GetDatabaseUserList 33 | // AlterDatabasePrivilege 34 | // AuthenticateClusterAdmin 35 | // GetShards // this returns LongTermShortTermShards which i think is not useful for >0.8 36 | 37 | // DropShardSpace 38 | // CreateShardSpace 39 | // DropShard 40 | // UpdateShardSpace 41 | 42 | // upto how many points to commit in 1 go? 43 | var AsyncCapacity = 1000 44 | 45 | // how long to wait max before flushing a commit payload 46 | var AsyncMaxWait = 500 * time.Millisecond 47 | 48 | var host, user, pass, db string 49 | var port int 50 | var cl *client.Client 51 | var cfg *client.ClientConfig 52 | var handlers []HandlerSpec 53 | var timing bool 54 | var dateTime bool 55 | var recordsOnly bool 56 | var async bool 57 | var asyncInserts chan *client.Series 58 | var asyncInsertsCommitted chan int 59 | var forceInsertsFlush chan bool 60 | var sync_inserts_timer metrics.Timer 61 | 62 | var path_rc, path_hist string 63 | 64 | type Handler func(cmd []string, out io.Writer) *Timing 65 | 66 | type HandlerSpec struct { 67 | Match string 68 | Handler 69 | } 70 | 71 | type Timing struct { 72 | Pre time.Time 73 | Executed time.Time 74 | Printed time.Time 75 | } 76 | 77 | func makeTiming() *Timing { 78 | return &Timing{Pre: time.Now()} 79 | } 80 | 81 | func (t *Timing) StringQuery() string { 82 | if t.Executed.IsZero() { 83 | return "unknown" 84 | } 85 | return t.Executed.Sub(t.Pre).String() 86 | } 87 | 88 | func (t *Timing) StringPrint() string { 89 | if t.Executed.IsZero() || t.Printed.IsZero() { 90 | return "unknown" 91 | } 92 | return t.Printed.Sub(t.Executed).String() 93 | } 94 | 95 | func (t *Timing) String() string { 96 | return "query+network: " + t.StringQuery() + "\ndisplaying : " + t.StringPrint() 97 | } 98 | 99 | var regexBind = "^bind" 100 | var regexConn = "^conn$" 101 | var regexCreateAdmin = "^create admin ([a-zA-Z0-9_-]+) (.+)" 102 | var regexCreateDb = "^create db ([a-zA-Z0-9_-]+)" 103 | var regexDeleteAdmin = "^delete admin ([a-zA-Z0-9_-]+)" 104 | var regexDeleteDb = "^delete db ([a-zA-Z0-9_-]+)" 105 | var regexDeleteServer = "^delete server (.+)" 106 | var regexDropSeries = "^drop series .+" 107 | var regexEcho = "^echo (.+)" 108 | var regexInsert = "^insert into ([a-zA-Z0-9_-]+) ?(\\(.+\\))? values \\((.*)\\)$" 109 | var regexInsertQuoted = "^insert into \"(.+)\" ?(\\(.+\\))? values \\((.*)\\)$" 110 | var regexListAdmin = "^list admin" 111 | var regexListDb = "^list db" 112 | var regexListSeries = "^list series.*" 113 | var regexListServers = "^list servers$" 114 | var regexListShardspaces = "^list shardspaces$" 115 | var regexOption = "^\\\\([a-z]+) ?([a-zA-Z0-9_-]+)?" 116 | var regexPing = "^ping$" 117 | var regexRaw = "^raw (.+)" 118 | var regexSelect = "^select .*" 119 | var regexUpdateAdmin = "^update admin ([a-zA-Z0-9_-]+) (.+)" 120 | var regexWriteRc = "^writerc" 121 | 122 | type Config struct { 123 | Host string 124 | Port int 125 | User string 126 | Pass string 127 | Db string 128 | AsyncCapacity int 129 | AsyncMaxWait int 130 | } 131 | 132 | func init() { 133 | path_rc = Expand("~/.influxrc") 134 | path_hist = Expand("~/.influx_history") 135 | 136 | flag.StringVar(&host, "host", "localhost", "host to connect to") 137 | flag.IntVar(&port, "port", 8086, "port to connect to") 138 | flag.StringVar(&user, "user", "root", "influxdb username") 139 | flag.StringVar(&pass, "pass", "root", "influxdb password") 140 | flag.StringVar(&db, "db", "", "database to use") 141 | flag.BoolVar(&recordsOnly, "recordsOnly", false, "when enabled, doesn't display header") 142 | flag.BoolVar(&async, "async", false, "when enabled, asynchronously flushes inserts") 143 | 144 | flag.Usage = func() { 145 | fmt.Fprintln(os.Stderr, "Usage: influx-cli [flags] [query to execute on start]") 146 | fmt.Fprintf(os.Stderr, "\nFlags:\n") 147 | flag.PrintDefaults() 148 | fmt.Fprintf(os.Stderr, "\nNote: you can also pipe queries into stdin, one line per query\n") 149 | } 150 | 151 | handlers = []HandlerSpec{ 152 | HandlerSpec{regexBind, bindHandler}, 153 | HandlerSpec{regexConn, connHandler}, 154 | HandlerSpec{regexCreateAdmin, createAdminHandler}, 155 | HandlerSpec{regexCreateDb, createDbHandler}, 156 | HandlerSpec{regexDeleteAdmin, deleteAdminHandler}, 157 | HandlerSpec{regexDeleteDb, deleteDbHandler}, 158 | HandlerSpec{regexDeleteServer, deleteServerHandler}, 159 | HandlerSpec{regexDropSeries, dropSeriesHandler}, 160 | HandlerSpec{regexEcho, echoHandler}, 161 | HandlerSpec{regexInsert, insertHandler}, 162 | HandlerSpec{regexInsertQuoted, insertHandler}, 163 | HandlerSpec{regexListAdmin, listAdminHandler}, 164 | HandlerSpec{regexListDb, listDbHandler}, 165 | HandlerSpec{regexListSeries, listSeriesHandler}, 166 | HandlerSpec{regexListServers, listServersHandler}, 167 | HandlerSpec{regexListShardspaces, listShardspacesHandler}, 168 | HandlerSpec{regexOption, optionHandler}, 169 | HandlerSpec{regexPing, pingHandler}, 170 | HandlerSpec{regexRaw, rawHandler}, 171 | HandlerSpec{regexSelect, selectHandler}, 172 | HandlerSpec{regexUpdateAdmin, updateAdminPassHandler}, 173 | HandlerSpec{regexWriteRc, writeRcHandler}, 174 | } 175 | 176 | asyncInserts = make(chan *client.Series) 177 | asyncInsertsCommitted = make(chan int) 178 | forceInsertsFlush = make(chan bool) 179 | 180 | sync_inserts_timer = metrics.NewTimer() 181 | metrics.Register("insert_sync", sync_inserts_timer) 182 | } 183 | 184 | func printHelp() { 185 | out := `Help: 186 | 187 | options & current session 188 | ------------------------- 189 | 190 | \dt : print timestamps as datetime strings 191 | \r : show records only, no headers 192 | \t : toggle timing, which displays timing of 193 | query execution + network and output displaying 194 | (default: false) 195 | \async : asynchronously flush inserts 196 | \comp : disable compression (client lib doesn't support enabling) 197 | \db : switch to databasename (requires a bind call to be effective) 198 | \user : switch to different user (requires a bind call to be effective) 199 | \pass : update password (requires a bind call to be effective) 200 | 201 | bind : bind again, possibly after updating db, user or pass 202 | ping : ping the server 203 | 204 | 205 | admin 206 | ----- 207 | 208 | create admin : add given admin user 209 | delete admin : delete admin user 210 | update admin : update the password for given admin user 211 | list admin : list admins 212 | 213 | create db : create database 214 | delete db : drop database 215 | list db : list databases 216 | 217 | list series [/regex/[i]] : list series, optionally filtered by regex 218 | drop series : drop series by given name 219 | 220 | delete server : delete server by id 221 | list servers : list servers 222 | 223 | list shardspaces : list shardspaces 224 | 225 | 226 | data i/o 227 | -------- 228 | 229 | insert into [(col1[,col2[...]])] values (val1[,val2[,val3[...]]]) 230 | : insert values into the given columns for given series name. 231 | columns is optional and defaults to (time, sequence_number, value) 232 | (timestamp is assumed to be in ms. ms/u/s prefixes don't work yet) 233 | select ... : select statement for data retrieval 234 | 235 | 236 | misc 237 | ---- 238 | 239 | conn : display info about current connection 240 | raw : execute query raw (fallback for unsupported queries) 241 | echo : echo string + newline. 242 | this is useful when the input is not visible, i.e. from scripts 243 | writerc : write current parameters to ~/.influxrc file 244 | commands : this menu 245 | help : this menu 246 | exit / ctrl-D : exit the program 247 | 248 | modifiers 249 | --------- 250 | 251 | ANY command above can be subject to piping to another command or writing output to a file, like so: 252 | 253 | command; | : pipe the output into an external command (example: list series; | sort) 254 | note: currently you can only pipe into one external command at a time 255 | command; > : redirect the output into a file 256 | 257 | ` 258 | fmt.Println(out) 259 | } 260 | 261 | func getClient() error { 262 | cfg = &client.ClientConfig{ 263 | Host: fmt.Sprintf("%s:%d", host, port), 264 | Username: user, 265 | Password: pass, 266 | Database: db, 267 | } 268 | var err error 269 | cl, err = client.NewClient(cfg) 270 | if err != nil { 271 | return err 272 | } 273 | err = cl.Ping() 274 | if err != nil { 275 | return err 276 | } 277 | //fmt.Printf("connected to %s:%s@%s:%d/%s\n", user, pass, host, port, db) 278 | return nil 279 | } 280 | 281 | func Expand(in string) (out string) { 282 | if in[:1] == "~" { 283 | cur_usr, err := usr.Current() 284 | if err != nil { 285 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 286 | os.Exit(2) 287 | } 288 | out := strings.Replace(in, "~", cur_usr.HomeDir, 1) 289 | return out 290 | } 291 | return in 292 | } 293 | 294 | func main() { 295 | var conf Config 296 | if _, err := os.Stat(path_rc); err == nil { 297 | if _, err := toml.DecodeFile(path_rc, &conf); err != nil { 298 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 299 | os.Exit(2) 300 | } 301 | } else if !os.IsNotExist(err) { 302 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 303 | os.Exit(2) 304 | } 305 | // else, rc doesn't exist, which is fine. 306 | 307 | if conf.Host != "" { 308 | host = conf.Host 309 | } 310 | if conf.Port != 0 { 311 | port = conf.Port 312 | } 313 | if conf.User != "" { 314 | user = conf.User 315 | } 316 | if conf.Pass != "" { 317 | pass = url.QueryEscape(conf.Pass) 318 | } 319 | if conf.Db != "" { 320 | db = conf.Db 321 | } 322 | if conf.AsyncCapacity > 0 { 323 | AsyncCapacity = conf.AsyncCapacity 324 | } 325 | if conf.AsyncMaxWait > 0 { 326 | AsyncMaxWait = time.Duration(conf.AsyncMaxWait) * time.Millisecond 327 | } 328 | 329 | flag.Parse() 330 | query := strings.Join(flag.Args(), " ") 331 | 332 | err := getClient() 333 | if err != nil { 334 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 335 | os.Exit(1) 336 | } 337 | 338 | //go metrics.Log(metrics.DefaultRegistry, 10e9, log.New(os.Stderr, "metrics: ", log.Lmicroseconds)) 339 | go committer() 340 | 341 | if query != "" { 342 | // execute query passed from cmd arg and stop 343 | cmd := strings.TrimSuffix(strings.TrimSpace(query), ";") 344 | handle(cmd) 345 | } else if !termutil.Isatty(os.Stdin.Fd()) { 346 | // execute all input from stdin and stop 347 | readStdin() 348 | } else { 349 | // if stdin is a tty, provide readline prompt with history. 350 | err = readline.ReadHistoryFile(path_hist) 351 | if err != nil && err.Error() != "no such file or directory" { 352 | fmt.Fprintf(os.Stderr, "Cannot read '%s': %s\n", path_hist, err.Error()) 353 | os.Exit(1) 354 | } 355 | ui() 356 | err = readline.WriteHistoryFile(path_hist) 357 | if err != nil { 358 | fmt.Fprintf(os.Stderr, "Cannot write to '%s': %s\n", path_hist, err.Error()) 359 | Exit(1) 360 | } 361 | } 362 | Exit(0) 363 | } 364 | func Exit(code int) { 365 | close(asyncInserts) 366 | select { 367 | case <-time.After(time.Second * 5): 368 | fmt.Fprintf(os.Stderr, "Could not flush all inserts. Closing anyway") 369 | case num := <-asyncInsertsCommitted: 370 | if num > 0 { 371 | fmt.Printf("Final %d async inserts committed\n", num) 372 | } 373 | } 374 | os.Exit(code) 375 | } 376 | 377 | func readStdin() { 378 | reader := bufio.NewReader(os.Stdin) 379 | for { 380 | line, err := reader.ReadString('\n') 381 | if err == io.EOF { 382 | return 383 | } 384 | if err != nil { 385 | fmt.Fprintln(os.Stderr, err.Error()) 386 | Exit(2) 387 | } 388 | cmd := strings.TrimSpace(line) 389 | handle(cmd) 390 | } 391 | } 392 | 393 | func ui() { 394 | prompt := "influx> " 395 | L: 396 | for { 397 | switch result := readline.ReadLine(&prompt); true { 398 | case result == nil: 399 | fmt.Println("") 400 | break L 401 | case *result == "exit": 402 | readline.AddHistory(*result) 403 | break L 404 | case *result == "commands": 405 | readline.AddHistory(*result) 406 | printHelp() 407 | case *result == "help": 408 | readline.AddHistory(*result) 409 | printHelp() 410 | case *result != "": //ignore blank lines 411 | readline.AddHistory(*result) 412 | cmd := strings.TrimSpace(*result) 413 | handle(cmd) 414 | } 415 | } 416 | } 417 | 418 | func handle(cmd string) { 419 | handled := false 420 | var writeTo io.WriteCloser 421 | var pipeTo *exec.Cmd 422 | writeTo = os.Stdout 423 | mode := 0 // 1 -> pipe to cmd, 2 -> write to file 424 | cmd = strings.Replace(cmd, "; |", ";|", 1) 425 | cmd = strings.Replace(cmd, "; >", ";>", 1) 426 | 427 | if strings.Contains(cmd, ";|") { 428 | mode = 1 429 | cmdArr := strings.Split(cmd, ";|") 430 | cmd = strings.TrimSpace(cmdArr[0]) 431 | cmdAndArgs := strings.Fields(strings.TrimSpace(cmdArr[1])) 432 | if len(cmdAndArgs) == 0 { 433 | fmt.Fprintln(os.Stderr, "error: no command specified to pipe to") 434 | Exit(2) 435 | } 436 | 437 | pipeTo = exec.Command(cmdAndArgs[0], cmdAndArgs[1:]...) 438 | var err error 439 | writeTo, err = pipeTo.StdinPipe() 440 | if err != nil { 441 | fmt.Fprintln(os.Stderr, "internal error: cannot open pipe", err.Error()) 442 | Exit(2) 443 | } 444 | pipeTo.Stdout = os.Stdout 445 | pipeTo.Stderr = os.Stderr 446 | } else if strings.Contains(cmd, ";>") { 447 | mode = 2 448 | cmdArr := strings.Split(cmd, ";>") 449 | cmd = cmdArr[0] 450 | file := strings.TrimSpace(cmdArr[1]) 451 | fd, err := os.Create(file) 452 | if err != nil { 453 | fmt.Fprintln(os.Stderr, "internal error: cannot open file", file, "for writing", err.Error()) 454 | Exit(2) 455 | } 456 | defer func() { fd.Close() }() 457 | writeTo = fd 458 | } else { 459 | // it may or may not have this ending delimiter 460 | cmd = strings.TrimSuffix(cmd, ";") 461 | } 462 | 463 | for _, spec := range handlers { 464 | re := regexp.MustCompile(spec.Match) 465 | if matches := re.FindStringSubmatch(cmd); len(matches) > 0 { 466 | if mode == 1 { 467 | err := pipeTo.Start() 468 | if err != nil { 469 | fmt.Fprintln(os.Stderr, "subcommand failed: ", err.Error()) 470 | fmt.Fprintln(os.Stderr, "aborting query") 471 | break 472 | } 473 | } 474 | t := spec.Handler(matches, writeTo) 475 | if mode == 1 { 476 | writeTo.Close() 477 | err := pipeTo.Wait() 478 | if err != nil { 479 | fmt.Fprintln(os.Stderr, "subcommand failed: ", err.Error()) 480 | } 481 | } 482 | 483 | if timing { 484 | // some functions return no timing, because it doesn't apply to them 485 | if t != nil { 486 | fmt.Println("timing>") 487 | fmt.Println(t) 488 | } 489 | } 490 | handled = true 491 | } 492 | } 493 | if !handled { 494 | fmt.Fprintln(os.Stderr, "Could not handle the command. type 'help' to get a help menu") 495 | } 496 | } 497 | 498 | func optionHandler(cmd []string, out io.Writer) *Timing { 499 | switch cmd[1] { 500 | case "async": 501 | if async { 502 | // so we don't get any insert errors after disabling async 503 | fmt.Fprintln(out, "flushing any pending async inserts", async) 504 | forceInsertsFlush <- true 505 | } 506 | async = !async 507 | fmt.Fprintln(out, "async is now", async) 508 | case "dt": 509 | dateTime = !dateTime 510 | fmt.Fprintln(out, "datetime printing is now", dateTime) 511 | case "r": 512 | recordsOnly = !recordsOnly 513 | fmt.Fprintln(out, "records-only is now", recordsOnly) 514 | case "t": 515 | timing = !timing 516 | fmt.Fprintln(out, "timing is now", timing) 517 | case "comp": 518 | cl.DisableCompression() 519 | fmt.Fprintln(out, "compression is now disabled") 520 | case "db": 521 | if cmd[2] == "" { 522 | fmt.Fprintf(os.Stderr, "database argument must be set") 523 | break 524 | } 525 | db = cmd[2] 526 | case "user": 527 | if cmd[2] == "" { 528 | fmt.Fprintf(os.Stderr, "user argument must be set") 529 | break 530 | } 531 | user = cmd[2] 532 | case "pass": 533 | if cmd[2] == "" { 534 | fmt.Fprintf(os.Stderr, "password argument must be set") 535 | break 536 | } 537 | pass = cmd[2] 538 | default: 539 | fmt.Fprintf(os.Stderr, "unrecognized option") 540 | } 541 | return nil 542 | } 543 | 544 | func createAdminHandler(cmd []string, out io.Writer) *Timing { 545 | timings := makeTiming() 546 | name := strings.TrimSpace(cmd[1]) 547 | pass := strings.TrimSpace(cmd[2]) 548 | err := cl.CreateClusterAdmin(name, pass) 549 | timings.Executed = time.Now() 550 | if err != nil { 551 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 552 | return timings 553 | } 554 | timings.Printed = time.Now() 555 | return timings 556 | } 557 | 558 | func updateAdminPassHandler(cmd []string, out io.Writer) *Timing { 559 | timings := makeTiming() 560 | name := strings.TrimSpace(cmd[1]) 561 | pass := strings.TrimSpace(cmd[2]) 562 | err := cl.UpdateClusterAdmin(name, pass) 563 | timings.Executed = time.Now() 564 | if err != nil { 565 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 566 | return timings 567 | } 568 | timings.Printed = time.Now() 569 | return timings 570 | } 571 | 572 | func listAdminHandler(cmd []string, out io.Writer) *Timing { 573 | timings := makeTiming() 574 | l, err := cl.GetClusterAdminList() 575 | timings.Executed = time.Now() 576 | if err != nil { 577 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 578 | return timings 579 | } 580 | for k, val := range l { 581 | fmt.Fprintln(out, "##", k) 582 | for k, v := range val { 583 | fmt.Fprintf(out, "%25s %v\n", k, v) 584 | } 585 | } 586 | timings.Printed = time.Now() 587 | return timings 588 | } 589 | 590 | func listDbHandler(cmd []string, out io.Writer) *Timing { 591 | timings := makeTiming() 592 | list, err := cl.GetDatabaseList() 593 | timings.Executed = time.Now() 594 | if err != nil { 595 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 596 | return timings 597 | } 598 | for _, item := range list { 599 | fmt.Fprintln(out, item["name"]) 600 | } 601 | timings.Printed = time.Now() 602 | return timings 603 | } 604 | 605 | func createDbHandler(cmd []string, out io.Writer) *Timing { 606 | timings := makeTiming() 607 | err := cl.CreateDatabase(cmd[1]) 608 | timings.Executed = time.Now() 609 | if err != nil { 610 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 611 | return timings 612 | } 613 | timings.Printed = time.Now() 614 | return timings 615 | } 616 | 617 | func deleteDbHandler(cmd []string, out io.Writer) *Timing { 618 | timings := makeTiming() 619 | err := cl.DeleteDatabase(cmd[1]) 620 | timings.Executed = time.Now() 621 | if err != nil { 622 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 623 | return timings 624 | } 625 | timings.Printed = time.Now() 626 | return timings 627 | } 628 | 629 | func deleteAdminHandler(cmd []string, out io.Writer) *Timing { 630 | timings := makeTiming() 631 | err := cl.DeleteClusterAdmin(strings.TrimSpace(cmd[1])) 632 | timings.Executed = time.Now() 633 | if err != nil { 634 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 635 | return timings 636 | } 637 | timings.Printed = time.Now() 638 | return timings 639 | } 640 | 641 | func deleteServerHandler(cmd []string, out io.Writer) *Timing { 642 | timings := makeTiming() 643 | id, err := strconv.ParseInt(cmd[1], 10, 32) 644 | err = cl.RemoveServer(int(id)) 645 | timings.Executed = time.Now() 646 | if err != nil { 647 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 648 | return timings 649 | } 650 | timings.Printed = time.Now() 651 | return timings 652 | } 653 | 654 | func dropSeriesHandler(cmd []string, out io.Writer) *Timing { 655 | timings := makeTiming() 656 | _, err := cl.Query(cmd[0] + ";") 657 | timings.Executed = time.Now() 658 | if err != nil { 659 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 660 | return timings 661 | } 662 | timings.Printed = time.Now() 663 | return timings 664 | } 665 | 666 | func echoHandler(cmd []string, out io.Writer) *Timing { 667 | timings := makeTiming() 668 | timings.Executed = time.Now() 669 | fmt.Fprintln(out, cmd[1]) 670 | timings.Printed = time.Now() 671 | return timings 672 | } 673 | 674 | // influxdb is typed, so try to parse as int, as float, and fall back to str 675 | func parseTyped(value_str string) interface{} { 676 | valueInt, err := strconv.ParseInt(strings.TrimSpace(value_str), 10, 64) 677 | if err == nil { 678 | return valueInt 679 | } 680 | valueFloat, err := strconv.ParseFloat(value_str, 64) 681 | if err == nil { 682 | return valueFloat 683 | } 684 | return value_str 685 | } 686 | 687 | func bindHandler(cmd []string, out io.Writer) *Timing { 688 | timings := makeTiming() 689 | // for some reason this call returns error (401): Invalid username/password 690 | //err := cl.AuthenticateDatabaseUser(db, user, pass) 691 | // so for now, the slightly less efficient way: 692 | err := getClient() 693 | timings.Executed = time.Now() 694 | if err != nil { 695 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 696 | return timings 697 | } 698 | timings.Printed = time.Now() 699 | return timings 700 | } 701 | 702 | func connHandler(cmd []string, out io.Writer) *Timing { 703 | fmt.Fprintf(out, "Host : %s\n", cfg.Host) 704 | fmt.Fprintf(out, "User : %s\n", cfg.Username) 705 | fmt.Fprintf(out, "Pass : %s\n", cfg.Password) 706 | fmt.Fprintf(out, "Db : %s\n", cfg.Database) 707 | fmt.Fprintf(out, "secure : %t\n", cfg.IsSecure) 708 | fmt.Fprintf(out, "udp : %t\n", cfg.IsUDP) 709 | fmt.Fprintf(out, "compression : ?\n") // can't query client for this 710 | fmt.Fprintf(out, "Client : %s\n", cfg.HttpClient) 711 | return nil 712 | } 713 | 714 | func insertHandler(cmd []string, out io.Writer) *Timing { 715 | timings := makeTiming() 716 | series_name := cmd[1] 717 | cols_str := strings.TrimPrefix(cmd[2], " ") 718 | var cols []string 719 | if cols_str != "" { 720 | cols_str = cols_str[1 : len(cols_str)-1] // strip surrounding () 721 | tmp_cols := strings.Split(cols_str, ",") 722 | cols = make([]string, len(tmp_cols)) 723 | for i, name := range tmp_cols { 724 | cols[i] = strings.TrimSpace(name) 725 | } 726 | } else { 727 | cols = []string{"time", "sequence_number", "value"} 728 | } 729 | vals_str := cmd[3] 730 | // vals_str could be: foo,bar,"avg(something,123)",quux 731 | reader := csv.NewReader(strings.NewReader(vals_str)) 732 | values, err := reader.Read() 733 | if err != nil { 734 | fmt.Fprintf(os.Stderr, "Could not parse values"+err.Error()+"\n") 735 | return timings 736 | } 737 | 738 | if len(values) != len(cols) { 739 | fmt.Fprintf(os.Stderr, "Number of values (%d) must match number of colums (%d): Columns are: %v\n", len(values), len(cols), cols) 740 | return timings 741 | } 742 | point := make([]interface{}, len(cols), len(cols)) 743 | 744 | for i, value_str := range values { 745 | point[i] = parseTyped(value_str) 746 | } 747 | 748 | serie := &client.Series{ 749 | Name: series_name, 750 | Columns: cols, 751 | Points: [][]interface{}{point}, 752 | } 753 | 754 | if async { 755 | asyncInserts <- serie 756 | err = nil 757 | } else { 758 | ts := time.Now() 759 | err = cl.WriteSeries([]*client.Series{serie}) 760 | sync_inserts_timer.Update(time.Since(ts)) 761 | } 762 | timings.Executed = time.Now() 763 | if err != nil { 764 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 765 | return timings 766 | } 767 | timings.Printed = time.Now() 768 | return timings 769 | } 770 | func committer() { 771 | toCommit := make([]*client.Series, 0, AsyncCapacity) 772 | 773 | commit := func() int { 774 | size := len(toCommit) 775 | if size == 0 { 776 | return 0 777 | } 778 | t := metrics.GetOrRegisterTimer("inserts_async_"+strconv.FormatInt(int64(len(toCommit)), 10), metrics.DefaultRegistry) 779 | defer func(start time.Time) { t.Update(time.Since(start)) }(time.Now()) 780 | err := cl.WriteSeries(toCommit) 781 | if err != nil { 782 | fmt.Fprintf(os.Stderr, "Failed to write %d series: %s\n", len(toCommit), err.Error()) 783 | } 784 | toCommit = make([]*client.Series, 0, AsyncCapacity) 785 | return size 786 | } 787 | 788 | timer := time.NewTimer(AsyncMaxWait) 789 | 790 | CommitLoop: 791 | for { 792 | select { 793 | case serie, ok := <-asyncInserts: 794 | if ok { 795 | toCommit = append(toCommit, serie) 796 | } else { 797 | // no more input, commit whatever we have and break 798 | asyncInsertsCommitted <- commit() 799 | break CommitLoop 800 | } 801 | // if capacity reached, commit 802 | if len(toCommit) == AsyncCapacity { 803 | commit() 804 | timer.Reset(AsyncMaxWait) 805 | } 806 | case <-timer.C: 807 | commit() 808 | timer.Reset(AsyncMaxWait) 809 | case <-forceInsertsFlush: 810 | commit() 811 | timer.Reset(AsyncMaxWait) 812 | } 813 | } 814 | } 815 | 816 | func pingHandler(cmd []string, out io.Writer) *Timing { 817 | timings := makeTiming() 818 | err := cl.Ping() 819 | timings.Executed = time.Now() 820 | if err != nil { 821 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 822 | return timings 823 | } 824 | timings.Printed = time.Now() 825 | return timings 826 | } 827 | 828 | func listServersHandler(cmd []string, out io.Writer) *Timing { 829 | timings := makeTiming() 830 | list, err := cl.Servers() 831 | timings.Executed = time.Now() 832 | if err != nil { 833 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 834 | return timings 835 | } 836 | for _, server := range list { 837 | fmt.Fprintln(out, "## id", server["id"]) 838 | for k, v := range server { 839 | if k != "id" { 840 | fmt.Fprintf(out, "%25s %v\n", k, v) 841 | } 842 | } 843 | } 844 | timings.Printed = time.Now() 845 | return timings 846 | } 847 | 848 | func listSeriesHandler(cmd []string, out io.Writer) *Timing { 849 | timings := makeTiming() 850 | list_series, err := cl.Query(cmd[0]) 851 | timings.Executed = time.Now() 852 | if err != nil { 853 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 854 | return timings 855 | } 856 | for _, series := range list_series { 857 | for _, p := range series.Points { 858 | fmt.Fprintln(out, p[1]) 859 | } 860 | } 861 | timings.Printed = time.Now() 862 | return timings 863 | } 864 | 865 | func listShardspacesHandler(cmd []string, out io.Writer) *Timing { 866 | timings := makeTiming() 867 | shardSpaces, err := cl.GetShardSpaces() 868 | timings.Executed = time.Now() 869 | if err != nil { 870 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 871 | return timings 872 | } 873 | dbLenMax := len("Database") 874 | nameLenMax := len("Name") 875 | regexLenMax := len("Regex") 876 | retentionLenMax := len("Retention") 877 | durationLenMax := len("Duration") 878 | 879 | for _, s := range shardSpaces { 880 | if len(s.Database) > dbLenMax { 881 | dbLenMax = len(s.Database) 882 | } 883 | if len(s.Name) > nameLenMax { 884 | nameLenMax = len(s.Name) 885 | } 886 | if len(s.Regex) > regexLenMax { 887 | regexLenMax = len(s.Regex) 888 | } 889 | if len(s.RetentionPolicy) > retentionLenMax { 890 | retentionLenMax = len(s.RetentionPolicy) 891 | } 892 | if len(s.ShardDuration) > durationLenMax { 893 | durationLenMax = len(s.ShardDuration) 894 | } 895 | } 896 | headerFmt := fmt.Sprintf("%%%ds %%%ds %%%ds %%%ds %%%ds %%2s %%5s\n", dbLenMax, nameLenMax, regexLenMax, retentionLenMax, durationLenMax) 897 | rowFmt := fmt.Sprintf("%%%ds %%%ds %%%ds %%%ds %%%ds %%2d %%5d\n", dbLenMax, nameLenMax, regexLenMax, retentionLenMax, durationLenMax) 898 | fmt.Fprintf(out, headerFmt, "Database", "Name", "Regex", "Retention", "Duration", "RF", "Split") 899 | for _, s := range shardSpaces { 900 | fmt.Fprintf(out, rowFmt, s.Database, s.Name, s.Regex, s.RetentionPolicy, s.ShardDuration, s.ReplicationFactor, s.Split) 901 | } 902 | timings.Printed = time.Now() 903 | return timings 904 | } 905 | 906 | func selectHandler(cmd []string, out io.Writer) *Timing { 907 | timings := makeTiming() 908 | series, err := cl.Query(cmd[0] + ";") 909 | timings.Executed = time.Now() 910 | if err != nil { 911 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 912 | return timings 913 | } 914 | type Spec struct { 915 | Header string 916 | Row string 917 | } 918 | specs := map[string]Spec{ 919 | "time": {"%20s", "%20f"}, 920 | "sequence_number": {"%16s", " %10f"}, 921 | "value": {"%20s", "%20f"}, 922 | } 923 | if dateTime { 924 | specs["time"] = Spec{"%33s", "%33s"} 925 | } 926 | defaultSpec := Spec{"%20s", "%20v"} 927 | var spec Spec 928 | var ok bool 929 | 930 | for _, serie := range series { 931 | if !recordsOnly { 932 | fmt.Fprintln(out, "##", serie.Name) 933 | } 934 | 935 | colrows := make([]string, len(serie.Columns), len(serie.Columns)) 936 | 937 | for i, col := range serie.Columns { 938 | if spec, ok = specs[col]; !ok { 939 | spec = defaultSpec 940 | } 941 | if !recordsOnly { 942 | fmt.Fprintf(out, spec.Header, col) 943 | } 944 | colrows[i] = spec.Row 945 | } 946 | if !recordsOnly { 947 | fmt.Fprintln(out) 948 | } 949 | for _, p := range serie.Points { 950 | for i, fmtStr := range colrows { 951 | if i == 0 && dateTime { 952 | msFloat := p[i].(float64) 953 | ns := (int64(msFloat) % 1000) * 1000000 954 | s := int64(msFloat / 1000) 955 | d := time.Unix(s, ns) 956 | fmt.Fprintf(out, fmtStr, d) 957 | } else { 958 | fmt.Fprintf(out, fmtStr, p[i]) 959 | } 960 | } 961 | fmt.Fprintln(out) 962 | } 963 | } 964 | timings.Printed = time.Now() 965 | return timings 966 | } 967 | 968 | func rawHandler(cmd []string, out io.Writer) *Timing { 969 | timings := makeTiming() 970 | result, err := cl.Query(cmd[1] + ";") 971 | timings.Executed = time.Now() 972 | if err != nil { 973 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 974 | return timings 975 | } 976 | spew.Dump(result) 977 | timings.Printed = time.Now() 978 | return timings 979 | } 980 | 981 | func writeRcHandler(cmd []string, out io.Writer) *Timing { 982 | timings := makeTiming() 983 | tpl := `host = "%s" 984 | port = %d 985 | user = "%s" 986 | pass = "%s" 987 | db = "%s" 988 | ` 989 | rc, err := os.Create(path_rc) 990 | if err != nil { 991 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 992 | return timings 993 | } 994 | _, err = fmt.Fprintf(rc, tpl, host, port, user, pass, db) 995 | 996 | timings.Executed = time.Now() 997 | if err != nil { 998 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 999 | return timings 1000 | } 1001 | return timings 1002 | } 1003 | -------------------------------------------------------------------------------- /influx-cli_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/davecgh/go-spew/spew" 5 | "reflect" 6 | "regexp" 7 | "testing" 8 | ) 9 | 10 | func regexTest(regex *regexp.Regexp, test string, expected []string, t *testing.T) { 11 | result := regex.FindStringSubmatch(test) 12 | if !reflect.DeepEqual(result, expected) { 13 | t.Errorf("regex : %s\n", regex) 14 | t.Errorf("subject : %s\n", test) 15 | t.Errorf("expected: %v\n", spew.Sdump(expected)) 16 | t.Errorf("got : %v\n", spew.Sdump(result)) 17 | } 18 | } 19 | 20 | func Test_ParseOption(t *testing.T) { 21 | re := regexp.MustCompile(regexOption) 22 | regexTest(re, 23 | "\\db foo", 24 | []string{"\\db foo", "db", "foo"}, 25 | t) 26 | regexTest(re, 27 | "\\db foo1", 28 | []string{"\\db foo1", "db", "foo1"}, 29 | t) 30 | } 31 | 32 | func Test_ParseInsert(t *testing.T) { 33 | re := regexp.MustCompile(regexInsert) 34 | regexTest(re, 35 | "insert into bar (col) values (1)", 36 | []string{"insert into bar (col) values (1)", "bar", "(col)", "1"}, 37 | t) 38 | regexTest(re, 39 | "insert into demo values (1406231160000, 0, 10)", 40 | []string{"insert into demo values (1406231160000, 0, 10)", "demo", "", "1406231160000, 0, 10"}, 41 | t) 42 | } 43 | --------------------------------------------------------------------------------