blob: b007e45f99de33c657c43169952c5c887920f309 [file] [log] [blame]
// 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.
// The following enables go generate to generate the doc.go file.
//go:generate go run $JIRI_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
// The following generates the embedded_gradle.go file from the madb_init.gradle file.
//go:generate go run scripts/embed_gradle_script.go madb_init.gradle embedded_gradle.go gradleInitScript
// The following generates the version.go file with the version string defined in the MADB_VERSION file.
//go:generate go run scripts/update_version.go
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"v.io/x/lib/cmdline"
"v.io/x/lib/gosh"
"v.io/x/lib/textutil"
)
var (
allDevicesFlag bool
allEmulatorsFlag bool
devicesFlag string
sequentialFlag bool
prefixFlag string
clearCacheFlag bool
moduleFlag string
variantFlag string
buildFlag bool
wd string // working directory
)
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, device indices (e.g., '@1', '@2'), nicknames (set by 'madb name'), or group names (set by 'madb group'). 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.`)
cmdMadb.Flags.BoolVar(&sequentialFlag, "seq", false, `Run the command sequentially, instead of running it in parallel.`)
cmdMadb.Flags.StringVar(&prefixFlag, "prefix", "name", `Specify which output prefix to use. You can choose from the following options:
name - Display the nickname of the device. The serial number is used instead if the
nickname is not set for the given device.
serial - Display the serial number of the device.
none - Do not display the output prefix.`)
// Store the current working directory.
var err error
wd, err = os.Getwd()
if err != nil {
panic(err)
}
}
// initializePropertyCacheFlags sets up the flags related to extracting and caching project properties.
func initializePropertyCacheFlags(flags *flag.FlagSet) {
flags.BoolVar(&clearCacheFlag, "clear-cache", false, `Clear the cache and re-extract the variant properties such as the application ID and the main activity name. Only takes effect when no arguments are provided.`)
flags.StringVar(&moduleFlag, "module", "", `Specify which application module to use, when the current directory is the top level Gradle project containing multiple sub-modules. When not specified, the first available application module is used. Only takes effect when no arguments are provided.`)
flags.StringVar(&variantFlag, "variant", "", `Specify which build variant to use. When not specified, the first available build variant is used. Only takes effect when no arguments are provided.`)
}
// initializeBuildFlags sets up the flags related to running Gradle build tasks.
func initializeBuildFlags(flags *flag.FlagSet) {
flags.BoolVar(&buildFlag, "build", true, `Build the target app variant before installing or running the app.`)
}
var cmdMadb = &cmdline.Command{
Children: []*cmdline.Command{
cmdMadbClearData,
cmdMadbExec,
cmdMadbExtern,
cmdMadbGroup,
cmdMadbInstall,
cmdMadbName,
cmdMadbResolve,
cmdMadbShell,
cmdMadbStart,
cmdMadbStop,
cmdMadbUninstall,
cmdMadbUser,
cmdMadbVersion,
},
Name: "madb",
Short: "Multi-device Android Debug Bridge",
Long: `
Multi-device Android Debug Bridge
The madb command wraps Android Debug Bridge (adb) command line tool
and provides various features for controlling multiple Android devices concurrently.
`,
}
func main() {
cmdline.Main(cmdMadb)
}
// Makes sure that adb server is running.
// Intended to be called at the beginning of each subcommand.
func startAdbServer() error {
// TODO(youngseokyoon): search for installed adb tool more rigourously.
if err := exec.Command("adb", "start-server").Run(); err != nil {
return fmt.Errorf("Failed to start adb server. Please make sure that adb is in your PATH: %v", err)
}
return nil
}
type deviceType string
const (
emulator deviceType = "Emulator"
realDevice deviceType = "RealDevice"
)
type device struct {
Serial string
Type deviceType
Qualifiers []string
Nickname string
Index int
UserID string
}
// Returns the display name which is intended to be used as the console output prefix.
// This would be the nickname of the device if there is one; otherwise, the serial number is used.
func (d device) displayName() string {
if d.Nickname != "" {
return d.Nickname
}
return d.Serial
}
// Runs "adb devices -l" command, and parses the result to get all the device serial numbers.
func getDevices(cfg *config) ([]device, error) {
sh := gosh.NewShell(nil)
defer sh.Cleanup()
output := sh.Cmd("adb", "devices", "-l").Stdout()
return parseDevicesOutput(output, cfg)
}
// Parses the output generated from "adb devices -l" command and return the list of device serial numbers
// Devices that are currently offline are excluded from the returned list.
func parseDevicesOutput(output string, cfg *config) ([]device, error) {
lines := strings.Split(output, "\n")
result := []device{}
// Check the first line of the output
if len(lines) <= 0 || strings.TrimSpace(lines[0]) != "List of devices attached" {
return result, fmt.Errorf("The output from 'adb devices -l' command does not look as expected.")
}
// Iterate over all the device serial numbers, starting from the second line.
for i, line := range lines[1:] {
fields := strings.Fields(line)
if len(fields) <= 1 || fields[1] == "offline" {
continue
}
// 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.
if strings.HasPrefix(d.Serial, "emulator") {
d.Type = emulator
} else {
d.Type = realDevice
}
if cfg != nil {
// Determine whether there is a nickname defined for this device,
// so that the console output prefix can display the nickname instead of the serial.
NSMLoop:
for nickname, serial := range cfg.Names {
if d.Serial == serial {
d.Nickname = nickname
break
}
for _, qualifier := range d.Qualifiers {
if qualifier == serial {
d.Nickname = nickname
break NSMLoop
}
}
}
// Determine whether there is a default user ID set by 'madb user'.
if userID, ok := cfg.UserIDs[d.Serial]; ok {
d.UserID = userID
}
}
result = append(result, d)
}
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) {
configFile, err := getDefaultConfigFilePath()
if err != nil {
return nil, err
}
cfg, err := readConfig(configFile)
if err != nil {
return nil, err
}
devices, err := getDevices(cfg)
if err != nil {
return nil, err
}
tokens := strings.Split(devicesFlag, ",")
filtered, err := filterSpecifiedDevices(devices, cfg, allDevicesFlag, allEmulatorsFlag, tokens)
if err != nil {
return nil, err
}
if len(filtered) == 0 {
return nil, fmt.Errorf("No devices matching the device specifiers.")
}
return filtered, nil
}
type deviceSpec struct {
index int
token string
}
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 allDevices == false && allEmulators == false && len(tokens) == 0 {
return devices, nil
}
result := make([]device, 0, len(devices))
var specs = []deviceSpec{}
if len(tokens) > 0 {
// Check if the provided specifiers are all valid.
for _, token := range tokens {
if err := isValidDeviceSpecifier(token); err != nil {
return nil, err
}
}
// Expand all the groups and get the device specs.
tokens = expandGroups(tokens, cfg)
specs = getDeviceSpecsFromTokens(tokens, cfg)
}
for _, d := range devices {
if shouldIncludeDevice(d, specs, allDevices, allEmulators) {
result = append(result, d)
}
}
return result, nil
}
// getDeviceSpecsFromTokens takes device specifier tokens and turns them into
// the corresponding deviceSpec structs.
func getDeviceSpecsFromTokens(tokens []string, cfg *config) []deviceSpec {
specs := make([]deviceSpec, 0, len(tokens)*2)
for _, token := range tokens {
if strings.HasPrefix(token, "@") {
index, _ := strconv.Atoi(token[1:])
specs = append(specs, deviceSpec{index, ""})
} else {
specs = append(specs, deviceSpec{0, token})
}
}
return specs
}
func shouldIncludeDevice(d device, specs []deviceSpec, allDevices, allEmulators bool) bool {
if allDevices && d.Type == realDevice {
return true
}
if allEmulators && d.Type == emulator {
return true
}
for _, spec := range specs {
// Ignore empty tokens
if spec.index == 0 && spec.token == "" {
continue
}
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 == spec.token {
return true
}
}
}
return false
}
// config contains various configuration information for madb.
type config struct {
// Version indicates the version string of madb binary by which this config
// was written to the file, in case it has to be migrated to a newer schema.
Version string
// Names keeps the mapping between device nicknames and their serials.
Names map[string]string
// Groups keeps the device group definitions. A group can contain multiple
// devices, each of which is denoted by its name, serial, or index. A group
// can also include other groups.
Groups map[string][]string
// UserIDs keeps the mapping between device serials and their default user
// IDs.
UserIDs map[string]string
}
func newConfig() *config {
return &config{
Names: make(map[string]string),
Groups: make(map[string][]string),
UserIDs: make(map[string]string),
}
}
// Returns the config dir located at "~/.madb"
func getConfigDir() (string, error) {
home := os.Getenv("HOME")
if home == "" {
return "", fmt.Errorf("Could not find the HOME directory.")
}
configDir := filepath.Join(home, ".madb")
if err := os.MkdirAll(configDir, 0755); err != nil {
return "", err
}
if err := migrateOldConfigFiles(configDir); err != nil {
fmt.Fprintf(os.Stderr, "WARNING: Could not successfully migrate the old config files to the newer format: %v", err)
}
return configDir, nil
}
// migrateOldConfigFiles checks if there are old config files (for madb v1.x) in
// the provided config directory. If there are, it migrates these configs to the
// new format, so that users can preserve their device nicknames and user IDs
// when upgrading madb to a newer version.
// TODO(youngseokyoon): remove this migration code in the future.
func migrateOldConfigFiles(configDir string) error {
// Do not try migrating if the new format "config" file already exists.
configFile := filepath.Join(configDir, "config")
if _, err := os.Stat(configFile); err == nil {
return nil
}
cfg := newConfig()
if err := migrateOldConfig(configDir, "nicknames", &cfg.Names); err != nil {
return err
}
if err := migrateOldConfig(configDir, "users", &cfg.UserIDs); err != nil {
return err
}
return writeConfig(cfg, configFile)
}
// migrateOldConfig reads an old config file, which contains a JSON-encoded map,
// and writes the contents to the given map pointer (data).
func migrateOldConfig(configDir, filename string, data *map[string]string) error {
configFile := filepath.Join(configDir, filename)
if _, err := os.Stat(configFile); os.IsNotExist(err) {
return nil
}
f, err := os.Open(configFile)
if err != nil {
return err
}
defer f.Close()
decoder := json.NewDecoder(f)
if err := decoder.Decode(data); err != nil {
data = new(map[string]string)
return fmt.Errorf("Could not read the old config file %q: %v", filename, err)
}
fmt.Printf("NOTE: Migrating the %q file to the newer format.\n", filename)
// Rename the old config as a backup
if err := os.Rename(configFile, configFile+".bak"); err != nil {
return fmt.Errorf("Could not rename the %q file: %v", filename, err)
}
fmt.Printf("NOTE: The backup file can be found at %q.\n", filepath.Join(configDir, filename+".bak"))
return nil
}
// getDefaultConfigFilePath returns the default location of the config file.
func getDefaultConfigFilePath() (string, error) {
configDir, err := getConfigDir()
if err != nil {
return "", err
}
return filepath.Join(configDir, "config"), nil
}
// readConfig reads the provided file and reconstructs the config struct.
// When the file does not exist, it returns an empty config with the members
// initialized as empty maps.
func readConfig(filename string) (*config, error) {
result := newConfig()
// The file may not exist or be empty when there are no stored data.
if stat, err := os.Stat(filename); os.IsNotExist(err) || (err == nil && stat.Size() == 0) {
return result, nil
}
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
decoder := json.NewDecoder(f)
// Decoding might fail when the file is somehow corrupted, or when the schema is updated.
// In such cases, move on after resetting the cache file instead of exiting the app.
if err := decoder.Decode(result); err != nil {
fmt.Fprintf(os.Stderr, "WARNING: Could not decode the file: %q. Resetting the file.\n", err)
if err := os.Remove(f.Name()); err != nil {
return nil, err
}
result = new(config)
}
if result.Names == nil {
result.Names = make(map[string]string)
}
if result.Groups == nil {
result.Groups = make(map[string][]string)
}
if result.UserIDs == nil {
result.UserIDs = make(map[string]string)
}
return result, nil
}
// writeConfig takes a config and writes it into the provided file name.
func writeConfig(cfg *config, filename string) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
cfg.Version = version
encoder := json.NewEncoder(f)
return encoder.Encode(*cfg)
}
func isNameInUse(name string, cfg *config) bool {
return isDeviceNickname(name, cfg) || isGroupName(name, cfg)
}
func isDeviceNickname(name string, cfg *config) bool {
_, ok := cfg.Names[name]
return ok
}
func isGroupName(name string, cfg *config) bool {
_, ok := cfg.Groups[name]
return ok
}
func isValidSerial(serial string) bool {
r := regexp.MustCompile(`^([A-Za-z0-9:\-\._]+|@\d+)$`)
return r.MatchString(serial)
}
func isValidName(name string) bool {
r := regexp.MustCompile(`^\w+$`)
return r.MatchString(name)
}
// isValidMember takes a member string given as an argument, and returns nil
// when the member string is valid. Otherwise, an error is returned indicating
// the reason why the given member string is not valid.
func isValidDeviceSpecifier(member string) error {
if strings.HasPrefix(member, "@") {
index, err := strconv.Atoi(member[1:])
if err != nil || index <= 0 {
return fmt.Errorf("Invalid device specifier %q. '@' sign must be followed by a numeric device index starting from 1.", member)
}
return nil
} else if !isValidSerial(member) && !isValidName(member) {
return fmt.Errorf("Invalid device specifier %q. Not a valid serial or a nickname.", member)
}
return nil
}
type subCommandRunner struct {
// init is an optional function that does some initial work that should only
// be performed once, before directing the command to all the devices.
// The returned string slice becomes the new set of arguments passed into
// the sub command.
init func(env *cmdline.Env, args []string, properties variantProperties) ([]string, error)
// subCmd defines the behavior of the sub command which will run on all the
// devices in parallel.
subCmd func(env *cmdline.Env, args []string, d device, properties variantProperties) error
// extractProperties indicates whether this subCommand needs the extracted
// project properties.
extractProperties bool
}
var _ cmdline.Runner = (*subCommandRunner)(nil)
// Invokes the sub command on all the devices in parallel.
func (r subCommandRunner) Run(env *cmdline.Env, args []string) error {
prefixFlag = strings.ToLower(prefixFlag)
allowed := []string{"name", "serial", "none"}
if !isStringInSlice(prefixFlag, allowed) {
return fmt.Errorf("The -prefix flag value must be one of %v", strings.Join(allowed, ", "))
}
if err := startAdbServer(); err != nil {
return err
}
devices, err := getSpecifiedDevices()
if err != nil {
return err
}
// Extract the properties if needed.
properties := variantProperties{}
if r.extractProperties && isGradleProject(wd) {
properties, err = getProjectPropertiesUsingDefaultCache()
if err != nil {
return err
}
}
// Run the init function when provided.
if r.init != nil {
newArgs, err := r.init(env, args, properties)
if err != nil {
return err
}
args = newArgs
}
var errs []error
var errDevices []device
if sequentialFlag {
for _, d := range devices {
if err := r.subCmd(env, args, d, properties); err != nil {
errs = append(errs, err)
errDevices = append(errDevices, d)
}
}
} else {
wg := sync.WaitGroup{}
for _, d := range devices {
// Capture the current device value, and run the command in a go-routine.
deviceCopy := d
wg.Add(1)
go func() {
if err := r.subCmd(env, args, deviceCopy, properties); err != nil {
errs = append(errs, err)
errDevices = append(errDevices, deviceCopy)
}
wg.Done()
}()
}
wg.Wait()
}
// Report any errors returned from the go-routines.
if errs != nil {
buffer := bytes.Buffer{}
buffer.WriteString("Error occurred while running the command on the following devices:")
for i := 0; i < len(errs); i++ {
buffer.WriteString("\n[" + errDevices[i].displayName() + "]\t" + errs[i].Error())
}
return fmt.Errorf(buffer.String())
}
return nil
}
func runGoshCommandForDevice(cmd *gosh.Cmd, d device, printUserID bool) error {
return runGoshCommandForDeviceWithWriters(cmd, d, printUserID, os.Stdout, os.Stderr)
}
func runGoshCommandForDeviceWithWriters(cmd *gosh.Cmd, d device, printUserID bool, stdout, stderr io.Writer) error {
prefix := ""
if prefixFlag != "none" {
name := d.Serial
if prefixFlag == "name" {
name = d.displayName()
}
if printUserID && d.UserID != "" {
name = name + ":" + d.UserID
}
prefix = "[" + name + "]\t"
}
prefixedStdout := textutil.PrefixLineWriter(stdout, prefix)
prefixedStderr := textutil.PrefixLineWriter(stderr, prefix)
cmd.AddStdoutWriter(prefixedStdout)
cmd.AddStderrWriter(prefixedStderr)
cmd.Run()
prefixedStdout.Flush()
prefixedStderr.Flush()
return cmd.Shell().Err
}
func initMadbCommand(env *cmdline.Env, args []string, properties variantProperties, flutterPassthrough bool, activityNameRequired bool) ([]string, error) {
var numRequiredArgs int
var requiredArgsStr string
if activityNameRequired {
numRequiredArgs = 2
requiredArgsStr = "two arguments"
} else {
numRequiredArgs = 1
requiredArgsStr = "one argument"
}
// Pass the arguments through if all the required arguments are provided, or if it is a flutter project.
if len(args) == numRequiredArgs || (flutterPassthrough && isFlutterProject(wd)) {
return args, nil
}
if len(args) != 0 {
return nil, fmt.Errorf("You mush provide either zero arguments or exactly %v.", requiredArgsStr)
}
// Try to extract the application ID and the main activity name from the Gradle scripts.
if isGradleProject(wd) {
args = []string{properties.AppID, properties.Activity}[:numRequiredArgs]
}
return args, nil
}
func getProjectPropertiesUsingDefaultCache() (variantProperties, error) {
cacheFile, err := getDefaultCacheFilePath()
if err != nil {
return variantProperties{}, err
}
key := variantKey{wd, moduleFlag, variantFlag}
return getProjectProperties(extractPropertiesFromGradle, key, clearCacheFlag, cacheFile)
}
type variantProperties struct {
ProjectPath string
VariantName string
CleanTask string
AssembleTask string
AppID string
Activity string
AbiFilters []string
VariantOutputs []variantOutput
}
type variantOutput struct {
Name string
OutputFilePath string
VersionCode int
Filters []filter
}
type filter struct {
FilterType string
Identifier string
}
type propertyExtractorFunc func(variantKey) (variantProperties, error)
// getProjectProperties returns the project properties for the given build variant.
// It returns the cached values when the variant is found in the cache file, unless the clearCache
// argument is true. Otherwise, it calls extractPropertiesFromGradle to extract those properties by
// running Gradle scripts.
func getProjectProperties(extractor propertyExtractorFunc, key variantKey, clearCache bool, cacheFile string) (variantProperties, error) {
if clearCache {
clearPropertyCacheEntry(key, cacheFile)
} else {
// See if the current configuration appears in the cache.
cache, err := getPropertyCache(cacheFile)
if err != nil {
return variantProperties{}, err
}
if properties, ok := cache[key]; ok {
fmt.Println("NOTE: Cached IDs are being used. Use '-clear-cache' flag to clear the cache and extract the IDs from Gradle scripts again.")
return properties, nil
}
}
fmt.Println("Running Gradle to extract the application ID and the main activity name...")
properties, err := extractor(key)
if err != nil {
return variantProperties{}, err
}
// Write these properties to the cache.
if err := writePropertyCacheEntry(key, properties, cacheFile); err != nil {
return variantProperties{}, fmt.Errorf("Could not write properties to the cache file: %v", err)
}
return properties, nil
}
func isFlutterProject(dir string) bool {
_, err := os.Stat(filepath.Join(dir, "flutter.yaml"))
return err == nil
}
func isGradleProject(dir string) bool {
_, err := os.Stat(filepath.Join(dir, "build.gradle"))
return err == nil
}
// Looks for the Gradle wrapper script file ("gradlew"), starting from the current directory.
func findGradleWrapper(dir string) (string, error) {
curDir, err := filepath.Abs(dir)
if err != nil {
return "", err
}
for {
wrapperPath := filepath.Join(curDir, "gradlew")
// Return the path of the gradle wrapper script if it is found.
_, err := os.Stat(wrapperPath)
if err == nil {
// Found Gradle wrapper. Return the absolute path.
return wrapperPath, nil
} else if !os.IsNotExist(err) {
// This is an unexpected error and should be returned.
return "", err
}
// Search again in the parent directory.
parentDir := path.Dir(curDir)
if curDir == parentDir || parentDir == "." {
break
}
curDir = parentDir
}
return "", fmt.Errorf("Could not find the Gradle wrapper in dir %q or its parent directories.", dir)
}
func extractPropertiesFromGradle(key variantKey) (variantProperties, error) {
sh := gosh.NewShell(nil)
defer sh.Cleanup()
// Continue on error instead of panicking and check the sh.Err value afterwards.
// Gradle build will finish with exit code other than 0, when it fails.
// In such cases, we want to show users meaningful error messages instead of stacktraces.
sh.PropagateChildOutput = true
sh.ContinueOnError = true
wrapper, err := findGradleWrapper(key.Dir)
if err != nil {
return variantProperties{}, err
}
// Write the init script in a temp file.
initScript := sh.MakeTempFile()
initScript.WriteString(gradleInitScript)
initScript.Sync()
// Create a temporary file in which Gradle can write the results.
outputFile := sh.MakeTempFile()
// Run the gradle wrapper to extract the application ID and the main activity name from the build scripts.
cmdArgs := []string{"--daemon", "-q", "-I", initScript.Name(), "-PmadbOutputFile=" + outputFile.Name()}
// Specify the project directory. If the module name is explicitly set, combine it with the base directory.
cmdArgs = append(cmdArgs, "-p", filepath.Join(key.Dir, key.Module))
// Specify the variant
if key.Variant != "" {
cmdArgs = append(cmdArgs, "-PmadbVariant="+key.Variant)
}
// Specify the tasks
cmdArgs = append(cmdArgs, "madbExtractVariantProperties")
cmd := sh.Cmd(wrapper, cmdArgs...)
cmd.Run()
if err = sh.Err; err != nil {
return variantProperties{}, err
}
// Read what is written in the temporary file.
// The file must be in JSON format.
result := variantProperties{}
decoder := json.NewDecoder(outputFile)
if err = decoder.Decode(&result); err != nil {
return variantProperties{}, fmt.Errorf("Could not extract the application ID and the main activity name: %v", err)
}
return result, nil
}
// 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
}
// isStringInSlice determines whether the given string appears in the slice.
func isStringInSlice(str string, slice []string) bool {
for _, elem := range slice {
if str == elem {
return true
}
}
return false
}
type pathProvider func() (string, error)
// 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
}
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.
func (f subCommandRunnerWithFilepath) Run(env *cmdline.Env, args []string) error {
p, err := f.pp()
if err != nil {
return err
}
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] }