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