feat(list): use tablewriter for pretty printing

This CL implements 'madb group list', and modifies the existing
'madb name list' and 'madb user list' command implementation.

These list commands now use the third-party tablewriter package.
The items are sorted before being printed on the screen.

Change-Id: I88e94bb0622641a97678660c13a8c76eda0e338d
Closes: #6
MultiPart: 2/2
diff --git a/doc.go b/doc.go
index c2dca3e..ebd7b87 100644
--- a/doc.go
+++ b/doc.go
@@ -234,6 +234,7 @@
    add         Add members to a device group
    clear-all   Clear all the existing device groups
    delete      Delete an existing device group
+   list        List all the existing device groups
    remove      Remove members from a device group
    rename      Rename an existing device group
 
@@ -275,6 +276,13 @@
 <group_name> the name of an existing device group. You can specify more than one
 group names.
 
+Madb group list - List all the existing device groups
+
+Lists the name and members of all the existing device groups.
+
+Usage:
+   madb group list [flags]
+
 Madb group remove - Remove members from a device group
 
 Removes members from an existing device group. If there are no remaining members
diff --git a/group.go b/group.go
index 2874a1b..60e5768 100644
--- a/group.go
+++ b/group.go
@@ -6,15 +6,16 @@
 
 import (
 	"fmt"
+	"os"
+	"sort"
 	"strconv"
 	"strings"
 
+	"github.com/olekukonko/tablewriter"
+
 	"v.io/x/lib/cmdline"
 )
 
-// TODO(youngseokyoon): implement the following sub-commands.
-//  - list:      list all the groups and their members
-
 // TODO(youngseokyoon): use the groups for filtering devices.
 
 var cmdMadbGroup = &cmdline.Command{
@@ -22,6 +23,7 @@
 		cmdMadbGroupAdd,
 		cmdMadbGroupClearAll,
 		cmdMadbGroupDelete,
+		cmdMadbGroupList,
 		cmdMadbGroupRemove,
 		cmdMadbGroupRename,
 	},
@@ -154,6 +156,42 @@
 	return writeConfig(cfg, filename)
 }
 
+var cmdMadbGroupList = &cmdline.Command{
+	Runner: subCommandRunnerWithFilepath{runMadbGroupList, getDefaultConfigFilePath},
+	Name:   "list",
+	Short:  "List all the existing device groups",
+	Long: `
+Lists the name and members of all the existing device groups.
+`,
+}
+
+func runMadbGroupList(env *cmdline.Env, args []string, filename string) error {
+	cfg, err := readConfig(filename)
+	if err != nil {
+		return err
+	}
+
+	tw := tablewriter.NewWriter(os.Stdout)
+	tw.SetHeader([]string{"Group Name", "Members"})
+	tw.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
+	tw.SetAutoFormatHeaders(false)
+	tw.SetAlignment(tablewriter.ALIGN_LEFT)
+
+	data := make([][]string, 0, len(cfg.Groups))
+	for group, members := range cfg.Groups {
+		data = append(data, []string{group, strings.Join(members, " ")})
+	}
+
+	sort.Sort(byFirstElement(data))
+
+	for _, row := range data {
+		tw.Append(row)
+	}
+	tw.Render()
+
+	return nil
+}
+
 var cmdMadbGroupRemove = &cmdline.Command{
 	Runner: subCommandRunnerWithFilepath{runMadbGroupRemove, getDefaultConfigFilePath},
 	Name:   "remove",
diff --git a/group_test.go b/group_test.go
index adf3b94..2d5f540 100644
--- a/group_test.go
+++ b/group_test.go
@@ -266,6 +266,26 @@
 	runGroupTests(t, tests)
 }
 
