devtools/madb: specify devices with their indices.

This allows users to specify devices with their devices indicies. For
example,

    madb -n=@1,@2 start

will only start the app on the first and second devices, and not on
the res.

Change-Id: I96573e50e5693e5ca5abbb7ad4b4d851ee33fa7b
diff --git a/doc.go b/doc.go
index 1e78339..5733921 100644
--- a/doc.go
+++ b/doc.go
@@ -30,8 +30,10 @@
  -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.
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
 
 The global flags are:
  -metadata=<just specify -metadata to activate>
@@ -78,8 +80,10 @@
  -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.
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
 
 Madb exec - Run the provided adb command on all devices and emulators concurrently
 
@@ -106,8 +110,10 @@
  -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.
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
 
 Madb name - Manage device nicknames
 
@@ -129,8 +135,10 @@
  -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.
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
 
 Madb name set
 
@@ -171,8 +179,10 @@
  -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.
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
 
 Madb name unset
 
@@ -191,8 +201,10 @@
  -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.
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
 
 Madb name list
 
@@ -207,8 +219,10 @@
  -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.
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
 
 Madb name clear-all
 
@@ -223,8 +237,10 @@
  -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.
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
 
 Madb start - Launch your app on all devices
 
@@ -279,8 +295,10 @@
  -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.
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
 
 Madb stop - Stop your app on all devices
 
@@ -324,8 +342,10 @@
  -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.
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
 
 Madb uninstall - Uninstall your app from all devices
 
@@ -368,8 +388,10 @@
  -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.
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
 
 Madb help - Display help for commands or topics
 
diff --git a/madb.go b/madb.go
index 5542a84..7078841 100644
--- a/madb.go
+++ b/madb.go
@@ -16,6 +16,7 @@
 	"os/exec"
 	"path"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"sync"
 
