madb: keyword expansion feature for 'madb exec'

This adds the keyword expansion feature to 'madb exec', as described
in issue #3.

Within any arguments of 'madb exec', one can use {{name}}, {{serial}},
{{index}}, which will be replaced by the actual value for the given
device before running the adb command.

Change-Id: Icf24c87e431af7932ecf198e66b529982d6c42aa
diff --git a/doc.go b/doc.go
index 31e2c8f..0253e9b 100644
--- a/doc.go
+++ b/doc.go
@@ -98,10 +98,25 @@
 
 For example, the following line:
 
-    madb -a exec push ./foo.txt /sdcard/foo.txt
+    madb exec push ./foo.txt /sdcard/foo.txt
 
 copies the ./foo.txt file to /sdcard/foo.txt for all the currently connected
-Android devices.
+devices.
+
+There are a few pre-defined keywords that can be expanded within an argument.
+
+    "{{index}}"  : the index of the current device, starting from 1.
+    "{{name}}"   : the nickname of the current device, or the serial number if a nickname is not set.
+    "{{serial}}" : the serial number of the current device.
+
+For example, the following line:
+
+    madb exec -n=Alice,Bob push ./{{name}}.txt /sdcard/{{name}}.txt
+
+copies the ./Alice.txt file to the device named Alice, and ./Bob.txt to the
+device named Bob. Note that you should type in "{{name}}" as-is, with the
+opening/closing curly braces, similar to when you're using a template library
+such as mustache.
 
 To see the list of available adb commands, type 'adb help'.
 
diff --git a/exec.go b/exec.go
index 7082644..9cf8c4d 100644
--- a/exec.go
+++ b/exec.go
@@ -18,9 +18,23 @@
 
 For example, the following line:
 
-    madb -a exec push ./foo.txt /sdcard/foo.txt
+    madb exec push ./foo.txt /sdcard/foo.txt
 
-copies the ./foo.txt file to /sdcard/foo.txt for all the currently connected Android devices.
+copies the ./foo.txt file to /sdcard/foo.txt for all the currently connected devices.
+
+There are a few pre-defined keywords that can be expanded within an argument.
+
+    "{{index}}"  : the index of the current device, starting from 1.
+    "{{name}}"   : the nickname of the current device, or the serial number if a nickname is not set.
+    "{{serial}}" : the serial number of the current device.
+
+For example, the following line:
+
+    madb exec -n=Alice,Bob push ./{{name}}.txt /sdcard/{{name}}.txt
+
+copies the ./Alice.txt file to the device named Alice, and ./Bob.txt to the device named Bob.
+Note that you should type in "{{name}}" as-is, with the opening/closing curly braces, similar to
+when you're using a template library such as mustache.
 
 To see the list of available adb commands, type 'adb help'.
 `,
@@ -36,7 +50,13 @@
 
 	sh.ContinueOnError = true
 
-	cmdArgs := append([]string{"-s", d.Serial}, args...)
+	// Expand the keywords before running the command.
+	expandedArgs := make([]string, len(args))
+	for i, arg := range args {
+		expandedArgs[i] = expandKeywords(arg, d)
+	}
+
+	cmdArgs := append([]string{"-s", d.Serial}, expandedArgs...)
 	cmd := sh.Cmd("adb", cmdArgs...)
 	return runGoshCommandForDevice(cmd, d, false)
 }
diff --git a/madb.go b/madb.go
index ae9648f..1dd3fbd 100644
--- a/madb.go
+++ b/madb.go
@@ -17,6 +17,7 @@
 	"os/exec"
 	"path"
 	"path/filepath"
+	"regexp"
 	"strconv"
 	"strings"
 	"sync"
@@ -672,6 +673,26 @@
 	return encoder.Encode(data)
 }
 
+// expandKeywords takes a command line argument and a device configuration, and returns a new
+// argument where the predefined keywords ("{{index}}", "{{name}}", "{{serial}}") are expanded.
+func expandKeywords(arg string, d device) string {
+	exp := regexp.MustCompile(`{{(index|name|serial)}}`)
+	result := exp.ReplaceAllStringFunc(arg, func(keyword string) string {
+		switch keyword {
+		case "{{index}}":
+			return strconv.Itoa(d.Index)
+		case "{{name}}":
+			return d.displayName()
+		case "{{serial}}":
+			return d.Serial
+		default:
+			return keyword
+		}
+	})
+
+	return result
+}
+
 type pathProvider func() (string, error)
 
 // subCommandRunnerWithFilepath is an adapter that turns the madb name/user subcommand functions into cmdline.Runners.
diff --git a/madb_test.go b/madb_test.go
index 65016b4..769137c 100644
--- a/madb_test.go
+++ b/madb_test.go
@@ -391,3 +391,46 @@
 		t.Fatalf(`The embedded Gradle script is out of date. Please run "jiri go generate" to regenerate the embedded script.`)
 	}
 }
+
+// TestExpandKeywords tests the "expandKeywords" function, which is used for expanding pre-defined
+// keywords such as "{{serial}}" and "{{name}}".
+func TestExpandKeywords(t *testing.T) {
+	// Sample devices.
+	d1 := device{
+		Serial:     "0123456789",
+		Type:       realDevice,
+		Qualifiers: nil,
+		Nickname:   "Alice",
+		Index:      1,
+		UserID:     "10",
+	}
+
+	d2 := device{
+		Serial:     "emulator-1234",
+		Type:       emulator,
+		Qualifiers: nil,
+		Nickname:   "",
+		Index:      2,
+		UserID:     "",
+	}
+
+	tests := []struct {
+		arg  string
+		d    device
+		want string
+	}{
+		{"{{name}}.txt", d1, "Alice.txt"},
+		{"{{serial}}.txt", d1, "0123456789.txt"},
+		{"{{index}}.txt", d1, "1.txt"},
+		{"Hello, {{name}}!", d2, "Hello, emulator-1234!"},
+		{"Hello, {{serial}}!", d2, "Hello, emulator-1234!"},
+		{"{{index}}.txt", d2, "2.txt"},
+		{"{{name}}-{{serial}}.txt", d1, "Alice-0123456789.txt"},
+	}
+
+	for i, test := range tests {
+		if got := expandKeywords(test.arg, test.d); got != test.want {
+			t.Fatalf("unmatched results for tests[%v]: got %v, want %v", i, got, test.want)
+		}
+	}
+}