wwctl node args and completions

Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
Jonathon Anderson
2025-02-08 16:57:49 -07:00
parent 5494424c09
commit 08a8eb430c
16 changed files with 213 additions and 246 deletions

View File

@@ -4,77 +4,113 @@ import (
"fmt"
"strconv"
"strings"
"unicode"
)
func toInt(i string) int {
ret, _ := strconv.Atoi(i)
return ret
}
func isDigit(s string) bool {
r := []rune(s)
for i := 0; i < len(r); i++ {
if !unicode.IsDigit(r[i]) {
return false
}
}
return true
}
func expand_iterate(list []string) ([]string, int) {
var ret []string
var count int
for _, i := range list {
bracketIndex1 := strings.Index(i, "[")
bracketIndex2 := strings.Index(i, "]")
if bracketIndex1 >= 0 && bracketIndex1 < bracketIndex2 {
prefix := i[:bracketIndex1]
suffix := i[bracketIndex2+1:]
ranges := strings.Split(i[bracketIndex1+1:bracketIndex2], ",")
count++
for _, r := range ranges {
iterate := strings.Split(r, "-")
if len(iterate) == 1 {
if !isDigit(iterate[0]) {
return ret, 0
} else {
ret = append(ret, prefix+iterate[0]+suffix)
}
} else if len(iterate) == 2 {
if !isDigit(iterate[0]) || !isDigit(iterate[1]) {
return ret, 0
} else {
sigfigures := len(iterate[0])
for i := toInt(iterate[0]); i <= toInt(iterate[1]); i++ {
ret = append(ret, fmt.Sprintf(`%s%.`+fmt.Sprintf("%d", sigfigures)+`d%s`, prefix, i, suffix))
}
}
}
}
}
}
return ret, count
}
// 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 {
ret := list
// 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 {
loop, count := expand_iterate(ret)
onceExpanded, count := expandOnce(expanded)
if count == 0 {
break
}
ret = loop
expanded = onceExpanded
}
return ret
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
}

View File

@@ -6,28 +6,48 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_Single(t *testing.T) {
assert.Equal(t, []string{"node1"}, Expand([]string{"node1"}))
}
func TestHostList(t *testing.T) {
tests := map[string]struct {
input []string
output []string
}{
"single": {
input: []string{"node1"},
output: []string{"node1"},
},
"multiple": {
input: []string{"node1", "node2"},
output: []string{"node1", "node2"},
},
"range": {
input: []string{"node[1-2]"},
output: []string{"node1", "node2"},
},
"internal comma": {
input: []string{"node[1,2]"},
output: []string{"node1", "node2"},
},
"mixed range comma": {
input: []string{"node[1,2-3]"},
output: []string{"node1", "node2", "node3"},
},
"external comma": {
input: []string{"node1,node2"},
output: []string{"node1", "node2"},
},
"mixed external comma with range": {
input: []string{"n[1-3],n5,n[7-8,10]"},
output: []string{"n1", "n2", "n3", "n5", "n7", "n8", "n10"},
},
"leading zeroes": {
input: []string{"n[01-03]"},
output: []string{"n01", "n02", "n03"},
},
}
func Test_Multiple(t *testing.T) {
assert.Equal(t, []string{"node1", "node2"}, Expand([]string{"node1", "node2"}))
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
assert.Equal(t, tt.output, Expand(tt.input))
})
}
}
func Test_Range(t *testing.T) {
assert.Equal(t, []string{"node1", "node2"}, Expand([]string{"node[1-2]"}))
}
func Test_Internal_Comma(t *testing.T) {
assert.Equal(t, []string{"node1", "node2"}, Expand([]string{"node[1,2]"}))
}
func Test_Mixed_Range_Comma(t *testing.T) {
assert.Equal(t, []string{"node1", "node2", "node3"}, Expand([]string{"node[1,2-3]"}))
}
// not currently supported
//
// func Test_External_Comma(t *testing.T) {
// assert.Equal(t, []string{"node1", "node2"}, Expand([]string{"node1,node2"}))
// }