blob: 4d6aa904354c2c7055dc0f0ace14b074967d36e2 [file] [log] [blame]
package utils
import (
"bufio"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"github.com/golang/glog"
)
// RunCommandString runs a command string from a provided directory and logs
// the combined stdout and stderr.
// If there is no error, the output will be logged at Info level. If there is
// an error, the output will be logged at Error level.
func RunCommandString(dir string, command string) error {
glog.Info(fmt.Sprintf("running command: %s", command))
cmd := exec.Command("bash", "-c", command)
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err == nil {
if len(out) > 0 {
glog.Info(string(out))
}
} else {
if len(out) > 0 {
glog.Error(string(out))
}
return err
}
return nil
}
// SourceFile sources a bash file and returns the shell variables as a map.
func SourceFile(file string) (map[string]string, error) {
contents, err := os.ReadFile(file)
if err != nil {
return nil, err
}
return SourceString(string(contents))
}
// SourceString sources a string as a bash file and returns the shell variables as a map.
func SourceString(contents string) (map[string]string, error) {
// This function is a bit weird. If we were running in a shell, we could just
// directly call `source ${path}`, and the variables set by the script ${path}
// would be available for us. Unfortunately, that's not an option. Instead, we
// execute contents as a script and write all of the shell variables to an
// anonymous pipe, which we consume to populate the the mapping that we return.
//
// Intuitively, this is similar to executing ${path}, writing the shell
// variables to a file, then reading them from that file, but this keeps
// everything in RAM and we don't have to worry about filenames. The
// anonymous pipe is passed to the child process as file descriptor 3 via
// cmd.ExtraFiles.
cmd := exec.Command("bash", "-c", fmt.Sprintf("%s\ndeclare -p >&3", contents))
envReader, envWriter, err := os.Pipe()
if err != nil {
return nil, fmt.Errorf("failed to open pipe when sourcing bash script: %v", err)
}
defer envReader.Close()
defer envWriter.Close()
cmd.ExtraFiles = []*os.File{envWriter}
out, err := cmd.CombinedOutput()
glog.V(2).Info(string(out))
if err != nil {
return nil, fmt.Errorf("failure while sourcing bash script: %v", err)
}
envWriter.Close()
scanner := bufio.NewScanner(envReader)
env := make(map[string]string)
for scanner.Scan() {
// `declare -p` outputs variables like this:
// declare -- key="value"
// And it outputs arrays like this:
// declare -A key=([0]="abc" [1]="def")
// So we drop the first two words and take the key to be the text left of the = and
// the value to be the text right of the =. If present, we remove one quote prefix
// and one quote suffix from value.
text := scanner.Text()
parts := strings.SplitN(text, " ", 3)
if len(parts) != 3 {
continue
}
text = parts[2]
parts = strings.SplitN(text, "=", 2)
if len(parts) != 2 {
continue
}
key := parts[0]
value := strings.TrimSuffix(strings.TrimPrefix(parts[1], `"`), `"`)
env[key] = value
}
envReader.Close()
return env, nil
}
var arrayElementPattern = regexp.MustCompile(`\[(\d+)\]="(.*?)"`)
// ArrayElements splits a bash array output by `declare -p` into its indexed elements
// and returns the resulting map from keys to values.
// For example, the input string `X=([0]="abc" [1]="def")` should return
// map[string]string { "0": "abc", "1": "def" }.
func ArrayElements(array string) map[string]string {
// declare escapes backslashes, which we need to un-escape
array = strings.ReplaceAll(array, `\\`, `\`)
// get rid of escaped quotes before we do our element matching; we make sure to keep
// the length of all substrings the same so that we can use the indices from the
// transformed string as the indices of matches in the original string
escapedArray := strings.ReplaceAll(array, "\\\"", "--")
elements := arrayElementPattern.FindAllStringSubmatchIndex(escapedArray, -1)
result := make(map[string]string)
for _, match := range elements {
if len(match) != 6 {
glog.Warningf("ill-formed array element match: %v", match)
continue
}
key := array[match[2]:match[3]]
value := array[match[4]:match[5]]
result[key] = value
}
return result
}