diff --git a/README.md b/README.md index d3fdccdb..f4622913 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,7 @@ The currently supported libraries include: * [c/clang](https://pkg.go.dev/github.com/goplus/llgo/c/clang) * [c/llama2](https://pkg.go.dev/github.com/goplus/llgo/c/llama2) * [c/lua](https://pkg.go.dev/github.com/goplus/llgo/c/lua) +* [c/neco](https://pkg.go.dev/github.com/goplus/llgo/c/neco) * [c/raylib](https://pkg.go.dev/github.com/goplus/llgo/c/raylib) * [c/sqlite](https://pkg.go.dev/github.com/goplus/llgo/c/sqlite) * [c/zlib](https://pkg.go.dev/github.com/goplus/llgo/c/zlib) diff --git a/_demo/osproc/exec.go b/_demo/osproc/exec.go new file mode 100644 index 00000000..9021d33a --- /dev/null +++ b/_demo/osproc/exec.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "runtime" +) + +func main() { + fmt.Println("os.Environ:", os.Environ()) + + ls := "ls" + args := []string{ls, "-l"} + if runtime.GOOS == "windows" { + ls = "dir" + args = []string{ls} + } + lspath, _ := exec.LookPath(ls) + if lspath != "" { + ls = lspath + } + proc, err := os.StartProcess(ls, args, &os.ProcAttr{ + Files: []*os.File{nil, os.Stdout, os.Stderr}, + }) + if err != nil { + fmt.Println("os.StartProcess error:", err) + return + } + proc.Wait() +} diff --git a/internal/lib/os/env.go b/internal/lib/os/env.go new file mode 100644 index 00000000..7c0f3174 --- /dev/null +++ b/internal/lib/os/env.go @@ -0,0 +1,161 @@ +// 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 ( + "syscall" +) + +/* TODO(xsw): +// 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 { + v, _ := syscall.Getenv(key) + return v +} + +/* TODO(xsw): +func Getenv(key string) string { + return c.GoString(os.Getenv(c.AllocaCStr(key))) +} +*/ + +// 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) { + 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 { + return NewSyscallError("setenv", err) + } + return nil +} + +/* TODO(xsw): +func Setenv(key, value string) error { + ret := os.Setenv(c.AllocaCStr(key), c.AllocaCStr(value), 1) + if ret == 0 { + return nil + } + return &SyscallError{"setenv", syscall.Errno(ret)} +} +*/ + +// Unsetenv unsets a single environment variable. +func Unsetenv(key string) error { + return syscall.Unsetenv(key) +} + +/* TODO(xsw): +func Unsetenv(key string) error { + ret := os.Unsetenv(c.AllocaCStr(key)) + if ret == 0 { + return nil + } + return syscall.Errno(ret) +} +*/ + +// Environ returns a copy of strings representing the environment, +// in the form "key=value". +func Environ() []string { + return syscall.Environ() +} diff --git a/internal/lib/os/os.go b/internal/lib/os/os.go index 31cfb89d..9350272c 100644 --- a/internal/lib/os/os.go +++ b/internal/lib/os/os.go @@ -148,10 +148,6 @@ func Clearenv() // TODO(xsw): // func DirFS(dir string) fs.FS -func Environ() []string { - panic("todo: os.Environ") -} - // func Executable() (string, error) // TODO(xsw): @@ -162,10 +158,6 @@ func Getegid() int { return int(os.Getegid()) } -func Getenv(key string) string { - return c.GoString(os.Getenv(c.AllocaCStr(key))) -} - func Geteuid() int { return int(os.Geteuid()) } @@ -330,14 +322,6 @@ func Rename(oldpath, newpath string) error { // TODO(xsw): // func SameFile(fi1, fi2 FileInfo) bool -func Setenv(key, value string) error { - ret := os.Setenv(c.AllocaCStr(key), c.AllocaCStr(value), 1) - if ret == 0 { - return nil - } - return &SyscallError{"setenv", syscall.Errno(ret)} -} - func Symlink(oldname, newname string) error { ret := os.Symlink(c.AllocaCStr(oldname), c.AllocaCStr(newname)) if ret == 0 { @@ -357,14 +341,6 @@ func Truncate(name string, size int64) error { return toPathErr("truncate", name, ret) } -func Unsetenv(key string) error { - ret := os.Unsetenv(c.AllocaCStr(key)) - if ret == 0 { - return nil - } - return syscall.Errno(ret) -} - // UserCacheDir returns the default root directory to use for user-specific // cached data. Users should create their own application-specific subdirectory // within this one and use that. diff --git a/internal/lib/syscall/env_unix.go b/internal/lib/syscall/env_unix.go new file mode 100644 index 00000000..a195fcee --- /dev/null +++ b/internal/lib/syscall/env_unix.go @@ -0,0 +1,173 @@ +// 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. + +//go:build unix || (js && wasm) || plan9 || wasip1 + +// Unix environment variables. + +package syscall + +import ( + "runtime" + "sync" + + "github.com/goplus/llgo/c" + "github.com/goplus/llgo/c/os" +) + +var ( + // envOnce guards initialization by copyenv, which populates env. + envOnce sync.Once + + // envLock guards env and envs. + envLock sync.RWMutex + + // env maps from an environment variable to its first occurrence in envs. + env map[string]int + + // envs is provided by the runtime. elements are expected to + // be of the form "key=value". An empty string means deleted + // (or a duplicate to be ignored). + envs []string = runtimeEnvs() +) + +func runtimeEnvs() []string { + ret := make([]string, 0, 8) + cenvs := os.Environ + i := 0 + for { + ce := *c.Advance(cenvs, i) + if ce == nil { + return ret + } + ret = append(ret, c.GoString(ce)) + i++ + } +} + +func copyenv() { + env = make(map[string]int) + for i, s := range envs { + for j := 0; j < len(s); j++ { + if s[j] == '=' { + key := s[:j] + if _, ok := env[key]; !ok { + env[key] = i // first mention of key + } else { + // Clear duplicate keys. This permits Unsetenv to + // safely delete only the first item without + // worrying about unshadowing a later one, + // which might be a security problem. + envs[i] = "" + } + break + } + } + } +} + +func Unsetenv(key string) error { + envOnce.Do(copyenv) + + envLock.Lock() + defer envLock.Unlock() + + if i, ok := env[key]; ok { + envs[i] = "" + delete(env, key) + } + os.Unsetenv(c.AllocaCStr(key)) + return nil +} + +func Getenv(key string) (value string, found bool) { + envOnce.Do(copyenv) + if len(key) == 0 { + return "", false + } + + envLock.RLock() + defer envLock.RUnlock() + + i, ok := env[key] + if !ok { + return "", false + } + s := envs[i] + for i := 0; i < len(s); i++ { + if s[i] == '=' { + return s[i+1:], true + } + } + return "", false +} + +/* TODO(xsw): +func Getenv(key string) (value string, found bool) { + ret := os.Getenv(c.AllocaCStr(key)) + if ret != nil { + return c.GoString(ret), true + } + return "", false +} +*/ + +func Setenv(key, value string) error { + envOnce.Do(copyenv) + if len(key) == 0 { + return EINVAL + } + for i := 0; i < len(key); i++ { + if key[i] == '=' || key[i] == 0 { + return EINVAL + } + } + // On Plan 9, null is used as a separator, eg in $path. + if runtime.GOOS != "plan9" { + for i := 0; i < len(value); i++ { + if value[i] == 0 { + return EINVAL + } + } + } + + envLock.Lock() + defer envLock.Unlock() + + i, ok := env[key] + kv := key + "=" + value + if ok { + envs[i] = kv + } else { + i = len(envs) + envs = append(envs, kv) + } + env[key] = i + os.Setenv(c.AllocaCStr(key), c.AllocaCStr(value), 1) + return nil +} + +func Clearenv() { + envOnce.Do(copyenv) // prevent copyenv in Getenv/Setenv + + envLock.Lock() + defer envLock.Unlock() + + os.Clearenv() + env = make(map[string]int) + envs = []string{} +} + +func Environ() []string { + envOnce.Do(copyenv) + envLock.RLock() + defer envLock.RUnlock() + a := make([]string, 0, len(envs)) + for _, env := range envs { + if env != "" { + a = append(a, env) + } + } + return a +} diff --git a/internal/lib/syscall/env_windows.go b/internal/lib/syscall/env_windows.go new file mode 100644 index 00000000..220a005e --- /dev/null +++ b/internal/lib/syscall/env_windows.go @@ -0,0 +1,96 @@ +// 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. + +// Windows environment variables. + +package syscall + +import ( + "unsafe" +) + +func Getenv(key string) (value string, found bool) { + keyp, err := UTF16PtrFromString(key) + if err != nil { + return "", false + } + n := uint32(100) + for { + b := make([]uint16, n) + n, err = GetEnvironmentVariable(keyp, &b[0], uint32(len(b))) + if n == 0 && err == ERROR_ENVVAR_NOT_FOUND { + return "", false + } + if n <= uint32(len(b)) { + return UTF16ToString(b[:n]), true + } + } +} + +func Setenv(key, value string) error { + v, err := UTF16PtrFromString(value) + if err != nil { + return err + } + keyp, err := UTF16PtrFromString(key) + if err != nil { + return err + } + e := SetEnvironmentVariable(keyp, v) + if e != nil { + return e + } + runtimeSetenv(key, value) + return nil +} + +func Unsetenv(key string) error { + keyp, err := UTF16PtrFromString(key) + if err != nil { + return err + } + e := SetEnvironmentVariable(keyp, nil) + if e != nil { + return e + } + runtimeUnsetenv(key) + return nil +} + +func Clearenv() { + for _, s := range Environ() { + // Environment variables can begin with = + // so start looking for the separator = at j=1. + // https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133 + for j := 1; j < len(s); j++ { + if s[j] == '=' { + Unsetenv(s[0:j]) + break + } + } + } +} + +func Environ() []string { + envp, e := GetEnvironmentStrings() + if e != nil { + return nil + } + defer FreeEnvironmentStrings(envp) + + r := make([]string, 0, 50) // Empty with room to grow. + const size = unsafe.Sizeof(*envp) + for *envp != 0 { // environment block ends with empty string + // find NUL terminator + end := unsafe.Pointer(envp) + for *(*uint16)(end) != 0 { + end = unsafe.Add(end, size) + } + + entry := unsafe.Slice(envp, (uintptr(end)-uintptr(unsafe.Pointer(envp)))/size) + r = append(r, UTF16ToString(entry)) + envp = (*uint16)(unsafe.Add(end, size)) + } + return r +} diff --git a/internal/lib/syscall/syscall.go b/internal/lib/syscall/syscall.go index 8d733ea8..e0df70e7 100644 --- a/internal/lib/syscall/syscall.go +++ b/internal/lib/syscall/syscall.go @@ -73,14 +73,6 @@ func Getwd() (string, error) { return "", Errno(os.Errno) } -func Getenv(key string) (value string, found bool) { - ret := os.Getenv(c.AllocaCStr(key)) - if ret != nil { - return c.GoString(ret), true - } - return "", false -} - func Getpid() (pid int) { return int(os.Getpid()) }