| 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 |
| } |