small pixel drawing of a pufferfish zoa

env/env.go

package env

import (
	"bufio"
	"fmt"
	"os"
	"strings"
	"syscall"

	"mvdan.cc/sh/v3/expand"
)

func GenerateEnv() (expand.Environ, error) {
	// syscall.Uname _should_ be supported on all *nix systems and is backed
	// by posix
	var env expand.Environ
	uname := syscall.Utsname{}
	err := syscall.Uname(&uname)
	if err != nil {
		return env, err
	}

	// shell := "/bin/sh"?

	// $PATH is annoyingly non-standard, so we hardcode the var
	// to the binary paths described in the fhs standard.
	// in most cases, this will "just work" for people, but special
	// cases should be evaluated - this may need some adjustment in the future.
	// i am resistant to making it over-rideable.
	//
	// users can always call their special binaries with their full paths
	// if they are resistant to moving them for some reason.
	path := envString("PATH", "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin")
	uname_os := envString("OS", charsToString(uname.Sysname[:]))
	uname_release := envString("RELEASE", charsToString(uname.Release[:]))
	uname_arch := envString("ARCH", charsToString(uname.Machine[:]))
	if err != nil {
		return env, err
	}

	// standards are extremely annoying about hostnames.
	//
	// "Note that there is no standard that
	// says that the hostname set by sethostname(2)
	// is the same string as the nodename field of
	// the struct returned by uname() (indeed, some
	// systems allow a 256-byte hostname and an 8-byte
	// nodename), but this is true on Linux. The same
	// holds for setdomainname(2) and the domainname field."
	//
	// in practice, there's usually not a difference between HOSTNAME
	// and NODENAME, so i've chosen to only expose HOSTNAME for the
	// sake of simplicity. i'm using the Golang implementation, which
	// does call out to uname, annoyingly.
	//
	// if this becomes an issue, i'll revisit it. i doubt it though.
	// tldr: i'm ignoring that golang's os.Hostname() implementation
	// isn't standards-compliant by the letter of the law.
	// in actual practice, the hostname and nodename
	// are always identical in every case i've observed.
	//
	// and i'm exposing only 1 because otherwise things get annoying.
	// shrug.
	h, err := os.Hostname()
	if err != nil {
		return env, err
	}
	uname_hostname := fmt.Sprintf("HOSTNAME=%s", h)

	// !OS_RELEASE_* VARS ARE NOT STANDARDS-BACKED!
	// OS_* vars may or may not exist depending on the distro in question, so
	// they're not reliable _at all_. they're scraped from /etc/os-release
	// and are useful for identifying specific Linux distros, or their versions.
	//
	// if you rely on these variables, I highly suggest checking for their
	// existence with test -z before utilizing them. there be no standards here.
	os_release, err := getOSRelease()
	if err != nil {
		return env, err
	}
	osReleaseID := envString("OS_RELEASE_ID", os_release.ID)
	osReleaseVersionID := envString("OS_RELEASE_VERSION_ID", os_release.VersionID)

	env = expand.ListEnviron(path, // normie shit
		uname_os, uname_hostname, uname_release, uname_arch, // uname-derivated env vars
		osReleaseID, osReleaseVersionID) // /etc/os-release
	return env, nil
}

func envString(key string, value string) string {
	return fmt.Sprintf("%s=%s", key, value)
}

func charsToString(arr []int8) string {
	b := make([]byte, 0, len(arr))
	for _, v := range arr {
		if v == 0x00 {
			break
		}
		b = append(b, byte(v))
	}
	return string(b)
}

// I want to keep this list as small as possible, since
// this struct is unreliable.
type OSRelease struct {
	ID        string // distro name - "arch"
	VersionID string // for debian distros, this is set to "22.04"
}

// getOsRelease parses /etc/os-release data
// into a struct
func getOSRelease() (OSRelease, error) {
	var osr = OSRelease{}
	f, err := os.Open("/etc/os-release")
	if err != nil {
		return osr, err
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		key, value, _ := strings.Cut(scanner.Text(), "=")
		value = strings.Trim(value, `"`)
		switch key {
		case "ID":
			osr.ID = value
		case "VERSION_ID":
			osr.VersionID = value
		}
	}

	return osr, nil
}