Browse Source

os: implement os.ExpandEnv

pull/2541/head
Dan Kegel 3 years ago
committed by Ron Evans
parent
commit
db0efc52c7
  1. 120
      src/os/env.go
  2. 112
      src/os/env_test.go
  3. 56
      src/os/env_unix_test.go
  4. 13
      src/syscall/syscall_libc.go

120
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()
}

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

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

13
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)

Loading…
Cancel
Save