feat(resolve): add 'madb resolve' command

This command prints out the device serials from the given nicknames or
other device specifiers. This would make it easier to use the device
nicknames defined by madb in other command line tools or in shell
scripts.

Change-Id: I7fcb92e5c017000a9c7bb1fd08895f7fc9aa268d
Closes: #10
diff --git a/doc.go b/doc.go
index 50d0ebb..b8612b2 100644
--- a/doc.go
+++ b/doc.go
@@ -22,6 +22,7 @@
    group       Manage device groups
    install     Install your app on all devices
    name        Manage device nicknames
+   resolve     Resolve device specifiers into device serials
    shell       Run the provided adb shell command on all devices and emulators
                concurrently
    start       Launch your app on all devices
@@ -455,6 +456,24 @@
 Usage:
    madb name clear-all [flags]
 
+Madb resolve - Resolve device specifiers into device serials
+
+Resolves the provided device specifiers and prints out their device serials,
+each in a separate line. This command only displays the unique serials of the
+devices that are currently available.
+
+This command can be useful when you want to use the device nicknames and groups
+defined by madb in other command line tools. For example, to run a flutter app
+on "MyTablet" device, you can use the following command (in Bash):
+
+    flutter run --device $(madb resolve MyTablet)
+
+Usage:
+   madb resolve [flags] <specifier1> [<specifier2> ...]
+
+<specifier> can be anything that is accepted in the '-n' flag (see 'madb help').
+It can be a device serial, qualifier, index, nickname, or a device group name.
+
 Madb shell - Run the provided adb shell command on all devices and emulators concurrently
 
 Runs the provided adb shell command on all devices and emulators concurrently.
diff --git a/madb.go b/madb.go
index b6560a1..b007e45 100644
--- a/madb.go
+++ b/madb.go
@@ -88,6 +88,7 @@
 		cmdMadbGroup,
 		cmdMadbInstall,
 		cmdMadbName,
+		cmdMadbResolve,
 		cmdMadbShell,
 		cmdMadbStart,
 		cmdMadbStop,
@@ -233,12 +234,13 @@
 		return nil, err
 	}
 
-	allDevices, err := getDevices(cfg)
+	devices, err := getDevices(cfg)
 	if err != nil {
 		return nil, err
 	}
 
-	filtered, err := filterSpecifiedDevices(allDevices, cfg)
+	tokens := strings.Split(devicesFlag, ",")
+	filtered, err := filterSpecifiedDevices(devices, cfg, allDevicesFlag, allEmulatorsFlag, tokens)
 	if err != nil {
 		return nil, err
 	}
@@ -255,18 +257,22 @@
 	token string
 }
 