+func ExampleMadbGroupList() {
+	filename := tempFilename(nil)
+	defer os.Remove(filename)
+
+	// Set some device groups.
+	runMadbGroupAdd(nil, []string{"GROUP1", "SERIAL1", "NICKNAME1", "@1"}, filename)
+	runMadbGroupAdd(nil, []string{"GROUP2", "GROUP1", "SERIAL2"}, filename)
+
+	// Call the list command.
+	runMadbGroupList(nil, []string{}, filename)
+
+	// Output:
+	// +------------+----------------------+
+	// | Group Name | Members              |
+	// +------------+----------------------+
+	// | GROUP1     | SERIAL1 NICKNAME1 @1 |
+	// | GROUP2     | GROUP1 SERIAL2       |
+	// +------------+----------------------+
+}
+
 func TestMadbGroupRemove(t *testing.T) {
 	tests := []testSequence{
 		{
diff --git a/madb.go b/madb.go
index 562436f..510d2a8 100644
--- a/madb.go
+++ b/madb.go
@@ -804,7 +804,8 @@
 
 type pathProvider func() (string, error)
 
-// subCommandRunnerWithFilepath is an adapter that turns the madb name/user subcommand functions into cmdline.Runners.
+// subCommandRunnerWithFilepath is an adapter that turns the madb
+// {group|name|user} subcommand functions into cmdline.Runners.
 type subCommandRunnerWithFilepath struct {
 	subCmd func(*cmdline.Env, []string, string) error
 	pp     pathProvider
@@ -812,8 +813,8 @@
 
 var _ cmdline.Runner = (*subCommandRunnerWithFilepath)(nil)
 
-// Run implements the cmdline.Runner interface by providing the default name file path
-// as the third string argument of the underlying run function.
+// Run implements the cmdline.Runner interface by providing the default name
+// file path as the third string argument of the underlying run function.
 func (f subCommandRunnerWithFilepath) Run(env *cmdline.Env, args []string) error {
 	p, err := f.pp()
 	if err != nil {
@@ -822,3 +823,11 @@
 
 	return f.subCmd(env, args, p)
 }
+
+// byFirstElement is used for sorting the groups by their names. Used in various
+// list commands.
+type byFirstElement [][]string
+
+func (a byFirstElement) Len() int           { return len(a) }
+func (a byFirstElement) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a byFirstElement) Less(i, j int) bool { return a[i][0] < a[j][0] }
diff --git a/name.go b/name.go
index d73d855..dbaf7da 100644
--- a/name.go
+++ b/name.go
@@ -6,6 +6,10 @@
 
 import (
 	"fmt"
+	"os"
+	"sort"
+
+	"github.com/olekukonko/tablewriter"
 
 	"v.io/x/lib/cmdline"
 )
@@ -155,14 +159,24 @@
 		return err
 	}
 
-	// TODO(youngseokyoon): pretty print this.
-	fmt.Println("Serial          Nickname")
-	fmt.Println("========================")
+	tw := tablewriter.NewWriter(os.Stdout)
+	tw.SetHeader([]string{"Serial", "Nickname"})
+	tw.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
+	tw.SetAutoFormatHeaders(false)
+	tw.SetAlignment(tablewriter.ALIGN_LEFT)
 
+	data := make([][]string, 0, len(cfg.Names))
 	for nickname, serial := range cfg.Names {
-		fmt.Printf("%v\t%v\n", serial, nickname)
+		data = append(data, []string{serial, nickname})
 	}
 
+	sort.Sort(byFirstElement(data))
+
+	for _, row := range data {
+		tw.Append(row)
+	}
+	tw.Render()
+
 	return nil
 }
 
diff --git a/name_test.go b/name_test.go
index d6aeed4..7b1b5b8 100644
--- a/name_test.go
+++ b/name_test.go
@@ -110,8 +110,26 @@
 	}
 }
 
-func TestMadbNameList(t *testing.T) {
-	// TODO(youngseokyoon): add some tests for the list command.
+func ExampleMadbNameList() {
+	filename := tempFilename(nil)
+	defer os.Remove(filename)
+
+	// Set up some nicknames first.
+	runMadbNameSet(nil, []string{"SERIAL1", "NICKNAME1"}, filename)
+	runMadbNameSet(nil, []string{"SERIAL2", "NICKNAME2"}, filename)
+	runMadbNameSet(nil, []string{"SERIAL3", "NICKNAME3"}, filename)
+
+	// Call the list command.
+	runMadbNameList(nil, []string{}, filename)
+
+	// Output:
+	// +---------+-----------+
+	// | Serial  | Nickname  |
+	// +---------+-----------+
+	// | SERIAL1 | NICKNAME1 |
+	// | SERIAL2 | NICKNAME2 |
+	// | SERIAL3 | NICKNAME3 |
+	// +---------+-----------+
 }
 
 func TestMadbNameClearAll(t *testing.T) {
diff --git a/user.go b/user.go
index 6bab71d..5e06541 100644
--- a/user.go
+++ b/user.go
@@ -6,8 +6,12 @@
 
 import (
 	"fmt"
+	"os"
+	"sort"
 	"strconv"
 
+	"github.com/olekukonko/tablewriter"
+
 	"v.io/x/lib/cmdline"
 )
 
@@ -165,14 +169,24 @@
 		return err
 	}
 
-	// TODO(youngseokyoon): pretty print this.
-	fmt.Println("Device Serial    User ID")
-	fmt.Println("========================")
+	tw := tablewriter.NewWriter(os.Stdout)
+	tw.SetHeader([]string{"Serial", "User ID"})
+	tw.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
+	tw.SetAutoFormatHeaders(false)
+	tw.SetAlignment(tablewriter.ALIGN_LEFT)
 
-	for s, u := range cfg.UserIDs {
-		fmt.Printf("%v\t%v\n", s, u)
+	data := make([][]string, 0, len(cfg.UserIDs))
+	for serial, userID := range cfg.UserIDs {
+		data = append(data, []string{serial, userID})
 	}
 
+	sort.Sort(byFirstElement(data))
+
+	for _, row := range data {
+		tw.Append(row)
+	}
+	tw.Render()
+
 	return nil
 }
 
diff --git a/user_test.go b/user_test.go
index fae4bc4..b335193 100644
--- a/user_test.go
+++ b/user_test.go
@@ -86,6 +86,28 @@
 	}
 }
 
+func ExampleMadbUserList() {
+	filename := tempFilename(nil)
+	defer os.Remove(filename)
+
+	// Set up some default users first.
+	runMadbUserSet(nil, []string{"SERIAL1", "0"}, filename)
+	runMadbUserSet(nil, []string{"SERIAL2", "0"}, filename)
+	runMadbUserSet(nil, []string{"SERIAL3", "10"}, filename)
+
+	// Call the list command.
+	runMadbUserList(nil, []string{}, filename)
+
+	// Output:
+	// +---------+---------+
+	// | Serial  | User ID |
+	// +---------+---------+
+	// | SERIAL1 | 0       |
+	// | SERIAL2 | 0       |
+	// | SERIAL3 | 10      |
+	// +---------+---------+
+}
+
 func TestMadbUserClearAll(t *testing.T) {
 	filename := tempFilename(t)
 	defer os.Remove(filename)