package envcmd
import (
+ "bytes"
"context"
"encoding/json"
"fmt"
"runtime"
"sort"
"strings"
+ "unicode"
"unicode/utf8"
"cmd/go/internal/base"
func PrintEnv(w io.Writer, env []cfg.EnvVar) {
for _, e := range env {
if e.Name != "TERM" {
+ if runtime.GOOS != "plan9" && bytes.Contains([]byte(e.Value), []byte{0}) {
+ base.Fatalf("go: internal error: encountered null byte in environment variable %s on non-plan9 platform", e.Name)
+ }
switch runtime.GOOS {
default:
- fmt.Fprintf(w, "%s=\"%s\"\n", e.Name, e.Value)
+ fmt.Fprintf(w, "%s=%s\n", e.Name, shellQuote(e.Value))
case "plan9":
if strings.IndexByte(e.Value, '\x00') < 0 {
fmt.Fprintf(w, "%s='%s'\n", e.Name, strings.ReplaceAll(e.Value, "'", "''"))
if x > 0 {
fmt.Fprintf(w, " ")
}
+ // TODO(#59979): Does this need to be quoted like above?
fmt.Fprintf(w, "%s", s)
}
fmt.Fprintf(w, ")\n")
}
case "windows":
- fmt.Fprintf(w, "set %s=%s\n", e.Name, e.Value)
+ if hasNonGraphic(e.Value) {
+ base.Errorf("go: stripping unprintable or unescapable characters from %%%q%%", e.Name)
+ }
+ fmt.Fprintf(w, "set %s=%s\n", e.Name, batchEscape(e.Value))
}
}
}
}
+func hasNonGraphic(s string) bool {
+ for _, c := range []byte(s) {
+ if c == '\r' || c == '\n' || (!unicode.IsGraphic(rune(c)) && !unicode.IsSpace(rune(c))) {
+ return true
+ }
+ }
+ return false
+}
+
+func shellQuote(s string) string {
+ var b bytes.Buffer
+ b.WriteByte('\'')
+ for _, x := range []byte(s) {
+ if x == '\'' {
+ // Close the single quoted string, add an escaped single quote,
+ // and start another single quoted string.
+ b.WriteString(`'\''`)
+ } else {
+ b.WriteByte(x)
+ }
+ }
+ b.WriteByte('\'')
+ return b.String()
+}
+
+func batchEscape(s string) string {
+ var b bytes.Buffer
+ for _, x := range []byte(s) {
+ if x == '\r' || x == '\n' || (!unicode.IsGraphic(rune(x)) && !unicode.IsSpace(rune(x))) {
+ b.WriteRune(unicode.ReplacementChar)
+ continue
+ }
+ switch x {
+ case '%':
+ b.WriteString("%%")
+ case '<', '>', '|', '&', '^':
+ // These are special characters that need to be escaped with ^. See
+ // https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/set_1.
+ b.WriteByte('^')
+ b.WriteByte(x)
+ default:
+ b.WriteByte(x)
+ }
+ }
+ return b.String()
+}
+
func printEnvAsJSON(env []cfg.EnvVar) {
m := make(map[string]string)
for _, e := range env {
--- /dev/null
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build unix || windows
+
+package envcmd
+
+import (
+ "bytes"
+ "cmd/go/internal/cfg"
+ "fmt"
+ "internal/testenv"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "testing"
+ "unicode"
+)
+
+func FuzzPrintEnvEscape(f *testing.F) {
+ f.Add(`$(echo 'cc"'; echo 'OOPS="oops')`)
+ f.Add("$(echo shell expansion 1>&2)")
+ f.Add("''")
+ f.Add(`C:\"Program Files"\`)
+ f.Add(`\\"Quoted Host"\\share`)
+ f.Add("\xfb")
+ f.Add("0")
+ f.Add("")
+ f.Add("''''''''")
+ f.Add("\r")
+ f.Add("\n")
+ f.Add("E,%")
+ f.Fuzz(func(t *testing.T, s string) {
+ t.Parallel()
+
+ for _, c := range []byte(s) {
+ if c == 0 {
+ t.Skipf("skipping %q: contains a null byte. Null bytes can't occur in the environment"+
+ " outside of Plan 9, which has different code path than Windows and Unix that this test"+
+ " isn't testing.", s)
+ }
+ if c > unicode.MaxASCII {
+ t.Skipf("skipping %#q: contains a non-ASCII character %q", s, c)
+ }
+ if !unicode.IsGraphic(rune(c)) && !unicode.IsSpace(rune(c)) {
+ t.Skipf("skipping %#q: contains non-graphic character %q", s, c)
+ }
+ if runtime.GOOS == "windows" && c == '\r' || c == '\n' {
+ t.Skipf("skipping %#q on Windows: contains unescapable character %q", s, c)
+ }
+ }
+
+ var b bytes.Buffer
+ if runtime.GOOS == "windows" {
+ b.WriteString("@echo off\n")
+ }
+ PrintEnv(&b, []cfg.EnvVar{{Name: "var", Value: s}})
+ var want string
+ if runtime.GOOS == "windows" {
+ fmt.Fprintf(&b, "echo \"%%var%%\"\n")
+ want += "\"" + s + "\"\r\n"
+ } else {
+ fmt.Fprintf(&b, "printf '%%s\\n' \"$var\"\n")
+ want += s + "\n"
+ }
+ scriptfilename := "script.sh"
+ if runtime.GOOS == "windows" {
+ scriptfilename = "script.bat"
+ }
+ scriptfile := filepath.Join(t.TempDir(), scriptfilename)
+ if err := os.WriteFile(scriptfile, b.Bytes(), 0777); err != nil {
+ t.Fatal(err)
+ }
+ t.Log(b.String())
+ var cmd *exec.Cmd
+ if runtime.GOOS == "windows" {
+ cmd = testenv.Command(t, "cmd.exe", "/C", scriptfile)
+ } else {
+ cmd = testenv.Command(t, "sh", "-c", scriptfile)
+ }
+ out, err := cmd.Output()
+ t.Log(string(out))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(out) != want {
+ t.Fatalf("output of running PrintEnv script and echoing variable: got: %q, want: %q",
+ string(out), want)
+ }
+ })
+}