-func filterSpecifiedDevices(devices []device, cfg *config) ([]device, error) {
+func filterSpecifiedDevices(devices []device, cfg *config, allDevices, allEmulators bool, tokens []string) ([]device, error) {
+	// If the tokens only contains one empty string, treat it as an empty slice.
+	if len(tokens) == 1 && tokens[0] == "" {
+		tokens = []string{}
+	}
+
 	// If no device specifier flags are set, run on all devices and emulators.
-	if noDevicesSpecified() {
+	if allDevices == false && allEmulators == false && len(tokens) == 0 {
 		return devices, nil
 	}
 
 	result := make([]device, 0, len(devices))
 
 	var specs = []deviceSpec{}
-	if devicesFlag != "" {
+	if len(tokens) > 0 {
 		// Check if the provided specifiers are all valid.
-		tokens := strings.Split(devicesFlag, ",")
 		for _, token := range tokens {
 			if err := isValidDeviceSpecifier(token); err != nil {
 				return nil, err
@@ -279,7 +285,7 @@
 	}
 
 	for _, d := range devices {
-		if shouldIncludeDevice(d, specs) {
+		if shouldIncludeDevice(d, specs, allDevices, allEmulators) {
 			result = append(result, d)
 		}
 	}
@@ -304,18 +310,12 @@
 	return specs
 }
 
-func noDevicesSpecified() bool {
-	return allDevicesFlag == false &&
-		allEmulatorsFlag == false &&
-		devicesFlag == ""
-}
-
-func shouldIncludeDevice(d device, specs []deviceSpec) bool {
-	if allDevicesFlag && d.Type == realDevice {
+func shouldIncludeDevice(d device, specs []deviceSpec, allDevices, allEmulators bool) bool {
+	if allDevices && d.Type == realDevice {
 		return true
 	}
 
-	if allEmulatorsFlag && d.Type == emulator {
+	if allEmulators && d.Type == emulator {
 		return true
 	}
 
diff --git a/madb_test.go b/madb_test.go
index 2444704..f2c5fc9 100644
--- a/madb_test.go
+++ b/madb_test.go
@@ -12,6 +12,7 @@
 	"os"
 	"path/filepath"
 	"reflect"
+	"strings"
 	"testing"
 
 	"v.io/x/lib/gosh"
@@ -210,7 +211,7 @@
 		devices      string
 	}
 
-	testCases := []struct {
+	tests := []struct {
 		flags deviceFlags
 		want  []device
 	}{
@@ -240,18 +241,15 @@
 		},
 	}
 
-	for i, testCase := range testCases {
-		allDevicesFlag = testCase.flags.allDevices
-		allEmulatorsFlag = testCase.flags.allEmulators
-		devicesFlag = testCase.flags.devices
-
-		got, err := filterSpecifiedDevices(allDevices, cfg)
+	for i, test := range tests {
+		tokens := strings.Split(test.flags.devices, ",")
+		got, err := filterSpecifiedDevices(allDevices, cfg, test.flags.allDevices, test.flags.allEmulators, tokens)
 		if err != nil {
 			t.Fatalf(err.Error())
 		}
 
-		if !reflect.DeepEqual(got, testCase.want) {
-			t.Fatalf("unmatched results for testCases[%v]: got %v, want %v", i, got, testCase.want)
+		if !reflect.DeepEqual(got, test.want) {
+			t.Fatalf("unmatched results for testCases[%v]: got %v, want %v", i, got, test.want)
 		}
 	}
 }
diff --git a/resolve.go b/resolve.go
new file mode 100644
index 0000000..8703c9e
--- /dev/null
+++ b/resolve.go
@@ -0,0 +1,57 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"fmt"
+
+	"v.io/x/lib/cmdline"
+)
+
+var cmdMadbResolve = &cmdline.Command{
+	Runner:           subCommandRunnerWithFilepath{runMadbResolve, getDefaultConfigFilePath},
+	Name:             "resolve",
+	DontInheritFlags: true,
+	Short:            "Resolve device specifiers into device serials",
+	Long: `
+Resolves the provided device specifiers and prints out their device serials,
+each in a separate line. This command only displays the unique serials of the
+devices that are currently available.
+
+This command can be useful when you want to use the device nicknames and groups
+defined by madb in other command line tools. For example, to run a flutter app
+on "MyTablet" device, you can use the following command (in Bash):
+
+    flutter run --device $(madb resolve MyTablet)
+`,
+	ArgsName: "<specifier1> [<specifier2> ...]",
+	ArgsLong: `
+<specifier> can be anything that is accepted in the '-n' flag (see 'madb help').
+It can be a device serial, qualifier, index, nickname, or a device group name.
+`,
+}
+
+func runMadbResolve(env *cmdline.Env, args []string, filename string) error {
+	cfg, err := readConfig(filename)
+	if err != nil {
+		return err
+	}
+
+	devices, err := getDevices(cfg)
+	if err != nil {
+		return err
+	}
+
+	filtered, err := filterSpecifiedDevices(devices, cfg, false, false, args)
+	if err != nil {
+		return err
+	}
+
+	for _, d := range filtered {
+		fmt.Println(d.Serial)
+	}
+
+	return nil
+}