2022-02-20 00:22:03 +00:00
|
|
|
// Copyright 2022 Robert S. Muhlestein.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
2022-02-17 22:47:28 +00:00
|
|
|
|
2022-03-23 08:31:00 +00:00
|
|
|
/*
|
2022-04-01 10:12:53 +00:00
|
|
|
Package Z (bonzai) provides a rooted node tree of commands and singular
|
2022-03-23 08:31:00 +00:00
|
|
|
parameters making tab completion a breeze and complicated applications
|
|
|
|
much easier to intuit without reading all the docs. Documentation is
|
|
|
|
embedded with each command removing the need for separate man pages and
|
|
|
|
such and can be viewed as text or a locally served web page.
|
|
|
|
|
|
|
|
Rooted Node Tree
|
|
|
|
|
|
|
|
Commands and parameters are linked to create a rooted node tree of the
|
|
|
|
following types of nodes:
|
|
|
|
|
|
|
|
* Leaves with a method and optional parameters
|
|
|
|
* Branches with leaves, other branches, and a optional method
|
|
|
|
* Parameters, single words that are passed to a leaf command
|
|
|
|
|
|
|
|
*/
|
2022-04-01 10:12:53 +00:00
|
|
|
package Z
|
2022-02-17 04:31:12 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"os"
|
2022-03-23 08:31:00 +00:00
|
|
|
"path/filepath"
|
2022-02-17 04:31:12 +00:00
|
|
|
"strings"
|
2022-03-23 08:31:00 +00:00
|
|
|
|
2022-04-08 17:38:25 +00:00
|
|
|
"github.com/rwxrob/bonzai"
|
2022-03-28 06:20:54 +00:00
|
|
|
"github.com/rwxrob/term"
|
2022-02-17 04:31:12 +00:00
|
|
|
)
|
|
|
|
|
2022-03-23 08:31:00 +00:00
|
|
|
func init() {
|
|
|
|
var err error
|
|
|
|
// get the full path to current running process executable
|
2022-03-26 21:12:50 +00:00
|
|
|
ExePath, err = os.Executable()
|
2022-03-23 08:31:00 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Print(err)
|
|
|
|
return
|
|
|
|
}
|
2022-03-26 21:12:50 +00:00
|
|
|
ExePath, err = filepath.EvalSymlinks(ExePath)
|
2022-03-23 08:31:00 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Print(err)
|
|
|
|
}
|
2022-03-26 21:12:50 +00:00
|
|
|
ExeName = strings.TrimSuffix(
|
|
|
|
filepath.Base(ExePath), filepath.Ext(ExePath))
|
2022-03-23 08:31:00 +00:00
|
|
|
}
|
|
|
|
|
2022-03-26 21:12:50 +00:00
|
|
|
// ExePath holds the full path to the current running process executable
|
2022-03-23 08:31:00 +00:00
|
|
|
// which is determined at init() time by calling os.Executable and
|
|
|
|
// passing it to path/filepath.EvalSymlinks to ensure it is the actual
|
|
|
|
// binary executable file. Errors are reported to stderr, but there
|
|
|
|
// should never be an error logged unless something is wrong with the Go
|
|
|
|
// runtime environment.
|
2022-03-26 21:12:50 +00:00
|
|
|
var ExePath string
|
|
|
|
|
|
|
|
// ExeName holds just the base name of the executable without any suffix
|
2022-03-27 08:53:11 +00:00
|
|
|
// (ex: .exe) and is set at init() time (see ExePath).
|
2022-03-26 21:12:50 +00:00
|
|
|
var ExeName string
|
2022-03-23 08:31:00 +00:00
|
|
|
|
2022-03-30 19:25:12 +00:00
|
|
|
// Commands contains the commands to lookup when Run-ing an executable
|
|
|
|
// in "multicall" mode. Each value must begin with a *Cmd and the rest
|
|
|
|
// will be assumed to be string arguments to prepend. See Run.
|
|
|
|
var Commands map[string][]any
|
|
|
|
|
2022-04-08 17:38:25 +00:00
|
|
|
// Conf may be optionally assigned any implementation of
|
|
|
|
// a bonzai.Configurer. Once assigned it should not be reassigned at any
|
|
|
|
// later time during runtime. Certain Bonzai branches and commands may
|
|
|
|
// require Z.Conf to be defined and those that do generally require the
|
|
|
|
// same implementation throughout all of runtime. Commands that require
|
2022-04-09 20:27:24 +00:00
|
|
|
// Z.Conf should set ReqConf to true. Other than the exceptional case
|
2022-04-08 17:38:25 +00:00
|
|
|
// of configuration commands that fulfill bonzai.Configurer (and usually
|
|
|
|
// assign themselves to Z.Conf at init() time), commands must never
|
|
|
|
// require a specific implementation of bonzai.Configurer. This
|
|
|
|
// encourages command creators and Bonzai tree composers to centralize
|
|
|
|
// on a single form of configuration without creating brittle
|
|
|
|
// dependencies and tight coupling. Configuration persistence can be
|
|
|
|
// implemented in any number of ways without a problem and Bonzai trees
|
|
|
|
// simply need to be recompiled with a different bonzai.Configurer
|
|
|
|
// implementation to switch everything that depends on configuration.
|
|
|
|
var Conf bonzai.Configurer
|
|
|
|
|
2022-04-10 21:05:53 +00:00
|
|
|
// Vars may be optionally assigned any implementation of
|
2022-04-09 20:27:24 +00:00
|
|
|
// a bonzai.CacheMap. Once assigned it should not be reassigned at any
|
|
|
|
// later time during runtime. Certain Bonzai branches and commands may
|
2022-04-10 21:05:53 +00:00
|
|
|
// require Z.Vars to be defined and those that do generally require the
|
2022-04-09 20:27:24 +00:00
|
|
|
// same implementation throughout all of runtime. Commands that require
|
2022-04-10 21:05:53 +00:00
|
|
|
// Z.Vars should set ReqVars to true. Other than the exceptional case
|
2022-04-09 20:27:24 +00:00
|
|
|
// of configuration commands that fulfill bonzai.CacheMap (and usually
|
2022-04-10 21:05:53 +00:00
|
|
|
// assign themselves to Z.Vars at init() time), commands must never
|
|
|
|
// require a specific implementation of bonzai.CacheMap. This
|
2022-04-09 20:27:24 +00:00
|
|
|
// encourages command creators and Bonzai tree composers to centralize
|
|
|
|
// on a single form of caching without creating brittle
|
|
|
|
// dependencies and tight coupling. Caching persistence can be
|
|
|
|
// implemented in any number of ways without a problem and Bonzai trees
|
2022-04-10 21:05:53 +00:00
|
|
|
// simply need to be recompiled with a different bonzai.CacheMap
|
2022-04-09 20:27:24 +00:00
|
|
|
// implementation to switch everything that depends on cached variables.
|
2022-04-10 21:05:53 +00:00
|
|
|
var Vars bonzai.CacheMap
|
2022-04-09 20:27:24 +00:00
|
|
|
|
2022-04-08 17:38:25 +00:00
|
|
|
// UsageFunc is the default first-class function called if a Cmd that
|
|
|
|
// does not already define its own when usage information is needed (see
|
|
|
|
// bonzai.UsageFunc and Cmd.UsageError for more). By default,
|
|
|
|
// InferredUsage is assigned.
|
|
|
|
//
|
|
|
|
// It is used to return a usage summary. Generally, it should only
|
|
|
|
// return a single line (even if that line is very long). Developers
|
|
|
|
// are encouraged to refer users to their chosen help command rather
|
2022-04-11 09:41:40 +00:00
|
|
|
// than producing usually long usage lines.
|
2022-04-08 17:38:25 +00:00
|
|
|
var UsageFunc = InferredUsage
|
2022-04-02 11:40:00 +00:00
|
|
|
|
|
|
|
// InferredUsage returns a single line of text summarizing only the
|
|
|
|
// Commands (less any Hidden commands), Params, and Aliases. If a Cmd
|
|
|
|
// is currently in an invalid state (Params without Call, no Call and no
|
|
|
|
// Commands) a string beginning with ERROR and wrapped in braces ({}) is
|
|
|
|
// returned instead. The string depends on the current language (see
|
|
|
|
// lang.go). Note that aliases does not include package Z.Aliases.
|
2022-04-08 17:38:25 +00:00
|
|
|
func InferredUsage(cmd bonzai.Command) string {
|
|
|
|
|
|
|
|
x, iscmd := cmd.(*Cmd)
|
|
|
|
if !iscmd {
|
|
|
|
return "{ERROR: not a bonzai.Command}"
|
|
|
|
}
|
2022-04-02 11:40:00 +00:00
|
|
|
|
|
|
|
if x.Call == nil && x.Commands == nil {
|
|
|
|
return "{ERROR: neither Call nor Commands defined}"
|
|
|
|
}
|
|
|
|
|
|
|
|
if x.Call == nil && x.Params != nil {
|
|
|
|
return "{ERROR: Params without Call: " + strings.Join(x.Params, ", ") + "}"
|
|
|
|
}
|
|
|
|
|
2022-04-02 20:38:16 +00:00
|
|
|
params := UsageGroup(x.Params, x.MinParm, x.MaxParm)
|
2022-04-02 11:40:00 +00:00
|
|
|
|
|
|
|
var names string
|
|
|
|
if x.Commands != nil {
|
|
|
|
var snames []string
|
|
|
|
for _, x := range x.Commands {
|
|
|
|
snames = append(snames, x.UsageNames())
|
|
|
|
}
|
|
|
|
if len(snames) > 0 {
|
2022-04-02 20:38:16 +00:00
|
|
|
names = UsageGroup(snames, 1, 1)
|
2022-04-02 11:40:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if params != "" && names != "" {
|
|
|
|
return "(" + params + "|" + names + ")"
|
|
|
|
}
|
|
|
|
|
|
|
|
if params != "" {
|
|
|
|
return params
|
|
|
|
}
|
|
|
|
|
|
|
|
return names
|
|
|
|
}
|
|
|
|
|
2022-03-30 19:25:12 +00:00
|
|
|
// Run infers the name of the command to run from the ExeName looked up
|
|
|
|
// in the Commands delegates accordingly, prepending any arguments
|
2022-04-07 09:36:25 +00:00
|
|
|
// provided in the Cmd.Run. Run produces an "unmapped multicall command"
|
2022-03-30 19:25:12 +00:00
|
|
|
// error if no match is found. This is an alternative to the simpler,
|
|
|
|
// direct Cmd.Run method from main where only one possible Cmd will ever
|
|
|
|
// be the root and allows for BusyBox (https://www.busybox.net)
|
|
|
|
// multicall binaries to be used for such things as very light-weight
|
|
|
|
// Linux distributions when used "FROM SCRATCH" in containers.
|
|
|
|
func Run() {
|
|
|
|
if v, has := Commands[ExeName]; has {
|
|
|
|
if len(v) < 1 {
|
|
|
|
ExitError(fmt.Errorf("multicall command missing"))
|
|
|
|
}
|
|
|
|
cmd, iscmd := v[0].(*Cmd)
|
|
|
|
if !iscmd {
|
|
|
|
ExitError(fmt.Errorf("first value must be *Cmd"))
|
|
|
|
}
|
|
|
|
args := []string{cmd.Name}
|
|
|
|
if len(v) > 1 {
|
|
|
|
rest := os.Args[1:]
|
|
|
|
for _, a := range v[1:] {
|
|
|
|
s, isstring := a.(string)
|
|
|
|
if !isstring {
|
|
|
|
ExitError(fmt.Errorf("only string arguments allowed"))
|
|
|
|
}
|
|
|
|
args = append(args, s)
|
|
|
|
}
|
|
|
|
args = append(args, rest...)
|
|
|
|
}
|
|
|
|
os.Args = args
|
|
|
|
cmd.Run()
|
|
|
|
Exit()
|
|
|
|
}
|
|
|
|
ExitError(fmt.Errorf("unmapped multicall command: %v", ExeName))
|
|
|
|
}
|
|
|
|
|
2022-02-17 04:31:12 +00:00
|
|
|
// Method defines the main code to execute for a command (Cmd). By
|
2022-02-24 13:06:10 +00:00
|
|
|
// convention the parameter list should be named "args" if there are
|
|
|
|
// args expected and underscore (_) if not. Methods must never write
|
|
|
|
// error output to anything but standard error and should almost always
|
|
|
|
// use the log package to do so.
|
2022-02-24 12:33:54 +00:00
|
|
|
type Method func(caller *Cmd, args ...string) error
|
2022-02-17 04:31:12 +00:00
|
|
|
|
2022-02-17 23:03:19 +00:00
|
|
|
// DoNotExit effectively disables Exit and ExitError allowing the
|
|
|
|
// program to continue running, usually for test evaluation.
|
2022-02-17 04:31:12 +00:00
|
|
|
var DoNotExit bool
|
|
|
|
|
|
|
|
// ExitOff sets DoNotExit to false.
|
|
|
|
func ExitOff() { DoNotExit = true }
|
|
|
|
|
|
|
|
// ExitOn sets DoNotExit to true.
|
|
|
|
func ExitOn() { DoNotExit = false }
|
|
|
|
|
2022-02-17 23:05:23 +00:00
|
|
|
// Exit calls os.Exit(0) unless DoNotExit has been set to true. Cmds
|
|
|
|
// should never call Exit themselves returning a nil error from their
|
|
|
|
// Methods instead.
|
2022-02-17 04:31:12 +00:00
|
|
|
func Exit() {
|
|
|
|
if !DoNotExit {
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ExitError prints err and exits with 1 return value unless DoNotExit
|
2022-02-17 23:05:23 +00:00
|
|
|
// has been set to true. Commands should usually never call ExitError
|
|
|
|
// themselves returning an error from their Method instead.
|
2022-02-17 04:31:12 +00:00
|
|
|
func ExitError(err ...interface{}) {
|
|
|
|
switch e := err[0].(type) {
|
|
|
|
case string:
|
|
|
|
if len(e) > 1 {
|
2022-02-18 03:06:48 +00:00
|
|
|
log.Printf(e+"\n", err[1:]...)
|
|
|
|
} else {
|
|
|
|
log.Println(e)
|
2022-02-17 04:31:12 +00:00
|
|
|
}
|
|
|
|
case error:
|
|
|
|
out := fmt.Sprintf("%v", e)
|
|
|
|
if len(out) > 0 {
|
|
|
|
log.Println(out)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !DoNotExit {
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-17 23:05:23 +00:00
|
|
|
// ArgsFrom returns a list of field strings split on space with an extra
|
2022-02-17 04:31:12 +00:00
|
|
|
// trailing special space item appended if the line has any trailing
|
|
|
|
// spaces at all signifying a definite word boundary and not a potential
|
|
|
|
// prefix.
|
|
|
|
func ArgsFrom(line string) []string {
|
|
|
|
args := []string{}
|
|
|
|
if line == "" {
|
|
|
|
return args
|
|
|
|
}
|
|
|
|
args = strings.Fields(line)
|
|
|
|
if line[len(line)-1] == ' ' {
|
2022-02-25 00:24:48 +00:00
|
|
|
args = append(args, "")
|
2022-02-17 04:31:12 +00:00
|
|
|
}
|
|
|
|
return args
|
|
|
|
}
|
2022-03-23 08:31:00 +00:00
|
|
|
|
2022-03-28 06:20:54 +00:00
|
|
|
// ArgsOrIn takes an slice or nil as argument and if the slice has any
|
|
|
|
// length greater than 0 returns all the argument joined together with
|
|
|
|
// a single space between them. Otherwise, will read standard input
|
|
|
|
// until end of file reached (Cntl-D).
|
|
|
|
func ArgsOrIn(args []string) string {
|
|
|
|
if args == nil || len(args) == 0 {
|
|
|
|
return term.Read()
|
|
|
|
}
|
|
|
|
return strings.Join(args, " ")
|
|
|
|
}
|
|
|
|
|
2022-03-28 16:32:05 +00:00
|
|
|
// Aliases allows Bonzai tree developers to create aliases (similar to
|
|
|
|
// shell aliases) that are directly translated into arguments to the
|
|
|
|
// Bonzai tree executable by overriding the os.Args in a controlled way.
|
|
|
|
// The value of an alias is always a slice of strings that will replace
|
|
|
|
// the os.Args[2:]. A slice is used (instead of a string parsed with
|
|
|
|
// strings.Fields) to ensure that hard-coded arguments containing
|
|
|
|
// whitespace are properly handled.
|
|
|
|
var Aliases = make(map[string][]string)
|
2022-04-06 16:36:27 +00:00
|
|
|
|
2022-04-08 07:59:50 +00:00
|
|
|
// AllowPanic disables TrapPanic stopping it from cleaning panic errors.
|
|
|
|
var AllowPanic = false
|
|
|
|
|
2022-04-06 16:36:27 +00:00
|
|
|
// TrapPanic recovers from any panic and more gracefully displays the
|
2022-04-06 16:39:55 +00:00
|
|
|
// panic by logging it before exiting with a return value of 1.
|
2022-04-06 16:36:27 +00:00
|
|
|
var TrapPanic = func() {
|
2022-04-08 07:59:50 +00:00
|
|
|
if !AllowPanic {
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
log.Println(r)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
2022-04-06 16:36:27 +00:00
|
|
|
}
|
|
|
|
}
|