310 lines
7.3 KiB
Go
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
|
|
}
|