os: implement os.ExpandEnv
Этот коммит содержится в:
родитель
94b075e423
коммит
db0efc52c7
4 изменённых файлов: 292 добавлений и 9 удалений
120
src/os/env.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()
|
||||
}
|
||||
|
|
|
@ -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
Обычный файл
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче