package main // Command substitution: $(command) is replaced by the output of command, // which runs in a subshell (a new gosh process), so that cd, set etc. in it // do not change the shell. import ( "bytes" "errors" "os" "os/exec" "strings" "syscall" ) // subshellPath is the gosh binary used for command substitution; empty means // the running executable. Tests set it, since there the running executable is // the test binary. var subshellPath string // substStatus is the exit status of the last command substitution of the // current command (a command that consists only of a substitution has this // status, like in sh). var substStatus int // isSubstStart reports whether a command substitution $( starts at s[i]. func isSubstStart(s string, i int) bool { return s[i] == '$' && i+1 < len(s) && s[i+1] == '(' } // substEnd returns the index of the ) that ends the command substitution // starting with $( at s[i], or -1 if it is not terminated. Quotes, escapes // and nested substitutions inside are taken into account. func substEnd(s string, i int) int { depth := 0 var quote byte for j := i + 1; j < len(s); j++ { ch := s[j] switch { case quote == '\'': if ch == '\'' { quote = 0 } case ch == '\\': j++ case quote == '"': if ch == '"' { quote = 0 } else if isSubstStart(s, j) { end := substEnd(s, j) if end < 0 { return -1 } j = end } case ch == '"' || ch == '\'': quote = ch case ch == '(': depth++ case ch == ')': depth-- if depth == 0 { return j } } } return -1 } // commandSubst runs command in a subshell and returns its output without // trailing newlines. stdin and stderr are those of the shell. func commandSubst(command string) (string, error) { if strings.HasPrefix(command, "(") { return "", errors.New("arithmetic expansion $((...)) is not supported") } path := subshellPath if path == "" { var err error if path, err = os.Executable(); err != nil { return "", err } } var out bytes.Buffer cmd := exec.Command(path, "-c", command) cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, &out, os.Stderr err := cmd.Run() substStatus = exitStatus(err) if substStatus == 128+int(syscall.SIGINT) { // Ctrl+C aborts the whole command, like in bash return "", statusError(substStatus) } var exitErr *exec.ExitError if err != nil && !errors.As(err, &exitErr) { return "", err } return strings.TrimRight(out.String(), "\n"), nil }