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. Functions also run in a subshell in pipelines and // in the background. 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 } // subshellCommand returns a command that runs script in a new gosh process // with the same $0, the given positional parameters and the functions of the // shell. (Variables need not be passed: they are all in the environment.) func subshellCommand(script string, params []string) (*exec.Cmd, error) { path := subshellPath if path == "" { var err error if path, err = os.Executable(); err != nil { return nil, err } } args := append([]string{"-c", functionDefs() + script, scriptName}, params...) return exec.Command(path, args...), nil } // 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") } var out bytes.Buffer cmd, err := subshellCommand(command, positional) if err != nil { return "", err } cmd.Stdin, cmd.Stdout, cmd.Stderr = stdio(0), &out, stdio(2) 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 }