7 Commits

Author SHA1 Message Date
brent saner
803be548cf v1.15.0
* IDState cleaned up. Should work on all *NIXes now.
* Can now get IDState of arbitrary PID.
* Shuffled some env stuff around.
2025-11-07 23:11:47 -05:00
brent saner
675a10addd v1.14.5
FIXED:
* Renamed sysutils.NsToFD to sysutils.NsToInode because that number's an
  inode, not an FD. D'oh.
2025-10-08 16:38:44 -04:00
brent saner
acb4352113 v1.14.4
UPDATED:
* docs for sysutils/paths now link to references.
2025-10-08 12:36:19 -04:00
brent saner
35c56d3f98 v1.14.3
ADDED:
* Convenience function to return a namespace FD and its type from a
  namespace ID (Linux only)
2025-10-07 17:11:37 -04:00
brent saner
d7db23d58c v1.14.2
FIXED:
* Small comparison issue in a couple libs based on recent discovery in
  r00t2.io/goutils/bitmask
2025-08-27 19:20:44 -04:00
brent saner
5a62622892 stubbing pdsh 2025-08-17 02:58:24 -04:00
brent saner
e797a14911 v1.14.1
FIXED:
* `envs/funcs.go:78:3: unknown field IgnoreWhiteSpace in struct literal of type EnvErrNoVal, but does have IgnoreWhitespace`
* `envs/funcs_enverrnoval.go:15:8: sb.WasFound undefined (type *strings.Builder has no field or method WasFound)`
2025-08-13 14:54:49 -04:00
43 changed files with 1555 additions and 307 deletions

11
consts_nix.go Normal file
View File

@@ -0,0 +1,11 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
package sysutils
const (
envSudoCmd string = "SUDO_COMMAND"
envSudoHome string = "SUDO_HOME"
envSudoGrp string = "SUDO_GID"
envSudoUid string = "SUDO_UID"
envSudoUname string = "SUDO_USER"
)

View File

@@ -1,15 +1,5 @@
package envs
import (
"regexp"
)
// Compiled regex patterns.
var (
reMaybeInt *regexp.Regexp = regexp.MustCompile(`^(?P<sign>\+|-)[0-9]+$`)
reMaybeFloat *regexp.Regexp = regexp.MustCompile(`(?P<sign>\+|-)?[0-9]+\.[0-9]+$`)
)
var (
StructTagInterpolate string = "envsub"
)

View File

