go_sysutils/pdsh/dshgroup/funcs_dshgrp.go
2025-08-17 02:58:24 -04:00

310 lines
7.3 KiB
Go

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
}