Browse Source

etcdctlv3: use spf13/cobra for cli interface

This replaces codegansta/cli with spf13/cobra base on
this guideline: https://github.com/coreos/docs/blob/master/golang/README.md#cli.
Gyu-Ho Lee 10 years ago
parent
commit
b7647e0b55

+ 19 - 15
etcdctlv3/command/compaction.go → etcdctlv3/command/compaction_command.go

@@ -15,38 +15,42 @@
 package command
 package command
 
 
 import (
 import (
+	"fmt"
 	"strconv"
 	"strconv"
 
 
-	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra"
 	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
 	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
 	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
 	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
 	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
 	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
 )
 )
 
 
-// NewCompactionCommand returns the CLI command for "compaction".
-func NewCompactionCommand() cli.Command {
-	return cli.Command{
-		Name: "compaction",
-		Action: func(c *cli.Context) {
-			compactionCommandFunc(c)
-		},
+// NewCompactionCommand returns the cobra command for "compaction".
+func NewCompactionCommand() *cobra.Command {
+	return &cobra.Command{
+		Use:   "compaction",
+		Short: "Compaction compacts the event history in etcd.",
+		Run:   compactionCommandFunc,
 	}
 	}
 }
 }
 
 
 // compactionCommandFunc executes the "compaction" command.
 // compactionCommandFunc executes the "compaction" command.
