redis-cli.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. package main
  2. import (
  3. "flag"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "regexp"
  8. "strconv"
  9. "strings"
  10. "github.com/holys/goredis"
  11. "github.com/peterh/liner"
  12. )
  13. var (
  14. hostname = flag.String("h", getEnv("REDIS_HOST", "127.0.0.1"), "Server hostname")
  15. port = flag.String("p", getEnv("REDIS_PORT", "6379"), "Server server port")
  16. socket = flag.String("s", "", "Server socket. (overwrites hostname and port)")
  17. dbn = flag.Int("n", 0, "Database number(default 0)")
  18. auth = flag.String("a", "", "Password to use when connecting to the server")
  19. outputRaw = flag.Bool("raw", false, "Use raw formatting for replies")
  20. showWelcome = flag.Bool("welcome", false, "show welcome message, mainly for web usage via gotty")
  21. )
  22. var (
  23. line *liner.State
  24. historyPath = filepath.Join(os.Getenv("HOME"), ".gorediscli_history") // $HOME/.gorediscli_history
  25. mode int
  26. client *goredis.Client
  27. )
  28. // output
  29. const (
  30. stdMode = iota
  31. rawMode
  32. )
  33. func main() {
  34. flag.Parse()
  35. if *outputRaw {
  36. mode = rawMode
  37. } else {
  38. mode = stdMode
  39. }
  40. // Start interactive mode when no command is provided
  41. if flag.NArg() == 0 {
  42. repl()
  43. }
  44. noninteractive(flag.Args())
  45. }
  46. func getEnv(key string, defaultValue string) string {
  47. value, found := os.LookupEnv(key)
  48. if !found {
  49. return defaultValue
  50. }
  51. return value
  52. }
  53. // Read-Eval-Print Loop
  54. func repl() {
  55. line = liner.NewLiner()
  56. defer line.Close()
  57. line.SetCtrlCAborts(true)
  58. setCompletionHandler()
  59. loadHistory()
  60. defer saveHistory()
  61. reg, _ := regexp.Compile(`'.*?'|".*?"|\S+`)
  62. prompt := ""
  63. cliConnect()
  64. if *showWelcome {
  65. showWelcomeMsg()
  66. }
  67. for {
  68. addr := addr()
  69. if *dbn > 0 && *dbn < 16 {
  70. prompt = fmt.Sprintf("%s[%d]> ", addr, *dbn)
  71. } else {
  72. prompt = fmt.Sprintf("%s> ", addr)
  73. }
  74. cmd, err := line.Prompt(prompt)
  75. if err != nil {
  76. fmt.Printf("%s\n", err.Error())
  77. return
  78. }
  79. cmds := reg.FindAllString(cmd, -1)
  80. if len(cmds) == 0 {
  81. continue
  82. } else {
  83. appendHistory(cmds)
  84. cmd := strings.ToLower(cmds[0])
  85. if cmd == "help" || cmd == "?" {
  86. printHelp(cmds)
  87. } else if cmd == "quit" || cmd == "exit" {
  88. os.Exit(0)
  89. } else if cmd == "clear" {
  90. println("Please use Ctrl + L instead")
  91. } else if cmd == "connect" {
  92. reconnect(cmds[1:])
  93. } else if cmd == "mode" {
  94. switchMode(cmds[1:])
  95. } else {
  96. cliSendCommand(cmds)
  97. }
  98. }
  99. }
  100. }
  101. func appendHistory(cmds []string) {
  102. // make a copy of cmds
  103. cloneCmds := make([]string, len(cmds))
  104. for i, cmd := range cmds {
  105. cloneCmds[i] = cmd
  106. }
  107. // for security reason, hide the password with ******
  108. if len(cloneCmds) == 2 && strings.ToLower(cloneCmds[0]) == "auth" {
  109. cloneCmds[1] = "******"
  110. }
  111. if len(cloneCmds) == 4 && strings.ToLower(cloneCmds[0]) == "connect" {
  112. cloneCmds[3] = "******"
  113. }
  114. line.AppendHistory(strings.Join(cloneCmds, " "))
  115. }
  116. func cliSendCommand(cmds []string) {
  117. cliConnect()
  118. if len(cmds) == 0 {
  119. return
  120. }
  121. args := make([]interface{}, len(cmds[1:]))
  122. for i := range args {
  123. args[i] = strings.Trim(string(cmds[1+i]), "\"'")
  124. }
  125. cmd := strings.ToLower(cmds[0])
  126. if cmd == "monitor" {
  127. respChan := make(chan interface{})
  128. stopChan := make(chan struct{})
  129. err := client.Monitor(respChan, stopChan)
  130. if err != nil {
  131. fmt.Printf("(error) %s\n", err.Error())
  132. return
  133. }
  134. for {
  135. select {
  136. case mr := <-respChan:
  137. printReply(0, mr, mode)
  138. fmt.Printf("\n")
  139. case <-stopChan:
  140. fmt.Println("Error: Server closed the connection")
  141. return
  142. }
  143. }
  144. }
  145. r, err := client.Do(cmd, args...)
  146. if err == nil && strings.ToLower(cmd) == "select" {
  147. *dbn, _ = strconv.Atoi(cmds[1])
  148. }
  149. if err != nil {
  150. fmt.Printf("(error) %s", err.Error())
  151. } else {
  152. if cmd == "info" {
  153. printInfo(r)
  154. } else {
  155. printReply(0, r, mode)
  156. }
  157. }
  158. fmt.Printf("\n")
  159. }
  160. func cliConnect() {
  161. if client == nil {
  162. addr := addr()
  163. client = goredis.NewClient(addr, "")
  164. client.SetMaxIdleConns(1)
  165. sendPing(client)
  166. sendSelect(client, *dbn)
  167. sendAuth(client, *auth)
  168. }
  169. }
  170. func reconnect(args []string) {
  171. if len(args) < 2 {
  172. fmt.Println("(error) invalid connect arguments. At least provides host and port.")
  173. return
  174. }
  175. h := args[0]
  176. p := args[1]
  177. var auth string
  178. if len(args) > 2 {
  179. auth = args[2]
  180. }
  181. if h != "" && p != "" {
  182. addr := fmt.Sprintf("%s:%s", h, p)
  183. client = goredis.NewClient(addr, "")
  184. }
  185. if err := sendPing(client); err != nil {
  186. return
  187. }
  188. // change prompt
  189. hostname = &h
  190. port = &p
  191. if auth != "" {
  192. err := sendAuth(client, auth)
  193. if err != nil {
  194. return
  195. }
  196. }
  197. fmt.Printf("connected %s:%s successfully \n", h, p)
  198. }
  199. func switchMode(args []string) {
  200. if len(args) != 1 {
  201. fmt.Println("invalid args. Should be MODE [raw|std]")
  202. return
  203. }
  204. m := strings.ToLower(args[0])
  205. if m != "raw" && m != "std" {
  206. fmt.Println("invalid args. Should be MODE [raw|std]")
  207. return
  208. }
  209. switch m {
  210. case "std":
  211. mode = stdMode
  212. case "raw":
  213. mode = rawMode
  214. }
  215. return
  216. }
  217. func addr() string {
  218. var addr string
  219. if len(*socket) > 0 {
  220. addr = *socket
  221. } else {
  222. addr = fmt.Sprintf("%s:%s", *hostname, *port)
  223. }
  224. return addr
  225. }
  226. func noninteractive(args []string) {
  227. cliSendCommand(args)
  228. }
  229. func printInfo(reply interface{}) {
  230. switch reply := reply.(type) {
  231. case []byte:
  232. fmt.Printf("%s", reply)
  233. //some redis proxies don't support this command.
  234. case goredis.Error:
  235. fmt.Printf("(error) %s", string(reply))
  236. }
  237. }
  238. func printReply(level int, reply interface{}, mode int) {
  239. switch mode {
  240. case stdMode:
  241. printStdReply(level, reply)
  242. case rawMode:
  243. printRawReply(level, reply)
  244. default:
  245. printStdReply(level, reply)
  246. }
  247. }
  248. func printStdReply(level int, reply interface{}) {
  249. switch reply := reply.(type) {
  250. case int64:
  251. fmt.Printf("(integer) %d", reply)
  252. case string:
  253. fmt.Printf("%s", reply)
  254. case []byte:
  255. fmt.Printf("%q", reply)
  256. case nil:
  257. fmt.Printf("(nil)")
  258. case goredis.Error:
  259. fmt.Printf("(error) %s", string(reply))
  260. case []interface{}:
  261. for i, v := range reply {
  262. if i != 0 {
  263. fmt.Printf("%s", strings.Repeat(" ", level*4))
  264. }
  265. s := fmt.Sprintf("%d) ", i+1)
  266. fmt.Printf("%-4s", s)
  267. printStdReply(level+1, v)
  268. if i != len(reply)-1 {
  269. fmt.Printf("\n")
  270. }
  271. }
  272. default:
  273. fmt.Printf("Unknown reply type: %+v", reply)
  274. }
  275. }
  276. func printRawReply(level int, reply interface{}) {
  277. switch reply := reply.(type) {
  278. case int64:
  279. fmt.Printf("%d", reply)
  280. case string:
  281. fmt.Printf("%s", reply)
  282. case []byte:
  283. fmt.Printf("%s", reply)
  284. case nil:
  285. // do nothing
  286. case goredis.Error:
  287. fmt.Printf("%s\n", string(reply))
  288. case []interface{}:
  289. for i, v := range reply {
  290. if i != 0 {
  291. fmt.Printf("%s", strings.Repeat(" ", level*4))
  292. }
  293. printRawReply(level+1, v)
  294. if i != len(reply)-1 {
  295. fmt.Printf("\n")
  296. }
  297. }
  298. default:
  299. fmt.Printf("Unknown reply type: %+v", reply)
  300. }
  301. }
  302. func printGenericHelp() {
  303. msg :=
  304. `redis-cli
  305. Type: "help <command>" for help on <command>
  306. `
  307. fmt.Println(msg)
  308. }
  309. func printCommandHelp(arr []string) {
  310. fmt.Println()
  311. fmt.Printf("\t%s %s \n", arr[0], arr[1])
  312. fmt.Printf("\tGroup: %s \n", arr[2])
  313. fmt.Println()
  314. }
  315. func printHelp(cmds []string) {
  316. args := cmds[1:]
  317. if len(args) == 0 {
  318. printGenericHelp()
  319. } else if len(args) > 1 {
  320. fmt.Println()
  321. } else {
  322. cmd := strings.ToUpper(args[0])
  323. for i := 0; i < len(helpCommands); i++ {
  324. if helpCommands[i][0] == cmd {
  325. printCommandHelp(helpCommands[i])
  326. }
  327. }
  328. }
  329. }
  330. func sendSelect(client *goredis.Client, index int) {
  331. if index == 0 {
  332. // do nothing
  333. return
  334. }
  335. if index > 16 || index < 0 {
  336. index = 0
  337. fmt.Println("index out of range, should less than 16")
  338. }
  339. _, err := client.Do("SELECT", index)
  340. if err != nil {
  341. fmt.Printf("%s\n", err.Error())
  342. }
  343. }
  344. func sendAuth(client *goredis.Client, passwd string) error {
  345. if passwd == "" {
  346. // do nothing
  347. return nil
  348. }
  349. resp, err := client.Do("AUTH", passwd)
  350. if err != nil {
  351. fmt.Printf("(error) %s\n", err.Error())
  352. return err
  353. }
  354. switch resp := resp.(type) {
  355. case goredis.Error:
  356. fmt.Printf("(error) %s\n", resp.Error())
  357. return resp
  358. }
  359. return nil
  360. }
  361. func sendPing(client *goredis.Client) error {
  362. _, err := client.Do("PING")
  363. if err != nil {
  364. fmt.Printf("%s\n", err.Error())
  365. return err
  366. }
  367. return nil
  368. }
  369. func setCompletionHandler() {
  370. line.SetCompleter(func(line string) (c []string) {
  371. for _, i := range helpCommands {
  372. if strings.HasPrefix(i[0], strings.ToUpper(line)) {
  373. c = append(c, i[0])
  374. }
  375. }
  376. return
  377. })
  378. }
  379. func loadHistory() {
  380. if f, err := os.Open(historyPath); err == nil {
  381. line.ReadHistory(f)
  382. f.Close()
  383. }
  384. }
  385. func saveHistory() {
  386. if f, err := os.Create(historyPath); err != nil {
  387. fmt.Printf("Error writing history file: %s", err.Error())
  388. } else {
  389. line.WriteHistory(f)
  390. f.Close()
  391. }
  392. }
  393. func showWelcomeMsg() {
  394. welcome := `
  395. Welcome to redis-cli online.
  396. You can switch to different redis instance with the CONNECT command.
  397. Usage: CONNECT host port [auth]
  398. Switch output mode with MODE command.
  399. Usage: MODE [std | raw]
  400. `
  401. fmt.Println(welcome)
  402. }