devtools/madb: device specifier flags

Users can now specify the devices on which the madb subcommand should
run, using command line flags. If no device specifiers are provided,
the command runs on all available devices and emulators.

Change-Id: I840c7a9b57fb04189aee90837bb738b3050286cd
diff --git a/doc.go b/doc.go
index c5201ad..3ca1637 100644
--- a/doc.go
+++ b/doc.go
@@ -15,41 +15,62 @@
    madb [flags] <command>
 
 The madb commands are:
-   exec        Run the provided adb command on all the specified devices
+   exec        Run the provided adb command on all devices and emulators
                concurrently
    name        Manage device nicknames
    help        Display help for commands or topics
 
+The madb flags are:
+ -d=false
+   Restrict the command to only run on real devices.
+ -e=false
+   Restrict the command to only run on emulators.
+ -n=
+   Comma-separated device serials, qualifiers, or nicknames (set by 'madb
+   name').  Command will be run only on specified devices.
+
 The global flags are:
  -metadata=<just specify -metadata to activate>
    Displays metadata for the program and exits.
  -time=false
    Dump timing information to stderr before exiting the program.
 
-Madb exec - Run the provided adb command on all the specified devices concurrently
+Madb exec - Run the provided adb command on all devices and emulators concurrently
 
-Runs the provided adb command on all the specified devices concurrently.
+Runs the provided adb command on all devices and emulators concurrently.
 
 For example, the following line:
 
     madb -a exec push ./foo.txt /sdcard/foo.txt
 
 copies the ./foo.txt file to /sdcard/foo.txt for all the currently connected
-Android devices (specified by -a flag).
+Android devices.
 
 To see the list of available adb commands, type 'adb help'.
 
 Usage:
    madb exec [flags] <command>
 
-<command> is a normal adb command, which will be executed on all the specified
-devices.
+<command> is a normal adb command, which will be executed on all devices and
+emulators.
+
+The madb exec flags are:
+ -d=false
+   Restrict the command to only run on real devices.
+ -e=false
+   Restrict the command to only run on emulators.
+ -n=
+   Comma-separated device serials, qualifiers, or nicknames (set by 'madb
+   name').  Command will be run only on specified devices.
 
 Madb name - Manage device nicknames
 
 Manages device nicknames, which are meant to be more human-friendly compared to
 the device serials provided by adb tool.
 
+NOTE: Device specifier flags (-d, -e, -n) are ignored in all 'madb name'
+commands.
+
 Usage:
    madb name [flags] <command>
 
@@ -59,6 +80,15 @@
    list        List all the existing nicknames.
    clear-all   Clear all the existing nicknames.
 
+The madb name flags are:
+ -d=false
+   Restrict the command to only run on real devices.
+ -e=false
+   Restrict the command to only run on emulators.
+ -n=
+   Comma-separated device serials, qualifiers, or nicknames (set by 'madb
+   name').  Command will be run only on specified devices.
+
 Madb name set
 
 Sets a human-friendly nickname that can be used when specifying the device in
@@ -92,6 +122,15 @@
 device specifier (e.g., 'usb:3-3.4.2') obtained from 'adb devices -l' command
 <nickname> is an alpha-numeric string with no special characters or spaces.
 
+The madb name set flags are:
+ -d=false
+   Restrict the command to only run on real devices.
+ -e=false
+   Restrict the command to only run on emulators.
+ -n=
+   Comma-separated device serials, qualifiers, or nicknames (set by 'madb
+   name').  Command will be run only on specified devices.
+
 Madb name unset
 
 Unsets a nickname assigned by the 'madb name set' command. Either the device
@@ -103,6 +142,15 @@
 There should be only one argument, which is either the device serial or the
 nickname.
 
+The madb name unset flags are:
+ -d=false
+   Restrict the command to only run on real devices.
+ -e=false
+   Restrict the command to only run on emulators.
+ -n=
+   Comma-separated device serials, qualifiers, or nicknames (set by 'madb
+   name').  Command will be run only on specified devices.
+
 Madb name list
 
 Lists all the currently stored nicknames of device serials.
@@ -110,6 +158,15 @@
 Usage:
    madb name list [flags]
 
+The madb name list flags are:
+ -d=false
+   Restrict the command to only run on real devices.
+ -e=false
+   Restrict the command to only run on emulators.
+ -n=
+   Comma-separated device serials, qualifiers, or nicknames (set by 'madb
+   name').  Command will be run only on specified devices.
+
 Madb name clear-all
 
 Clears all the currently stored nicknames of device serials.
@@ -117,6 +174,15 @@
 Usage:
    madb name clear-all [flags]
 
+The madb name clear-all flags are:
+ -d=false
+   Restrict the command to only run on real devices.
+ -e=false
+   Restrict the command to only run on emulators.
+ -n=
+   Comma-separated device serials, qualifiers, or nicknames (set by 'madb
+   name').  Command will be run only on specified devices.
+
 Madb help - Display help for commands or topics
 
 Help with no args displays the usage of the parent command.
diff --git a/exec.go b/exec.go
index 71470c4..7b5307f 100644
--- a/exec.go
+++ b/exec.go
@@ -16,21 +16,21 @@
 var cmdMadbExec = &cmdline.Command{
 	Runner: cmdline.RunnerFunc(runMadbExec),
 	Name:   "exec",
-	Short:  "Run the provided adb command on all the specified devices concurrently",
+	Short:  "Run the provided adb command on all devices and emulators concurrently",
 	Long: `
-Runs the provided adb command on all the specified devices concurrently.
+Runs the provided adb command on all devices and emulators concurrently.
 
 For example, the following line:
 
     madb -a exec push ./foo.txt /sdcard/foo.txt
 
-copies the ./foo.txt file to /sdcard/foo.txt for all the currently connected Android devices (specified by -a flag).
+copies the ./foo.txt file to /sdcard/foo.txt for all the currently connected Android devices.
 
 To see the list of available adb commands, type 'adb help'.
 `,
 	ArgsName: "<command>",
 	ArgsLong: `
-<command> is a normal adb command, which will be executed on all the specified devices.
+<command> is a normal adb command, which will be executed on all devices and emulators.
 `,
 }
 
@@ -41,7 +41,7 @@
 		return err
 	}
 
-	devices, err := getDevices(getDefaultNameFilePath())
+	devices, err := getSpecifiedDevices()
 	if err != nil {
 		return err
 	}
diff --git a/madb.go b/madb.go
index 085db51..2f7efb2 100644
--- a/madb.go
+++ b/madb.go
@@ -17,6 +17,18 @@
 	"v.io/x/lib/gosh"
 )
 
+var (
+	allDevicesFlag   bool
+	allEmulatorsFlag bool
+	devicesFlag      string
+)
+
+func init() {
+	cmdMadb.Flags.BoolVar(&allDevicesFlag, "d", false, `Restrict the command to only run on real devices.`)
+	cmdMadb.Flags.BoolVar(&allEmulatorsFlag, "e", false, `Restrict the command to only run on emulators.`)
+	cmdMadb.Flags.StringVar(&devicesFlag, "n", "", `Comma-separated device serials, qualifiers, or nicknames (set by 'madb name').  Command will be run only on specified devices.`)
+}
+
 var cmdMadb = &cmdline.Command{
 	Children: []*cmdline.Command{cmdMadbExec, cmdMadbName},
 	Name:     "madb",
@@ -69,13 +81,13 @@
 }
 
 // Runs "adb devices -l" command, and parses the result to get all the device serial numbers.
-func getDevices(filename string) ([]device, error) {
+func getDevices(nicknameFile string) ([]device, error) {
 	sh := gosh.NewShell(gosh.Opts{})
 	defer sh.Cleanup()
 
 	output := sh.Cmd("adb", "devices", "-l").Stdout()
 
-	nsm, err := readNicknameSerialMap(filename)
+	nsm, err := readNicknameSerialMap(nicknameFile)
 	if err != nil {
 		fmt.Fprintln(os.Stderr, "Warning: Could not read the nickname file.")
 	}
@@ -138,3 +150,73 @@
 
 	return result, nil
 }
+
+// Gets all the devices specified by the device specifier flags.
+// Intended to be used by most of the madb sub-commands except for 'madb name'.
+func getSpecifiedDevices() ([]device, error) {
+	allDevices, err := getDevices(getDefaultNameFilePath())
+	if err != nil {
+		return nil, err
+	}
+
+	filtered := filterSpecifiedDevices(allDevices)
+
+	if len(filtered) == 0 {
+		return nil, fmt.Errorf("No devices matching the device specifiers.")
+	}
+
+	return filtered, nil
+}
+
+func filterSpecifiedDevices(devices []device) []device {
+	// If no device specifier flags are set, run on all devices and emulators.
+	if noDevicesSpecified() {
+		return devices
+	}
+
+	result := make([]device, 0, len(devices))
+
+	for _, d := range devices {
+		if shouldIncludeDevice(d) {
+			result = append(result, d)
+		}
+	}
+
+	return result
+}
+
+func noDevicesSpecified() bool {
+	return allDevicesFlag == false &&
+		allEmulatorsFlag == false &&
+		devicesFlag == ""
+}
+
+func shouldIncludeDevice(d device) bool {
+	if allDevicesFlag && d.Type == realDevice {
+		return true
+	}
+
+	if allEmulatorsFlag && d.Type == emulator {
+		return true
+	}
+
+	tokens := strings.Split(devicesFlag, ",")
+	for _, token := range tokens {
+		// Ignore empty tokens
+		if token == "" {
+			continue
+		}
+
+		if d.Serial == token || d.Nickname == token {
+			return true
+		}
+
+		for _, qualifier := range d.Qualifiers {
+			if qualifier == token {
+				return true
+			}
+		}
+	}
+
+	return false
+}
diff --git a/madb_test.go b/madb_test.go
index 2f0567e..1fbbf4b 100644
--- a/madb_test.go
+++ b/madb_test.go
@@ -116,3 +116,75 @@
 		t.Fatalf("unmatched results: got %v, want %v", got, want)
 	}
 }
+
+func TestGetSpecifiedDevices(t *testing.T) {
+	// First, define some devices (three real devices, and two emulators).
+	d1 := device{
+		Serial:     "deviceid01",
+		Type:       realDevice,
+		Qualifiers: []string{"usb:3-3.4.3", "product:bullhead", "model:Nexus_5X", "device:bullhead"},
+		Nickname:   "MyPhone",
+	}
+
+	d2 := device{
+		Serial:     "deviceid02",
+		Type:       realDevice,
+		Qualifiers: []string{"usb:3-3.4.1", "product:volantisg", "model:Nexus_9", "device:flounder_lte"},
+		Nickname:   "",
+	}
+
+	e1 := device{
+		Serial:     "emulator-5554",
+		Type:       emulator,
+		Qualifiers: []string{"product:sdk_phone_armv7", "model:sdk_phone_armv7", "device:generic"},
+		Nickname:   "ARMv7",
+	}
+
+	d3 := device{
+		Serial:     "deviceid03",
+		Type:       realDevice,
+		Qualifiers: []string{"usb:3-3.3", "product:bullhead", "model:Nexus_5X", "device:bullhead"},
+		Nickname:   "SecondPhone",
+	}
+
+	e2 := device{
+		Serial:     "emulator-5555",
+		Type:       emulator,
+		Qualifiers: []string{"product:sdk_phone_armv7", "model:sdk_phone_armv7", "device:generic"},
+		Nickname:   "",
+	}
+
+	allDevices := []device{d1, d2, e1, d3, e2}
+
+	type deviceFlags struct {
+		allDevices   bool
+		allEmulators bool
+		devices      string
+	}
+
+	type testCase struct {
+		flags deviceFlags
+		want  []device
+	}
+
+	testCases := []testCase{
+		testCase{deviceFlags{false, false, ""}, allDevices},                        // Nothing is specified
+		testCase{deviceFlags{true, true, ""}, allDevices},                          // Both -d and -e are specified
+		testCase{deviceFlags{true, false, ""}, []device{d1, d2, d3}},               // Only -d is specified
+		testCase{deviceFlags{false, true, ""}, []device{e1, e2}},                   // Only -e is specified
+		testCase{deviceFlags{false, false, "device:bullhead"}, []device{d1, d3}},   // Device qualifier
+		testCase{deviceFlags{false, false, "ARMv7,SecondPhone"}, []device{e1, d3}}, // Nicknames
+		testCase{deviceFlags{true, false, "ARMv7"}, []device{d1, d2, e1, d3}},      // Combinations
+		testCase{deviceFlags{false, true, "model:Nexus_9"}, []device{d2, e1, e2}},  // Combinations
+	}
+
+	for i, testCase := range testCases {
+		allDevicesFlag = testCase.flags.allDevices
+		allEmulatorsFlag = testCase.flags.allEmulators
+		devicesFlag = testCase.flags.devices
+
+		if got := filterSpecifiedDevices(allDevices); !reflect.DeepEqual(got, testCase.want) {
+			t.Fatalf("unmatched results for testCases[%v]: got %v, want %v", i, got, testCase.want)
+		}
+	}
+}
diff --git a/name.go b/name.go
index 6d1d5d7..c8773af 100644
--- a/name.go
+++ b/name.go
@@ -22,6 +22,8 @@
 	Long: `
 Manages device nicknames, which are meant to be more human-friendly compared to
 the device serials provided by adb tool.
+
+NOTE: Device specifier flags (-d, -e, -n) are ignored in all 'madb name' commands.
 `,
 }