Files
warewulf/internal/pkg/hostlist/hostlist.go
Tobias Ribizel 301082cdd7 add docstring for node patterns
Signed-off-by: Tobias Ribizel <mail@ribizel.de>
2025-07-06 16:36:45 +02:00

119 lines
2.8 KiB
Go

package hostlist
import (
"fmt"
"strconv"
"strings"
)
const Docstring = "Node patterns are a comma-separated list of individual pattern. Each pattern can either be a full node name or a node range like node[01-03,05]."
// Expand takes a slice of host strings, possibly containing comma-separated
// values and bracketed ranges (e.g. "node[01-03]") and returns a fully expanded
// slice of host names.
func Expand(list []string) []string {
// First, split each input string on commas that occur outside brackets.
var preList []string
for _, s := range list {
parts := splitTopLevel(s)
preList = append(preList, parts...)
}
expanded := preList
for {
onceExpanded, count := expandOnce(expanded)
if count == 0 {
break
}
expanded = onceExpanded
}
return expanded
}
// expandOnce performs a single round of bracket expansion.
func expandOnce(hosts []string) ([]string, int) {
var result []string
var expansionCount int
for _, host := range hosts {
bracketStart := strings.Index(host, "[")
bracketEnd := strings.Index(host, "]")
if bracketStart >= 0 && bracketStart < bracketEnd {
prefix := host[:bracketStart]
suffix := host[bracketEnd+1:]
// Extract the content between brackets and split on commas.
ranges := strings.Split(host[bracketStart+1:bracketEnd], ",")
expansionCount++
for _, rng := range ranges {
parts := strings.Split(rng, "-")
if len(parts) == 1 {
if !isDigit(parts[0]) {
// Abort expansion on invalid input.
return result, 0
}
result = append(result, prefix+parts[0]+suffix)
} else if len(parts) == 2 {
if !isDigit(parts[0]) || !isDigit(parts[1]) {
return result, 0
}
sigFigures := len(parts[0])
startNum := toInt(parts[0])
endNum := toInt(parts[1])
for num := startNum; num <= endNum; num++ {
result = append(result, fmt.Sprintf("%s%0*d%s", prefix, sigFigures, num, suffix))
}
}
}
} else {
// No brackets; keep the string as is.
result = append(result, host)
}
}
return result, expansionCount
}
// splitTopLevel splits s on commas that are not inside square brackets.
func splitTopLevel(s string) []string {
var parts []string
depth := 0
start := 0
for i, ch := range s {
switch ch {
case '[':
depth++
case ']':
if depth > 0 {
depth--
}
case ',':
if depth == 0 {
parts = append(parts, s[start:i])
start = i + 1
}
}
}
parts = append(parts, s[start:])
return parts
}
// toInt converts a numeric string to an integer. Assumes valid input.
func toInt(s string) int {
num, _ := strconv.Atoi(s)
return num
}
// isDigit returns true if s consists solely of ASCII digits.
func isDigit(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
}
}
return true
}