Initial commit: Go clone of daemontools envdir
This commit is contained in:
commit
696bd783ac
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
envdir
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Jeffrey Paul
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
47
README.md
Normal file
47
README.md
Normal file
@ -0,0 +1,47 @@
|
||||
# envdir
|
||||
|
||||
A Go clone of the [daemontools](https://cr.yp.to/daemontools.html) `envdir`
|
||||
program.
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
go install sneak.berlin/go/envdir@latest
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
envdir dir child [args...]
|
||||
```
|
||||
|
||||
`envdir` sets various environment variables as specified by files in `dir`,
|
||||
then runs `child` with the given arguments.
|
||||
|
||||
If `dir` contains a file named `FOO` whose first line is `bar`, `envdir`
|
||||
sets the environment variable `FOO` to `bar`, then runs `child`.
|
||||
|
||||
### Rules
|
||||
|
||||
- Each file in `dir` corresponds to one environment variable. The filename
|
||||
is the variable name and the first line of the file is the value.
|
||||
- If a file is empty, the corresponding variable is **unset**.
|
||||
- NUL bytes (`\0`) in file contents are replaced with newlines.
|
||||
- Trailing spaces and tabs are stripped from the value.
|
||||
- Lines after the first newline are ignored.
|
||||
- Subdirectories and files with `=` in the name are skipped.
|
||||
- Exit code is **111** if `envdir` encounters an error.
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
$ mkdir /tmp/env
|
||||
$ echo -n "production" > /tmp/env/APP_ENV
|
||||
$ echo -n "localhost:5432" > /tmp/env/DB_HOST
|
||||
$ envdir /tmp/env printenv APP_ENV
|
||||
production
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
120
main.go
Normal file
120
main.go
Normal file
@ -0,0 +1,120 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Fprintf(os.Stderr, "usage: envdir dir child\n")
|
||||
os.Exit(111)
|
||||
}
|
||||
|
||||
dir := os.Args[1]
|
||||
child := os.Args[2]
|
||||
childArgs := os.Args[2:]
|
||||
|
||||
env, err := buildEnv(dir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "envdir: %s\n", err)
|
||||
os.Exit(111)
|
||||
}
|
||||
|
||||
path, err := findExecutable(child)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "envdir: %s\n", err)
|
||||
os.Exit(111)
|
||||
}
|
||||
|
||||
err = syscall.Exec(path, childArgs, env)
|
||||
fmt.Fprintf(os.Stderr, "envdir: exec: %s\n", err)
|
||||
os.Exit(111)
|
||||
}
|
||||
|
||||
func buildEnv(dir string) ([]string, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read directory %s: %w", dir, err)
|
||||
}
|
||||
|
||||
// Start with the current environment.
|
||||
envMap := make(map[string]string)
|
||||
unset := make(map[string]bool)
|
||||
for _, e := range os.Environ() {
|
||||
k, v, _ := strings.Cut(e, "=")
|
||||
envMap[k] = v
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
|
||||
// Skip subdirectories.
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip names containing '='.
|
||||
if strings.Contains(name, "=") {
|
||||
continue
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, name)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read %s: %w", path, err)
|
||||
}
|
||||
|
||||
// If the file is empty, unset the variable.
|
||||
if len(data) == 0 {
|
||||
delete(envMap, name)
|
||||
unset[name] = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Use only the first line.
|
||||
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
||||
data = data[:i]
|
||||
}
|
||||
|
||||
// Replace NUL bytes with newlines.
|
||||
data = bytes.ReplaceAll(data, []byte{0}, []byte{'\n'})
|
||||
|
||||
// Trim trailing spaces and tabs.
|
||||
value := strings.TrimRight(string(data), " \t")
|
||||
|
||||
envMap[name] = value
|
||||
delete(unset, name)
|
||||
}
|
||||
|
||||
env := make([]string, 0, len(envMap))
|
||||
for k, v := range envMap {
|
||||
env = append(env, k+"="+v)
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
func findExecutable(name string) (string, error) {
|
||||
// If name contains a slash, use it directly.
|
||||
if strings.Contains(name, "/") {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// Search PATH.
|
||||
pathEnv := os.Getenv("PATH")
|
||||
for _, dir := range filepath.SplitList(pathEnv) {
|
||||
path := filepath.Join(dir, name)
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.Mode().IsRegular() && info.Mode()&0111 != 0 {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("command not found: %s", name)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user