Files
indra/pkg/proc/cmds/args.go

225 lines
5.7 KiB
Go

package cmds
import (
"fmt"
"strings"
"github.com/indra-labs/indra"
log2 "github.com/indra-labs/indra/pkg/proc/log"
"github.com/indra-labs/indra/pkg/proc/opts/meta"
"github.com/indra-labs/indra/pkg/util/norm"
)
var (
log = log2.GetLogger(indra.PathBase)
check = log.E.Chk
)
// ParseCLIArgs reads a command line argument slice (presumably from os.Args),
// identifies the command to run and sets options provided.
//
// Rules for constructing CLI args:
//
// Commands are identified by name, and must appear in their hierarchic order to
// invoke subcommands. They are matched as normalised to lower case.
//
// Options can be preceded by "--" or "-", and the full name, or the alias,
// normalised to lower case for matching, and if there is an "=" after it, the
// value is after this, otherwise, the next element in the args is the value,
// except booleans, which default to true unless set to false. For these, the
// prefix "no" or similar indicates the semantics of the true value.
//
// Options only match when preceded by their relevant Command, except for the
// root Command, and these options must precede any other Command.
//
// If no command is selected, the root Command.Default is selected. This can
// optionally be used for subcommands as well, though it is unlikely needed, if
// found, the Default of the tip of the Command branch selected by the CLI if
// there is one, otherwise the Command itself.
func (c *Command) ParseCLIArgs(a []string) (run *Command, runArgs []string,
e error) {
var args []string
var cursor int
for i := range a {
if len(a[i]) > 0 {
args = append(args, a[i])
cursor++
}
}
var segments [][]string
commands := Commands{c}
var depth, last int
cur := c
cursor = 0
// First pass matches Command names in order to slice up the sections
// where relevant config items will be found.
for ; cursor < len(args); cursor++ {
for i := range cur.Commands {
if norm.Eq(args[cursor], cur.Commands[i].Name) {
// the command to run is the last, so update
// each new command found
run = cur.Commands[i]
commands = append(commands, run)
depth++
cur = cur.Commands[i]
segments = append(segments, args[last:cursor])
last = cursor
break
}
}
}
// append the remainder to the last segment
var tmp []string
for _, item := range args[last:cursor] {
if len(item) > 0 {
tmp = append(tmp, item)
}
}
segments = append(segments, tmp)
// The segments that have been cut from args will now provide the root
// level command name, and all subsequent items until the next segment
// should be names found in the configs map.
for i := range segments {
if len(segments[i]) < 1 {
continue
}
iArgs := segments[i][1:]
cmd := commands[i]
// the final command can accept arbitrary arguments,
// that are passed into the entrypoint
runArgs = iArgs
if norm.Norm(commands[i].Name) == "help" {
break
}
for cursor = 0; cursor < len(iArgs); {
inc := 1
arg := iArgs[cursor]
if len(arg) == 0 {
cursor++
continue
}
if !strings.HasPrefix(arg, "-") {
e = fmt.Errorf("argument %s missing '-', "+
"context %s, most likely misspelled "+
"subcommand", arg, iArgs)
return
}
arg = arg[1:]
if strings.HasPrefix(arg, "-") {
arg = arg[1:]
}
if strings.Contains(arg, "=") {
if e = valueInArg(cmd, arg); check(e) {
return
}
cursor += inc
continue
}
found := false
for cfgName := range cmd.Configs {
found, cursor, inc, e =
lookAhead(cmd, cfgName, arg, iArgs,
found, cursor, inc)
}
if !found {
e = fmt.Errorf(
"option not found: '%s' context %v",
arg, segments[i])
return
}
cursor += inc
}
}
// if no Command was found, return the default. If there is no default,
// the top level Command will be returned
if len(c.Default) > 0 && len(segments) < 2 {
run = c
def := c.Default
var lastFound int
for i := range def {
for _, sc := range run.Commands {
if sc.Name == def[i] {
lastFound = i
run = sc
}
}
}
if lastFound != len(def)-1 {
e = fmt.Errorf("default command %v not found at %s",
c.Default, def)
}
}
return
}
func lookAhead(cmd *Command, cfgName, arg string, iArgs []string,
found bool, cursor, inc int) (fnd bool, curs, ino int, e error) {
aliases := cmd.Configs[cfgName].Meta().Aliases()
names := append(
[]string{cfgName}, aliases...)
for _, name := range names {
if norm.Norm(name) != norm.Norm(arg) {
continue
}
// check for booleans, which can only be
// followed by true or false
if cmd.Configs[cfgName].Type() != meta.Bool {
if len(iArgs)-1 <= cursor {
continue
}
e = cmd.Configs[cfgName].FromString(iArgs[cursor+1])
if e != nil {
inc++
found = true
break
}
}
// If we find a truth value in the next arg, assign it.
if len(iArgs)-1 > cursor {
e = cmd.Configs[cfgName].FromString(iArgs[cursor+1])
if e == nil {
inc++
found = true
break
}
}
cur := cmd.Configs[cfgName].Meta().Default()
cmd.Configs[cfgName].FromString(cur)
v := !cmd.Configs[cfgName].Value().Bool()
cmd.Configs[cfgName].FromString(fmt.Sprint(v))
found = true
break
}
return found, cursor, inc, e
}
func valueInArg(cmd *Command, arg string) (e error) {
split := strings.Split(arg, "=")
if len(split) > 2 {
split = append(split[:1],
strings.Join(split[1:], "="))
}
for cfgName := range cmd.Configs {
aliases := cmd.Configs[cfgName].Meta().
Aliases()
names := append(
[]string{cfgName}, aliases...)
for _, name := range names {
if norm.Norm(name) !=
norm.Norm(split[0]) {
continue
}
e = cmd.Configs[cfgName].
FromString(split[1])
if log.E.Chk(e) {
return
}
}
}
return
}