364 lines
6.1 KiB
Go
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
|
||
|
|
}
|