@@ -1,10 +1,6 @@
package envs
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"reflect"
"strings"
@@ -14,7 +10,6 @@ import (
"r00t2.io/goutils/structutils"
"r00t2.io/sysutils/errs"
"r00t2.io/sysutils/internal"
"r00t2.io/sysutils/paths"
)
/*
@@ -34,7 +29,7 @@ func DefEnv(key, fallback string) (value string) {
return
}
// DefEnvBlank is like DefEnv but will ADDITIONALLY/ALSO apply fallback if key is *defined/exists but is an empty string*.
// DefEnvBlank is like [DefEnv] but will ADDITIONALLY/ALSO apply fallback if key is *defined/exists but is an empty string*.
func DefEnvBlank(key, fallback string) (value string) {
value = DefEnv(key, fallback)
@@ -45,7 +40,7 @@ func DefEnvBlank(key, fallback string) (value string) {
return
}
// GetEnvErr returns the value of key if it exists. If it does not exist, err will be an EnvErrNoVal.
// GetEnvErr returns the value of key if it exists. If it does not exist, err will be an [EnvErrNoVal].
func GetEnvErr(key string) (value string, err error) {
var exists bool
@@ -61,13 +56,13 @@ func GetEnvErr(key string) (value string, err error) {
}
/*
GetEnvErrNoBlank behaves exactly like GetEnvErr with the
GetEnvErrNoBlank behaves exactly like [GetEnvErr] with the
additional stipulation that the value must not be empty.
An error for a value that is non-empty but whitespace only (e.g. VARNM="\t")
can be returned if ignoreWhitespace == true.
(If it is, an EnvErrNoVal will also be returned.)
(If it is, an [EnvErrNoVal] will also be returned.)
*/
func GetEnvErrNoBlank(key string, ignoreWhitespace bool) (value string, err error) {
@@ -75,7 +70,7 @@ func GetEnvErrNoBlank(key string, ignoreWhitespace bool) (value string, err erro
var e *EnvErrNoVal = &EnvErrNoVal{
VarName: key,
WasRequiredNonEmpty: true,
IgnoreWhiteSpace: ignoreWhitespace,
IgnoreWhitespace: ignoreWhitespace,
}
if value, exists = os.LookupEnv(key); !exists {
@@ -98,7 +93,7 @@ func GetEnvMap() (envVars map[string]string) {
var envList []string = os.Environ()
envVars = envListToMap(envList)
envVars = internal.EnvListToMap(envList)
return
}
@@ -108,20 +103,20 @@ GetEnvMapNative returns a map of all environment variables, but attempts to "nat
All values are interfaces. It is up to the caller to typeswitch them to proper types.
Note that the PATH/Path environment variable (for *Nix and Windows, respectively) will be
a []string (as per GetPathEnv). No other env vars, even if they contain os.PathListSeparator,
a []string (as per [GetPathEnv]). No other env vars, even if they contain [os.PathListSeparator],
will be transformed to a slice or the like.
If an error occurs during parsing the path env var, it will be rendered as a string.
All number types will attempt to be their 64-bit version (i.e. int64, uint64, float64, etc.).
If a type cannot be determined for a value, its string form will be used
(as it would be found in GetEnvMap).
(as it would be found in [GetEnvMap]).
*/
func GetEnvMapNative() (envMap map[string]interface{}) {
var stringMap map[string]string = GetEnvMap()
envMap = nativizeEnvMap(stringMap)
envMap = internal.NativizeEnvMap(stringMap)
return
}
@@ -144,7 +139,7 @@ If val is "" and ok is true, this means that one of the specified variable names
set but is set to an empty value. If ok is false, none of the specified variables
are set.
It is a thin wrapper around GetFirstWithRef.
It is a thin wrapper around [GetFirstWithRef].
*/
func GetFirst(varNames []string) (val string, ok bool) {
@@ -154,7 +149,7 @@ func GetFirst(varNames []string) (val string, ok bool) {
}
/*
GetFirstWithRef behaves exactly like GetFirst, but with an additional returned value, idx,
GetFirstWithRef behaves exactly like [GetFirst], but with an additional returned value, idx,
which specifies the index in varNames in which a set variable was found. e.g. if:
GetFirstWithRef([]string{"FOO", "FOOBAR", "FOOBAZ"})
@@ -182,16 +177,10 @@ func GetFirstWithRef(varNames []string) (val string, ok bool, idx int) {
// GetPathEnv returns a slice of the PATH variable's items.
func GetPathEnv() (pathList []string, err error) {
var pathVar string = internal.GetPathEnvName()
pathList = make([]string, 0)
for _, p := range strings.Split(os.Getenv(pathVar), string(os.PathListSeparator)) {
if err = paths.RealPath(&p); err != nil {
return
}
pathList = append(pathList, p)
if pathList, err = internal.GetPathEnv(); err != nil {
return
}
return
}
@@ -201,60 +190,34 @@ It gets the environment variables of a given process' PID.
*/
func GetPidEnvMap(pid uint32) (envMap map[string]string, err error) {
var envBytes []byte
var envList []string
var envArr [][]byte
var procPath string
var exists bool
envMap = make(map[string]string)
procPath = fmt.Sprintf("/proc/%v/environ", pid)
if exists, err = paths.RealPathExists(&procPath); err != nil {
if envMap, err = internal.GetPidEnvMap(pid); err != nil {
return
}
if !exists {
err = errors.New(fmt.Sprintf("information for pid %v does not exist", pid))
return
}
if envBytes, err = ioutil.ReadFile(procPath); err != nil {
return
}
envArr = bytes.Split(envBytes, []byte{0x0})
envList = make([]string, len(envArr))
for idx, b := range envArr {
envList[idx] = string(b)
}
envMap = envListToMap(envList)
return
}
/*
GetPidEnvMapNative, like GetEnvMapNative, returns a map of all environment variables, but attempts to "nativize" them.
GetPidEnvMapNative returns a map of all environment variables (like [GetEnvMapNative]), but attempts to "nativize" them.
All values are interfaces. It is up to the caller to typeswitch them to proper types.
See the documentation for GetEnvMapNative for details.
See the documentation for [GetEnvMapNative] for details.
*/
func GetPidEnvMapNative(pid uint32) (envMap map[string]interface{}, err error) {
var stringMap map[string]string
if stringMap, err = GetPidEnvMap(pid); err != nil {
if stringMap, err = internal.GetPidEnvMap(pid); err != nil {
return
}
envMap = nativizeEnvMap(stringMap)
envMap = internal.NativizeEnvMap(stringMap)
return
}
/*
HasEnv is much like os.LookupEnv, but only returns a boolean for
HasEnv is much like [os.LookupEnv], but only returns a boolean indicating
if the environment variable key exists or not.
This is useful anywhere you may need to set a boolean in a func call
@@ -280,7 +243,7 @@ and performs variable substitution on strings from environment variables.
It supports both UNIX/Linux/POSIX syntax formats (e.g. $VARNAME, ${VARNAME}) and,
if on Windows, it *additionally* supports the EXPAND_SZ format (e.g. %VARNAME%).
For structs, the tag name used can be changed by setting the StructTagInterpolate
For structs, the tag name used can be changed by setting the [StructTagInterpolate]
variable in this submodule; the default is `envsub`.
If the tag value is "-", the field will be skipped.
For map fields within structs etc., the default is to apply interpolation to both keys and values.
@@ -347,9 +310,9 @@ from environment variables.
It supports both UNIX/Linux/POSIX syntax formats (e.g. $VARNAME, ${VARNAME}) and,
if on Windows, it *additionally* supports the EXPAND_SZ format (e.g. %VARNAME%).
If s is nil, nothing will be done and err will be ErrNilPtr.
If s is nil, nothing will be done and err will be [errs.ErrNilPtr].
This is a standalone function that is much more performant than Interpolate
This is a standalone function that is much more performant than [Interpolate]
at the cost of rigidity.
*/
func InterpolateString(s *string) (err error) {
@@ -369,7 +332,7 @@ func InterpolateString(s *string) (err error) {
return
}
// interpolateMap is used by Interpolate for maps. v should be a reflect.Value of a map.
// interpolateMap is used by [Interpolate] for maps. v should be a [reflect.Value] of a map.
func interpolateMap(v reflect.Value) (err error) {
var kVal reflect.Value
@@ -467,7 +430,7 @@ func interpolateMap(v reflect.Value) (err error) {
return
}
// interpolateSlice is used by Interpolate for slices and arrays. v should be a reflect.Value of a slice/array.
// interpolateSlice is used by [Interpolate] for slices and arrays. v should be a [reflect.Value] of a slice/array.
func interpolateSlice(v reflect.Value) (err error) {
var wg sync.WaitGroup
@@ -558,7 +521,7 @@ func interpolateStringReflect(v reflect.Value) (err error) {
return
}
// interpolateStruct is used by Interpolate for structs. v should be a reflect.Value of a struct.
// interpolateStruct is used by [Interpolate] for structs. v should be a [reflect.Value] of a struct.
func interpolateStruct(v reflect.Value) (err error) {
var field reflect.StructField

View File

@@ -12,13 +12,13 @@ func (e *EnvErrNoVal) Error() (errStr string) {
sb.WriteString("the variable '")
sb.WriteString(e.VarName)
sb.WriteString("' was ")
if sb.WasFound {
if e.WasFound {
sb.WriteString("found")
} else {
sb.WriteString("not found")
}
if e.WasRequiredNonEmpty && e.WasFound {
sb.WriteString(" but is empty and was required to be non-empty")
sb.WriteString(" but is empty and was required to be non-empty")
}
errStr = sb.String()

View File

@@ -1,87 +0,0 @@
package envs
import (
`strconv`
`strings`
`r00t2.io/sysutils/internal`
)
// envListToMap splits a []string of env var keypairs to a map.
func envListToMap(envs []string) (envMap map[string]string) {
var kv []string
var k, v string
envMap = make(map[string]string)
for _, ev := range envs {
kv = strings.SplitN(ev, "=", 2)
// I *think* SplitN does this for me, but...
if len(kv) == 1 {
kv = append(kv, "")
}
k = kv[0]
v = kv[1]
envMap[k] = v
}
return
}
// nativizeEnvMap returns a native-typed env map from a string version.
func nativizeEnvMap(stringMap map[string]string) (envMap map[string]interface{}) {
var pathVar string = internal.GetPathEnvName()
var err error
envMap = make(map[string]interface{}, 0)
for k, v := range stringMap {
// Check for PATH/Path - we handle this uniquely.
if k == pathVar {
if envMap[k], err = GetPathEnv(); err != nil {
envMap[k] = v
err = nil
}
continue
}
// It might be...
// a float
if reMaybeFloat.MatchString(v) {
if envMap[k], err = strconv.ParseFloat(v, 64); err == nil {
continue
}
err = nil
}
// an int
if reMaybeInt.MatchString(v) {
if envMap[k], err = strconv.Atoi(v); err == nil {
continue
}
err = nil
}
// a uint
if envMap[k], err = strconv.ParseUint(v, 10, 64); err == nil {
continue
} else {
err = nil
}
// a boolean
if envMap[k], err = strconv.ParseBool(v); err == nil {
continue
} else {
err = nil
}
// ok so... guess it's a string, then.
envMap[k] = v
}
return
}

View File

@@ -8,3 +8,9 @@ var (
ErrBadType error = errors.New("a bad type was passed")
ErrNilPtr error = errors.New("a nil pointer was passed")
)
var (
ErrHighPid error = errors.New("a provided PID exceeds the possible maximum")
// ErrInvalidNs indicates an invalid Linux namespace identifier format.
ErrInvalidNs error = errors.New("invalid namespace identifier")
)

View File

@@ -118,6 +118,7 @@ func unsetAttrs(f *os.File, attrs fsAttr) (err error) {
}
ab = bitmask.MaskBit(curAttrs)
// TODO: Should this be IsOneOf instad of HasFlag?
if !ab.HasFlag(bitmask.MaskBit(attrs)) {
return
}

32
funcs.go Normal file
View File

@@ -0,0 +1,32 @@
package sysutils
import (
"strconv"
"strings"
"r00t2.io/sysutils/errs"
)
// NsToInode splits a Linux namespace identifier (e.g. `net:[12345]`) to its type (e.g. `net`) and inode (e.g. `12345`).
func NsToInode(ns string) (typ string, inode uint64, err error) {
var fields []string
fields = strings.SplitN(ns, ":", 2)
if len(fields) != 2 {
err = errs.ErrInvalidNs
return
}
fields[1] = strings.TrimPrefix(fields[1], "[")
fields[1] = strings.TrimSuffix(fields[1], "]")
if inode, err = strconv.ParseUint(fields[1], 10, 64); err != nil {
return
}
typ = fields[0]
return
}

View File

@@ -1,3 +1,5 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
package sysutils
// Checked consolidates all the provided checked functions.

View File

@@ -1,50 +0,0 @@
package sysutils
import (
`fmt`
`os`
`golang.org/x/sys/unix`
`r00t2.io/sysutils/envs`
)
// GetIDState returns current ID/elevation information. An IDState should *not* be explicitly created/defined.
func GetIDState() (ids IDState) {
var err error
ids.RUID, ids.EUID, ids.SUID = unix.Getresuid()
ids.uidsChecked = true
ids.RGID, ids.EGID, ids.SGID = unix.Getresgid()
ids.gidsChecked = true
ids.SudoEnvCmd = envs.HasEnv("SUDO_COMMAND")
ids.SudoEnvHome = envs.HasEnv("SUDO_HOME")
ids.SudoEnvGroup = envs.HasEnv("SUDO_GID")
ids.SudoEnvUser = envs.HasEnv("SUDO_UID") || envs.HasEnv("SUDO_USER")
if ids.SudoEnvCmd || ids.SudoEnvHome || ids.SudoEnvGroup || ids.SudoEnvUser {
ids.SudoEnvVars = true
}
ids.sudoChecked = true
// PID 1 will *always* be root, so that can return a false positive for sudo.
if os.Getppid() != 1 {
ids.stat = new(unix.Stat_t)
if err = unix.Stat(
fmt.Sprintf("/proc/%d/stat", os.Getppid()),
ids.stat,
); err != nil {
err = nil
} else {
ids.PPIDUidMatch = ids.RUID == int(ids.stat.Uid)
ids.ppidUidChecked = true
ids.PPIDGidMatch = ids.RGID == int(ids.stat.Gid)
ids.ppidGidChecked = true
}
} else {
ids.ppidUidChecked = true
ids.ppidGidChecked = true
}
return
}

158
funcs_nix.go Normal file
View File

@@ -0,0 +1,158 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
package sysutils
import (
"fmt"
`math`
"os"
`github.com/shirou/gopsutil/v3/process`
"golang.org/x/sys/unix"
"r00t2.io/sysutils/envs"
`r00t2.io/sysutils/errs`
`r00t2.io/sysutils/internal`
)
// GetIDState returns current ID/elevation information. An IDState should *not* be explicitly created/defined.
func GetIDState() (ids IDState) {
var err error
ids.RUID, ids.EUID, ids.SUID = unix.Getresuid()
ids.uidsChecked = true
ids.RGID, ids.EGID, ids.SGID = unix.Getresgid()
ids.gidsChecked = true
ids.SudoEnvCmd = envs.HasEnv(envSudoCmd)
ids.SudoEnvHome = envs.HasEnv(envSudoHome)
ids.SudoEnvGroup = envs.HasEnv(envSudoGrp)
ids.SudoEnvUser = envs.HasEnv(envSudoUid) || envs.HasEnv(envSudoUname)
if ids.SudoEnvCmd || ids.SudoEnvHome || ids.SudoEnvGroup || ids.SudoEnvUser {
ids.SudoEnvVars = true
}
ids.sudoChecked = true
// PID 1 will *always* be root, so that can return a false positive for sudo.
if os.Getppid() != 1 {
ids.stat = new(unix.Stat_t)
if err = unix.Stat(
fmt.Sprintf("/proc/%d/stat", os.Getppid()),
ids.stat,
); err != nil {
err = nil
} else {
ids.PPIDUidMatch = ids.RUID == int(ids.stat.Uid)
ids.ppidUidChecked = true
ids.PPIDGidMatch = ids.RGID == int(ids.stat.Gid)
ids.ppidGidChecked = true
}
} else {
ids.ppidUidChecked = true
ids.ppidGidChecked = true
}
return
}
// GetIDStateProc is like GetIDState but for an arbitrary PID.
func GetIDStateProc(pid uint32) (ids IDState, err error) {
var i32 int32
var ints []int32
var sudoUid bool
var sudoUname bool
var proc *process.Process
var envMap map[string]string
if pid > math.MaxInt32 {
err = errs.ErrHighPid
return
}
ids = IDState{
RUID: -1,
EUID: -1,
SUID: -1,
RGID: -1,
EGID: -1,
SGID: -1,
}
if proc, err = process.NewProcess(int32(pid)); err != nil {
return
}
// UIDs
if ints, err = proc.Uids(); err != nil {
return
}
if ints != nil && len(ints) > 0 {
if len(ints) >= 3 {
ids.SUID = int(ints[2])
}
if len(ints) >= 2 {
ids.EUID = int(ints[1])
}
if len(ints) >= 1 {
ids.RUID = int(ints[0])
}
}
ids.uidsChecked = true
// GIDs
if ints, err = proc.Gids(); err != nil {
return
}
if ints != nil && len(ints) > 0 {
if len(ints) >= 3 {
ids.SGID = int(ints[2])
}
if len(ints) >= 2 {
ids.EGID = int(ints[1])
}
if len(ints) >= 1 {
ids.SGID = int(ints[0])
}
}
ids.gidsChecked = true
// Sudo (env check)
if envMap, err = internal.GetPidEnvMap(uint32(pid)); err != nil {
return
}
_, ids.SudoEnvCmd = envMap[envSudoCmd]
_, ids.SudoEnvHome = envMap[envSudoHome]
_, ids.SudoEnvGroup = envMap[envSudoGrp]
_, sudoUid = envMap[envSudoUid]
_, sudoUname = envMap[envSudoUname]
ids.SudoEnvUser = sudoUid || sudoUname
if ids.SudoEnvCmd || ids.SudoEnvHome || ids.SudoEnvGroup || ids.SudoEnvUser {
ids.SudoEnvVars = true
}
ids.sudoChecked = true
// Sudo (PPID check)
if i32, err = proc.Ppid(); err != nil {
return
}
if i32 != 1 {
ids.stat = new(unix.Stat_t)
if err = unix.Stat(
fmt.Sprintf("/proc/%d/stat", i32),
ids.stat,
); err != nil {
err = nil
} else {
ids.PPIDUidMatch = ids.RUID == int(ids.stat.Uid)
ids.ppidUidChecked = true
ids.PPIDGidMatch = ids.SGID == int(ids.stat.Gid)
ids.ppidGidChecked = true
}
} else {
ids.ppidUidChecked = true
ids.ppidGidChecked = true
}
return
}

16
go.mod
View File

@@ -1,23 +1,25 @@
module r00t2.io/sysutils
go 1.23.2
go 1.24.5
require (
github.com/davecgh/go-spew v1.1.1
github.com/djherbis/times v1.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/shirou/gopsutil/v4 v4.25.6
golang.org/x/sync v0.16.0
golang.org/x/sys v0.34.0
github.com/shirou/gopsutil/v3 v3.24.5
github.com/shirou/gopsutil/v4 v4.25.10
golang.org/x/sync v0.17.0
golang.org/x/sys v0.37.0
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8
r00t2.io/goutils v1.9.0
r00t2.io/goutils v1.10.3
)
require (
github.com/ebitengine/purego v0.8.4 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shoenig/go-m1cpu v0.1.7 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect

46
go.sum
View File

@@ -1,10 +1,9 @@
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@@ -12,44 +11,39 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA=
github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM=
github.com/shoenig/go-m1cpu v0.1.7 h1:C76Yd0ObKR82W4vhfjZiCp0HxcSZ8Nqd84v+HZ0qyI0=
github.com/shoenig/go-m1cpu v0.1.7/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=
github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8 h1:FW42yWB1sGClqswyHIB68wo0+oPrav1IuQ+Tdy8Qp8E=
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8/go.mod h1:44w9OfBSQ9l3o59rc2w3AnABtE44bmtNnRMNC7z+oKE=
r00t2.io/goutils v1.8.1 h1:TQcUycPKsYn0QI4uCqb56utmvu/vVSxlblBg98iXStg=
r00t2.io/goutils v1.8.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
r00t2.io/goutils v1.9.0 h1:iEwa9LinCzabpTD03/2oUrFE3QinxszTzL48pBV9cD4=
r00t2.io/goutils v1.9.0/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=
r00t2.io/goutils v1.10.3 h1:GmEtsM/nrrVWooYJllXDRsgInobEinv2dn5ccU4zGAA=
r00t2.io/goutils v1.10.3/go.mod h1:76AxpXUeL10uFklxRB11kQsrtj2AKiNm8AwG1bNoBCA=

View File

@@ -1,8 +1,18 @@
package internal
import (
`regexp`
)
// OS-specific path environment variable name. The default is "PATH".
var (
pathEnvVarName map[string]string = map[string]string{
"windows": "Path",
}
)
// Compiled regex patterns.
var (
reMaybeInt *regexp.Regexp = regexp.MustCompile(`^(?P<sign>\+|-)[0-9]+$`)
reMaybeFloat *regexp.Regexp = regexp.MustCompile(`(?P<sign>\+|-)?[0-9]+\.[0-9]+$`)
)

160
internal/funcs.go Normal file
View File

@@ -0,0 +1,160 @@
package internal
import (
`bytes`
`errors`
`fmt`
`os`
"runtime"
"strconv"
"strings"
`r00t2.io/sysutils/paths`
)
// EnvListToMap splits a []string of env var keypairs to a map.
func EnvListToMap(envs []string) (envMap map[string]string) {
var kv []string
var k, v string
envMap = make(map[string]string)
for _, ev := range envs {
kv = strings.SplitN(ev, "=", 2)
// I *think* SplitN does this for me, but...
if len(kv) == 1 {
kv = append(kv, "")
}
k = kv[0]
v = kv[1]
envMap[k] = v
}
return
}
// GetPathEnv returns a slice of the PATH variable's items.
func GetPathEnv() (pathList []string, err error) {
var pathVar string = GetPathEnvName()
pathList = make([]string, 0)
for _, p := range strings.Split(os.Getenv(pathVar), string(os.PathListSeparator)) {
if err = paths.RealPath(&p); err != nil {
return
}
pathList = append(pathList, p)
}
return
}
/*
GetPidEnvMap will only work on *NIX-like systems with procfs.
It gets the environment variables of a given process' PID.
*/
func GetPidEnvMap(pid uint32) (envMap map[string]string, err error) {
var envBytes []byte
var envList []string
var envArr [][]byte
var procPath string
var exists bool
envMap = make(map[string]string)
procPath = fmt.Sprintf("/proc/%v/environ", pid)
if exists, err = paths.RealPathExists(&procPath); err != nil {
return
}
if !exists {
err = errors.New(fmt.Sprintf("information for pid %v does not exist", pid))
return
}
if envBytes, err = os.ReadFile(procPath); err != nil {
return
}
envArr = bytes.Split(envBytes, []byte{0x0})
envList = make([]string, len(envArr))
for idx, b := range envArr {
envList[idx] = string(b)
}
envMap = EnvListToMap(envList)
return
}
// GetPathEnvName gets the OS-specific path environment variable name.
func GetPathEnvName() (envVarName string) {
var ok bool
if envVarName, ok = pathEnvVarName[runtime.GOOS]; !ok {
// *NIX/the default.
envVarName = "PATH"
}
return
}
// NativizeEnvMap returns a native-typed env map from a string version.
func NativizeEnvMap(stringMap map[string]string) (envMap map[string]interface{}) {
var pathVar string = GetPathEnvName()
var err error
envMap = make(map[string]interface{})
for k, v := range stringMap {
// Check for PATH/Path - we handle this uniquely.
if k == pathVar {
if envMap[k], err = GetPathEnv(); err != nil {
envMap[k] = v
err = nil
}
continue
}
// It might be...
// a float
if reMaybeFloat.MatchString(v) {
if envMap[k], err = strconv.ParseFloat(v, 64); err == nil {
continue
}
err = nil
}
// an int
if reMaybeInt.MatchString(v) {
if envMap[k], err = strconv.Atoi(v); err == nil {
continue
}
err = nil
}
// a uint
if envMap[k], err = strconv.ParseUint(v, 10, 64); err == nil {
continue
} else {
err = nil
}
// a boolean
if envMap[k], err = strconv.ParseBool(v); err == nil {
continue
} else {
err = nil
}
// ok so... guess it's a string, then.
envMap[k] = v
}
return
}

View File

@@ -1,18 +0,0 @@
package internal
import (
`runtime`
)
// GetPathEnvName gets the OS-specific path environment variable name.
func GetPathEnvName() (envVarName string) {
var ok bool
if envVarName, ok = pathEnvVarName[runtime.GOOS]; !ok {
// *NIX/the default.
envVarName = "PATH"
}
return
}

View File

@@ -10,7 +10,6 @@ const (
// Mostly just for reference.
const (
// ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeCharDevice | ModeIrregular
modeDir pathMode = pathMode(fs.ModeDir)
modeSymlink pathMode = pathMode(fs.ModeSymlink)
modePipe pathMode = pathMode(fs.ModeNamedPipe)

17
paths/consts_unix.go Normal file
View File

@@ -0,0 +1,17 @@
//go:build !windows
package paths
const (
/*
MaxSymlinkLevel is hardcoded into the kernel for macOS, BSDs and Linux. It's unlikely to change.
Thankfully, it's the same on all of them.
On all, it's defined as MAXSYMLINKS in the following headers:
macOS (no, macOS is not a BSD; no, it is not FreeBSD; yes, I *will* fight you on it and win): sys/param.h
BSDs: sys/sys/param.h
Linux: include/linux/namei.h
*/
MaxSymlinkLevel uint = 40
)

15
paths/consts_windows.go Normal file
View File

@@ -0,0 +1,15 @@
//go:build windows
package paths
const (
/*
MaxSymLinkLevel on Windows is weird; Microsoft calls them "reparse points".
And it changes on the Windows version you're on, but it's been 63 past Windows Server 2003/Windows XP.
They're *very* EOL, so I'm completely ignoring earlier cases.
https://learn.microsoft.com/en-us/windows/win32/fileio/symbolic-link-programming-consideration
*/
MaxSymlinkLevel uint = 63
)

View File

@@ -1,10 +1,12 @@
package paths
import (
`errors`
"errors"
"fmt"
)
var (
ErrMaxSymlinkLevel = fmt.Errorf("max symlink level met/exceeded")
ErrNilErrChan error = errors.New("an initialized error channel is required")
ErrNilMatchChan error = errors.New("an initialized matches channel is required")
ErrNilMismatchChan error = errors.New("an initialized mismatches channel is required")

View File

@@ -93,7 +93,7 @@ func ExpandHome(p *string) (err error) {
}
/*
GetFirst is the file equivalent of envs.GetFirst.
GetFirst is the file equivalent of [r00t2.io/sysutils/envs.GetFirst].
It iterates through paths, normalizing them along the way
(so abstracted paths such as ~/foo/bar.txt and relative paths
@@ -107,7 +107,7 @@ If no path exists, ok will be false.
As always, results are not guaranteed due to permissions, etc.
potentially returning an inaccurate result.
This is a thin wrapper around GetFirstWithRef.
This is a thin wrapper around [GetFirstWithRef].
*/
func GetFirst(p []string) (content []byte, isDir, ok bool) {
@@ -117,9 +117,9 @@ func GetFirst(p []string) (content []byte, isDir, ok bool) {
}
/*
GetFirstWithRef is the file equivalent of envs.GetFirstWithRef.
GetFirstWithRef is the file equivalent of [r00t2.io/sysutils/envs.GetFirstWithRef].
It behaves exactly like GetFirst, but with an additional returned value, idx,
It behaves exactly like [GetFirst], but with an additional returned value, idx,
which specifies the index in p in which a path was found.
As always, results are not guaranteed due to permissions, etc.
@@ -129,7 +129,7 @@ func GetFirstWithRef(p []string) (content []byte, isDir, ok bool, idx int) {
var locPaths []string
var exists bool
var stat os.FileInfo
var stat fs.FileInfo
var err error
idx = -1
@@ -162,9 +162,9 @@ func GetFirstWithRef(p []string) (content []byte, isDir, ok bool, idx int) {
}
/*
Len returns the number of path segments in p, as split with the same param signature to Segment.
Len returns the number of path segments in p, as split with the same param signature to [Segment].
See Segment for details on abs and strict.
See [Segment] for details on abs and strict.
*/
func Len(p string, abs, strict bool) (segments int) {
@@ -174,9 +174,9 @@ func Len(p string, abs, strict bool) (segments int) {
}
/*
LenSys returns the number of path segments in p, as split with the same param signature to SegmentSys.
LenSys returns the number of path segments in p, as split with the same param signature to [SegmentSys].
See Segment for details on abs and strict.
See [Segment] for details on abs and strict.
*/
func LenSys(p string, abs, strict bool) (segments int) {
@@ -188,13 +188,13 @@ func LenSys(p string, abs, strict bool) (segments int) {
/*
MakeDirIfNotExist will create a directory at a given path if it doesn't exist.
See also the documentation for RealPath.
See also the documentation for [RealPath].
This is a bit more sane option than os.MkdirAll as it will normalize paths a little better.
This is a bit more sane option than [os.MkdirAll] as it will normalize paths a little better.
*/
func MakeDirIfNotExist(p string) (err error) {
var stat os.FileInfo
var stat fs.FileInfo
var exists bool
var locPath string = p
@@ -234,7 +234,9 @@ path syntax/string itself is not supported on the runtime OS. This can be done v
if errors.Is(err, fs.ErrInvalid) {...}
RealPath is simply a wrapper around ExpandHome(path) and filepath.Abs(*path).
RealPath is simply a wrapper around [ExpandHome] and [filepath.Abs].
Note that RealPath does *not* resolve symlinks. Only [RealPathExistsStatTarget] does that.
*/
func RealPath(p *string) (err error) {
@@ -250,7 +252,7 @@ func RealPath(p *string) (err error) {
}
/*
RealPathJoin combines RealPath with (path).Join.
RealPathJoin combines [RealPath] with [path.Join].
If dst is nil, then p will be updated with the new value.
You probably don't want that.
@@ -281,7 +283,7 @@ func RealPathJoin(p, dst *string, subPaths ...string) (err error) {
}
/*
RealPathJoinSys combines RealPath with (path/filepath).Join.
RealPathJoinSys combines [RealPath] with [path/filepath.Join].
If dst is nil, then path will be updated with the new value.
You probably don't want that.
@@ -312,10 +314,10 @@ func RealPathJoinSys(p, dst *string, subPaths ...string) (err error) {
}
/*
RealPathExists is like RealPath, but will also return a boolean as to whether the path
RealPathExists is like [RealPath], but will also return a boolean as to whether the path
actually exists or not.
Note that err *may* be os.ErrPermission/fs.ErrPermission, in which case the exists value
Note that err *may* be [os.ErrPermission] / [fs.ErrPermission], in which case the exists value
cannot be trusted as a permission error occurred when trying to stat the path - if the
calling user/process does not have read permission on e.g. a parent directory, then
exists may be false but the path may actually exist. This condition can be checked via
@@ -323,9 +325,9 @@ via:
if errors.Is(err, fs.ErrPermission) {...}
See also the documentation for RealPath.
See also the documentation for [RealPath].
In those cases, it may be preferable to use RealPathExistsStat and checking stat for nil.
In those cases, it may be preferable to use [RealPathExistsStat] and checking stat for nil.
*/
func RealPathExists(p *string) (exists bool, err error) {
@@ -346,18 +348,22 @@ func RealPathExists(p *string) (exists bool, err error) {
}
/*
RealPathExistsStat is like RealPathExists except it will also return the os.FileInfo
RealPathExistsStat is like [RealPathExists] except it will also return the [io/fs.FileInfo]
for the path (assuming it exists).
If stat is nil, it is highly recommended to check err via the methods suggested
in the documentation for RealPath and RealPathExists.
in the documentation for [RealPath] and [RealPathExists].
*/
func RealPathExistsStat(p *string) (exists bool, stat os.FileInfo, err error) {
func RealPathExistsStat(p *string) (exists bool, stat fs.FileInfo, err error) {
if exists, err = RealPathExists(p); err != nil {
return
}
if !exists {
return
}
if stat, err = os.Stat(*p); err != nil {
return
}
@@ -365,7 +371,49 @@ func RealPathExistsStat(p *string) (exists bool, stat os.FileInfo, err error) {
return
}
// SearchFsPaths gets a file/directory/etc. path list based on the provided criteria.
/*
RealPathExistsStatTarget is the only "RealPather" that will resolve p to the (final) *target* of p if p is a symlink.
If p is not a symlink but does exist, the tgt* will reflect the same as p*.
See [WalkLink] for details on relRoot and other assorted rules/logic (RealPathExistsStatTarget wraps [WalkLink]).
*/
func RealPathExistsStatTarget(p *string, relRoot string) (pExists, tgtExists, wasLink bool, pStat fs.FileInfo, tgtStat fs.FileInfo, err error) {
var tgts []string
if pExists, err = RealPathExists(p); err != nil {
return
}
tgtExists = pExists
if !pExists {
return
}
// Can't use RealPathExistsStat because it calls os.Stat, not os.Lstat... thus defeating the purpose.
if pStat, err = os.Lstat(*p); err != nil {
return
}
tgtStat = pStat
wasLink = pStat.Mode().Type()&fs.ModeSymlink == fs.ModeSymlink
if wasLink {
if tgts, err = WalkLink(*p, relRoot); err != nil || tgts == nil || len(tgts) == 0 {
tgtExists = false
tgtStat = nil
return
}
if tgtExists, tgtStat, err = RealPathExistsStat(&tgts[len(tgts)-1]); err != nil {
return
}
*p = tgts[len(tgts)-1]
}
return
}
// SearchFsPaths gets a file/directory/etc. path list based on the provided [FsSearchCriteria].
func SearchFsPaths(matcher FsSearchCriteria) (found, miss []*FsSearchResult, err error) {
var matched *FsSearchResult
@@ -417,11 +465,11 @@ func SearchFsPaths(matcher FsSearchCriteria) (found, miss []*FsSearchResult, err
}
/*
SearchFsPathsAsync is exactly like SearchFsPaths, but dispatches off concurrent
SearchFsPathsAsync is exactly like [SearchFsPaths], but dispatches off concurrent
workers for the filtering logic instead of performing iteratively/recursively.
It may, in some cases, be *slightly more* performant and *slightly less* in others.
Note that unlike SearchFsPaths, the results written to the
FsSearchCriteriaAsync.ResChan are not guaranteed to be in any predictable order.
Note that unlike [SearchFsPaths], the results written to the
[FsSearchCriteriaAsync].ResChan are not guaranteed to be in any predictable order.
All channels are expected to have already been initialized by the caller.
They will not be closed by this function.
@@ -526,7 +574,7 @@ func SearchFsPathsAsync(matcher FsSearchCriteriaAsync) {
}
/*
Segment returns path p's segments as a slice of strings, using GenericSeparator as a separator.
Segment returns path p's segments as a slice of strings, using [GenericSeparator] as a separator.
If abs is true, the placeholder leading prefix(es) (if any) of GenericSeparator will be kept in-place;
otherwise it/they will be trimmed out.
@@ -535,14 +583,14 @@ e.g.:
abs == true: //foo/bar/baz => []string{"", "", "foo", "bar", "baz"}
abs == false: /foo/bar/baz => []string{"foo", "bar", "baz"}
If strict is true, any trailing GenericSeparator will be kept in-place;
If strict is true, any trailing [GenericSeparator] will be kept in-place;
otherwise they will be trimmed out.
e.g. (assuming abs == false):
strict == true: /foo/bar/baz// => []string{"foo", "bar", "baz", "", ""}
strict == false: /foo/bar/baz/ => []string{"foo", "bar", "baz"}
It is recommended to call RealPath for path's ptr first for normalization.
It is recommended to call [RealPath] for path's ptr first for normalization.
*/
func Segment(p string, abs, strict bool) (segments []string) {
@@ -558,7 +606,7 @@ func Segment(p string, abs, strict bool) (segments []string) {
return
}
// SegmentSys is exactly like Segment, except using os.PathSeparator instead of GenericSeparator.
// SegmentSys is exactly like [Segment], except using [os.PathSeparator] instead of [GenericSeparator].
func SegmentSys(p string, abs, strict bool) (segments []string) {
if !abs {
@@ -574,14 +622,17 @@ func SegmentSys(p string, abs, strict bool) (segments []string) {
}
/*
Strip is like Segment but trims out the leading n number of segments and reassembles the path using path.Join.
Strip is like [Segment] but trims out the leading n number of segments and reassembles the path using [path.Join].
n may be negative, in which case the *trailing* n number of segments will be trimmed out.
(i.e. n == -1, p == `foo/bar/baz/quux` would be `foo/bar/baz`, not `bar/baz/quux`)
If you require more traditional slicing (e.g. with interval),
you may want to use path.Join with a sliced result of Segment instead.
e.g.: *only* the *last* n segments: path.Join(Segment(p, ...)[Len(p, ...)-n:]...)
you may want to use [path.Join] with a sliced result of [Segment] instead.
e.g.: *only* the *last* n segments:
path.Join(Segment(p, ...)[Len(p, ...)-n:]...)
If n == 0 or int(math.Abs(float64(n))) >= len(Segment(p, ...)), no transformation will be done.
@@ -616,7 +667,7 @@ func Strip(p string, abs, strict bool, n int) (slicedPath string) {
return
}
// StripSys is exactly like Strip but using (path/filepath).Join and SegmentSys.
// StripSys is exactly like [Strip] but using [path/filepath.Join] and [SegmentSys].
func StripSys(p string, abs, strict bool, n int) (slicedPath string) {
var pLen int
@@ -644,7 +695,81 @@ func StripSys(p string, abs, strict bool, n int) (slicedPath string) {
}
/*
filterTimes checks a times.Timespec of a file using:
WalkLink walks the recursive target(s) of lnk (unless/until [MaxSymlinkLevel] is hit, which will trigger [ErrMaxSymlinkLevel])
until it reaches a real (non-symlink) target.
lnk will have RealPath called on it first.
If lnk is not a symlink, then tgts == []string{lnk} and err = nil.
A broken link will return [io/fs.ErrNotExist], with tgts containing the targets up to and including the path that triggered the error.
If lnk itself does not exist, tgts will be nil and err will be that of [io/fs.ErrNotExist].
relRoot is a root directory to resolve relative links to. If empty, relative link target `t` from link `l` will be treated
as relative to [path/filepath.Dir] on l (that is to say, `t = filepath.Join(filepath.Dir(l), os.Readlink(l))`).
*/
func WalkLink(lnk, relRoot string) (tgts []string, err error) {
var exists bool
var curDepth uint
var stat fs.FileInfo
var curTgt string
var prevTgt string
if exists, err = RealPathExists(&lnk); err != nil {
return
} else if !exists {
err = fs.ErrNotExist
return
}
if relRoot != "" {
if err = RealPath(&relRoot); err != nil {
return
}
}
tgts = []string{}
curTgt = lnk
for curDepth = 0; curDepth < MaxSymlinkLevel; curDepth++ {
if exists, err = RealPathExists(&curTgt); err != nil {
return
}
prevTgt = curTgt
tgts = append(tgts, curTgt)
if !exists {
err = fs.ErrNotExist
return
}
if stat, err = os.Lstat(curTgt); err != nil {
return
}
if stat.Mode().Type()&os.ModeSymlink != os.ModeSymlink {
break
}
if curTgt, err = os.Readlink(curTgt); err != nil {
return
}
if !filepath.IsAbs(curTgt) {
if relRoot != "" {
curTgt = filepath.Join(relRoot, curTgt)
} else {
curTgt = filepath.Join(filepath.Dir(prevTgt), curTgt)
}
}
}
if curDepth >= MaxSymlinkLevel {
err = ErrMaxSymlinkLevel
return
}
return
}
/*
filterTimes checks a [times.Timespec] of a file using:
- an age specified by the caller
- an ageType bitmask for types of times to compare
- an olderThan bool (if false, the file must be younger than)

View File

@@ -11,13 +11,15 @@ import (
)
/*
Match returns match (a ptr to a FsSearchResult if the specified path matches, otherwise nil),
miss (ptr the specified path does not match, otherwise nil), and an fs.DirEntry and fs.FileInfo
for path. d and/or fi may be nil.
Match returns match (a ptr to a [FsSearchResult] if the specified path matches, otherwise nil),
miss (ptr the specified path does not match, otherwise nil), and an [io/fs.DirEntry] and [io/fs.FileInfo]
for path.
d and/or fi may be nil.
If err is not nil, it represents an unexpected error and as such, both match and miss should be nil.
Match, miss, and err will all be nil if the filesystem object/path does not exist.
match, miss, and err will all be nil if the filesystem object/path does not exist.
*/
func (f *FsSearchCriteria) Match(path string, d fs.DirEntry, fi fs.FileInfo) (match, miss *FsSearchResult, err error) {
@@ -111,7 +113,7 @@ func (f *FsSearchCriteria) Match(path string, d fs.DirEntry, fi fs.FileInfo) (ma
miss = &m
return
} else if typeMode != 0 {
if !typeFilter.HasFlag(bitmask.MaskBit(typeMode)) {
if !typeFilter.IsOneOf(bitmask.MaskBit(typeMode)) {
m.MissReason = MissType
miss = &m
return

View File

@@ -4,7 +4,7 @@ import (
`r00t2.io/goutils/bitmask`
)
// Mask returns a bitmask.MaskBit from a pathTimeType.
// Mask returns a [r00t2.io/goutils/bitmask.MaskBit] from a pathTimeType.
func (p *pathTimeType) Mask() (mask *bitmask.MaskBit) {
mask = bitmask.NewMaskBitExplicit(uint(*p))

View File

@@ -109,7 +109,7 @@ type FsSearchCriteriaAsync struct {
SemaphoreCtx context.Context
}
// FsSearchResult contains a match/miss result for FsSearchCriteria and FsSearchCriteriaAsync.
// FsSearchResult contains a match/miss result for [FsSearchCriteria] and [FsSearchCriteriaAsync].
type FsSearchResult struct {
/*
Path is the path to the object on the filesystem.

27
pdsh/docs.go Normal file
View File

@@ -0,0 +1,27 @@
/*
Package pdsh (!! WIP !!) provides PDSH-compatible functionality for parsing group/gender/etc. files/sources.
Note that this library will *only* generate the host list/etc.,
it will not actually connect to anything.
It simply provides ways of returning lists of hosts using generation rules/patterns.
Said another way, it does not implement any of PDSH's "rcmd" modules, only the "misc" modules.
(As a hint, you can implement SSH connections via [golang.org/x/crypto/ssh] in goroutine'd functions
using this package to generate the target addresses, etc.)
Currently, the only supported PDSH module is misc/dshgroup (as [r00t2.io/sysutils/pdsh/dshgroup]) but additional/all other
host list modules are planned.
This package deviates slightly from PDSH in some areas; allowing for more loose or more strict behavior occasionally.
Whenever a deviation is offered, this package allows for configuring the generator to behave exactly like PDSH instead
(if the deviating behavior is enabled by default).
For details, see the [chaos/pdsh GitHub], the associated [MAN page source], and/or the [rendered MAN page] (via ManKier).
You may also want to see the ManKier rendered MAN pages for the [pdsh package].
[chaos/pdsh GitHub]: https://github.com/chaos/pdsh/
[MAN page source]: https://github.com/chaos/pdsh/blob/master/doc/pdsh.1.in
[rendered MAN page]: https://www.mankier.com/1/pdsh
[pdsh package]: https://www.mankier.com/package/pdsh
*/
package pdsh

18
pdsh/dshgroup/consts.go Normal file
View File

@@ -0,0 +1,18 @@
package dshgroup
import (
"regexp"
"r00t2.io/goutils/remap"
)
const (
dshGrpPathEnv string = "DSHGROUP_PATH"
)
// DSH Groups
var (
dshGrpDefGrpDir string = "/etc/dsh/group"
dshGrpInclPtrn *remap.ReMap = &remap.ReMap{Regexp: regexp.MustCompile(`^\s*#include\s+(?P<incl>.+)$`)}
dshGrpSubTokenPtrn *remap.ReMap = &remap.ReMap{Regexp: regexp.MustCompile(`^(?P<start_pad>0+)?(?P<start>[0-9]+)(-(?P<end_pad>0+)?(?P<end>[0-9]+))?$`)}
)

30
pdsh/dshgroup/docs.go Normal file
View File

@@ -0,0 +1,30 @@
/*
Package dshgroup implements so-called "DSH (Dancer's SHell) Group" files.
It is equivalent to PDSH's [misc/dshgroup] module. ([source])
Be sure to read the [HOSTLIST EXPRESSIONS] section in the MAN page.
# Notable Differences
* This package allows for *never* reading the DSHGROUP_PATH env var (PDSH always reads it) via the "NoEnv" option.
* This package allows for not adding /etc/dsh/group/<group> files by default via the "NoDefault" option.
* This package allows for not adding ~/.dsh/group/<group> files by default via the "NoHome" option.
* This package allows for a "ForceLegacy" mode, disabled by default, that DISABLES the PDSH
extension for "#include <path/group>" extension.
If ForceLegacy is enabled, "#include ..." lines will be treated as comment lines (ignored) instead.
* This package allows for whitespace between group patterns. This can be disabled by the "StrictWhitespace" option.
# TODO/WIP/Not Yet Implemented
This package is not yet complete.
[misc/dshgroup]: https://www.mankier.com/1/pdsh#dshgroup_module_options
[source]: https://github.com/chaos/pdsh/blob/master/src/modules/dshgroup.c
[HOSTLIST EXPRESSIONS]: https://www.mankier.com/1/pdsh#Hostlist_Expressions
*/
package dshgroup

11
pdsh/dshgroup/errs.go Normal file
View File

@@ -0,0 +1,11 @@
package dshgroup
import (
"errors"
)
var (
ErrEmptyDshGroupTok error = errors.New("empty dsh group pattern token")
ErrInvalidDshGrpSyntax error = errors.New("invalid dsh group file syntax")
ErrInvalidDshGrpPtrn error = errors.New("invalid dsh group pattern syntax")
)

View File

@@ -0,0 +1,176 @@
package dshgroup
import (
"io/fs"
"os"
"path/filepath"
"strings"
"r00t2.io/sysutils/envs"
"r00t2.io/sysutils/paths"
)
/*
Evaluate returns a list of directories and files that would be searched/read with
the given call and DshGroupLister configuration, in order of parsing.
The behavior is the same as DshGroupLister.GroupedHosts, including searchPaths.
If DshGroupLister.ForceLegacy is false, include files will also be parsed in.
(This may incur slightly additional processing time.)
Only existing dirs/files are returned. Symlinks are evaluated to their target.
If dedupe is true, deduplication is performed. This adds some cycles, but may be desired if you make heavy use of symlinks.
*/
func (d *DshGroupLister) Evaluate(dedupe bool, searchPaths ...string) (dirs, files []string, err error) {
var exists bool
// var u *user.User
var spl []string
var dPath string
var fPath string
var incls []string
var de fs.DirEntry
var stat fs.FileInfo
var entries []fs.DirEntry
var tmpF []string
var fpathMap map[string]bool = make(map[string]bool)
// TODO: Does/how does pdsh resolve relative symlinks?
// Dirs first
if searchPaths != nil {
for _, dPath = range searchPaths {
if _, exists, _, _, stat, err = paths.RealPathExistsStatTarget(&dPath, "."); err != nil {
return
} else if !exists {
continue
}
if !stat.IsDir() {
continue
}
dirs = append(dirs, dPath)
}
}
if !d.NoHome && envs.HasEnv("HOME") {
// So pdsh actually checks $HOME, it doesn't pull the homedir for the user.
/*
if u, err = user.Current(); err != nil {
return
}
dPath = filepath.Join(u.HomeDir, ".dsh", "group")
*/
dPath = filepath.Join(os.Getenv("HOME"), ".dsh", "group")
if _, exists, _, _, stat, err = paths.RealPathExistsStatTarget(&dPath, "."); err != nil {
return
} else if exists {
if stat.IsDir() {
dirs = append(dirs, dPath)
}
}
}
if !d.NoEnv && envs.HasEnv(dshGrpPathEnv) {
spl = strings.Split(os.Getenv(dshGrpPathEnv), string(os.PathListSeparator))
for _, dPath = range spl {
if strings.TrimSpace(dPath) == "" {
continue
}
if _, exists, _, _, stat, err = paths.RealPathExistsStatTarget(&dPath, "."); err != nil {
return
} else if !exists {
continue
}
if !stat.IsDir() {
continue
}
dirs = append(dirs, dPath)
}
}
if !d.NoDefault && !envs.HasEnv(dshGrpPathEnv) {
dPath = dshGrpDefGrpDir
if _, exists, _, _, stat, err = paths.RealPathExistsStatTarget(&dPath, "."); err != nil {
return
} else if exists {
if stat.IsDir() {
dirs = append(dirs, dPath)
}
}
}
// Then files. Do *not* walk the dirs; only first-level is parsed by pdsh so this does the same.
for _, dPath = range dirs {
if entries, err = os.ReadDir(dPath); err != nil {
return
}
for _, de = range entries {
fPath = filepath.Join(dPath, de.Name())
// NORMALLY, os.Stat calls stat(2), which follows symlinks. (os.Lstat()/lstat(2) does not.)
// But the stat for an fs.DirEntry? Uses lstat.
// Whatever, we want to resolve symlinks anyways.
if _, exists, _, _, stat, err = paths.RealPathExistsStatTarget(&fPath, "."); err != nil {
return
} else if exists {
if !stat.Mode().IsRegular() {
continue
}
if dedupe {
if _, exists = fpathMap[fPath]; !exists {
fpathMap[fPath] = true
files = append(files, fPath)
}
} else {
files = append(files, fPath)
}
if !d.ForceLegacy {
if incls, err = getDshGrpIncludes(fPath); err != nil {
return
}
if dedupe {
for _, i := range incls {
if _, exists = fpathMap[i]; !exists {
fpathMap[i] = true
files = append(files, i)
}
}
} else {
files = append(files, incls...)
}
}
}
}
}
files = tmpF
return
}
/*
GroupedHosts returns a map of `map[<GROUP>][]string{<HOST>[, <HOST>, ...]}.
Additional search paths may be specified via searchpaths.
If there are any conflicting group names, the first found group name is used.
For example, assuming the group name `<GROUP>`, the following files will be checked in this order:
0. IF searchPaths is not nil:
a. searchpaths[0]/<GROUP>
b. searchpaths[1]/<GROUP>
c. searchpaths[2]/<GROUP>
d. ( ... )
1. IF DshGroupLister.NoHome is false:
a. `~/.dsh/group/<GROUP>`
2. IF $DSHGROUP_PATH is defined AND DshGroupLister.NoEnv is false:
a. `strings.Split(os.Getenv("DSHGROUP_PATH", string(os.PathListSeparator)))[0]/<GROUP>`
b. `strings.Split(os.Getenv("DSHGROUP_PATH", string(os.PathListSeparator)))[1]/<GROUP>`
c. `strings.Split(os.Getenv("DSHGROUP_PATH", string(os.PathListSeparator)))[2]/<GROUP>`
d. ( ... )
3. IF $DSHGROUP_PATH is NOT defined AND DshGroupLister.NoDefault is false:
a. `/etc/dsh/group/<GROUP>`
*/
func (d *DshGroupLister) GroupedHosts(dedupe bool, searchPaths ...string) (groupedHosts map[string][]string, err error) {
// TODO
return
}

View File

@@ -0,0 +1,309 @@
package dshgroup
import (
"bufio"
"bytes"
"os"
"strconv"
"strings"
"r00t2.io/sysutils/paths"
)
/*
ParseDshPtrn parses ptrn using the DSH group pattern ptrn as according to `HOSTLIST EXPRESSSIONS` in pdsh(1).
`#include` directives are explicitly skipped; this only parses actual generation pattern strings.
The returning generator may either be iterated over with `range` or have `Hosts()` called explicitly. // TODO
*/
func ParseDshPtrn(ptrn string) (generator *DshGrpGenerator, err error) {
var r rune
var pos int
var s string
var inToken bool
var tokStr string
var tok dshGrpToken
var strBuf *bytes.Buffer = new(bytes.Buffer)
var tokBuf *bytes.Buffer = new(bytes.Buffer)
// TODO: users can be specified per-pattern.
generator = &DshGrpGenerator{
tokens: make([]dshGrpToken, 0),
tokenized: make([]string, 0),
text: ptrn,
}
s = strings.TrimSpace(ptrn)
if s == "" {
return
}
if strings.HasPrefix(s, "#") {
return
}
// A quick sanity check. The end-state from the state machine below will catch any weird bracket issues beyond this.
if strings.Count(s, "[") != strings.Count(s, "]") {
err = ErrInvalidDshGrpSyntax
return
}
// Now the hacky bits. We read until we get to a start-token ('['), end-token (']'), or a pattern separator (',') that is *outside* a range token.
for pos, r = range s {
switch r {
case '[':
if inToken {
// Nested [...[
err = &PtrnParseErr{
pos: uint(pos),
ptrn: ptrn,
r: r,
err: ErrInvalidDshGrpSyntax,
inToken: inToken,
}
return
}
generator.tokenized = append(generator.tokenized, strBuf.String())
strBuf.Reset()
inToken = true
case ']':
if !inToken {
// Nested ]...]
err = &PtrnParseErr{
pos: uint(pos),
ptrn: ptrn,
r: r,
err: ErrInvalidDshGrpSyntax,
inToken: inToken,
}
return
}
tokStr = tokBuf.String()
if tok, err = parseDshGrpToken(tokStr); err != nil {
err = &PtrnParseErr{
pos: uint(pos),
ptrn: ptrn,
r: r,
err: err,
inToken: inToken,
}
return
}
generator.tokens = append(generator.tokens, tok)
tokBuf.Reset()
// Don't forget the empty element placeholder.
generator.tokenized = append(generator.tokenized, "")
inToken = false
default:
if inToken {
// If it isn't between '0' and '9', isn't '-', and isn't ','...
if !(0x30 <= r && r <= 0x39) && (r != 0x2d) && (r != 0x2c) {
// It's not a valid token. (The actual syntax is validated in parseDshGrpToken and parseDshGrpSubtoken)
err = &PtrnParseErr{
pos: uint(pos),
ptrn: ptrn,
r: r,
err: ErrInvalidDshGrpSyntax,
inToken: inToken,
}
return
}
tokBuf.WriteRune(r)
} else {
// TODO: confirm if inline comments and/or trailing/leading whitespace are handled by pdsh?
if strings.TrimSpace(string(r)) == "" || r == '#' {
// Whitespace is "invalid" (treat it as the end of the pattern).
// Same for end-of-line octothorpes.
if tokBuf.Len() > 0 {
// This should never happen.
err = &PtrnParseErr{
pos: uint(pos),
ptrn: ptrn,
r: r,
err: ErrInvalidDshGrpSyntax,
inToken: inToken,
}
return
}
if strBuf.Len() > 0 {
generator.tokenized = append(generator.tokenized, strBuf.String())
}
break
}
// Otherwise we just check for valid DNS chars.
if !(0x30 <= r && r <= 0x39) && // '0'-'9'
(r != 0x2d) && // '-'
(r != 0x2e) && // '.'
!(0x41 <= r && r <= 0x5a) && // 'A' through 'Z' (inclusive)
!(0x61 <= r && r <= 0x7a) { // 'a' through 'z' (inclusive)
err = &PtrnParseErr{
pos: uint(pos),
ptrn: ptrn,
r: r,
err: ErrInvalidDshGrpPtrn,
inToken: inToken,
}
return
}
// (Probably) valid(-ish), so add it.
strBuf.WriteRune(r)
}
}
}
// If the token never closed, it's also invalid.
if inToken {
err = ErrInvalidDshGrpSyntax
return
}
return
}
// parseDshGrpToken parses a token string into a dshGrpToken.
func parseDshGrpToken(tokenStr string) (token dshGrpToken, err error) {
var s string
var st []string
var sub dshGrpSubtoken
s = strings.TrimSpace(tokenStr)
if s == "" {
err = ErrEmptyDshGroupTok
return
}
st = strings.Split(s, ",")
token = dshGrpToken{
token: tokenStr,
subtokens: make([]dshGrpSubtoken, 0, len(st)),
}
for _, s = range st {
if strings.TrimSpace(s) == "" {
continue
}
if sub, err = parseDshGrpSubtoken(s); err != nil {
return
}
token.subtokens = append(token.subtokens, sub)
}
return
}
// parseDshGrpSubtoken parses a subtoken string into a dshGrpSubtoken.
func parseDshGrpSubtoken(subTokenStr string) (subtoken dshGrpSubtoken, err error) {
var u64 uint64
var vals []string
var endPad string
var startPad string
var st dshGrpSubtoken
var matches map[string][]string
if matches = dshGrpSubTokenPtrn.MapString(subTokenStr, false, false, true); matches == nil || len(matches) == 0 {
err = ErrInvalidDshGrpPtrn
return
}
if vals = matches["start_pad"]; vals != nil && len(vals) == 1 {
startPad = vals[0]
}
if vals = matches["start"]; vals != nil && len(vals) == 1 {
if u64, err = strconv.ParseUint(vals[0], 10, 64); err != nil {
return
}
st.start = uint(u64)
}
if vals = matches["end_pad"]; vals != nil && len(vals) == 1 {
endPad = vals[0]
}
if vals = matches["end"]; vals != nil && len(vals) == 1 {
if u64, err = strconv.ParseUint(vals[0], 10, 64); err != nil {
return
}
st.end = uint(u64)
}
if startPad != "" && endPad != "" {
// We set the pad to the largest.
if len(startPad) > len(endPad) {
st.pad = startPad
} else {
st.pad = endPad
}
} else if startPad != "" {
st.pad = startPad
} else if endPad != "" {
st.pad = endPad
}
subtoken = st
return
}
/*
getDshGrpIncludes parses fpath for `#include ...` directives. It skips any entries in which
`len(paths.SegmentSys(p) == []string{p}`, as these are inherently included by the dir read.
It is assumed that fpath is a cleaned, absolute filepath.
*/
func getDshGrpIncludes(fpath string) (includes []string, err error) {
var f *os.File
var line string
var exists bool
var inclpath string
var subIncl []string
var segs []string
var scanner *bufio.Scanner
var matches map[string][]string
if f, err = os.Open(fpath); err != nil {
return
}
defer f.Close()
scanner = bufio.NewScanner(f)
for scanner.Scan() {
line = strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
if !dshGrpInclPtrn.MatchString(line) {
continue
}
matches = dshGrpInclPtrn.MapString(line, false, false, true)
if matches == nil {
err = ErrInvalidDshGrpSyntax
return
}
if matches["incl"] == nil || len(matches["incl"]) == 0 {
err = ErrInvalidDshGrpSyntax
return
}
inclpath = matches["incl"][0]
segs = paths.SegmentSys(inclpath, false, false)
if segs == nil || len(segs) == 0 || (len(segs) == 1 && segs[0] == inclpath) {
continue
}
if exists, err = paths.RealPathExists(&inclpath); err != nil {
return
}
if !exists {
continue
}
includes = append(includes, inclpath)
if subIncl, err = getDshGrpIncludes(inclpath); err != nil {
return
}
if subIncl != nil && len(subIncl) > 0 {
includes = append(includes, subIncl...)
}
}
return
}

View File

@@ -0,0 +1,59 @@
package dshgroup
import (
`log`
`testing`
`github.com/davecgh/go-spew/spew`
)
func TestParseDshPtrn(t *testing.T) {
var err error
var idx int
var s string
var generator *DshGrpGenerator
var hostList []string
var tgtList []string = []string{
"0foo1bar46004quux", "0foo1bar46005quux", "0foo1bar46006quux", "0foo1bar46007quux", "0foo1bar46008quux", "0foo1bar46009quux",
"0foo1bar4615quux", "0foo1bar47004quux", "0foo1bar47005quux", "0foo1bar47006quux", "0foo1bar47007quux", "0foo1bar47008quux",
"0foo1bar47009quux", "0foo1bar4715quux", "0foo2bar46004quux", "0foo2bar46005quux", "0foo2bar46006quux", "0foo2bar46007quux",
"0foo2bar46008quux", "0foo2bar46009quux", "0foo2bar4615quux", "0foo2bar47004quux", "0foo2bar47005quux", "0foo2bar47006quux",
"0foo2bar47007quux", "0foo2bar47008quux", "0foo2bar47009quux", "0foo2bar4715quux", "0foo3bar46004quux", "0foo3bar46005quux",
"0foo3bar46006quux", "0foo3bar46007quux", "0foo3bar46008quux", "0foo3bar46009quux", "0foo3bar4615quux", "0foo3bar47004quux",
"0foo3bar47005quux", "0foo3bar47006quux", "0foo3bar47007quux", "0foo3bar47008quux", "0foo3bar47009quux", "0foo3bar4715quux",
"1foo1bar46004quux", "1foo1bar46005quux", "1foo1bar46006quux", "1foo1bar46007quux", "1foo1bar46008quux", "1foo1bar46009quux",
"1foo1bar4615quux", "1foo1bar47004quux", "1foo1bar47005quux", "1foo1bar47006quux", "1foo1bar47007quux", "1foo1bar47008quux",
"1foo1bar47009quux", "1foo1bar4715quux", "1foo2bar46004quux", "1foo2bar46005quux", "1foo2bar46006quux", "1foo2bar46007quux",
"1foo2bar46008quux", "1foo2bar46009quux", "1foo2bar4615quux", "1foo2bar47004quux", "1foo2bar47005quux", "1foo2bar47006quux",
"1foo2bar47007quux", "1foo2bar47008quux", "1foo2bar47009quux", "1foo2bar4715quux", "1foo3bar46004quux", "1foo3bar46005quux",
"1foo3bar46006quux", "1foo3bar46007quux", "1foo3bar46008quux", "1foo3bar46009quux", "1foo3bar4615quux", "1foo3bar47004quux",
"1foo3bar47005quux", "1foo3bar47006quux", "1foo3bar47007quux", "1foo3bar47008quux", "1foo3bar47009quux", "1foo3bar4715quux",
"2foo1bar46004quux", "2foo1bar46005quux", "2foo1bar46006quux", "2foo1bar46007quux", "2foo1bar46008quux", "2foo1bar46009quux",
"2foo1bar4615quux", "2foo1bar47004quux", "2foo1bar47005quux", "2foo1bar47006quux", "2foo1bar47007quux", "2foo1bar47008quux",
"2foo1bar47009quux", "2foo1bar4715quux", "2foo2bar46004quux", "2foo2bar46005quux", "2foo2bar46006quux", "2foo2bar46007quux",
"2foo2bar46008quux", "2foo2bar46009quux", "2foo2bar4615quux", "2foo2bar47004quux", "2foo2bar47005quux", "2foo2bar47006quux",
"2foo2bar47007quux", "2foo2bar47008quux", "2foo2bar47009quux", "2foo2bar4715quux", "2foo3bar46004quux", "2foo3bar46005quux",
"2foo3bar46006quux", "2foo3bar46007quux", "2foo3bar46008quux", "2foo3bar46009quux", "2foo3bar4615quux", "2foo3bar47004quux",
"2foo3bar47005quux", "2foo3bar47006quux", "2foo3bar47007quux", "2foo3bar47008quux", "2foo3bar47009quux", "2foo3bar4715quux",
}
if generator, err = ParseDshPtrn("[0-2]foo[1-3]bar[4][6-7]baz[004-009,15]quux"); err != nil {
t.Fatal(err)
}
_ = spew.Sdump(generator)
hostList = generator.Hosts()
t.Log(hostList)
if len(hostList) != len(tgtList) {
t.Fatalf("Generated list length (%d) does not match target (%d)", len(hostList), len(tgtList))
}
for idx, s = range hostList {
if s != tgtList[idx] {
log.Fatalf("Test vector %d ('%s') does not match generated value '%s'", idx+1, tgtList[idx], s)
}
}
}

View File

@@ -0,0 +1,36 @@
package dshgroup
func (d *DshGrpGenerator) Generate() (yieldFunc func(yield func(host string) (done bool))) {
// TODO
return
}
func (d *DshGrpGenerator) Hosts() (hostList []string) {
// TODO
return
}
func (d *DshGrpGenerator) Host() (host string) {
// TODO
return
}
func (d *DshGrpGenerator) Next() (done bool) {
// TODO
return
}
func (d *DshGrpGenerator) Reset() {
// TODO
return
}

View File

@@ -0,0 +1,16 @@
package dshgroup
import (
"fmt"
)
// Error conforms a PtrnParseErr to error interface.
func (p *PtrnParseErr) Error() (errStr string) {
errStr = fmt.Sprintf(
"Parse error in pattern '%s', position %d rune '%s' (%#x) (in token: %v): %v",
p.ptrn, p.pos, string(p.r), p.r, p.inToken, p.err,
)
return
}

90
pdsh/dshgroup/types.go Normal file
View File

@@ -0,0 +1,90 @@
package dshgroup
// TODO: This... doesn't really have much usefulness, does it?
/*
type (
HostLister interface {
// Hosts returns ALL hsots (where applicable) that are considered/generated for a Lister.
Hosts() (hosts []string, err error)
}
)
*/
type (
/*
DshGroupLister behaves like the host list generator
for pdsh(1)'s "dshgroup module options" (the `misc/dshgroup`
module for pdsh).
*/
DshGroupLister struct {
/*
NoEnv, if true, will *not* use DSHGROUP_PATH (force-defaulting to /etc/dsh/group/,
but see NoDefault).
*/
NoEnv bool
/*
NoDefault, if true, will *not* add the default path `/etc/dsh/group/`
to the search paths.
If NoDefault is false, this path is only added if DSHGROUP_PATH is not defined
(or, if it IS defined, if NoEnv is true).
*/
NoDefault bool
// NoHome, if true, will *not* add the `~/.dsh/group/` path to the search paths.
NoHome bool
/*
ForceLegacy, if true, will disable the PDSH `#include <PATH|GROUP>` modification --
treating the source as a traditional DSH group file instead (e.g. `#include ...`
is treated as just a comment).
*/
ForceLegacy bool
// StrictWhitespace follows the same behavior as PDSH regarding no whitespace between patterns.
StrictWhitespace bool
}
)
type (
// DshGrpGenerator generates a list of hosts according to the pdsh "dshgroup" module.
DshGrpGenerator struct {
/*
tokens are interleaved with tokenized and indexed *after*;
in other words, str = <substr0><token0><substr1><token1>...
*/
tokens []dshGrpToken
// tokenized holds the split original text with tokens removed and split where the tokens occur.
tokenized []string
// text holds the original pattern.
text string
}
dshGrpToken struct {
/*
token contains the original range specifier.
Tokens may be e.g.:
* 3: str3
* 3-5: str3, str4, str5
* 3,5: str3, str5
*/
token string
// subtokens hold a split of the individual range specifiers.
subtokens []dshGrpSubtoken
}
dshGrpSubtoken struct {
// start indicates either the single value or the start of the range.
start uint
// end, if 0 or less than start, indicates a single-value range.
end uint
// pad, if non-empty, is a string to add to the beginning of each of the generated substrings for this subtoken.
pad string
}
)
type (
PtrnParseErr struct {
pos uint
ptrn string
r rune
err error
inToken bool
}
)

11
pdsh/genders/docs.go Normal file
View File

@@ -0,0 +1,11 @@
/*
Package genders implements the [misc/genders] PDSH module. ([source])
# TODO/WIP/Not Yet Implemented
This package is not yet complete.
[misc/genders]: https://www.mankier.com/1/pdsh#genders_module_options
[source]: https://github.com/chaos/pdsh/blob/master/src/modules/genders.c
*/
package genders

11
pdsh/machines/docs.go Normal file
View File

@@ -0,0 +1,11 @@
/*
Package machines implements the [misc/machines] PDSH module. ([source])
# TODO/WIP/Not Yet Implemented
This package is not yet complete.
[misc/machines]: https://www.mankier.com/1/pdsh#machines_module_options
[source]: https://github.com/chaos/pdsh/blob/master/src/modules/machines.c
*/
package machines

11
pdsh/netgroup/docs.go Normal file
View File

@@ -0,0 +1,11 @@
/*
Package netgroup implements the [misc/netgroup] PDSH module. ([source])
# TODO/WIP/Not Yet Implemented
This package is not yet complete.
[misc/netgroup]: https://www.mankier.com/1/pdsh#netgroup_module_options
[source]: https://github.com/chaos/pdsh/blob/master/src/modules/netgroup.c
*/
package netgroup

11
pdsh/nodeupdown/docs.go Normal file
View File

@@ -0,0 +1,11 @@
/*
Package nodeupdown implements the [misc/nodeupdown] PDSH module. ([source])
# TODO/WIP/Not Yet Implemented
This package is not yet complete.
[misc/nodeupdown]: https://www.mankier.com/1/pdsh#nodeupdown_module_options
[source]: https://github.com/chaos/pdsh/blob/master/src/modules/nodeupdown.c
*/
package nodeupdown

11
pdsh/slurm/docs.go Normal file
View File

@@ -0,0 +1,11 @@
/*
Package slurm implements the [misc/slurm] PDSH module. ([source])
# TODO/WIP/Not Yet Implemented
This package is not yet complete.
[misc/slurm]: https://www.mankier.com/1/pdsh#slurm_module_options
[source]: https://github.com/chaos/pdsh/blob/master/src/modules/slurm.c
*/
package slurm

11
pdsh/torque/docs.go Normal file
View File

@@ -0,0 +1,11 @@
/*
Package torque implements the [misc/torque] PDSH module. ([source])
# TODO/WIP/Not Yet Implemented
This package is not yet complete.
[misc/torque]: https://www.mankier.com/1/pdsh#torque_module_options
[source]: https://github.com/chaos/pdsh/blob/master/src/modules/torque.c
*/
package torque

60
pdsh/types.go Normal file
View File

@@ -0,0 +1,60 @@
package pdsh
type (
/*
Generator is one of the PDSH host generators/iterators offered by this module.
Note that these generators/iterators are *stateful*, which means they shouldn't
(probably; I'm not your dad) be used concurrently (unless you want some hard-to-debug results)
and all methods advance the generator - so you probably don't want to call both Generate() and
Next()/Host() on the same instance, for example.
*/
Generator interface {
/*
Generate provides a Go-native iterator (also called a "RangeFunc" or "range over function type")
as found in Go 1.23 onwards.
See the assocaied blog entry for details: https://go.dev/blog/range-functions
Essentially it allows for e.g.:
for host := range (Generator).Generate() {
// ...
}
which is the "new standard" approach for iteration.
*/
Generate() (yieldFunc func(yield func(host string) (done bool)))
/*
Reset is used to reset a Generator, allowing one to "restart" the generation at the beginning.
Generators in this module are generally single-use, but can be reset/reused with this method.
*/
Reset()
/*
Hosts returns a complete generated hostlist at once if you'd rather not iterate.
Hosts() *does* perform an iteration in runtime, so the recommendation against concurrency
stands, but it calls Reset() when done generating to allow other methods of a Generator to be used.
*/
Hosts() (hostList []string)
/*
Next and Host behave like more "traditional" iterators, e.g. like (database/sql).Row.Next().
Next advances the internal state to the next host, and Host() returns it.
*/
Next() (done bool)
/*
Host returns the current host string (or "" if done).
Be sure to e.g.:
for (Generator).Next() {
host := (Generator).Host()
}
otherwise the Host return value will not change.
*/
Host() (host string)
}
)

14
pdsh/wcoll/docs.go Normal file
View File

@@ -0,0 +1,14 @@
/*
Package wcoll implements the "default" [WCOLL] method for PDSH. ([source])
Be sure to read the [HOSTLIST EXPRESSIONS] section in the MAN page.
# TODO/WIP/Not Yet Implemented
This package is not yet complete.
[WCOLL]: https://www.mankier.com/1/pdsh#Environment_Variables
[source]: https://github.com/chaos/pdsh/blob/master/src/pdsh/wcoll.c
[HOSTLIST EXPRESSIONS]: https://www.mankier.com/1/pdsh#Hostlist_Expressions
*/
package wcoll

View File

@@ -1,3 +1,5 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
package sysutils
import (