wand/commandline.go
2025-08-18 14:24:31 +02:00

364 lines
6.1 KiB
Go

package wand
import (
"reflect"
"slices"
"strconv"
"strings"
"unicode"
)
type value struct {
isBool bool
boolean bool
str string
}
type option struct {
name string
value value
shortForm bool
}
type commandLine struct {
options []option
positional []string
}
func boolValue(b bool) value {
return value{isBool: true, boolean: b}
}
func stringValue(s string) value {
return value{str: s}
}
func insertHelpOption(names []string) []string {
for _, n := range names {
if n == "help" {
return names
}
}
return append(names, "help")
}
func insertHelpShortForm(shortForms []string) []string {
for _, sf := range shortForms {
if sf == "h" {
return shortForms
}
}
return append(shortForms, "h")
}
func boolOptions(cmd Cmd) []string {
v := reflect.ValueOf(cmd.impl)
v = unpack(v)
t := v.Type()
s := structParameters(t)
f := fields(s...)
b := boolFields(f)
var n []string
for _, fi := range b {
n = append(n, fi.name)
}
n = insertHelpOption(n)
sfm := make(map[string][]string)
for i := 0; i < len(cmd.shortForms); i += 2 {
l, s := cmd.shortForms[i], cmd.shortForms[i+1]
sfm[l] = append(sfm[l], s)
}
var sf []string
for _, ni := range n {
if sn, ok := sfm[ni]; ok {
sf = append(sf, sn...)
}
}
sf = insertHelpShortForm(sf)
return append(n, sf...)
}
func isOption(arg string) bool {
a := []rune(arg)
if len(a) <= 2 {
return false
}
if string(a[:2]) != "--" {
return false
}
if !unicode.IsLower(a[2]) {
return false
}
for _, r := range a[3:] {
if unicode.IsLower(r) {
continue
}
if unicode.IsDigit(r) {
continue
}
if r == '-' {
continue
}
if r == '=' {
return true
}
return false
}
return true
}
func isShortOptionSet(arg string) bool {
a := []rune(arg)
if len(a) < 2 {
return false
}
if a[0] != '-' {
return false
}
if !unicode.IsLower(a[1]) {
return false
}
for _, r := range a[2:] {
if r == '=' {
return true
}
if !unicode.IsLower(r) {
return false
}
}
return true
}
func defaultCommand(cmd Cmd) Cmd {
if cmd.impl != nil {
return cmd
}
for _, sc := range cmd.subcommands {
if sc.isDefault {
return sc
}
}
return cmd
}
func subcommand(cmd Cmd, name string) (Cmd, bool) {
for _, sc := range cmd.subcommands {
if sc.name == name {
return sc, true
}
}
return Cmd{}, false
}
func selectCommand(cmd Cmd, args []string) (Cmd, []string, []string) {
if len(args) == 0 {
return defaultCommand(cmd), []string{cmd.name}, nil
}
sc, ok := subcommand(cmd, args[0])
if !ok {
return defaultCommand(cmd), []string{cmd.name}, args
}
if sc.isHelp {
cmd.helpRequested = true
return cmd, []string{cmd.name}, args[1:]
}
cmd, fullCommand, args := selectCommand(sc, args[1:])
fullCommand = append([]string{cmd.name}, fullCommand...)
return cmd, fullCommand, args
}
func boolOption(name string, value bool) option {
return option{
name: name,
value: boolValue(value),
}
}
func stringOption(name, value string) option {
return option{
name: name,
value: stringValue(value),
}
}
func shortForm(o option) option {
o.shortForm = true
return o
}
func canBeValue(arg string) bool {
if arg == "--" {
return false
}
if isOption(arg) {
return false
}
if isShortOptionSet(arg) {
return false
}
return true
}
func canBeBoolValue(arg string) bool {
_, err := strconv.ParseBool(arg)
return err == nil
}
func readOption(boolOptions []string, arg string, args []string) (option, []string) {
eqi := strings.Index(arg, "=")
if eqi >= 0 {
arg, value := arg[:eqi], arg[eqi+1:]
if slices.Contains(boolOptions, arg) && canBeBoolValue(value) {
v, _ := strconv.ParseBool(value)
return boolOption(arg, v), args
}
return stringOption(arg, value), args
}
var (
next string
nextCanBeValue, nextCanBeBoolValue bool
)
if len(args) > 0 {
next = args[0]
nextCanBeValue = canBeValue(next)
nextCanBeBoolValue = canBeBoolValue(next)
}
if slices.Contains(boolOptions, arg) {
value := true
if nextCanBeBoolValue {
value, _ = strconv.ParseBool(next)
args = args[1:]
}
return boolOption(arg, value), args
}
if !nextCanBeValue {
return boolOption(arg, true), args
}
return stringOption(arg, next), args[1:]
}
func readShortOptionSet(boolOptions []string, arg string, args []string) ([]option, []string) {
last := len(arg) - 1
eqi := strings.Index(arg, "=")
if eqi >= 0 {
last = eqi - 1
}
var o []option
for _, a := range arg[:last] {
o = append(o, shortForm(boolOption(string(a), true)))
}
if slices.Contains(boolOptions, arg[last:]) && (len(args) == 0 || !canBeBoolValue(args[0])) {
o = append(o, shortForm(boolOption(arg[last:], true)))
return o, args
}
var lastOption option
lastOption, args = readOption(boolOptions, arg[last:], args)
o = append(o, shortForm(lastOption))
return o, args
}
func readArgs(boolOptions, args []string) commandLine {
var c commandLine
if len(args) == 0 {
return c
}
arg, args := args[0], args[1:]
switch {
case arg == "--":
if len(args) > 0 {
arg, args = args[0], args[1:]
c.positional = append(c.positional, arg)
args = append([]string{"--"}, args...)
}
case isOption(arg):
var f option
arg = arg[2:]
f, args = readOption(boolOptions, arg, args)
c.options = append(c.options, f)
case isShortOptionSet(arg):
var f []option
arg = arg[1:]
f, args = readShortOptionSet(boolOptions, arg, args)
c.options = append(c.options, f...)
default:
c.positional = append(c.positional, arg)
}
cc := readArgs(boolOptions, args)
c.options = append(c.options, cc.options...)
c.positional = append(c.positional, cc.positional...)
return c
}
func hasHelpOption(cmd Cmd, o []option) bool {
var mf map[string][]field
if cmd.impl != nil {
mf = mapFields(cmd.impl)
}
sf := make(map[string]bool)
for i := 0; i < len(cmd.shortForms); i += 2 {
s := cmd.shortForms[i+1]
sf[s] = true
}
for _, oi := range o {
if !oi.value.isBool {
continue
}
if oi.name == "help" {
if _, ok := mf["help"]; !ok {
return true
}
}
if oi.shortForm && oi.name == "h" {
if !sf["h"] {
return true
}
}
}
return false
}