@@ -39,7 +40,7 @@
 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.`)
+	cmdMadb.Flags.StringVar(&devicesFlag, "n", "", `Comma-separated device serials, qualifiers, device indices (e.g., '@1', '@2'), or nicknames (set by 'madb name').  A device index is specified by an '@' sign followed by the index of the device in the output of 'adb devices' command, starting from 1.  Command will be run only on specified devices.`)
 
 	// Store the current working directory.
 	var err error
@@ -95,6 +96,7 @@
 	Type       deviceType
 	Qualifiers []string
 	Nickname   string
+	Index      int
 }
 
 // Returns the display name which is intended to be used as the console output prefix.
@@ -135,17 +137,18 @@
 	}
 
 	// Iterate over all the device serial numbers, starting from the second line.
-	for _, line := range lines[1:] {
+	for i, line := range lines[1:] {
 		fields := strings.Fields(line)
 
 		if len(fields) <= 1 || fields[1] == "offline" {
 			continue
 		}
 
-		// Fill in the device serial and all the qualifiers.
+		// Fill in the device serial, all the qualifiers, and the device index.
 		d := device{
 			Serial:     fields[0],
 			Qualifiers: fields[2:],
+			Index:      i + 1,
 		}
 
 		// Determine whether this device is an emulator or a real device.
@@ -191,7 +194,10 @@
 		return nil, err
 	}
 
-	filtered := filterSpecifiedDevices(allDevices)
+	filtered, err := filterSpecifiedDevices(allDevices)
+	if err != nil {
+		return nil, err
+	}
 
 	if len(filtered) == 0 {
 		return nil, fmt.Errorf("No devices matching the device specifiers.")
@@ -200,21 +206,42 @@
 	return filtered, nil
 }
 
-func filterSpecifiedDevices(devices []device) []device {
+type deviceSpec struct {
+	index int
+	token string
+}
+
+func filterSpecifiedDevices(devices []device) ([]device, error) {
 	// If no device specifier flags are set, run on all devices and emulators.
 	if noDevicesSpecified() {
-		return devices
+		return devices, nil
 	}
 
 	result := make([]device, 0, len(devices))
 
+	var specs = []deviceSpec{}
+	if devicesFlag != "" {
+		tokens := strings.Split(devicesFlag, ",")
+		for _, token := range tokens {
+			if strings.HasPrefix(token, "@") {
+				index, err := strconv.Atoi(token[1:])
+				if err != nil || index <= 0 {
+					return nil, fmt.Errorf("Invalid device specifier %q. '@' sign must be followed by a numeric device index starting from 1.", token)
+				}
+				specs = append(specs, deviceSpec{index, ""})
+			} else {
+				specs = append(specs, deviceSpec{0, token})
+			}
+		}
+	}
+
 	for _, d := range devices {
-		if shouldIncludeDevice(d) {
+		if shouldIncludeDevice(d, specs) {
 			result = append(result, d)
 		}
 	}
 
-	return result
+	return result, nil
 }
 
 func noDevicesSpecified() bool {
@@ -223,7 +250,7 @@
 		devicesFlag == ""
 }
 
-func shouldIncludeDevice(d device) bool {
+func shouldIncludeDevice(d device, specs []deviceSpec) bool {
 	if allDevicesFlag && d.Type == realDevice {
 		return true
 	}
@@ -232,19 +259,25 @@
 		return true
 	}
 
-	tokens := strings.Split(devicesFlag, ",")
-	for _, token := range tokens {
+	for _, spec := range specs {
 		// Ignore empty tokens
-		if token == "" {
+		if spec.index == 0 && spec.token == "" {
 			continue
 		}
 
-		if d.Serial == token || d.Nickname == token {
+		if spec.index > 0 {
+			if d.Index == spec.index {
+				return true
+			}
+			continue
+		}
+
+		if d.Serial == spec.token || d.Nickname == spec.token {
 			return true
 		}
 
 		for _, qualifier := range d.Qualifiers {
-			if qualifier == token {
+			if qualifier == spec.token {
 				return true
 			}
 		}
diff --git a/madb_test.go b/madb_test.go
index 261c592..6c4ec79 100644
--- a/madb_test.go
+++ b/madb_test.go
@@ -43,12 +43,14 @@
 			Type:       realDevice,
 			Qualifiers: []string{"usb:3-3.4.3", "product:bullhead", "model:Nexus_5X", "device:bullhead"},
 			Nickname:   "",
+			Index:      1,
 		},
 		device{
 			Serial:     "emulator-5554",
 			Type:       emulator,
 			Qualifiers: []string{"product:sdk_phone_armv7", "model:sdk_phone_armv7", "device:generic"},
 			Nickname:   "",
+			Index:      2,
 		},
 	}
 
@@ -86,6 +88,7 @@
 			Type:       realDevice,
 			Qualifiers: []string{"product:sdk_phone_armv7", "model:sdk_phone_armv7", "device:generic"},
 			Nickname:   "",
+			Index:      2,
 		},
 	}
 
@@ -116,12 +119,14 @@
 			Type:       realDevice,
 			Qualifiers: []string{"usb:3-3.4.3", "product:bullhead", "model:Nexus_5X", "device:bullhead"},
 			Nickname:   "MyPhone",
+			Index:      1,
 		},
 		device{
 			Serial:     "emulator-5554",
 			Type:       emulator,
 			Qualifiers: []string{"product:sdk_phone_armv7", "model:sdk_phone_armv7", "device:generic"},
 			Nickname:   "ARMv7",
+			Index:      2,
 		},
 	}
 
@@ -137,6 +142,7 @@
 		Type:       realDevice,
 		Qualifiers: []string{"usb:3-3.4.3", "product:bullhead", "model:Nexus_5X", "device:bullhead"},
 		Nickname:   "MyPhone",
+		Index:      1,
 	}
 
 	d2 := device{
@@ -144,6 +150,7 @@
 		Type:       realDevice,
 		Qualifiers: []string{"usb:3-3.4.1", "product:volantisg", "model:Nexus_9", "device:flounder_lte"},
 		Nickname:   "",
+		Index:      2,
 	}
 
 	e1 := device{
@@ -151,6 +158,7 @@
 		Type:       emulator,
 		Qualifiers: []string{"product:sdk_phone_armv7", "model:sdk_phone_armv7", "device:generic"},
 		Nickname:   "ARMv7",
+		Index:      3,
 	}
 
 	d3 := device{
@@ -158,6 +166,7 @@
 		Type:       realDevice,
 		Qualifiers: []string{"usb:3-3.3", "product:bullhead", "model:Nexus_5X", "device:bullhead"},
 		Nickname:   "SecondPhone",
+		Index:      4,
 	}
 
 	e2 := device{
@@ -165,6 +174,7 @@
 		Type:       emulator,
 		Qualifiers: []string{"product:sdk_phone_armv7", "model:sdk_phone_armv7", "device:generic"},
 		Nickname:   "",
+		Index:      5,
 	}
 
 	allDevices := []device{d1, d2, e1, d3, e2}
@@ -185,8 +195,10 @@
 		{deviceFlags{false, true, ""}, []device{e1, e2}},                   // Only -e is specified
 		{deviceFlags{false, false, "device:bullhead"}, []device{d1, d3}},   // Device qualifier
 		{deviceFlags{false, false, "ARMv7,SecondPhone"}, []device{e1, d3}}, // Nicknames
+		{deviceFlags{false, false, "@2,@4"}, []device{d2, d3}},             // Device Indices
 		{deviceFlags{true, false, "ARMv7"}, []device{d1, d2, e1, d3}},      // Combinations
 		{deviceFlags{false, true, "model:Nexus_9"}, []device{d2, e1, e2}},  // Combinations
+		{deviceFlags{false, false, "@1,SecondPhone"}, []device{d1, d3}},    // Combinations
 	}
 
 	for i, testCase := range testCases {
@@ -194,7 +206,12 @@
 		allEmulatorsFlag = testCase.flags.allEmulators
 		devicesFlag = testCase.flags.devices
 
-		if got := filterSpecifiedDevices(allDevices); !reflect.DeepEqual(got, testCase.want) {
+		got, err := filterSpecifiedDevices(allDevices)
+		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)
 		}
 	}