-func compactionCommandFunc(c *cli.Context) {
-	if len(c.Args()) != 1 {
-		panic("bad arg")
+func compactionCommandFunc(cmd *cobra.Command, args []string) {
+	if len(args) != 1 {
+		ExitWithError(ExitBadArgs, fmt.Errorf("compaction command needs 1 argument."))
 	}
 	}
 
 
-	rev, err := strconv.ParseInt(c.Args()[0], 10, 64)
+	rev, err := strconv.ParseInt(args[0], 10, 64)
 	if err != nil {
 	if err != nil {
-		panic("bad arg")
+		ExitWithError(ExitError, err)
 	}
 	}
 
 
-	conn, err := grpc.Dial(c.GlobalString("endpoint"))
+	endpoint, err := cmd.Flags().GetString("endpoint")
 	if err != nil {
 	if err != nil {
-		panic(err)
+		ExitWithError(ExitError, err)
+	}
+	conn, err := grpc.Dial(endpoint)
+	if err != nil {
+		ExitWithError(ExitBadConnection, err)
 	}
 	}
 	kv := pb.NewKVClient(conn)
 	kv := pb.NewKVClient(conn)
 	req := &pb.CompactionRequest{Revision: rev}
 	req := &pb.CompactionRequest{Revision: rev}

+ 21 - 17
etcdctlv3/command/delete_range_command.go

@@ -17,36 +17,40 @@ package command
 import (
 import (
 	"fmt"
 	"fmt"
 
 
-	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra"
 	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
 	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
 	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
 	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
 	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
 	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
 )
 )
 
 
-// NewDeleteRangeCommand returns the CLI command for "deleteRange".
-func NewDeleteRangeCommand() cli.Command {
-	return cli.Command{
-		Name: "delete-range",
-		Action: func(c *cli.Context) {
-			deleteRangeCommandFunc(c)
-		},
+// NewDeleteRangeCommand returns the cobra command for "deleteRange".
+func NewDeleteRangeCommand() *cobra.Command {
+	return &cobra.Command{
+		Use:   "delete-range",
+		Short: "DeleteRange deletes the given range from the store.",
+		Run:   deleteRangeCommandFunc,
 	}
 	}
 }
 }
 
 
-// deleteRangeCommandFunc executes the "delegeRange" command.
-func deleteRangeCommandFunc(c *cli.Context) {
-	if len(c.Args()) == 0 {
-		panic("bad arg")
+// deleteRangeCommandFunc executes the "deleteRange" command.
+func deleteRangeCommandFunc(cmd *cobra.Command, args []string) {
+	if len(args) == 0 {
+		ExitWithError(ExitBadArgs, fmt.Errorf("delete-range command needs arguments."))
 	}
 	}
 
 
 	var rangeEnd []byte
 	var rangeEnd []byte
-	key := []byte(c.Args()[0])
-	if len(c.Args()) > 1 {
-		rangeEnd = []byte(c.Args()[1])
+	key := []byte(args[0])
+	if len(args) > 1 {
+		rangeEnd = []byte(args[1])
 	}
 	}
-	conn, err := grpc.Dial(c.GlobalString("endpoint"))
+
+	endpoint, err := cmd.Flags().GetString("endpoint")
+	if err != nil {
+		ExitWithError(ExitError, err)
+	}
+	conn, err := grpc.Dial(endpoint)
 	if err != nil {
 	if err != nil {
-		panic(err)
+		ExitWithError(ExitBadConnection, err)
 	}
 	}
 	kv := pb.NewKVClient(conn)
 	kv := pb.NewKVClient(conn)
 	req := &pb.DeleteRangeRequest{Key: key, RangeEnd: rangeEnd}
 	req := &pb.DeleteRangeRequest{Key: key, RangeEnd: rangeEnd}

+ 39 - 0
etcdctlv3/command/error.go

@@ -0,0 +1,39 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package command
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/coreos/etcd/client"
+)
+
+const (
+	// http://tldp.org/LDP/abs/html/exitcodes.html
+	ExitSuccess = iota
+	ExitError
+	ExitBadConnection
+	ExitInvalidInput // for txn, watch command
+	ExitBadArgs      = 128
+)
+
+func ExitWithError(code int, err error) {
+	fmt.Fprintln(os.Stderr, "Error: ", err)
+	if cerr, ok := err.(*client.ClusterError); ok {
+		fmt.Fprintln(os.Stderr, cerr.Detail())
+	}
+	os.Exit(code)
+}

+ 21 - 0
etcdctlv3/command/global.go

@@ -0,0 +1,21 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package command
+
+// GlobalFlags are flags that defined globally
+// and are inherited to all sub-commands.
+type GlobalFlags struct {
+	Endpoints string
+}

+ 19 - 15
etcdctlv3/command/put_command.go

@@ -17,33 +17,37 @@ package command
 import (
 import (
 	"fmt"
 	"fmt"
 
 
-	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra"
 	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
 	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
 	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
 	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
 	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
 	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
 )
 )
 
 
-// NewPutCommand returns the CLI command for "put".
-func NewPutCommand() cli.Command {
-	return cli.Command{
-		Name: "put",
-		Action: func(c *cli.Context) {
-			putCommandFunc(c)
-		},
+// NewPutCommand returns the cobra command for "put".
+func NewPutCommand() *cobra.Command {
+	return &cobra.Command{
+		Use:   "put",
+		Short: "Put puts the given key into the store.",
+		Run:   putCommandFunc,
 	}
 	}
 }
 }
 
 
 // putCommandFunc executes the "put" command.
 // putCommandFunc executes the "put" command.
-func putCommandFunc(c *cli.Context) {
-	if len(c.Args()) != 2 {
-		panic("bad arg")
+func putCommandFunc(cmd *cobra.Command, args []string) {
+	if len(args) != 2 {
+		ExitWithError(ExitBadArgs, fmt.Errorf("put command needs 2 arguments."))
 	}
 	}
 
 
-	key := []byte(c.Args()[0])
-	value := []byte(c.Args()[1])
-	conn, err := grpc.Dial(c.GlobalString("endpoint"))
+	key := []byte(args[0])
+	value := []byte(args[1])
+
+	endpoint, err := cmd.Flags().GetString("endpoint")
+	if err != nil {
+		ExitWithError(ExitError, err)
+	}
+	conn, err := grpc.Dial(endpoint)
 	if err != nil {
 	if err != nil {
-		panic(err)
+		ExitWithError(ExitBadConnection, err)
 	}
 	}
 	kv := pb.NewKVClient(conn)
 	kv := pb.NewKVClient(conn)
 	req := &pb.PutRequest{Key: key, Value: value}
 	req := &pb.PutRequest{Key: key, Value: value}

+ 20 - 16
etcdctlv3/command/range_command.go

@@ -17,36 +17,40 @@ package command
 import (
 import (
 	"fmt"
 	"fmt"
 
 
-	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra"
 	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
 	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
 	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
 	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
 	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
 	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
 )
 )
 
 
-// NewRangeCommand returns the CLI command for "range".
-func NewRangeCommand() cli.Command {
-	return cli.Command{
-		Name: "range",
-		Action: func(c *cli.Context) {
-			rangeCommandFunc(c)
-		},
+// NewRangeCommand returns the cobra command for "range".
+func NewRangeCommand() *cobra.Command {
+	return &cobra.Command{
+		Use:   "range",
+		Short: "Range gets the keys in the range from the store.",
+		Run:   rangeCommandFunc,
 	}
 	}
 }
 }
 
 
 // rangeCommandFunc executes the "range" command.
 // rangeCommandFunc executes the "range" command.
-func rangeCommandFunc(c *cli.Context) {
-	if len(c.Args()) == 0 {
-		panic("bad arg")
+func rangeCommandFunc(cmd *cobra.Command, args []string) {
+	if len(args) == 0 {
+		ExitWithError(ExitBadArgs, fmt.Errorf("range command needs arguments."))
 	}
 	}
 
 
 	var rangeEnd []byte
 	var rangeEnd []byte
-	key := []byte(c.Args()[0])
-	if len(c.Args()) > 1 {
-		rangeEnd = []byte(c.Args()[1])
+	key := []byte(args[0])
+	if len(args) > 1 {
+		rangeEnd = []byte(args[1])
 	}
 	}
-	conn, err := grpc.Dial(c.GlobalString("endpoint"))
+
+	endpoint, err := cmd.Flags().GetString("endpoint")
+	if err != nil {
+		ExitWithError(ExitError, err)
+	}
+	conn, err := grpc.Dial(endpoint)
 	if err != nil {
 	if err != nil {
-		panic(err)
+		ExitWithError(ExitBadConnection, err)
 	}
 	}
 	kv := pb.NewKVClient(conn)
 	kv := pb.NewKVClient(conn)
 	req := &pb.RangeRequest{Key: key, RangeEnd: rangeEnd}
 	req := &pb.RangeRequest{Key: key, RangeEnd: rangeEnd}

+ 23 - 23
etcdctlv3/command/txn_command.go

@@ -21,26 +21,25 @@ import (
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 
 
-	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra"
 	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
 	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
 	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
 	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
 	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
 	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
 )
 )
 
 
-// NewTxnCommand returns the CLI command for "txn".
-func NewTxnCommand() cli.Command {
-	return cli.Command{
-		Name: "txn",
-		Action: func(c *cli.Context) {
-			txnCommandFunc(c)
-		},
+// NewTxnCommand returns the cobra command for "txn".
+func NewTxnCommand() *cobra.Command {
+	return &cobra.Command{
+		Use:   "txn",
+		Short: "Txn processes all the requests in one transaction.",
+		Run:   txnCommandFunc,
 	}
 	}
 }
 }
 
 
 // txnCommandFunc executes the "txn" command.
 // txnCommandFunc executes the "txn" command.
-func txnCommandFunc(c *cli.Context) {
-	if len(c.Args()) != 0 {
-		panic("unexpected args")
+func txnCommandFunc(cmd *cobra.Command, args []string) {
+	if len(args) != 0 {
+		ExitWithError(ExitBadArgs, fmt.Errorf("txn command does not accept argument."))
 	}
 	}
 
 
 	reader := bufio.NewReader(os.Stdin)
 	reader := bufio.NewReader(os.Stdin)
@@ -51,15 +50,19 @@ func txnCommandFunc(c *cli.Context) {
 		next = next(txn, reader)
 		next = next(txn, reader)
 	}
 	}
 
 
-	conn, err := grpc.Dial(c.GlobalString("endpoint"))
+	endpoint, err := cmd.Flags().GetString("endpoint")
 	if err != nil {
 	if err != nil {
-		panic(err)
+		ExitWithError(ExitError, err)
+	}
+	conn, err := grpc.Dial(endpoint)
+	if err != nil {
+		ExitWithError(ExitBadConnection, err)
 	}
 	}
 	kv := pb.NewKVClient(conn)
 	kv := pb.NewKVClient(conn)
 
 
 	resp, err := kv.Txn(context.Background(), txn)
 	resp, err := kv.Txn(context.Background(), txn)
 	if err != nil {
 	if err != nil {
-		fmt.Println(err)
+		ExitWithError(ExitError, err)
 	}
 	}
 	if resp.Succeeded {
 	if resp.Succeeded {
 		fmt.Println("executed success request list")
 		fmt.Println("executed success request list")
@@ -75,7 +78,7 @@ func compareState(txn *pb.TxnRequest, r *bufio.Reader) stateFunc {
 
 
 	line, err := r.ReadString('\n')
 	line, err := r.ReadString('\n')
 	if err != nil {
 	if err != nil {
-		os.Exit(1)
+		ExitWithError(ExitInvalidInput, err)
 	}
 	}
 
 
 	if len(line) == 1 {
 	if len(line) == 1 {
@@ -86,8 +89,7 @@ func compareState(txn *pb.TxnRequest, r *bufio.Reader) stateFunc {
 	line = line[:len(line)-1]
 	line = line[:len(line)-1]
 	c, err := parseCompare(line)
 	c, err := parseCompare(line)
 	if err != nil {
 	if err != nil {
-		fmt.Println(err)
-		os.Exit(1)
+		ExitWithError(ExitInvalidInput, err)
 	}
 	}
 
 
 	txn.Compare = append(txn.Compare, c)
 	txn.Compare = append(txn.Compare, c)
@@ -100,7 +102,7 @@ func successState(txn *pb.TxnRequest, r *bufio.Reader) stateFunc {
 
 
 	line, err := r.ReadString('\n')
 	line, err := r.ReadString('\n')
 	if err != nil {
 	if err != nil {
-		os.Exit(1)
+		ExitWithError(ExitInvalidInput, err)
 	}
 	}
 
 
 	if len(line) == 1 {
 	if len(line) == 1 {
@@ -111,8 +113,7 @@ func successState(txn *pb.TxnRequest, r *bufio.Reader) stateFunc {
 	line = line[:len(line)-1]
 	line = line[:len(line)-1]
 	ru, err := parseRequestUnion(line)
 	ru, err := parseRequestUnion(line)
 	if err != nil {
 	if err != nil {
-		fmt.Println(err)
-		os.Exit(1)
+		ExitWithError(ExitInvalidInput, err)
 	}
 	}
 
 
 	txn.Success = append(txn.Success, ru)
 	txn.Success = append(txn.Success, ru)
@@ -125,7 +126,7 @@ func failureState(txn *pb.TxnRequest, r *bufio.Reader) stateFunc {
 
 
 	line, err := r.ReadString('\n')
 	line, err := r.ReadString('\n')
 	if err != nil {
 	if err != nil {
-		os.Exit(1)
+		ExitWithError(ExitInvalidInput, err)
 	}
 	}
 
 
 	if len(line) == 1 {
 	if len(line) == 1 {
@@ -136,8 +137,7 @@ func failureState(txn *pb.TxnRequest, r *bufio.Reader) stateFunc {
 	line = line[:len(line)-1]
 	line = line[:len(line)-1]
 	ru, err := parseRequestUnion(line)
 	ru, err := parseRequestUnion(line)
 	if err != nil {
 	if err != nil {
-		fmt.Println(err)
-		os.Exit(1)
+		ExitWithError(ExitInvalidInput, err)
 	}
 	}
 
 
 	txn.Failure = append(txn.Failure, ru)
 	txn.Failure = append(txn.Failure, ru)

+ 35 - 0
etcdctlv3/command/version_command.go

@@ -0,0 +1,35 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package command
+
+import (
+	"fmt"
+
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra"
+	"github.com/coreos/etcd/version"
+)
+
+// NewVersionCommand prints out the version of etcd.
+func NewVersionCommand() *cobra.Command {
+	return &cobra.Command{
+		Use:   "version",
+		Short: "Print the version of etcdctlv3.",
+		Run:   versionCommandFunc,
+	}
+}
+
+func versionCommandFunc(cmd *cobra.Command, args []string) {
+	fmt.Println(version.Version)
+}

+ 18 - 16
etcdctlv3/command/watch_command.go

@@ -21,33 +21,36 @@ import (
 	"os"
 	"os"
 	"strings"
 	"strings"
 
 
-	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra"
 	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
 	"github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
 	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
 	"github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc"
 	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
 	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
 )
 )
 
 
-// NewWatchCommand returns the CLI command for "watch".
-func NewWatchCommand() cli.Command {
-	return cli.Command{
-		Name: "watch",
-		Action: func(c *cli.Context) {
-			watchCommandFunc(c)
-		},
+// NewWatchCommand returns the cobra command for "watch".
+func NewWatchCommand() *cobra.Command {
+	return &cobra.Command{
+		Use:   "watch",
+		Short: "Watch watches the events happening or happened.",
+		Run:   watchCommandFunc,
 	}
 	}
 }
 }
 
 
 // watchCommandFunc executes the "watch" command.
 // watchCommandFunc executes the "watch" command.
-func watchCommandFunc(c *cli.Context) {
-	conn, err := grpc.Dial(c.GlobalString("endpoint"))
+func watchCommandFunc(cmd *cobra.Command, args []string) {
+	endpoint, err := cmd.Flags().GetString("endpoint")
 	if err != nil {
 	if err != nil {
-		panic(err)
+		ExitWithError(ExitInvalidInput, err)
+	}
+	conn, err := grpc.Dial(endpoint)
+	if err != nil {
+		ExitWithError(ExitBadConnection, err)
 	}
 	}
 
 
 	wAPI := pb.NewWatchClient(conn)
 	wAPI := pb.NewWatchClient(conn)
 	wStream, err := wAPI.Watch(context.TODO())
 	wStream, err := wAPI.Watch(context.TODO())
 	if err != nil {
 	if err != nil {
-		panic(err)
+		ExitWithError(ExitBadConnection, err)
 	}
 	}
 
 
 	go recvLoop(wStream)
 	go recvLoop(wStream)
@@ -57,8 +60,7 @@ func watchCommandFunc(c *cli.Context) {
 	for {
 	for {
 		l, err := reader.ReadString('\n')
 		l, err := reader.ReadString('\n')
 		if err != nil {
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "Error reading watch request line: %v", err)
-			os.Exit(1)
+			ExitWithError(ExitInvalidInput, fmt.Errorf("Error reading watch request line: %v", err))
 		}
 		}
 		l = strings.TrimSuffix(l, "\n")
 		l = strings.TrimSuffix(l, "\n")
 
 
@@ -91,10 +93,10 @@ func recvLoop(wStream pb.Watch_WatchClient) {
 	for {
 	for {
 		resp, err := wStream.Recv()
 		resp, err := wStream.Recv()
 		if err == io.EOF {
 		if err == io.EOF {
-			os.Exit(0)
+			os.Exit(ExitSuccess)
 		}
 		}
 		if err != nil {
 		if err != nil {
-			panic(err)
+			ExitWithError(ExitError, err)
 		}
 		}
 		fmt.Printf("%s: %s %s\n", resp.Event.Type, string(resp.Event.Kv.Key), string(resp.Event.Kv.Value))
 		fmt.Printf("%s: %s %s\n", resp.Event.Type, string(resp.Event.Kv.Key), string(resp.Event.Kv.Value))
 	}
 	}

+ 166 - 0
etcdctlv3/help.go

@@ -0,0 +1,166 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// copied from https://github.com/coreos/rkt/blob/master/rkt/help.go
+
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"os"
+	"strings"
+	"text/tabwriter"
+	"text/template"
+
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra"
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/pflag"
+	"github.com/coreos/etcd/version"
+)
+
+var (
+	commandUsageTemplate *template.Template
+	templFuncs           = template.FuncMap{
+		"descToLines": func(s string) []string {
+			// trim leading/trailing whitespace and split into slice of lines
+			return strings.Split(strings.Trim(s, "\n\t "), "\n")
+		},
+		"cmdName": func(cmd *cobra.Command, startCmd *cobra.Command) string {
+			parts := []string{cmd.Name()}
+			for cmd.HasParent() && cmd.Parent().Name() != startCmd.Name() {
+				cmd = cmd.Parent()
+				parts = append([]string{cmd.Name()}, parts...)
+			}
+			return strings.Join(parts, " ")
+		},
+	}
+)
+
+func init() {
+	commandUsage := `
+{{ $cmd := .Cmd }}\
+{{ $cmdname := cmdName .Cmd .Cmd.Root }}\
+NAME:
+{{ if not .Cmd.HasParent }}\
+{{printf "\t%s - %s" .Cmd.Name .Cmd.Short}}
+{{else}}\
+{{printf "\t%s - %s" $cmdname .Cmd.Short}}
+{{end}}\
+
+USAGE:
+{{printf "\t%s" .Cmd.UseLine}}
+{{ if not .Cmd.HasParent }}\
+
+VERSION:
+{{printf "\t%s" .Version}}
+{{end}}\
+{{if .Cmd.HasSubCommands}}\
+
+COMMANDS:
+{{range .SubCommands}}\
+{{ $cmdname := cmdName . $cmd }}\
+{{ if .Runnable }}\
+{{printf "\t%s\t%s" $cmdname .Short}}
+{{end}}\
+{{end}}\
+{{end}}\
+{{ if .Cmd.Long }}\
+
+DESCRIPTION:
+{{range $line := descToLines .Cmd.Long}}{{printf "\t%s" $line}}
+{{end}}\
+{{end}}\
+{{if .Cmd.HasLocalFlags}}\
+
+OPTIONS:
+{{.LocalFlags}}\
+{{end}}\
+{{if .Cmd.HasInheritedFlags}}\
+
+GLOBAL OPTIONS:
+{{.GlobalFlags}}\
+{{end}}
+`[1:]
+
+	commandUsageTemplate = template.Must(template.New("command_usage").Funcs(templFuncs).Parse(strings.Replace(commandUsage, "\\\n", "", -1)))
+}
+
+func etcdFlagUsages(flagSet *pflag.FlagSet) string {
+	x := new(bytes.Buffer)
+
+	flagSet.VisitAll(func(flag *pflag.Flag) {
+		if len(flag.Deprecated) > 0 {
+			return
+		}
+		format := ""
+		if len(flag.Shorthand) > 0 {
+			format = "  -%s, --%s"
+		} else {
+			format = "   %s   --%s"
+		}
+		if len(flag.NoOptDefVal) > 0 {
+			format = format + "["
+		}
+		if flag.Value.Type() == "string" {
+			// put quotes on the value
+			format = format + "=%q"
+		} else {
+			format = format + "=%s"
+		}
+		if len(flag.NoOptDefVal) > 0 {
+			format = format + "]"
+		}
+		format = format + "\t%s\n"
+		shorthand := flag.Shorthand
+		fmt.Fprintf(x, format, shorthand, flag.Name, flag.DefValue, flag.Usage)
+	})
+
+	return x.String()
+}
+
+func getSubCommands(cmd *cobra.Command) []*cobra.Command {
+	var subCommands []*cobra.Command
+	for _, subCmd := range cmd.Commands() {
+		subCommands = append(subCommands, subCmd)
+		subCommands = append(subCommands, getSubCommands(subCmd)...)
+	}
+	return subCommands
+}
+
+func usageFunc(cmd *cobra.Command) error {
+	subCommands := getSubCommands(cmd)
+	tabOut := getTabOutWithWriter(os.Stdout)
+	commandUsageTemplate.Execute(tabOut, struct {
+		Cmd         *cobra.Command
+		LocalFlags  string
+		GlobalFlags string
+		SubCommands []*cobra.Command
+		Version     string
+	}{
+		cmd,
+		etcdFlagUsages(cmd.LocalFlags()),
+		etcdFlagUsages(cmd.InheritedFlags()),
+		subCommands,
+		version.Version,
+	})
+	tabOut.Flush()
+	return nil
+}
+
+func getTabOutWithWriter(writer io.Writer) *tabwriter.Writer {
+	aTabOut := new(tabwriter.Writer)
+	aTabOut.Init(writer, 0, 8, 1, '\t', 0)
+	return aTabOut
+}

+ 39 - 13
etcdctlv3/main.go

@@ -16,29 +16,55 @@
 package main
 package main
 
 
 import (
 import (
-	"os"
+	"text/tabwriter"
 
 
-	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli"
+	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra"
 	"github.com/coreos/etcd/etcdctlv3/command"
 	"github.com/coreos/etcd/etcdctlv3/command"
-	"github.com/coreos/etcd/version"
 )
 )
 
 
-func main() {
-	app := cli.NewApp()
-	app.Name = "etcdctlv3"
-	app.Version = version.Version
-	app.Usage = "A simple command line client for etcd3."
-	app.Flags = []cli.Flag{
-		cli.StringFlag{Name: "endpoint", Value: "127.0.0.1:2378", Usage: "gRPC endpoint"},
+const (
+	cliName        = "etcdctlv3"
+	cliDescription = "A simple command line client for etcd3."
+)
+
+var (
+	tabOut      *tabwriter.Writer
+	globalFlags = command.GlobalFlags{}
+)
+
+var (
+	rootCmd = &cobra.Command{
+		Use:        cliName,
+		Short:      cliDescription,
+		SuggestFor: []string{"etcctlv3", "etcdcltv3", "etlctlv3"},
 	}
 	}
-	app.Commands = []cli.Command{
+)
+
+func init() {
+	rootCmd.PersistentFlags().StringVar(&globalFlags.Endpoints, "endpoint", "127.0.0.1:2378", "gRPC endpoint")
+
+	rootCmd.AddCommand(
 		command.NewRangeCommand(),
 		command.NewRangeCommand(),
 		command.NewPutCommand(),
 		command.NewPutCommand(),
 		command.NewDeleteRangeCommand(),
 		command.NewDeleteRangeCommand(),
 		command.NewTxnCommand(),
 		command.NewTxnCommand(),
 		command.NewCompactionCommand(),
 		command.NewCompactionCommand(),
 		command.NewWatchCommand(),
 		command.NewWatchCommand(),
-	}
+		command.NewVersionCommand(),
+	)
+}
+
+func init() {
+	cobra.EnablePrefixMatching = true
+}
 
 
-	app.Run(os.Args)
+func main() {
+	rootCmd.SetUsageFunc(usageFunc)
+
+	// Make help just show the usage
+	rootCmd.SetHelpTemplate(`{{.UsageString}}`)
+
+	if err := rootCmd.Execute(); err != nil {
+		command.ExitWithError(command.ExitError, err)
+	}
 }
 }