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)`
This commit is contained in:
parent
8260e4fa93
commit
e797a14911
@ -75,7 +75,7 @@ func GetEnvErrNoBlank(key string, ignoreWhitespace bool) (value string, err erro
|
|||||||
var e *EnvErrNoVal = &EnvErrNoVal{
|
var e *EnvErrNoVal = &EnvErrNoVal{
|
||||||
VarName: key,
|
VarName: key,
|
||||||
WasRequiredNonEmpty: true,
|
WasRequiredNonEmpty: true,
|
||||||
IgnoreWhiteSpace: ignoreWhitespace,
|
IgnoreWhitespace: ignoreWhitespace,
|
||||||
}
|
}
|
||||||
|
|
||||||
if value, exists = os.LookupEnv(key); !exists {
|
if value, exists = os.LookupEnv(key); !exists {
|
||||||
|
@ -12,7 +12,7 @@ func (e *EnvErrNoVal) Error() (errStr string) {
|
|||||||
sb.WriteString("the variable '")
|
sb.WriteString("the variable '")
|
||||||
sb.WriteString(e.VarName)
|
sb.WriteString(e.VarName)
|
||||||
sb.WriteString("' was ")
|
sb.WriteString("' was ")
|
||||||
if sb.WasFound {
|
if e.WasFound {
|
||||||
sb.WriteString("found")
|
sb.WriteString("found")
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString("not found")
|
sb.WriteString("not found")
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
package envs
|
package envs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`strconv`
|
"strconv"
|
||||||
`strings`
|
"strings"
|
||||||
|
|
||||||
`r00t2.io/sysutils/internal`
|
"r00t2.io/sysutils/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// envListToMap splits a []string of env var keypairs to a map.
|
// envListToMap splits a []string of env var keypairs to a map.
|
||||||
@ -35,7 +35,7 @@ func nativizeEnvMap(stringMap map[string]string) (envMap map[string]interface{})
|
|||||||
var pathVar string = internal.GetPathEnvName()
|
var pathVar string = internal.GetPathEnvName()
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
envMap = make(map[string]interface{}, 0)
|
envMap = make(map[string]interface{})
|
||||||
|
|
||||||
for k, v := range stringMap {
|
for k, v := range stringMap {
|
||||||
|
|
||||||
|
4
go.mod
4
go.mod
@ -1,6 +1,6 @@
|
|||||||
module r00t2.io/sysutils
|
module r00t2.io/sysutils
|
||||||
|
|
||||||
go 1.23.2
|
go 1.24.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
@ -10,7 +10,7 @@ require (
|
|||||||
golang.org/x/sync v0.16.0
|
golang.org/x/sync v0.16.0
|
||||||
golang.org/x/sys v0.34.0
|
golang.org/x/sys v0.34.0
|
||||||
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8
|
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8
|
||||||
r00t2.io/goutils v1.9.0
|
r00t2.io/goutils v1.9.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
16
go.sum
16
go.sum
@ -1,4 +1,3 @@
|
|||||||
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
@ -12,15 +11,12 @@ 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/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 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
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 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/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 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
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 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
@ -31,25 +27,17 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
|
|||||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
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 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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-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.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.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 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 h1:FW42yWB1sGClqswyHIB68wo0+oPrav1IuQ+Tdy8Qp8E=
|
||||||
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8/go.mod h1:44w9OfBSQ9l3o59rc2w3AnABtE44bmtNnRMNC7z+oKE=
|
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.9.2 h1:1rcDgJ3MorWVBmZSvLpbAUNC+J+ctRfJQq5Wliucjww=
|
||||||
r00t2.io/goutils v1.8.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
|
r00t2.io/goutils v1.9.2/go.mod h1:76AxpXUeL10uFklxRB11kQsrtj2AKiNm8AwG1bNoBCA=
|
||||||
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=
|
|
||||||
|
17
paths/consts_unix.go
Normal file
17
paths/consts_unix.go
Normal 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
15
paths/consts_windows.go
Normal 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 them.
|
||||||
|
|
||||||
|
https://learn.microsoft.com/en-us/windows/win32/fileio/symbolic-link-programming-consideration
|
||||||
|
*/
|
||||||
|
MaxSymlinkLevel uint = 63
|
||||||
|
)
|
@ -1,10 +1,12 @@
|
|||||||
package paths
|
package paths
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`errors`
|
"errors"
|
||||||
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
ErrMaxSymlinkLevel = fmt.Errorf("max symlink level met/exceeded")
|
||||||
ErrNilErrChan error = errors.New("an initialized error channel is required")
|
ErrNilErrChan error = errors.New("an initialized error channel is required")
|
||||||
ErrNilMatchChan error = errors.New("an initialized matches channel is required")
|
ErrNilMatchChan error = errors.New("an initialized matches channel is required")
|
||||||
ErrNilMismatchChan error = errors.New("an initialized mismatches channel is required")
|
ErrNilMismatchChan error = errors.New("an initialized mismatches channel is required")
|
||||||
|
130
paths/funcs.go
130
paths/funcs.go
@ -129,7 +129,7 @@ func GetFirstWithRef(p []string) (content []byte, isDir, ok bool, idx int) {
|
|||||||
|
|
||||||
var locPaths []string
|
var locPaths []string
|
||||||
var exists bool
|
var exists bool
|
||||||
var stat os.FileInfo
|
var stat fs.FileInfo
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
idx = -1
|
idx = -1
|
||||||
@ -194,7 +194,7 @@ This is a bit more sane option than os.MkdirAll as it will normalize paths a lit
|
|||||||
*/
|
*/
|
||||||
func MakeDirIfNotExist(p string) (err error) {
|
func MakeDirIfNotExist(p string) (err error) {
|
||||||
|
|
||||||
var stat os.FileInfo
|
var stat fs.FileInfo
|
||||||
var exists bool
|
var exists bool
|
||||||
var locPath string = p
|
var locPath string = p
|
||||||
|
|
||||||
@ -235,6 +235,8 @@ path syntax/string itself is not supported on the runtime OS. This can be done v
|
|||||||
if errors.Is(err, fs.ErrInvalid) {...}
|
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(path) and filepath.Abs(*path).
|
||||||
|
|
||||||
|
Note that RealPath does *not* resolve symlinks. Only RealPathExistsStatTarget does that.
|
||||||
*/
|
*/
|
||||||
func RealPath(p *string) (err error) {
|
func RealPath(p *string) (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 fs.FileInfo
|
||||||
for the path (assuming it exists).
|
for the path (assuming it exists).
|
||||||
|
|
||||||
If stat is nil, it is highly recommended to check err via the methods suggested
|
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 {
|
if exists, err = RealPathExists(p); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if stat, err = os.Stat(*p); err != nil {
|
if stat, err = os.Stat(*p); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -365,6 +371,48 @@ func RealPathExistsStat(p *string) (exists bool, stat os.FileInfo, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
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 criteria.
|
// SearchFsPaths gets a file/directory/etc. path list based on the provided criteria.
|
||||||
func SearchFsPaths(matcher FsSearchCriteria) (found, miss []*FsSearchResult, err error) {
|
func SearchFsPaths(matcher FsSearchCriteria) (found, miss []*FsSearchResult, err error) {
|
||||||
|
|
||||||
@ -643,6 +691,80 @@ func StripSys(p string, abs, strict bool, n int) (slicedPath string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
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 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 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(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:
|
filterTimes checks a times.Timespec of a file using:
|
||||||
- an age specified by the caller
|
- an age specified by the caller
|
||||||
|
18
pdsh/consts.go
Normal file
18
pdsh/consts.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package pdsh
|
||||||
|
|
||||||
|
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>[1-9]+[0-9]*)?(?:-(?P<end_pad>0*)(?P<end>[1-9]+[0-9]*))?$`)}
|
||||||
|
)
|
16
pdsh/docs.go
Normal file
16
pdsh/docs.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
Package pdsh (!! WIP !!) provides PDSH-compatible functionality for parsing group files.
|
||||||
|
|
||||||
|
Note that this library will *only* source and parse PDSH-compatible host/group files,
|
||||||
|
it will not actually connect to anything.
|
||||||
|
It simply provides ways of returning lists of hosts using generation rules/patterns.
|
||||||
|
|
||||||
|
Currently, the only supported PDSH module is `misc/dshgroup` but additional/all other
|
||||||
|
host list modules are planned.
|
||||||
|
|
||||||
|
For details, see:
|
||||||
|
|
||||||
|
- https://github.com/chaos/pdsh/
|
||||||
|
- https://github.com/chaos/pdsh/blob/master/doc/pdsh.1.in
|
||||||
|
*/
|
||||||
|
package pdsh
|
10
pdsh/errs.go
Normal file
10
pdsh/errs.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package pdsh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidDshGrpSyntax error = errors.New("invalid dsh group file syntax")
|
||||||
|
ErrInvalidDshGrpPtrn error = errors.New("invalid dsh group pattern syntax")
|
||||||
|
)
|
174
pdsh/funcs_dshgrouplister.go
Normal file
174
pdsh/funcs_dshgrouplister.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package pdsh
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
298
pdsh/funcs_dshgrp.go
Normal file
298
pdsh/funcs_dshgrp.go
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
package pdsh
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
func ParseDshPtrn(ptrn string) (hostList []string, 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)
|
||||||
|
var parser *dshGrpGenerator = &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,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parser.tokenized = append(parser.tokenized, strBuf.String())
|
||||||
|
strBuf.Reset()
|
||||||
|
inToken = true
|
||||||
|
case ']':
|
||||||
|
if !inToken {
|
||||||
|
// Nested ]...]
|
||||||
|
err = &PtrnParseErr{
|
||||||
|
pos: uint(pos),
|
||||||
|
ptrn: ptrn,
|
||||||
|
r: r,
|
||||||
|
err: ErrInvalidDshGrpSyntax,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tokStr = tokBuf.String()
|
||||||
|
if tok, err = parseDshGrpToken(tokStr); err != nil {
|
||||||
|
err = &PtrnParseErr{
|
||||||
|
pos: uint(pos),
|
||||||
|
ptrn: ptrn,
|
||||||
|
r: r,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parser.tokens = append(parser.tokens, tok)
|
||||||
|
tokBuf.Reset()
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tokBuf.WriteRune(r)
|
||||||
|
} else {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strBuf.Len() > 0 {
|
||||||
|
parser.tokenized = append(parser.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)
|
||||||
|
!(0x6a <= r && r <= 0x7a) { // 'a' through 'z' (inclusive)
|
||||||
|
err = &PtrnParseErr{
|
||||||
|
pos: uint(pos),
|
||||||
|
ptrn: ptrn,
|
||||||
|
r: r,
|
||||||
|
err: ErrInvalidDshGrpPtrn,
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Due to a... particular quirk in the regex that I'm too tired to fix,
|
||||||
|
the start_pad may be e.g. "0" (or "00", etc.) and start may be "" if the range starts *at* 0
|
||||||
|
(or 00, 000, etc.).
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
} else if startPad != "" {
|
||||||
|
// Yeah, regex bug. So we remove one 0 from startPad, and set st.start to 0.
|
||||||
|
st.start = 0 // This is implicit, though.
|
||||||
|
startPad = startPad[:len(startPad)-1]
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
16
pdsh/funcs_ptrnparseerr.go
Normal file
16
pdsh/funcs_ptrnparseerr.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package pdsh
|
||||||
|
|
||||||
|
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': %v",
|
||||||
|
p.ptrn, p.pos, string(p.r), p.err,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
86
pdsh/types.go
Normal file
86
pdsh/types.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package pdsh
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
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
|
||||||
|
}
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user