From db0efc52c78a83326d114d8a5455f1db945a9c74 Mon Sep 17 00:00:00 2001 From: Dan Kegel Date: Sun, 16 Jan 2022 03:20:05 +0000 Subject: [PATCH] os: implement os.ExpandEnv --- src/os/env.go | 120 +++++++++++++++++++++++++++++++++--- src/os/env_test.go | 112 +++++++++++++++++++++++++++++++++ src/os/env_unix_test.go | 56 +++++++++++++++++ src/syscall/syscall_libc.go | 13 ++++ 4 files changed, 292 insertions(+), 9 deletions(-) create mode 100644 src/os/env_unix_test.go diff --git a/src/os/env.go b/src/os/env.go index 3698e07e..330297b3 100644 --- a/src/os/env.go +++ b/src/os/env.go @@ -1,14 +1,121 @@ +// Copyright 2010 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. + +// General environment variables. + package os import ( + "internal/testlog" "syscall" ) +// Expand replaces ${var} or $var in the string based on the mapping function. +// For example, os.ExpandEnv(s) is equivalent to os.Expand(s, os.Getenv). +func Expand(s string, mapping func(string) string) string { + var buf []byte + // ${} is all ASCII, so bytes are fine for this operation. + i := 0 + for j := 0; j < len(s); j++ { + if s[j] == '$' && j+1 < len(s) { + if buf == nil { + buf = make([]byte, 0, 2*len(s)) + } + buf = append(buf, s[i:j]...) + name, w := getShellName(s[j+1:]) + if name == "" && w > 0 { + // Encountered invalid syntax; eat the + // characters. + } else if name == "" { + // Valid syntax, but $ was not followed by a + // name. Leave the dollar character untouched. + buf = append(buf, s[j]) + } else { + buf = append(buf, mapping(name)...) + } + j += w + i = j + 1 + } + } + if buf == nil { + return s + } + return string(buf) + s[i:] +} + +// ExpandEnv replaces ${var} or $var in the string according to the values +// of the current environment variables. References to undefined +// variables are replaced by the empty string. +func ExpandEnv(s string) string { + return Expand(s, Getenv) +} + +// isShellSpecialVar reports whether the character identifies a special +// shell variable such as $*. +func isShellSpecialVar(c uint8) bool { + switch c { + case '*', '#', '$', '@', '!', '?', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + return true + } + return false +} + +// isAlphaNum reports whether the byte is an ASCII letter, number, or underscore +func isAlphaNum(c uint8) bool { + return c == '_' || '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' +} + +// getShellName returns the name that begins the string and the number of bytes +// consumed to extract it. If the name is enclosed in {}, it's part of a ${} +// expansion and two more bytes are needed than the length of the name. +func getShellName(s string) (string, int) { + switch { + case s[0] == '{': + if len(s) > 2 && isShellSpecialVar(s[1]) && s[2] == '}' { + return s[1:2], 3 + } + // Scan to closing brace + for i := 1; i < len(s); i++ { + if s[i] == '}' { + if i == 1 { + return "", 2 // Bad syntax; eat "${}" + } + return s[1:i], i + 1 + } + } + return "", 1 // Bad syntax; eat "${" + case isShellSpecialVar(s[0]): + return s[0:1], 1 + } + // Scan alphanumerics. + var i int + for i = 0; i < len(s) && isAlphaNum(s[i]); i++ { + } + return s[:i], i +} + +// Getenv retrieves the value of the environment variable named by the key. +// It returns the value, which will be empty if the variable is not present. +// To distinguish between an empty value and an unset value, use LookupEnv. func Getenv(key string) string { + testlog.Getenv(key) v, _ := syscall.Getenv(key) return v } +// LookupEnv retrieves the value of the environment variable named +// by the key. If the variable is present in the environment the +// value (which may be empty) is returned and the boolean is true. +// Otherwise the returned value will be empty and the boolean will +// be false. +func LookupEnv(key string) (string, bool) { + testlog.Getenv(key) + return syscall.Getenv(key) +} + +// Setenv sets the value of the environment variable named by the key. +// It returns an error, if any. func Setenv(key, value string) error { err := syscall.Setenv(key, value) if err != nil { @@ -17,12 +124,9 @@ func Setenv(key, value string) error { return nil } +// Unsetenv unsets a single environment variable. func Unsetenv(key string) error { - err := syscall.Unsetenv(key) - if err != nil { - return NewSyscallError("unsetenv", err) - } - return nil + return syscall.Unsetenv(key) } // Clearenv deletes all environment variables. @@ -30,10 +134,8 @@ func Clearenv() { syscall.Clearenv() } -func LookupEnv(key string) (string, bool) { - return syscall.Getenv(key) -} - +// Environ returns a copy of strings representing the environment, +// in the form "key=value". func Environ() []string { return syscall.Environ() } diff --git a/src/os/env_test.go b/src/os/env_test.go index feec0c87..11b3b897 100644 --- a/src/os/env_test.go +++ b/src/os/env_test.go @@ -11,6 +11,82 @@ import ( "testing" ) +// testGetenv gives us a controlled set of variables for testing Expand. +func testGetenv(s string) string { + switch s { + case "*": + return "all the args" + case "#": + return "NARGS" + case "$": + return "PID" + case "1": + return "ARGUMENT1" + case "HOME": + return "/usr/gopher" + case "H": + return "(Value of H)" + case "home_1": + return "/usr/foo" + case "_": + return "underscore" + } + return "" +} + +var expandTests = []struct { + in, out string +}{ + {"", ""}, + {"$*", "all the args"}, + {"$$", "PID"}, + {"${*}", "all the args"}, + {"$1", "ARGUMENT1"}, + {"${1}", "ARGUMENT1"}, + {"now is the time", "now is the time"}, + {"$HOME", "/usr/gopher"}, + {"$home_1", "/usr/foo"}, + {"${HOME}", "/usr/gopher"}, + {"${H}OME", "(Value of H)OME"}, + {"A$$$#$1$H$home_1*B", "APIDNARGSARGUMENT1(Value of H)/usr/foo*B"}, + {"start$+middle$^end$", "start$+middle$^end$"}, + {"mixed$|bag$$$", "mixed$|bagPID$"}, + {"$", "$"}, + {"$}", "$}"}, + {"${", ""}, // invalid syntax; eat up the characters + {"${}", ""}, // invalid syntax; eat up the characters +} + +func TestExpand(t *testing.T) { + for _, test := range expandTests { + result := Expand(test.in, testGetenv) + if result != test.out { + t.Errorf("Expand(%q)=%q; expected %q", test.in, result, test.out) + } + } +} + +var global interface{} + +func BenchmarkExpand(b *testing.B) { + b.Run("noop", func(b *testing.B) { + var s string + b.ReportAllocs() + for i := 0; i < b.N; i++ { + s = Expand("tick tick tick tick", func(string) string { return "" }) + } + global = s + }) + b.Run("multiple", func(b *testing.B) { + var s string + b.ReportAllocs() + for i := 0; i < b.N; i++ { + s = Expand("$a $a $a $a", func(string) string { return "boom" }) + } + global = s + }) +} + func TestConsistentEnviron(t *testing.T) { e0 := Environ() for i := 0; i < 10; i++ { @@ -90,3 +166,39 @@ func TestLookupEnv(t *testing.T) { t.Errorf("smallpox release failed; world remains safe but LookupEnv is broken") } } + +// On Windows, Environ was observed to report keys with a single leading "=". +// Check that they are properly reported by LookupEnv and can be set by SetEnv. +// See https://golang.org/issue/49886. +func TestEnvironConsistency(t *testing.T) { + for _, kv := range Environ() { + i := strings.Index(kv, "=") + if i == 0 { + // We observe in practice keys with a single leading "=" on Windows. + // TODO(#49886): Should we consume only the first leading "=" as part + // of the key, or parse through arbitrarily many of them until a non-=, + // or try each possible key/value boundary until LookupEnv succeeds? + i = strings.Index(kv[1:], "=") + 1 + } + if i < 0 { + t.Errorf("Environ entry missing '=': %q", kv) + } + + k := kv[:i] + v := kv[i+1:] + v2, ok := LookupEnv(k) + if ok && v == v2 { + t.Logf("LookupEnv(%q) = %q, %t", k, v2, ok) + } else { + t.Errorf("Environ contains %q, but LookupEnv(%q) = %q, %t", kv, k, v2, ok) + } + + // Since k=v is already present in the environment, + // setting it should be a no-op. + if err := Setenv(k, v); err == nil { + t.Logf("Setenv(%q, %q)", k, v) + } else { + t.Errorf("Environ contains %q, but SetEnv(%q, %q) = %q", kv, k, v, err) + } + } +} diff --git a/src/os/env_unix_test.go b/src/os/env_unix_test.go new file mode 100644 index 00000000..9a4ec6fc --- /dev/null +++ b/src/os/env_unix_test.go @@ -0,0 +1,56 @@ +// Copyright 2013 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. + +//+build darwin linux + +package os_test + +import ( + "fmt" + . "os" + "testing" +) + +var setenvEinvalTests = []struct { + k, v string +}{ + {"", ""}, // empty key + {"k=v", ""}, // '=' in key + {"\x00", ""}, // '\x00' in key + {"k", "\x00"}, // '\x00' in value +} + +func TestSetenvUnixEinval(t *testing.T) { + for _, tt := range setenvEinvalTests { + err := Setenv(tt.k, tt.v) + if err == nil { + t.Errorf(`Setenv(%q, %q) == nil, want error`, tt.k, tt.v) + } + } +} + +var shellSpecialVarTests = []struct { + k, v string +}{ + {"*", "asterisk"}, + {"#", "pound"}, + {"$", "dollar"}, + {"@", "at"}, + {"!", "exclamation mark"}, + {"?", "question mark"}, + {"-", "dash"}, +} + +func TestExpandEnvShellSpecialVar(t *testing.T) { + for _, tt := range shellSpecialVarTests { + Setenv(tt.k, tt.v) + defer Unsetenv(tt.k) + + argRaw := fmt.Sprintf("$%s", tt.k) + argWithBrace := fmt.Sprintf("${%s}", tt.k) + if gotRaw, gotBrace := ExpandEnv(argRaw), ExpandEnv(argWithBrace); gotRaw != gotBrace { + t.Errorf("ExpandEnv(%q) = %q, ExpandEnv(%q) = %q; expect them to be equal", argRaw, gotRaw, argWithBrace, gotBrace) + } + } +} diff --git a/src/syscall/syscall_libc.go b/src/syscall/syscall_libc.go index 88e920aa..d8d68bca 100644 --- a/src/syscall/syscall_libc.go +++ b/src/syscall/syscall_libc.go @@ -147,6 +147,19 @@ func Getenv(key string) (value string, found bool) { } func Setenv(key, val string) (err error) { + if len(key) == 0 { + return EINVAL + } + for i := 0; i < len(key); i++ { + if key[i] == '=' || key[i] == 0 { + return EINVAL + } + } + for i := 0; i < len(val); i++ { + if val[i] == 0 { + return EINVAL + } + } keydata := cstring(key) valdata := cstring(val) errCode := libc_setenv(&keydata[0], &valdata[0], 1)